From dd63040d3e544de5fc20086f2d245aa5f77204fc Mon Sep 17 00:00:00 2001 From: Mark Bumiller Date: Thu, 15 Feb 2024 05:59:13 -0500 Subject: [PATCH] more FPN parsing - adding procedures - adding company route - making routes in H1_M1BPOS and H1_POS consistent --- lib/plugins/Label_H1_FPN.test.ts | 45 +++++++++++++------- lib/plugins/Label_H1_FPN.ts | 70 ++++++++++++++++++++++++++++---- lib/plugins/Label_H1_M1BPOS.ts | 7 ++-- lib/plugins/Label_H1_POS.ts | 18 ++++---- lib/types/route.ts | 17 ++++++++ lib/utils/route_utils.ts | 34 ++++++++++++++-- 6 files changed, 154 insertions(+), 37 deletions(-) create mode 100644 lib/types/route.ts diff --git a/lib/plugins/Label_H1_FPN.test.ts b/lib/plugins/Label_H1_FPN.test.ts index 41b2c51..6594f2a 100644 --- a/lib/plugins/Label_H1_FPN.test.ts +++ b/lib/plugins/Label_H1_FPN.test.ts @@ -18,22 +18,28 @@ test('decodes Label H1 Preamble FPN landing', () => { console.log(JSON.stringify(decodeResult, null, 2)); expect(decodeResult.decoded).toBe(true); - expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.decoder.decodeLevel).toBe('full'); expect(decodeResult.decoder.name).toBe('label-h1-fpn'); expect(decodeResult.formatted.description).toBe('Flight Plan'); - expect(decodeResult.formatted.items.length).toBe(6); + expect(decodeResult.formatted.items.length).toBe(9); expect(decodeResult.formatted.items[0].label).toBe('Route Status'); expect(decodeResult.formatted.items[0].value).toBe('Route Inactive'); expect(decodeResult.formatted.items[1].label).toBe('Origin'); expect(decodeResult.formatted.items[1].value).toBe('KEWR'); expect(decodeResult.formatted.items[2].label).toBe('Destination'); expect(decodeResult.formatted.items[2].value).toBe('KDFW'); - expect(decodeResult.formatted.items[3].label).toBe('Aircraft Route'); - expect(decodeResult.formatted.items[3].value).toBe('VECTOR >> DISCO >> RIVET'); - expect(decodeResult.formatted.items[4].label).toBe('Aircraft Route'); - expect(decodeResult.formatted.items[4].value).toBe('TACKE'); - expect(decodeResult.formatted.items[5].label).toBe('Message Checksum'); - expect(decodeResult.formatted.items[5].value).toBe('0xc8b5'); + expect(decodeResult.formatted.items[3].label).toBe('Company Route'); + expect(decodeResult.formatted.items[3].value).toBe('EWRDFW01(17L): >> SAAME > J6 > HVQ > Q68 > LITTR >> MEEOW >> FEWWW'); + expect(decodeResult.formatted.items[4].label).toBe('Arrival Procedure'); + expect(decodeResult.formatted.items[4].value).toBe('SEEVR4 starting at FEWWW'); + expect(decodeResult.formatted.items[5].label).toBe('Aircraft Route'); + expect(decodeResult.formatted.items[5].value).toBe('VECTOR >> DISCO >> RIVET'); + expect(decodeResult.formatted.items[6].label).toBe('Approach Procedure'); + expect(decodeResult.formatted.items[6].value).toBe('ILS 17L starting at RIVET'); + expect(decodeResult.formatted.items[7].label).toBe('Aircraft Route'); + expect(decodeResult.formatted.items[7].value).toBe('TACKE'); + expect(decodeResult.formatted.items[8].label).toBe('Message Checksum'); + expect(decodeResult.formatted.items[8].value).toBe('0xc8b5'); }); test('decodes Label H1 Preamble FPN full flight', () => { const decoder = new MessageDecoder(); @@ -53,21 +59,30 @@ test('decodes Label H1 Preamble FPN full flight', () => { console.log(JSON.stringify(decodeResult, null, 2)); expect(decodeResult.decoded).toBe(true); - expect(decodeResult.decoder.decodeLevel).toBe('partial'); + expect(decodeResult.decoder.decodeLevel).toBe('full'); expect(decodeResult.decoder.name).toBe('label-h1-fpn'); - expect(decodeResult.raw.flight_number).toBe('AAL1956') + expect(decodeResult.raw.flight_number).toBe('AAL1956'); + expect(decodeResult.raw.company_route.waypoints).toBeUndefined(); expect(decodeResult.formatted.description).toBe('Flight Plan'); - expect(decodeResult.formatted.items.length).toBe(5); + expect(decodeResult.formatted.items.length).toBe(9); expect(decodeResult.formatted.items[0].label).toBe('Route Status'); expect(decodeResult.formatted.items[0].value).toBe('Route Planned'); expect(decodeResult.formatted.items[1].label).toBe('Origin'); expect(decodeResult.formatted.items[1].value).toBe('KPHL'); expect(decodeResult.formatted.items[2].label).toBe('Destination'); expect(decodeResult.formatted.items[2].value).toBe('KPHX'); - expect(decodeResult.formatted.items[3].label).toBe('Runway'); - expect(decodeResult.formatted.items[3].value).toBe('27L(26O)'); - expect(decodeResult.formatted.items[4].label).toBe('Message Checksum'); - expect(decodeResult.formatted.items[4].value).toBe('0x293b'); + expect(decodeResult.formatted.items[3].label).toBe('Company Route'); + expect(decodeResult.formatted.items[3].value).toBe('PHLPHX61'); + expect(decodeResult.formatted.items[4].label).toBe('Runway'); + expect(decodeResult.formatted.items[4].value).toBe('27L(26O)'); + expect(decodeResult.formatted.items[5].label).toBe('Departure Procedure'); + expect(decodeResult.formatted.items[5].value).toBe('PHL3'); + expect(decodeResult.formatted.items[6].label).toBe('Arrival Procedure'); + expect(decodeResult.formatted.items[6].value).toBe('EAGUL6 starting at ZUN'); + expect(decodeResult.formatted.items[7].label).toBe('Approach Procedure'); + expect(decodeResult.formatted.items[7].value).toBe('ILS26: >> AIR(40.01 N, 80.49 W) > J110 > BOWRR >> VLA(39.056 N, 89.097 W) >> STL(38.516 N, 90.289 W) >> GIBSN(38.43 N, 92.244 W) >> TYGER(38.41 N, 94.05 W) >> GCK(37.551 N, 100.435 W) >> DIXAN(36.169 N, 105.573 W) >> ZUN(34.579 N, 109.093 W)'); + expect(decodeResult.formatted.items[8].label).toBe('Message Checksum'); + expect(decodeResult.formatted.items[8].value).toBe('0x293b'); }); test('decodes Label H1 Preamble FPN in-flight', () => { diff --git a/lib/plugins/Label_H1_FPN.ts b/lib/plugins/Label_H1_FPN.ts index 390382d..c3a9bf5 100644 --- a/lib/plugins/Label_H1_FPN.ts +++ b/lib/plugins/Label_H1_FPN.ts @@ -30,15 +30,21 @@ export class Label_H1_FPN extends DecoderPlugin { const value = data[i+1]; // TODO: discuss how store commented out bits as both raw and formatted switch(key) { - // case 'A': // Arrival Procedure (?) - // break; + case 'A': // Arrival Procedure (?) + addProcedure(decodeResult, value, 'arrival'); + break; case 'AA': addArrivalAirport(decodeResult, value); break; - // case 'CR': // Current Route (?) - // break; - // case 'D': // Departure Procedure - // break; + case 'AP': + addProcedure(decodeResult, value, 'approach'); + break; + case 'CR': + addCompanyRoute(decodeResult, value); + break; + case 'D': // Departure Procedure + addProcedure(decodeResult, value, 'departure'); + break; case 'DA': addDepartureAirport(decodeResult, value); break; @@ -47,6 +53,7 @@ export class Label_H1_FPN extends DecoderPlugin { break; case 'R': addDepartureRunway(decodeResult, value); + break; default: if(allKnownFields) { decodeResult.remaining.text = ''; @@ -124,6 +131,55 @@ function addArrivalAirport(decodeResult: any, value: string) { }); }; +function addProcedure(decodeResult: any, value: string, type: string) { + if(decodeResult.raw.procedures === undefined) { + decodeResult.raw.procedures = []; + } + const data = value.split('.'); + let waypoints; + if(data.length>1) { + waypoints = data.slice(1).map((leg)=> RouteUtils.getWaypoint(leg)); + } + const route = {name: data[0], waypoints: waypoints}; + decodeResult.raw.procedures.push({type: type, route: route}); + const procedureName = type.substring(0,1).toUpperCase() + type.slice(1); + let procedureValue = route.name; + decodeResult.formatted.items.push({ + type: `procedure`, + code: 'proc', + label: `${procedureName} Procedure`, + value: RouteUtils.routeToString(route), + }); +}; + +function addCompanyRoute(decodeResult: any, value: string) { + const segments = value.split('.'); + const parens_idx = segments[0].indexOf('('); + let name; + let runway; + if(parens_idx === -1) { + name = segments[0]; + } else { + name = segments[0].slice(0, parens_idx); + runway = segments[0].slice(parens_idx+1, segments[0].indexOf(')')); + } + let waypoints; + if(segments.length > 1) { + waypoints = segments.slice(1).map((leg) => RouteUtils.getWaypoint(leg)); + } + decodeResult.raw.company_route = { + name: name, + runway: runway, + waypoints: waypoints, + }; + decodeResult.formatted.items.push({ + type: 'company_route', + code: 'CR', + label: 'Company Route', + value: RouteUtils.routeToString(decodeResult.raw.company_route), + }); +}; + function addDepartureAirport(decodeResult: any, value: string) { decodeResult.raw.departure_icao = value; decodeResult.formatted.items.push({ @@ -145,7 +201,7 @@ function addDepartureRunway(decodeResult: any, value: string) { function addRoute(decodeResult: any, value: string) { const route = value.split('.'); - decodeResult.raw.route = route.map((leg)=> RouteUtils.getWaypoint(leg)); + decodeResult.raw.route = {waypoints: route.map((leg)=> RouteUtils.getWaypoint(leg))}; decodeResult.formatted.items.push({ type: 'aircraft_route', code: 'ROUTE', diff --git a/lib/plugins/Label_H1_M1BPOS.ts b/lib/plugins/Label_H1_M1BPOS.ts index 4f06aac..8d4a7db 100644 --- a/lib/plugins/Label_H1_M1BPOS.ts +++ b/lib/plugins/Label_H1_M1BPOS.ts @@ -1,5 +1,6 @@ import { DecoderPlugin } from '../DecoderPlugin'; import { CoordinateUtils } from '../utils/coordinate_utils'; +import { RouteUtils } from '../utils/route_utils'; export class Label_H1_M1BPOS extends DecoderPlugin { // eslint-disable-line camelcase name = 'label-h1-m1bpos'; @@ -33,9 +34,9 @@ export class Label_H1_M1BPOS extends DecoderPlugin { // eslint-disable-line came }); } - let route = items.slice(1).filter((part: any) => !/^\d(.+)$/.test(part)); - route = route.map((hop: any) => hop || '?'); - decodeResult.raw.route = route; + const route = items.slice(1).filter((part: any) => !/^\d(.+)$/.test(part)); + const waypoints = route.map((hop: any) => RouteUtils.getWaypoint(hop || '?')); + decodeResult.raw.route = {waypoints: waypoints}; decodeResult.formatted.description = 'Position Report'; diff --git a/lib/plugins/Label_H1_POS.ts b/lib/plugins/Label_H1_POS.ts index 27032a5..0dff74a 100644 --- a/lib/plugins/Label_H1_POS.ts +++ b/lib/plugins/Label_H1_POS.ts @@ -1,5 +1,6 @@ import { DateTimeUtils } from '../DateTimeUtils'; import { DecoderPlugin } from '../DecoderPlugin'; +import { Waypoint } from '../types/waypoint'; import { RouteUtils } from '../utils/route_utils'; export class Label_H1_POS extends DecoderPlugin { @@ -24,7 +25,7 @@ export class Label_H1_POS extends DecoderPlugin { const fields = data.split(','); if(fields.length==1 && data.startsWith('/RF')) { - decodeResult.raw.route = data.substring(3,data.length).split('.').map((leg: string) => {return {name: leg}}); + decodeResult.raw.route = {waypoints: data.substring(3,data.length).split('.').map((leg: string) => RouteUtils.getWaypoint(leg))}; decodeResult.formatted.items.push({ type: 'aircraft_route', code: 'ROUTE', @@ -126,16 +127,17 @@ export class Label_H1_POS extends DecoderPlugin { longitude: decodeResult.raw.longitude * (decodeResult.raw.longitude_direction === 'W' ? -1 : 1), }; + let waypoints : Waypoint[]; if(fields.length == 11) {//variant 1 - decodeResult.raw.route = [{name: fields[1] || '?,', time: convertDateTimeToEpoch(fields[2], fields[10]), timeFormat: 'epoch'}, - {name: fields[4] || '?', time: convertDateTimeToEpoch(fields[5], fields[10]), timeFormat: 'epoch'}, - {name: fields[6] || '?'}]; + waypoints = [{name: fields[1] || '?,', time: convertDateTimeToEpoch(fields[2], fields[10]), timeFormat: 'epoch'}, + {name: fields[4] || '?', time: convertDateTimeToEpoch(fields[5], fields[10]), timeFormat: 'epoch'}, + {name: fields[6] || '?'}] } else { - decodeResult.raw.route = [{name: fields[1] || '?,', time: convertHHMMSSToTod(fields[2]), timeFormat: 'tod'}, - {name: fields[4] || '?', time: convertHHMMSSToTod(fields[5]), timeFormat: 'tod'}, - {name: fields[6] || '?'}]; + waypoints = [{name: fields[1] || '?,', time: convertHHMMSSToTod(fields[2]), timeFormat: 'tod'}, + {name: fields[4] || '?', time: convertHHMMSSToTod(fields[5]), timeFormat: 'tod'}, + {name: fields[6] || '?'}]; } - + decodeResult.raw.route = {waypoints: waypoints}; decodeResult.raw.outside_air_temperature = Number(fields[7].substring(1)) * (fields[7].charAt(0) === 'M' ? -1 : 1); decodeResult.formatted.items.push({ diff --git a/lib/types/route.ts b/lib/types/route.ts new file mode 100644 index 0000000..962a4c0 --- /dev/null +++ b/lib/types/route.ts @@ -0,0 +1,17 @@ +import { Waypoint } from "./waypoint"; + +/** + * Representation of a route + * + * Typically a list of waypoints, this can also be a named company route. + */ +export interface Route { + /** optional name. If not set, `waypoints` is required */ + name?: string, + + /** optional runway */ + runway?: string, + + /** optional list of waypoints. If not set, `name` is required */ + waypoints?: Waypoint[], +} \ No newline at end of file diff --git a/lib/utils/route_utils.ts b/lib/utils/route_utils.ts index 603df49..a485744 100644 --- a/lib/utils/route_utils.ts +++ b/lib/utils/route_utils.ts @@ -1,10 +1,29 @@ +import { Route } from "../types/route"; import { Waypoint } from "../types/waypoint"; import { CoordinateUtils } from "./coordinate_utils"; export class RouteUtils { - public static routeToString(route: Waypoint[]): string { - return route.map((x) => RouteUtils.waypointToString(x)).join( ' > ').replaceAll('> >', '>>'); + public static routeToString(route: Route): string { + let str = ''; + if(route.name) { + str += route.name; + } + if(route.runway) { + str += `(${route.runway})`; + } + if(str.length!==0 && route.waypoints && route.waypoints.length === 1) { + str += ' starting at ' + } + else if(str.length!==0 && route.waypoints) { + str += ': '; + } + + if(route.waypoints) { + str += RouteUtils.waypointsToString(route.waypoints); + } + + return str; } public static waypointToString(waypoint: Waypoint): string { @@ -33,11 +52,18 @@ export class RouteUtils { // move out if we want public private static timestampToString(time: number, format: 'tod' | 'epoch'): string { - const date = new Date(time * 1000); - if(format == 'tod') { + const date = new Date(time * 1000); if(format == 'tod') { return date.toISOString().slice(11, 19); } //strip off millis return date.toISOString().slice(0,-5)+"Z"; } + + private static waypointsToString(waypoints: Waypoint[]): string { + let str = waypoints.map((x) => RouteUtils.waypointToString(x)).join( ' > ').replaceAll('> >', '>>'); + if(str.startsWith(' > ')) { + str = '>>' + str.slice(2); + } + return str; + } } \ No newline at end of file