import BaseModel from '../../entities/BaseModel' import FixePosition from '../../entities/FixePosition' import Task from '../../entities/Task' import { IGCParser } from '../IGCParser' import ClimbSinkParser from './ClimbSinkParser' import TakeoffDetection from './TakeoffDetection' import { getDistanceAndBearing } from './getDistanceAndBearing' import { MathHelpers } from '@igortrindade/lazyfy'; import { generate64ByteHash } from './generate64ByteHash' import dayjs from '../../util/dayjs' import { getTerrainElevation } from './getTerrainElevation' export class FlightParser extends BaseModel { public SOARING_MIN_TIME = 60 * 20 // 20 minutes public CROSS_MIN_DISTANCE = 15000 // km public MIN_SPEED_TO_DETECT_FLIGHT = 6 // km/h public CLIMB_SINK_CORRECTION = 0.7 public API_URL = 'http://127.0.0.1:3333' public numFlight: number = 0 public pilot: string = '' public copilot: string | null = '' public gliderType: string = '' public registration: string | null = '' public callsign: string | null = '' public competitionClass: string = '' public loggerType: string = '' public firmwareVersion: string = '' public hardwareVersion: string | null = '' public dataRecords: unknown[] = [] public security: string = '' public errors: unknown[] = [] public loggerId: string = '' public loggerManufacturer: string = '' public site: string = '' public date: string = '' public fixes: Array = [] public hash: string = '' public type = '' as 'CROSS' | 'SOARING' | 'SHORT' | 'TRIANGLE' public takeoff_at: any = null public takeoff_lng: number = 0 public takeoff_lat: number = 0 public takeoff_bearing: number = 0 public takeoff_gps_altitude: number = 0 public landing_at: any = null public landing_lng: number = 0 public landing_lat: number = 0 public landing_gps_altitude: number = 0 public takeoff_landing_height_diff: number = 0 public duration: string = '' public duration_in_seconds: number = 0 public free_distance: number = 0 public route_distance: number = 0 public route_bearing: number = 0 public average_general_speed: number = 0 public average_route_speed: number = 0 public max_speed: number = 0 public min_gps_altitude: number = 0 public max_gps_altitude: number = 0 public max_climb_rate: number = 0 public max_sink_rate: number = 0 public task: Task | any = null public climbSinkParser = new ClimbSinkParser() public takeoffDetection = new TakeoffDetection() public timezone = 'America/Sao_Paulo' constructor(fileContent: any, API_URL?: string) { super({}) this.resetDefaults() this.parseIgc(fileContent) this.API_URL = API_URL || this.API_URL return this } private resetDefaults () { this.route_distance = 0 this.duration_in_seconds = 0 this.max_speed = 0 this.max_climb_rate = 0 this.max_sink_rate = 0 } private async parseIgc(fileContent: any) { try { const parsed = IGCParser.parse(fileContent) this.setFillableKeys(parsed) } catch (error) { console.error(error) } return this } public async processFile() { try { this.fixes = this.fixes.filter((fix, index) => index % 5 === 0) await this.processFlightFixes() this.setFlightResume() this.setFlightType() await this.setFlightHash() } catch (error) { throw error } } public get fillable (): Array { return [ 'numFlight', 'pilot', 'copilot', 'gliderType', 'registration', 'callsign', 'competitionClass', 'loggerType', 'firmwareVersion', 'hardwareVersion', 'dataRecords', 'security', 'errors', 'loggerId', 'loggerManufacturer', 'site', 'date', 'fixes', 'title', 'type', 'min_gps_altitude', 'max_gps_altitude', 'takeoff_gps_altitude', 'landing_gps_altitude', 'takeoff_landing_height_diff', 'takeoff_lat', 'takeoff_lng', 'landing_lat', 'landing_lng', 'free_distance', 'route_bearing', 'average_general_speed', 'average_route_speed', 'max_speed', 'task', ] } private async processFlightFixes() { if(!this.fixes.length) return this.setTakeoffInfo() const numFixesToLoadElevantion = Math.round(this.fixes.length * 0.3) const selectedFixesToLoadElevation = [] for (let i = 0; i < this.fixes.length - 1; i++) { this.processFix(i) if(i > 0) { const { distance: distanceFromTakeoff } = getDistanceAndBearing(this.fixes[0].latitude, this.fixes[0].longitude, this.fixes[i].latitude, this.fixes[i].longitude) this.fixes[i].distanceFromTakeoff = distanceFromTakeoff this.fixes[i].timeFromTakeoff = this.fixes[i].timestamp - this.fixes[0].timestamp / 1000 } if(i % Math.ceil(this.fixes.length / numFixesToLoadElevantion) === 0) { selectedFixesToLoadElevation.push(this.fixes[i]) } } await getTerrainElevation(selectedFixesToLoadElevation, this.API_URL) this.climbSinkParser.climbSinkFixesIndexesToAdd = [] } private processFix(index: number) { const { distance, bearing, timeDiff, speed, altitudeDiff, climbSinkRate } = this.processFixInfo(index) this.fixes[index].distance = distance this.fixes[index].bearing = bearing this.fixes[index].timeDiff = timeDiff this.fixes[index].speed = speed this.fixes[index].altitudeDiff = altitudeDiff this.fixes[index].climbSinkRate = climbSinkRate this.climbSinkParser.processClimbSinkSegment(this.fixes[index], index) this.takeoffDetection.processFixe(this.fixes[index], this) return { distance, bearing, timeDiff, speed, altitudeDiff, climbSinkRate } } private processFixInfo(index: number) { const { distance, bearing } = getDistanceAndBearing(this.fixes[index].latitude, this.fixes[index].longitude, this.fixes[index + 1].latitude, this.fixes[index + 1].longitude) this.route_distance = MathHelpers.round(this.route_distance + distance, 2) const timeDiff = (this.fixes[index + 1].timestamp - this.fixes[index].timestamp) / 1000 // Convert from milliseconds to seconds const speed = MathHelpers.round((distance / timeDiff * 3.6), 1) const altitudeDiff = this.fixes[index + 1].gpsAltitude - this.fixes[index].gpsAltitude const climbSinkRate = MathHelpers.round(((altitudeDiff / timeDiff) * this.CLIMB_SINK_CORRECTION), 1) if(!this.checkFixeHasSomeDiscrepancy(speed, climbSinkRate)) { if(speed > this.max_speed) this.max_speed = speed if(this.max_climb_rate < climbSinkRate) this.max_climb_rate = climbSinkRate if(this.max_sink_rate > climbSinkRate) this.max_sink_rate = climbSinkRate } return { distance, bearing, timeDiff, speed, altitudeDiff, climbSinkRate } } private checkFixeHasSomeDiscrepancy(speed: number, climbSinkRate: number) { if(speed > 95) return true if(Math.abs(climbSinkRate) > 16) return true return false } private setTakeoffInfo() { this.takeoff_lat = this.fixes[0].latitude this.takeoff_lng = this.fixes[0].longitude this.landing_lat = this.fixes[this.fixes.length - 1].latitude this.landing_lng = this.fixes[this.fixes.length - 1].longitude const { distance: free_distance, bearing: route_bearing } = getDistanceAndBearing(this.takeoff_lat, this.takeoff_lng, this.landing_lat, this.landing_lng) this.free_distance = free_distance this.route_bearing = route_bearing } private setFlightType() { if(this.free_distance > this.CROSS_MIN_DISTANCE) { this.type = 'CROSS' } else if(this.duration_in_seconds > this.SOARING_MIN_TIME) { this.type = 'SOARING' } else { this.type = 'SHORT' } } private setFlightResume() { if(!this.fixes.length) return const gpsAltitudes = this.fixes.map(l => l.gpsAltitude) this.min_gps_altitude = Math.min.apply(null, gpsAltitudes) this.max_gps_altitude = Math.max.apply(null, gpsAltitudes) this.takeoff_at = dayjs.tz(this.fixes[0].timestamp, this.timezone).toDate() this.takeoff_gps_altitude = this.fixes[0].gpsAltitude this.landing_gps_altitude = this.fixes[this.fixes.length - 1].gpsAltitude this.landing_at = dayjs.tz(this.fixes[this.fixes.length - 1].timestamp, this.timezone).toDate() this.takeoff_landing_height_diff = this.landing_gps_altitude - this.takeoff_gps_altitude this.duration_in_seconds = (this.fixes[this.fixes.length - 1].timestamp - this.fixes[0].timestamp ) / 1000 this.average_general_speed = MathHelpers.round((this.route_distance / this.duration_in_seconds) * 3.6, 1) this.average_route_speed = MathHelpers.round((this.free_distance / this.duration_in_seconds) * 3.6, 1) } private async setFlightHash() { const content = [ this.loggerId, this.fixes[0].timestamp, this.fixes[0].latitude, this.fixes[0].longitude, this.fixes[this.fixes.length-1].timestamp, this.fixes[this.fixes.length-1].latitude, this.fixes[this.fixes.length-1].longitude, ].join('') this.hash = await generate64ByteHash(content) } }