/* * Copyright (C) 2017 Apple Inc. All rights reserved. * Copyright (C) 2016-2017 Michael Saboff. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ "use strict"; let earthRadius = 3440; // In nautical miles. let TwoPI = Math.PI * 2; let degreeCharacter = "\u00b0"; let regExpOptionalUnicodeFlag; var keywords; if (this.useUnicode) { regExpOptionalUnicodeFlag = "u"; keywords = UnicodeStrings; } else { regExpOptionalUnicodeFlag = ""; keywords = { get: function(str) { return str; } }; } function status(text) { console.debug("Status: " + text); } function error(text) { console.error("Error: " + text); } if (typeof(Number.prototype.toRadians) === "undefined") { Number.prototype.toRadians = function() { return this * Math.PI / 180; } } if (typeof(Number.prototype.toDegrees) === "undefined") { Number.prototype.toDegrees = function() { return this * 180 / Math.PI; } } function distanceFromSpeedAndTime(speed, time) { return speed * time.hours(); } let LatRE = new RegExp("^([NS\\-])?(90|[0-8]?\\d)(?:( [0-5]?\\d\\.\\d{0,3})'?|(\\.\\d{0,6})|( ([0-5]?\\d)\" ?([0-5]?\\d)'?))?", "i" + regExpOptionalUnicodeFlag); function decimalLatitudeFromString(latitudeString) { if (typeof latitudeString != "string") return 0; let match = latitudeString.match(LatRE); if (!match) return 0; let result = 0; let sign = 1; if (match[1] && (match[1].toUpperCase() == "S" || match[1] == "-")) sign = -1; result = Number(match[2]); if (result != 90) { if (match[3]) { // e.g. N37 42.874 let minutes = Number(match[3]); result = result + (minutes / 60); } else if (match[4]) { // e.g. N37.30697 let decimalDegrees = Number(match[4]); result = result + decimalDegrees; } else if (match[5]) { // e.g. N37 18" 27' let degrees = Number(match[6]); let minutes = Number(match[7]); result = result + (degrees + minutes / 60) / 60; } } return result * sign; } let LongRE = new RegExp("^([EW\\-]?)(180|(?:1[0-7]|\\d)?\\d)(?:( [0-5]?\\d\\.\\d{0,3})|(\\.\\d{0,6})|( ([0-5]?\\d)\" ?([0-5]?\\d)'?)?)", "i" + regExpOptionalUnicodeFlag); function decimalLongitudeFromString(longitudeString) { if (typeof longitudeString != "string") return 0; let match = longitudeString.match(LongRE); if (!match) return 0; let result = 0; let sign = 1; if (match[1] && (match[1].toUpperCase() == "W" || match[1] == "-")) sign = -1; result = Number(match[2]); if (result != 180) { if (match[3]) { // e.g. W121 53.254 let minutes = Number(match[3]); result = result + (minutes / 60); } else if (match[4]) { // e.g. W121.8876 let decimalDegrees = Number(match[4]); result = result + decimalDegrees; } else if (match[5]) { // e.g. W121 53" 15' let degrees = Number(match[6]); let minutes = Number(match[7]); result = result + (degrees + minutes / 60) / 60; } } return result * sign; } let TimeRE = new RegExp("^([0-9][0-9]?)(?:\:([0-5][0-9]))?(?:\:([0-5][0-9]))?$"); class Time { constructor(time) { if (time instanceof Date) { this._seconds = Math.Round(time.valueOf() / 1000); return; } if (typeof time == "string") { let match = time.match(TimeRE); if (!match) { this._seconds = 0; return; } if (match[3]) { let hours = parseInt(match[1].toString()); let minutes = parseInt(match[2].toString()); let seconds = parseInt(match[3].toString()); this._seconds = (hours * 60 + minutes) * 60 + seconds; } else if (match[2]) { let minutes = parseInt(match[1].toString()); let seconds = parseInt(match[2].toString()); this._seconds = minutes * 60 + seconds; } else this._seconds = parseInt(match[1].toString()); return; } if (typeof time == "number") { this._seconds = Math.round(time); return; } this._seconds = 0; } add(otherTime) { return new Time(this._seconds + otherTime._seconds); } addDate(otherDate) { return new Date(this._seconds * 1000 + otherDate.valueOf()); } static differenceBetween(time2, time1) { let seconds1; let seconds2; if (time1 instanceof Time) seconds1 = time1.seconds(); else seconds1 = Math.Round(time1.valueOf() / 1000); if (time2 instanceof Time) seconds2 = time2.seconds(); else seconds2 = Math.Round(time2.valueOf() / 1000); return new Time(seconds2 - seconds1); } seconds() { return this._seconds; } minutes() { return this._seconds / 60; } hours() { return this._seconds / 3600; } toString() { let result = ""; let seconds = this._seconds % 60; if (seconds < 0) { result = "-"; seconds = -seconds; } let minutes = this._seconds / 60 | 0; let hours = minutes / 60 | 0; minutes = minutes % 60; if (hours) result = result + hours + ":"; if (minutes < 10 && hours) result = result + "0"; result = result + minutes + ":"; if (seconds < 10) result = result + "0"; result = result + seconds; return result; } } class GeoLocation { constructor(latitude, longitude) { this.latitude = latitude; this.longitude = longitude; } latitudeString() { let latitude = this.latitude; let latitudePrefix = "N"; if (latitude < 0) { latitude = -latitude; latitudePrefix = "S" } let latitudeDegrees = Math.floor(latitude); let latitudeMinutes = ((latitude - latitudeDegrees) * 60).toFixed(3); let latitudeMinutesFiller = latitudeMinutes < 10 ? " " : ""; return latitudePrefix + latitudeDegrees + degreeCharacter + latitudeMinutesFiller + latitudeMinutes + "'"; } longitudeString() { let longitude = this.longitude; let longitudePrefix = "E"; if (longitude < 0) { longitude = -longitude; longitudePrefix = "W" } let longitudeDegrees = Math.floor(longitude); let longitudeMinutes = ((longitude - longitudeDegrees) * 60).toFixed(3); let longitudeMinutesFiller = longitudeMinutes < 10 ? " " : ""; return longitudePrefix + longitudeDegrees + degreeCharacter + longitudeMinutesFiller + longitudeMinutes + "'"; } distanceTo(otherLocation) { let dLat = (otherLocation.latitude - this.latitude).toRadians(); let dLon = (otherLocation.longitude - this.longitude).toRadians(); let a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(this.latitude.toRadians()) * Math.cos(otherLocation.latitude.toRadians()) * Math.sin(dLon/2) * Math.sin(dLon/2); let c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return earthRadius * c; } bearingFrom(otherLocation, magneticVariation) { if (magneticVariation == undefined) magneticVariation = 0; let dLon = (this.longitude - otherLocation.longitude).toRadians(); let thisLatitudeRadians = this.latitude.toRadians(); let otherLatitudeRadians = otherLocation.latitude.toRadians(); let y = Math.sin(dLon) * Math.cos(this.latitude.toRadians()); let x = Math.cos(otherLatitudeRadians) * Math.sin(thisLatitudeRadians) - Math.sin(otherLatitudeRadians) * Math.cos(thisLatitudeRadians) * Math.cos(dLon); return (Math.atan2(y, x).toDegrees() + 720 + magneticVariation) % 360; } bearingTo(otherLocation, magneticVariation) { if (magneticVariation == undefined) magneticVariation = 0; let dLon = (otherLocation.longitude - this.longitude).toRadians(); let thisLatitudeRadians = this.latitude.toRadians(); let otherLatitudeRadians = otherLocation.latitude.toRadians(); let y = Math.sin(dLon) * Math.cos(otherLocation.latitude.toRadians()); let x = Math.cos(thisLatitudeRadians) * Math.sin(otherLatitudeRadians) - Math.sin(thisLatitudeRadians) * Math.cos(otherLatitudeRadians) * Math.cos(dLon); return (Math.atan2(y, x).toDegrees() + 720 + magneticVariation) % 360 } locationFrom(bearing, distance, magneticVariation) { if (magneticVariation == undefined) magneticVariation = 0; let bearingRadians = (bearing - magneticVariation).toRadians(); let thisLatitudeRadians = this.latitude.toRadians(); let angularDistance = distance / earthRadius; let latitudeRadians = Math.asin(Math.sin(thisLatitudeRadians) * Math.cos(angularDistance) + Math.cos(thisLatitudeRadians) * Math.sin(angularDistance) * Math.cos(bearingRadians)); let longitudeRadians = this.longitude.toRadians() + Math.atan2(Math.sin(bearingRadians) * Math.sin(angularDistance) * Math.cos(thisLatitudeRadians), Math.cos(angularDistance) - Math.sin(thisLatitudeRadians) * Math.sin(latitudeRadians)); return new GeoLocation(latitudeRadians.toDegrees(), longitudeRadians.toDegrees()); } toString() { return "(" + this.latitudeString() + ", " + this.longitudeString() + ")"; } } function findFaaWaypoint(waypoint) { return faaWaypoints[waypoint]; } class FaaWaypoints { constructor() { if (!FaaWaypoints.instance) { FaaWaypoints.instance = this; this.waypoints = _faaWaypoints; } return FaaWaypoints.instance; } find(waypoint) { return this.waypoints[waypoint]; } } FaaWaypoints.instance = undefined; let faaWaypoints = new FaaWaypoints(); class FaaAirways { constructor() { if (!FaaAirways.instance) { FaaAirways.instance = this; this.airways = _faaAirways; } return FaaAirways.instance; } isAirway(identifier) { return !!this.airways[identifier]; } resolveAirway(airwayID, entryPoint, exitPoint) { let airway = this.airways[airwayID]; if (!airway) return ""; let entryIndex = airway.fixes.indexOf(entryPoint); let exitIndex = airway.fixes.indexOf(exitPoint); if (entryIndex == -1 || exitIndex == -1) return ""; let stride = (entryIndex <= exitIndex) ? 1 : -1; let route = []; for (let idx = entryIndex; idx != exitIndex; idx = idx + stride) route.push(airway.fixes[idx]); route.push(airway.fixes[exitIndex]); return route; } } FaaAirways.instance = undefined; let faaAirways = new FaaAirways(); class UserWaypoints { constructor() { if (!UserWaypoints.instance) { UserWaypoints.instance = this; this.waypoints = {}; } return UserWaypoints.instance; } clear() { this.waypoints = {}; } find(waypoint) { return this.waypoints[waypoint]; } update(name, description, latitude, longitude) { if (typeof latitude == "string") latitude = decimalLatitudeFromString(latitude); if (typeof longitude == "string") longitude = decimalLongitudeFromString(longitude); this.waypoints[name.toUpperCase()] = { "name": name, "description": description, "latitude": latitude, "longitude": longitude }; } } UserWaypoints.instance = undefined; let userWaypoints = new UserWaypoints(); class EngineConfig { constructor(type, fuelFlow, trueAirspeed) { this.type = type; this._fuelFlow = fuelFlow; this._trueAirspeed = trueAirspeed; } trueAirspeed() { return this._trueAirspeed; } fuelFlow() { return this._fuelFlow; } static appendConfig(type, fuelFlow, trueAirspeed) { if (this.allConfigsByType[type]) { status("Duplicate Engine configuration: " + type); return; } var newConfig = new EngineConfig(type, fuelFlow, trueAirspeed); this.allConfigs.push(newConfig); this.allConfigsByType[type] = newConfig; } static getConfig(n) { if (n >= this.allConfigs.length) return undefined; return this.allConfigs[n]; } } EngineConfig.allConfigs = []; EngineConfig.allConfigsByType = {}; EngineConfig.Taxi = 0; EngineConfig.Runup = 1; EngineConfig.Takeoff = 2; EngineConfig.Climb = 3; EngineConfig.Cruise = 4; EngineConfig.Pattern= 5; class Waypoint { constructor(name, type, description, latitude, longitude) { this.name = name; this.type = type; this.description = description; this.latitude = latitude; this.longitude = longitude; } } class Leg { constructor(fix, location) { this.previous = undefined; this.next = undefined; this.fix = fix; this.location = location; this.course = 0; this.distance = 0; this.trueAirspeed = 0; this.windDirection = 0; this.windSpeed = 0; this.heading = 0; this.estGS = 0; this.startFlightTiming = false; this.stopFlightTiming = false; this.engineConfig = EngineConfig.Cruise; this.fuelFlow = 0; this.distanceRemaining = 0; this.estimatedTimeEnroute = undefined; this.estTimeRemaining = 0; this.estFuel = 0; } fixName() { return this.fix; } toString() { return this.fix; } setPrevious(leg) { this.previous = leg; } previousLeg() { return this.previous; } setNext(leg) { this.next = leg; } nextLeg() { return this.next; } setWind(windDirection, windSpeed) { this.windDirection = windDirection; this.windSpeed = windSpeed; } isSameWind(windDirection, windSpeed) { return this.windDirection == windDirection && this.windSpeed == windSpeed; } windToString() { if (!this.windSpeed) return ""; return (this.windDirection ? this.windDirection : "360") + "@" + this.windSpeed; } setTrueAirspeed(trueAirspeed) { this.trueAirspeed = trueAirspeed; } isStandardTrueAirspeed() { let engineConfig = EngineConfig.getConfig(this.engineConfig); return (this.trueAirspeed == engineConfig.trueAirspeed()); } trueAirspeedToString() { return this.trueAirspeed + "kts"; } updateDistanceAndBearing(other) { this.distance = this.location.distanceTo(other); this.course = Math.round(this.location.bearingFrom(other)); if (this.estimatedTimeEnroute == undefined && this.estGS != 0) { let estimatedTimeEnrouteInSeconds = Math.round(this.distance * 3600 / this.estGS); this.estimatedTimeEnroute = new Time(estimatedTimeEnrouteInSeconds); } if (this.estimatedTimeEnroute.seconds()) this.estFuel = this.fuelFlow * this.estimatedTimeEnroute.hours(); } propagateWind() { let windDirection = this.windDirection; let windSpeed = this.windSpeed; windDirection = (windDirection + 360) % 360; if (!windDirection) windDirection = 360; for (let currLeg = this; currLeg; currLeg = currLeg.nextLeg()) { currLeg.windDirection = windDirection; currLeg.windSpeed = windSpeed; if (currLeg.stopFlightTiming) break; } } updateForWind() { if (!this.windSpeed || !this.trueAirspeed) { this.heading = this.course; this.estGS = this.trueAirspeed; return; } let windDirectionRadians = this.windDirection.toRadians(); let courseRadians = this.course.toRadians(); let swc = (this.windSpeed / this.trueAirspeed) * Math.sin(windDirectionRadians - courseRadians); if (Math.abs(swc) > 1) { status("Wind to strong to fly!"); return; } let headingRadians = courseRadians + Math.asin(swc); if (headingRadians < 0) headingRadians += TwoPI; if (headingRadians > TwoPI) headingRadians -= TwoPI let groundSpeed = this.trueAirspeed * Math.sqrt(1 - swc * swc) - this.windSpeed * Math.cos(windDirectionRadians - courseRadians); if (groundSpeed < 0) { status("Wind to strong to fly!"); return; } this.estGS = groundSpeed; this.heading = Math.round(headingRadians.toDegrees()); } calculate() { let engineConfig = EngineConfig.getConfig(this.engineConfig); if (!this.trueAirspeed) this.trueAirspeed = engineConfig.trueAirspeed(); this.fuelFlow = engineConfig.fuelFlow(); this.updateForWind(); } updateForward() { if (this.specialUpdateForward) this.specialUpdateForward(); let previousLeg = this.previousLeg(); let havePrevious = true; if (!previousLeg) { havePrevious = false; previousLeg = this; if (!this.estimatedTimeEnroute) this.estimatedTimeEnroute = new Time(0); } let thisLegType = this.type; if (thisLegType == "Climb" && havePrevious) this.location = previousLeg.location; else { this.updateDistanceAndBearing(previousLeg.location); this.updateForWind(); let nextLeg = this.nextLeg(); let previousLegType = previousLeg.type; if (havePrevious) { if (previousLegType == "Climb") { let climbDistance = distanceFromSpeedAndTime(previousLeg.estGS, previousLeg.climbTime); if (climbDistance < this.distance) { let climbStartLocation = previousLeg.location; let climbEndLocation = climbStartLocation.locationFrom(this.course, climbDistance); previousLeg.location = climbEndLocation; previousLeg.updateDistanceAndBearing(climbStartLocation); this.estimatedTimeEnroute = undefined; this.updateDistanceAndBearing(climbEndLocation); } else { status("Not enough distance to climb in leg #" + previousLeg.index); } } else if ((thisLegType == "Left" || thisLegType == "Right") && nextLeg && nextLeg.location) { let standardRateCircumference = this.trueAirspeed / 30; let standardRateRadius = standardRateCircumference / TwoPI; let offsetInboundBearing = 360 + previousLeg.course + (thisLegType == "Left" ? -90 : 90); offsetInboundBearing = Math.round((offsetInboundBearing + 360) % 360); // Save original location if (!previousLeg.originalLocation) previousLeg.originalLocation = previousLeg.location; let previousLocation = previousLeg.originalLocation; let inboundLocation = previousLocation.locationFrom(offsetInboundBearing, standardRateRadius); let bearingToNext = Math.round(nextLeg.location.bearingFrom(previousLocation)); let offsetOutboundBearing = bearingToNext + (thisLegType == "Left" ? 90 : -90); offsetOutboundBearing = (offsetOutboundBearing + 360) % 360; let outboundLocation = previousLocation.locationFrom(offsetOutboundBearing, standardRateRadius); let turnAngle = thisLegType == "Left" ? (360 + bearingToNext - previousLeg.course) : (360 + previousLeg.course - bearingToNext); turnAngle = (turnAngle + 360) % 360; let totalDegrees = turnAngle + 360 * this.extraTurns; let secondsInTurn = Math.round(totalDegrees / 3); this.estimatedTimeEnroute = new Time(Math.round((turnAngle + 360 * this.extraTurns) / 3)); this.estFuel = this.fuelFlow * this.estimatedTimeEnroute.hours(); this.location = outboundLocation; this.distance = distanceFromSpeedAndTime(this.trueAirspeed, this.estimatedTimeEnroute); previousLeg.location = inboundLocation; let prevPrevLeg = previousLeg.previousLeg(); if (prevPrevLeg && prevPrevLeg.location) { previousLeg.estimatedTimeEnroute = undefined; previousLeg.updateDistanceAndBearing(prevPrevLeg.location); } } } } } updateBackward() { let nextLeg = this.nextLeg(); let distanceRemaining; let timeRemaining; if (nextLeg) { distanceRemaining = nextLeg.distanceRemaining; timeRemaining = nextLeg.estTimeRemaining; } else { distanceRemaining = 0; timeRemaining = new Time(0); } if (this.stopFlightTiming || timeRemaining.seconds()) { this.distanceRemaining = distanceRemaining + this.distance;; this.estTimeRemaining = timeRemaining.add(this.estimatedTimeEnroute); } else this.estTimeRemaining = new Time(0); } } let RallyLegWithFixRE = new RegExp("^([0-9a-z\.]{3,16})\\|(" + keywords.get("START") + "|" + keywords.get("TIMING") + ")", "i" + regExpOptionalUnicodeFlag); class RallyLeg extends Leg { constructor(type, fix, location, engineConfig) { super(fix, location); this.type = type; this.engineConfig = engineConfig; } fixName() { return this.type; } toString() { return this.fixName(); } static reset() { RallyLeg.startLocation = undefined; RallyLeg.startFix = ""; RallyLeg.totalTaxiTime = new Time(0); RallyLeg.taxiSegments = []; } static fixNeeded(fix) { let match = fix.match(RallyLegWithFixRE); if (!match) return ""; return match[1].toString(); } static getLegWithFix(waypointText, fix, location) { let match = waypointText.match(RallyLegWithFixRE); if (!match) return undefined; let legType = match[2].toString(); if (legType == keywords.get("START")) { if (this.startLocation) { status("Trying to create second start leg"); return undefined; } this.startLocation = location; this.startFix = fix; this.totalTaxiTime = new Time(0); this.taxiSegments = []; return new StartLeg(waypointText, fix, location); } if (legType == keywords.get("TIMING")) return new TimingLeg(waypointText, fix, location); error("Unhandled Rally Leg type " + legType); return undefined; } } RallyLeg.startLocation = undefined; RallyLeg.startFix = ""; RallyLeg.totalTaxiTime = new Time(0); RallyLeg.taxiSegments = []; class StartLeg extends RallyLeg { constructor(fixText, fix, location) { super("Start", fix, location, EngineConfig.Taxi); } fixName() { return this.fix + "|Start"; } } class TimingLeg extends RallyLeg { constructor(fixText, fix, location) { super("Timing", fix, location, EngineConfig.Cruise); this.stopFlightTiming = true; } fixName() { return this.fix + "|Timing"; } } let RallyLegNoFixRE = new RegExp(keywords.get("TAXI") + "|" + keywords.get("RUNUP") + "|" + keywords.get("TAKEOFF") + "|" + keywords.get("CLIMB") + "|" + keywords.get("PATTERN") + "|" + keywords.get("LEFT") + "|" + keywords.get("RIGHT"), "i" + regExpOptionalUnicodeFlag); class RallyLegWithoutFix extends RallyLeg { constructor(type, CommentsAsFix, location, engineConfig) { super(type, CommentsAsFix, location, engineConfig); } setPrevious(previous) { if (this.setLocationFromPrevious() && previous) this.location = previous.location; super.setPrevious(previous); } setLocationFromPrevious() { return false; } static isRallyLegWithoutFix(fix) { let barPosition = fix.indexOf("|"); let firstPart = barPosition < 0 ? fix : fix.substring(0, barPosition); return RallyLegNoFixRE.test(firstPart); } static getLegNoFix(waypointText) { let barPosition = waypointText.indexOf("|"); let firstPart = barPosition < 0 ? waypointText : waypointText.substring(0, barPosition); firstPart = firstPart.toUpperCase(); let match = firstPart.match(RallyLegNoFixRE); if (!match) return undefined; let legType = match[0].toString(); if (legType == keywords.get("TAXI")) return new TaxiLeg(waypointText); if (legType == keywords.get("RUNUP")) return new RunupLeg(waypointText); if (legType == keywords.get("TAKEOFF")) { if (!this.startLocation) { status("Trying to create a Takeoff leg without start leg"); return undefined; } return new TakeoffLeg(waypointText); } if (legType == keywords.get("CLIMB")) return new ClimbLeg(waypointText); if (legType == keywords.get("PATTERN")) return new PatternLeg(waypointText); if (legType == keywords.get("LEFT") || legType == keywords.get("RIGHT")) return new TurnLeg(waypointText, legType == keywords.get("RIGHT")); error("Unhandled Rally Leg type " + legType); return undefined; } } // TAXI[|