Created
December 12, 2017 13:47
-
-
Save tgherzog/c5a1b951510c6ac6ef254175c2424352 to your computer and use it in GitHub Desktop.
SailBuddy code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SBData.swift | |
// Sail Buddy | |
// | |
// Created by Tim on 6/27/15. | |
// Copyright (c) 2015 Zog Software. All rights reserved. | |
// | |
import Foundation | |
import CoreLocation | |
@objc(SBData) | |
class SBData : NSObject /*, NSCoding */ { | |
var location:CLLocation? | |
// this is only set and implemented at recording time to ignore GPS data where hAccuracy is too high | |
var gpsTolerance = 0 | |
var _apparentWindSpeed: CLLocationSpeed?, _trueWindSpeed: CLLocationSpeed? | |
var _apparentWindAngle: CLLocationDirection? { | |
didSet { | |
if _apparentWindAngle != nil { | |
_apparentWindAngle = normalizeAngle(angle: _apparentWindAngle!) | |
} | |
} | |
} | |
var _trueWindAngle: CLLocationDirection? { | |
didSet { | |
if _trueWindAngle != nil { | |
_trueWindAngle = normalizeAngle(angle: _trueWindAngle!) | |
} | |
} | |
} | |
var course:CLLocationDirection? | |
var speed:CLLocationSpeed? | |
var depthBelowKeel:Double?, depthBelowTransducer:Double?, depthBelowSurface:Double? | |
var lastGPSUpdate:Date? // timestamp of last location update, including COG and SOG | |
var lastWindDataUpdate:Date? | |
var lastDepthDataUpdate:Date? | |
var windBatteryLevel:Int? | |
func startInstruments() { | |
location = nil | |
course = nil | |
speed = nil | |
_apparentWindSpeed = nil | |
_apparentWindAngle = nil | |
_trueWindSpeed = nil | |
_trueWindAngle = nil | |
depthBelowKeel = nil | |
depthBelowTransducer = nil | |
depthBelowSurface = nil | |
lastGPSUpdate = nil | |
lastWindDataUpdate = nil | |
lastDepthDataUpdate = nil | |
} | |
func normalizeAngle(angle:CLLocationDirection) -> CLLocationDirection { | |
var angle = angle | |
while angle < 0 { | |
angle += 360.0 | |
} | |
while angle >= 360.0 { | |
angle -= 360.0 | |
} | |
return angle | |
} | |
/* | |
override init() { | |
} | |
required init(coder adecoder: NSCoder) { | |
course = adecoder.decodeObjectForKey("cog") as? CLLocationDirection | |
speed = adecoder.decodeObjectForKey("sog") as? CLLocationSpeed | |
apparentWindAngle = adecoder.decodeObjectForKey("awa") as? CLLocationDirection | |
apparentWindSpeed = adecoder.decodeObjectForKey("aws") as? CLLocationSpeed | |
lastCourseUpdate = adecoder.decodeObjectForKey("lastCourseUpdate") as? Date | |
lastWindDataUpdate = adecoder.decodeObjectForKey("lastWindDataUpdate") as? Date | |
} | |
func encodeWithCoder(coder: NSCoder) { | |
coder.encodeObject(course, forKey: "cog") | |
coder.encodeObject(speed, forKey: "sog") | |
coder.encodeObject(apparentWindSpeed, forKey: "awa") | |
coder.encodeObject(apparentWindAngle, forKey: "aws") | |
if let location = self.location { | |
coder.encodeObject(location.coordinate.latitude, forKey: "lat") | |
coder.encodeObject(location.coordinate.longitude, forKey: "lon") | |
coder.encodeObject(location.timestamp.timeIntervalSinceReferenceDate, forKey: "ts") | |
} | |
coder.encodeObject(lastCourseUpdate, forKey: "lastCourseUpdate") | |
coder.encodeObject(lastWindDataUpdate, forKey: "lastWindDataUpdate") | |
} | |
*/ | |
func didReceiveLocation(location:CLLocation) { | |
self.location = location | |
self.lastGPSUpdate = location.timestamp | |
if location.speed >= 0 { | |
self.speed = location.speed | |
} | |
if location.course >= 0 { | |
self.course = location.course | |
} | |
} | |
var apparentWindSpeed: CLLocationSpeed? { | |
if _apparentWindSpeed != nil { | |
return _apparentWindSpeed | |
} | |
// TODO: calculate apparent wind speed | |
return nil | |
} | |
var apparentWindAngle: CLLocationDirection? { | |
if _apparentWindAngle != nil { | |
return _apparentWindAngle | |
} | |
// TODO: calculate apparent wind angle | |
return nil | |
} | |
// Using these calculations: https://en.wikipedia.org/wiki/Apparent_wind | |
var trueWindSpeed:CLLocationSpeed? { | |
if _trueWindSpeed != nil { | |
return _trueWindSpeed | |
} | |
if let aws = _apparentWindSpeed, let awa = _apparentWindAngle, let sog = self.speed { | |
let speed:Double = aws*aws + sog*sog - 2.0*aws*sog * cos(deg2rad(degrees: awa)) | |
return sqrt(speed) | |
} | |
return nil | |
} | |
var trueWindAngle:CLLocationDirection? { | |
if _trueWindAngle != nil { | |
return _trueWindAngle | |
} | |
if let aws = _apparentWindSpeed, let awa = _apparentWindAngle, let sog = self.speed, let tws = trueWindSpeed { | |
// trap for div by 0 | |
if tws == 0.0 { | |
return 0 | |
} | |
var angle:Double = rad2deg( rad: acos( (aws*cos(deg2rad(degrees: awa)) - sog) / tws) ) | |
if awa > 180 { | |
angle *= -1 | |
} | |
return normalizeAngle(angle: angle) | |
} | |
return nil | |
} | |
func angleFormatted(angle:CLLocationDirection, settings:SBSettings) -> CLLocationDirection { | |
var angle = angle | |
if settings.angleRelativity == .North, let cog = course { | |
angle += cog | |
} | |
return normalizeAngle(angle: angle) | |
} | |
func cogFormatted(settings:SBSettings, units:Bool=false) -> String { | |
return cogExport | |
} | |
func sogFormatted(settings:SBSettings, units:Bool=false) -> String { | |
return speedFormatted(value: speed, settings: settings, units:units) | |
} | |
func awaFormatted(settings:SBSettings, units:Bool=false) -> String { | |
if let awa = apparentWindAngle { | |
return String(format: "%.0f", angleFormatted(angle: awa, settings: settings)) | |
} | |
return "" | |
} | |
func twaFormatted(settings:SBSettings, units:Bool=false) -> String { | |
if let twa = trueWindAngle { | |
return String(format: "%.0f", angleFormatted(angle: twa, settings: settings)) | |
} | |
return "" | |
} | |
func awsFormatted(settings:SBSettings, units:Bool=false) -> String { | |
return speedFormatted(value: apparentWindSpeed, settings: settings, units:units) | |
} | |
func twsFormatted(settings:SBSettings, units:Bool=false) -> String { | |
return speedFormatted(value: trueWindSpeed, settings: settings, units:units) | |
} | |
func speedFormatted(value:CLLocationSpeed?, settings:SBSettings, units:Bool=false) -> String { | |
if let x = value { | |
return String(format: "%.1f", x * settings.msToSpeed) + (units ? settings.speedUnitsLabel : "") | |
} | |
return "" | |
} | |
func distanceFormatted(value:Double?, settings:SBSettings, units:Bool=false) -> String { | |
if let x = value { | |
return String(format: "%.1f", x * settings.mToDistance) + (units ? settings.distUnitsLabel : "") | |
} | |
return "" | |
} | |
func degreeFormatted(degree:CLLocationDegrees?, settings:SBSettings) -> String { | |
if let d = degree { | |
if settings.degreeUnits == .DMS { | |
let degrees = Int(d) | |
let fraction = (d - Double(degrees)) * (d < 0 ? -1 : 1) | |
let minutes = Int(fraction*60) | |
let seconds = fraction*3600 - Double(minutes*60) | |
return String(format: "%d\u{00b0}%d'%.0f\"", degrees, minutes, seconds) | |
} | |
return String(format: "%.6f", d) | |
} | |
return "" | |
} | |
func depthFormatted(value:Double?, settings:SBSettings, units:Bool=false) -> String { | |
if let depth = value { | |
return String(format: "%.1f", depth * settings.mToDepth) + (units ? settings.depthUnitsLabel : "") | |
} | |
return "" | |
} | |
/* Exported varaibles always use the same units of measurement regardless of configuration settings. | |
velocities are in meters/second since that's what gpx specifies | |
distances would be in meters | |
angles are in degrees (0 to 360) | |
lon/lat in decimal degrees (-180 to +180) | |
current wind directions are relative to cog, not compass north. I haven't found any information that | |
there is a standard for this one way or another. But cog makes sense to me because: 1) if cog is undefined | |
then wind directions relative to compass north would also be undefined, which would mean less data, and 2) | |
it's easy for users to calculate wind relative to cog if both are given. | |
*/ | |
var cogExport:String { | |
if let cog = course { | |
return String(format: "%.0f", cog) | |
} | |
return "" | |
} | |
var awaExport:String { | |
if let awa = apparentWindAngle { | |
return String(format: "%.0f", awa) | |
} | |
return "" | |
} | |
var twaExport:String { | |
if let twa = trueWindAngle { | |
return String(format: "%.0f", twa) | |
} | |
return "" | |
} | |
var sogExport:String { | |
if let sog = speed { | |
return String(format: "%.3f", sog * SB.kMetersPerSecond) | |
} | |
return "" | |
} | |
var awsExport:String { | |
if let aws = apparentWindSpeed { | |
return String(format: "%.3f", aws * SB.kMetersPerSecond) | |
} | |
return "" | |
} | |
var twsExport:String { | |
if let tws = trueWindSpeed { | |
return String(format: "%.3f", tws * SB.kMetersPerSecond) | |
} | |
return "" | |
} | |
var latExport:String { | |
if let loc = location { | |
return String(format: "%.6f", loc.coordinate.latitude) | |
} | |
return "" | |
} | |
var lonExport:String { | |
if let loc = location { | |
return String(format: "%.6f", loc.coordinate.longitude) | |
} | |
return "" | |
} | |
func depthObservation(index:Int) -> (key:String, label:String, value:Double)? { | |
let obs = [(SB.Fields.dbk, "Below Keel" /*"ΔKeel"*/, depthBelowKeel), (SB.Fields.dbs, "Below Surface" /*"ΔSurface"*/, depthBelowSurface), (SB.Fields.dbt, "Below Transducer" /*"ΔTrans."*/, depthBelowTransducer)] | |
var n=0 | |
for i in 0..<obs.count { | |
if obs[i].2 == nil { | |
continue | |
} | |
if n == index { | |
return (obs[i].0, obs[i].1, obs[i].2!) | |
} | |
n += 1 | |
} | |
return nil | |
} | |
} | |
func deg2rad(degrees: CLLocationDirection) -> Double { | |
return degrees * 3.14159 / 180.0 | |
} | |
func rad2deg(rad: Double) -> CLLocationDirection { | |
return round(rad * 180.0 / 3.14159) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment