UNPKG

aerofly-missions

Version:

The Aerofly Missionsgerät converts simulator flight plan files for Aerofly FS 4, Microsoft Flight Simulator, X-Plane, GeoFS, and Garmin / Infinite Flight flight plan files. It also imports SimBrief flight plans.

261 lines (216 loc) 7.1 kB
import { Units } from "./Units.js"; type LonLatMinute = { degree: number; minutes: number; minutesDecimal: number; seconds: number; secondsDecimal: number; }; export class LonLat { /** * In degrees, -180..180. Positive for East, negative for West */ lon: number; /** * In degrees, -90..90. Positive for North, negative for South */ lat: number; // In Nautical Miles static EARTH_MEAN_RADIUS = 3441.037; /** * Magnetic declination at this coordinate in degrees. "+" is to the East, "-" is to the West * @see https://en.wikipedia.org/wiki/Magnetic_declination */ magnetic_declination: number = 0; constructor(lon: number, lat: number, public altitude_m = 0) { this.lon = lon % 360; if (lon > 180) { this.lon -= 360; } this.lat = lat; } get lonRad(): number { return (this.lon / 180) * Math.PI; } get latRad(): number { return (this.lat / 180) * Math.PI; } protected convertMinute(lonOrLat: number): LonLatMinute { const l = { degree: lonOrLat > 0 ? Math.floor(lonOrLat) : Math.ceil(lonOrLat), minutes: 0, minutesDecimal: (Math.abs(lonOrLat) % 1) * 60, seconds: 0, secondsDecimal: 0, }; l.secondsDecimal = (l.minutesDecimal % 1) * 60; l.seconds = Math.floor(l.secondsDecimal); l.minutes = Math.floor(l.minutesDecimal); return l; } get lonMinute(): LonLatMinute { return this.convertMinute(this.lon); } get latMinute(): LonLatMinute { return this.convertMinute(this.lat); } /** * Returns E or W */ get lonHemisphere(): "E" | "W" { return this.lon > 0 ? "E" : "W"; } /** * Returns N or S */ get latHemisphere(): "N" | "S" { return this.lat > 0 ? "N" : "S"; } get altitude_ft(): number { return this.altitude_m * Units.feetPerMeter; } set altitude_ft(altitude_ft: number) { this.altitude_m = altitude_ft / Units.feetPerMeter; } toNavString(): string { // 360351N1151159W const lat = this.latMinute; const lon = this.lonMinute; return ( Math.abs(lat.degree).toFixed().padStart(2, "0") + lat.minutes.toFixed().padStart(2, "0") + lat.seconds.toFixed().padStart(2, "0") + this.latHemisphere + Math.abs(lon.degree).toFixed().padStart(3, "0") + lon.minutes.toFixed().padStart(2, "0") + lon.seconds.toFixed().padStart(2, "0") + this.lonHemisphere ); } toString(): string { return this.lon.toFixed(6) + " " + this.lat.toFixed(6); } /** * @returns a true bearing between coordinates in degrees */ getBearingTo(lonLat: LonLat): number { const lat1 = this.latRad; const lon1 = this.lonRad; const lat2 = lonLat.latRad; const lon2 = lonLat.lonRad; const dLon = lon2 - lon1; const y = Math.sin(dLon) * Math.cos(lat2); const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon); return ((Math.atan2(y, x) * 180) / Math.PI + 360) % 360; } /** * * @param lonLat * @returns number in Nautical miles */ getDistanceTo(lonLat: LonLat): number { const dLat = lonLat.latRad - this.latRad; const dLon = lonLat.lonRad - this.lonRad; const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(this.latRad) * Math.cos(lonLat.latRad); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const averageAltInNm = (lonLat.altitude_m + this.altitude_m) / (2 * Units.meterPerNauticalMile); // multiply with earth's mean radius in Nautical Miles return (LonLat.EARTH_MEAN_RADIUS + averageAltInNm) * c; } /** * * @param d number in nautical miles * @param bearing number in degree * @returns LonLat */ getRelativeCoordinates(distance: number, bearing: number): LonLat { const d = distance; const brng = (((bearing + 360) % 360) / 180) * Math.PI; const lat1 = this.latRad; const lon1 = this.lonRad; const R = LonLat.EARTH_MEAN_RADIUS; const lat2 = Math.asin(Math.sin(lat1) * Math.cos(d / R) + Math.cos(lat1) * Math.sin(d / R) * Math.cos(brng)); const lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(d / R) * Math.cos(lat1), Math.cos(d / R) - Math.sin(lat1) * Math.sin(lat2)); return new LonLat((lon2 * 180) / Math.PI, (lat2 * 180) / Math.PI, this.altitude_m); } /** * @see https://www.aerofly.com/community/forum/index.php?thread/19105-custom-missions-converting-coordinates/ */ static fromMainMcf(coordinates: number[], altitude_m: number = 0): LonLat { const f = 1.0 / 298.257223563; // WGS84 const e2 = 2 * f - f * f; //const lambda = VectorToAngle( coordinates[0], coordinates[1] ); let lambda = 0; if (coordinates[0] > 0) { if (coordinates[1] < 0) { lambda = 2 * Math.PI + Math.atan(coordinates[1] / coordinates[0]); } else { lambda = Math.atan(coordinates[1] / coordinates[0]); } } else if (coordinates[0] < 0) { lambda = Math.PI + Math.atan(coordinates[1] / coordinates[0]); } else if (coordinates[1] > 0) { lambda = 0.5 * Math.PI; } else { lambda = 1.5 * Math.PI; } const rho = Math.sqrt(coordinates[0] * coordinates[0] + coordinates[1] * coordinates[1]); const phi = Math.atan(coordinates[2] / ((1.0 - e2) * rho)); return new LonLat((lambda * 180) / Math.PI, (phi * 180) / Math.PI, altitude_m); } clone(): LonLat { const l = new LonLat(this.lon, this.lat, this.altitude_m); l.magnetic_declination = this.magnetic_declination; return l; } } export class LonLatArea { protected coordinates: LonLat[] = []; min: LonLat; max: LonLat; constructor(lonLat: LonLat) { this.min = new LonLat(lonLat.lon, lonLat.lat); this.max = new LonLat(lonLat.lon, lonLat.lat); } push(lonLat: LonLat) { this.min.lon = Math.min(this.min.lon, lonLat.lon); this.min.lat = Math.min(this.min.lat, lonLat.lat); this.max.lon = Math.max(this.max.lon, lonLat.lon); this.max.lat = Math.max(this.max.lat, lonLat.lat); } get maxDistance(): number { return this.min.getDistanceTo(this.max); } get center(): LonLat { return new LonLat((this.min.lon + this.max.lon) / 2, (this.min.lat + this.max.lat) / 2); } get lonRange(): number { return this.max.lon - this.min.lon; } get latRange(): number { return this.max.lat - this.min.lat; } /** * @param aspectRatio to fit lonRange & latRange into * @returns a lon/latRange to fit into */ getMaxRange(aspectRatio = 2 / 1): number { let x = this.lonRange, y = this.latRange; const rangeAspectRatio = x / y; // 0.5 if (aspectRatio > rangeAspectRatio) { x *= aspectRatio / rangeAspectRatio; } else { y *= rangeAspectRatio / aspectRatio; } return Math.max(x, y); // 0..360 } getZoomLevel(aspectRatio = 2 / 1, factor = 1, fraction = false): number { const zoom = 3 + Math.pow(360 / this.getMaxRange(aspectRatio), 0.3) * factor; return fraction ? zoom : Math.ceil(zoom); } }