@squawk/flightplan 0.5.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/resolver.d.ts +14 -0
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +33 -18
- package/dist/route-distance.d.ts +10 -2
- package/dist/route-distance.d.ts.map +1 -1
- package/dist/route-distance.js +7 -87
- package/dist/route-geometry.d.ts +108 -0
- package/dist/route-geometry.d.ts.map +1 -0
- package/dist/route-geometry.js +163 -0
- package/dist/route-timing.d.ts +169 -0
- package/dist/route-timing.d.ts.map +1 -0
- package/dist/route-timing.js +129 -0
- package/package.json +11 -8
package/README.md
CHANGED
|
@@ -95,8 +95,8 @@ Creates a resolver from optional lookup providers.
|
|
|
95
95
|
**Parameters:**
|
|
96
96
|
|
|
97
97
|
- `options.airports` - airport lookup (must provide `byFaaId` and `byIcao`)
|
|
98
|
-
- `options.navaids` - navaid lookup (must provide `byIdent`)
|
|
99
|
-
- `options.fixes` - fix lookup (must provide `byIdent`)
|
|
98
|
+
- `options.navaids` - navaid lookup (must provide `byIdent`; may also provide `byIdentAtPosition` for proximity disambiguation)
|
|
99
|
+
- `options.fixes` - fix lookup (must provide `byIdent`; may also provide `byIdentAtPosition` for proximity disambiguation)
|
|
100
100
|
- `options.airways` - airway lookup (must provide `byDesignation` and `expand`)
|
|
101
101
|
- `options.procedures` - procedure lookup (must provide `byIdentifier` and `expand`)
|
|
102
102
|
|
|
@@ -159,3 +159,119 @@ both an airport and a navaid), the resolver uses this priority order:
|
|
|
159
159
|
3. Procedure (SID/STAR)
|
|
160
160
|
4. Fix
|
|
161
161
|
5. Navaid
|
|
162
|
+
|
|
163
|
+
The same fix or navaid identifier is sometimes published in more than one
|
|
164
|
+
region. When the `fixes` or `navaids` provider exposes `byIdentAtPosition`, the
|
|
165
|
+
resolver disambiguates such a token by proximity to the most recently resolved
|
|
166
|
+
positional element (a preceding airport, coordinate, waypoint, airway exit fix,
|
|
167
|
+
or procedure terminus). The first token in a route has no such anchor, and a
|
|
168
|
+
provider that exposes only `byIdent` cannot disambiguate; both cases fall back
|
|
169
|
+
to the first `byIdent` match.
|
|
170
|
+
|
|
171
|
+
### `computeRouteDistance(route, groundSpeedKt?)`
|
|
172
|
+
|
|
173
|
+
Computes the total great-circle route distance and optional estimated time
|
|
174
|
+
enroute from a parsed route. Walks the same ordered point sequence as the
|
|
175
|
+
geometry helpers below, so distance and geometry stay in agreement. Airway
|
|
176
|
+
segments use FAA-published per-segment distances when available, falling back
|
|
177
|
+
to great-circle computation.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { computeRouteDistance } from '@squawk/flightplan';
|
|
181
|
+
|
|
182
|
+
const route = resolver.parse('KJFK DCT MERIT J60 MARTN DCT KLAX');
|
|
183
|
+
const result = computeRouteDistance(route, 450);
|
|
184
|
+
console.log(result.totalDistanceNm, result.estimatedTimeEnrouteHrs);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Parameters:**
|
|
188
|
+
|
|
189
|
+
- `route` - a `ParsedRoute` from `resolver.parse`
|
|
190
|
+
- `groundSpeedKt` - optional ground speed in knots; omit to skip ETE
|
|
191
|
+
|
|
192
|
+
**Returns:** `RouteDistanceResult` with:
|
|
193
|
+
|
|
194
|
+
- `legs` - ordered `RouteLeg` values, each with `from`/`to` point labels, their `fromLat`/`fromLon`/`toLat`/`toLon` coordinates in decimal degrees, `distanceNm`, and `cumulativeDistanceNm`
|
|
195
|
+
- `totalDistanceNm` - total great-circle distance in nautical miles
|
|
196
|
+
- `estimatedTimeEnrouteHrs` - ETE in hours, or `undefined` when no ground speed was given
|
|
197
|
+
- `unresolvedElements` - route elements that could not contribute coordinates
|
|
198
|
+
|
|
199
|
+
### `computeRouteTiming(route, options)`
|
|
200
|
+
|
|
201
|
+
Layers per-leg, wind-corrected timing over the same legs as
|
|
202
|
+
`computeRouteDistance`. For each leg it derives the initial great-circle true
|
|
203
|
+
course, samples the wind at the leg midpoint, and solves the wind triangle for
|
|
204
|
+
true heading, wind correction angle, and ground speed. Estimated time enroute
|
|
205
|
+
follows from leg distance and ground speed, and fuel burn follows from the same
|
|
206
|
+
ground speed.
|
|
207
|
+
|
|
208
|
+
Winds are supplied through an optional `windProvider` callback, keeping this
|
|
209
|
+
function independent of any weather source. The provider takes only a position;
|
|
210
|
+
bake the sampling altitude into the closure. A `@squawk/weather` winds-aloft
|
|
211
|
+
forecast read at a chosen cruise altitude is one way to satisfy it. When the
|
|
212
|
+
provider is omitted, or returns `undefined` for a leg (no data, or light and
|
|
213
|
+
variable), that leg is timed as calm: ground speed equals true airspeed and the
|
|
214
|
+
wind correction angle is zero.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import { computeRouteTiming } from '@squawk/flightplan';
|
|
218
|
+
|
|
219
|
+
const route = resolver.parse('KJFK DCT MERIT J60 MARTN DCT KLAX');
|
|
220
|
+
const result = computeRouteTiming(route, {
|
|
221
|
+
trueAirspeedKt: 450,
|
|
222
|
+
windProvider: (lat, lon) => ({ directionDeg: 270, speedKt: 80 }),
|
|
223
|
+
fuelBurnPerHr: 600,
|
|
224
|
+
fuelAvailable: 2400,
|
|
225
|
+
});
|
|
226
|
+
console.log(result.totalEteHrs, result.totalFuelRequired, result.fuelSufficient);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Parameters:**
|
|
230
|
+
|
|
231
|
+
- `route` - a `ParsedRoute` from `resolver.parse`
|
|
232
|
+
- `options.trueAirspeedKt` - true airspeed in knots flown on every leg
|
|
233
|
+
- `options.windProvider` - optional `(lat, lon) => WindVector | undefined` callback sampling the wind at each leg midpoint
|
|
234
|
+
- `options.fuelBurnPerHr` - optional fuel burn rate per hour (any consistent unit); enables per-leg and total fuel
|
|
235
|
+
- `options.fuelAvailable` - optional fuel on board (same unit as `fuelBurnPerHr`); with the burn rate, enables endurance and sufficiency
|
|
236
|
+
|
|
237
|
+
**Returns:** `RouteTimingResult` with:
|
|
238
|
+
|
|
239
|
+
- `legs` - ordered `RouteTimingLeg` values, each carrying the distance fields above plus `trueCourseDeg`, `trueHeadingDeg`, `windCorrectionAngleDeg`, `groundSpeedKt`, the applied `wind` (or `undefined` when calm), `eteHrs`, `cumulativeEteHrs`, and `fuelRequired`
|
|
240
|
+
- `totalDistanceNm` - total great-circle distance in nautical miles
|
|
241
|
+
- `totalEteHrs` - total estimated time enroute in hours, or `undefined` if any leg could not be timed
|
|
242
|
+
- `totalFuelRequired` - total fuel across all legs, or `undefined` without a burn rate or when any leg could not be timed
|
|
243
|
+
- `enduranceHrs` - endurance in hours from `fuelAvailable` and `fuelBurnPerHr`, or `undefined`
|
|
244
|
+
- `fuelSufficient` - whether endurance covers the total ETE, or `undefined`
|
|
245
|
+
- `unresolvedElements` - route elements that could not contribute coordinates
|
|
246
|
+
|
|
247
|
+
Ground speed comes from the wind triangle as a magnitude, so a leg is left
|
|
248
|
+
untimed (`eteHrs` is `undefined`) only when ground speed is not positive, which
|
|
249
|
+
occurs where a pure headwind equals true airspeed. A headwind exceeding true
|
|
250
|
+
airspeed still yields a positive magnitude and is not separately flagged.
|
|
251
|
+
|
|
252
|
+
### `extractRoutePoints(route)`
|
|
253
|
+
|
|
254
|
+
Extracts the ordered sequence of drawable geographic points from a parsed
|
|
255
|
+
route. Expands airway and SID/STAR segments into their constituent fixes and
|
|
256
|
+
suppresses consecutive duplicate points (e.g. an airway entry fix that matches
|
|
257
|
+
the preceding waypoint). Elements without coordinates (DCT, speed/altitude
|
|
258
|
+
groups, unresolved tokens) contribute no points.
|
|
259
|
+
|
|
260
|
+
**Returns:** `RoutePoint[]`, each with `label`, `lat`, and `lon`.
|
|
261
|
+
|
|
262
|
+
### `routeToLineString(route)`
|
|
263
|
+
|
|
264
|
+
Builds a GeoJSON `LineString` from a parsed route, ready to render as a
|
|
265
|
+
polyline on a map (e.g. MapLibre, Leaflet). Coordinates follow the GeoJSON
|
|
266
|
+
`[lon, lat]` ordering. Returns `undefined` when the route yields fewer than
|
|
267
|
+
two drawable points, since a `LineString` requires at least two positions.
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { routeToLineString } from '@squawk/flightplan';
|
|
271
|
+
|
|
272
|
+
const route = resolver.parse('KJFK DCT MERIT J60 MARTN DCT KLAX');
|
|
273
|
+
const line = routeToLineString(route);
|
|
274
|
+
if (line) {
|
|
275
|
+
map.addSource('route', { type: 'geojson', data: line });
|
|
276
|
+
}
|
|
277
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -8,4 +8,9 @@ export { createFlightplanResolver } from './resolver.js';
|
|
|
8
8
|
export type { FlightplanResolver, FlightplanResolverOptions, FlightplanAirportLookup, FlightplanNavaidLookup, FlightplanFixLookup, FlightplanAirwayLookup, FlightplanProcedureLookup, ParsedRoute, RouteElement, AirportRouteElement, SidRouteElement, StarRouteElement, AirwayRouteElement, DirectRouteElement, WaypointRouteElement, CoordinateRouteElement, SpeedAltitudeRouteElement, UnresolvedRouteElement, } from './resolver.js';
|
|
9
9
|
export { computeRouteDistance } from './route-distance.js';
|
|
10
10
|
export type { RouteLeg, RouteDistanceResult } from './route-distance.js';
|
|
11
|
+
export { computeRouteTiming } from './route-timing.js';
|
|
12
|
+
export type { WindProvider, RouteTimingOptions, RouteTimingLeg, RouteTimingResult, } from './route-timing.js';
|
|
13
|
+
export type { WindVector } from '@squawk/flight-math';
|
|
14
|
+
export { extractRoutePoints, routeToLineString } from './route-geometry.js';
|
|
15
|
+
export type { RoutePoint } from './route-geometry.js';
|
|
11
16
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,YAAY,EACV,kBAAkB,EAClB,yBAAyB,EACzB,uBAAuB,EACvB,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,EACtB,yBAAyB,EACzB,WAAW,EACX,YAAY,EACZ,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,EACtB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,YAAY,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,YAAY,EACV,kBAAkB,EAClB,yBAAyB,EACzB,uBAAuB,EACvB,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,EACtB,yBAAyB,EACzB,WAAW,EACX,YAAY,EACZ,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,EACtB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,YAAY,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC5E,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/resolver.d.ts
CHANGED
|
@@ -16,6 +16,13 @@ export interface FlightplanAirportLookup {
|
|
|
16
16
|
export interface FlightplanNavaidLookup {
|
|
17
17
|
/** Looks up navaids by identifier. Returns an empty array if none found. */
|
|
18
18
|
byIdent(ident: string): Navaid[];
|
|
19
|
+
/**
|
|
20
|
+
* Optionally looks up the single navaid sharing the identifier that lies
|
|
21
|
+
* nearest a reference position, disambiguating same-identifier collisions by
|
|
22
|
+
* proximity to the previously resolved route waypoint. When omitted, the
|
|
23
|
+
* resolver falls back to the first `byIdent` match.
|
|
24
|
+
*/
|
|
25
|
+
byIdentAtPosition?(ident: string, lat: number, lon: number, toleranceNm?: number): Navaid | undefined;
|
|
19
26
|
}
|
|
20
27
|
/**
|
|
21
28
|
* Minimal fix lookup interface consumed by the flightplan resolver.
|
|
@@ -24,6 +31,13 @@ export interface FlightplanNavaidLookup {
|
|
|
24
31
|
export interface FlightplanFixLookup {
|
|
25
32
|
/** Looks up fixes by identifier. Returns an empty array if none found. */
|
|
26
33
|
byIdent(ident: string): Fix[];
|
|
34
|
+
/**
|
|
35
|
+
* Optionally looks up the single fix sharing the identifier that lies nearest
|
|
36
|
+
* a reference position, disambiguating same-identifier collisions by proximity
|
|
37
|
+
* to the previously resolved route waypoint. When omitted, the resolver falls
|
|
38
|
+
* back to the first `byIdent` match.
|
|
39
|
+
*/
|
|
40
|
+
byIdentAtPosition?(ident: string, lat: number, lon: number, toleranceNm?: number): Fix | undefined;
|
|
27
41
|
}
|
|
28
42
|
/**
|
|
29
43
|
* Minimal airway lookup interface consumed by the flightplan resolver.
|
package/dist/resolver.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,MAAM,EACN,cAAc,EACd,GAAG,EACH,MAAM,EACN,SAAS,EACT,YAAY,EACb,MAAM,eAAe,CAAC;AAMvB;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,mEAAmE;IACnE,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;IAC5C,sDAAsD;IACtD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;CAC3C;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,4EAA4E;IAC5E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"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"}
|
package/dist/resolver.js
CHANGED
|
@@ -84,9 +84,6 @@ function parseSpeedAltitude(token) {
|
|
|
84
84
|
}
|
|
85
85
|
return result;
|
|
86
86
|
}
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
// Token resolution helpers
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
87
|
/**
|
|
91
88
|
* Attempts to resolve a token as an airport (ICAO or FAA ID).
|
|
92
89
|
*/
|
|
@@ -147,29 +144,35 @@ function tryProcedure(token, procedures, airportContext) {
|
|
|
147
144
|
return { type: 'star', raw: token, procedure: proc, legs };
|
|
148
145
|
}
|
|
149
146
|
/**
|
|
150
|
-
* Attempts to resolve a token as a fix waypoint.
|
|
147
|
+
* Attempts to resolve a token as a fix waypoint. When an anchor is supplied and
|
|
148
|
+
* the lookup provides `byIdentAtPosition`, the identifier is disambiguated by
|
|
149
|
+
* proximity to the anchor; otherwise the first `byIdent` match is used.
|
|
151
150
|
*/
|
|
152
|
-
function tryFix(token, fixes) {
|
|
151
|
+
function tryFix(token, fixes, anchor) {
|
|
153
152
|
if (!fixes) {
|
|
154
153
|
return undefined;
|
|
155
154
|
}
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
const fix = anchor && fixes.byIdentAtPosition
|
|
156
|
+
? fixes.byIdentAtPosition(token, anchor.lat, anchor.lon)
|
|
157
|
+
: fixes.byIdent(token)[0];
|
|
158
|
+
if (fix) {
|
|
159
159
|
return { type: 'waypoint', raw: token, fix, lat: fix.lat, lon: fix.lon };
|
|
160
160
|
}
|
|
161
161
|
return undefined;
|
|
162
162
|
}
|
|
163
163
|
/**
|
|
164
|
-
* Attempts to resolve a token as a navaid waypoint.
|
|
164
|
+
* Attempts to resolve a token as a navaid waypoint. When an anchor is supplied
|
|
165
|
+
* and the lookup provides `byIdentAtPosition`, the identifier is disambiguated
|
|
166
|
+
* by proximity to the anchor; otherwise the first `byIdent` match is used.
|
|
165
167
|
*/
|
|
166
|
-
function tryNavaid(token, navaids) {
|
|
168
|
+
function tryNavaid(token, navaids, anchor) {
|
|
167
169
|
if (!navaids) {
|
|
168
170
|
return undefined;
|
|
169
171
|
}
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
const navaid = anchor && navaids.byIdentAtPosition
|
|
173
|
+
? navaids.byIdentAtPosition(token, anchor.lat, anchor.lon)
|
|
174
|
+
: navaids.byIdent(token)[0];
|
|
175
|
+
if (navaid) {
|
|
173
176
|
return { type: 'waypoint', raw: token, navaid, lat: navaid.lat, lon: navaid.lon };
|
|
174
177
|
}
|
|
175
178
|
return undefined;
|
|
@@ -226,6 +229,7 @@ export function createFlightplanResolver(options) {
|
|
|
226
229
|
const elements = [];
|
|
227
230
|
let lastWaypointIdent;
|
|
228
231
|
let lastAirportIdent;
|
|
232
|
+
let lastPosition;
|
|
229
233
|
let i = 0;
|
|
230
234
|
while (i < tokens.length) {
|
|
231
235
|
const token = tokens[i].toUpperCase();
|
|
@@ -240,6 +244,7 @@ export function createFlightplanResolver(options) {
|
|
|
240
244
|
if (coord) {
|
|
241
245
|
elements.push({ type: 'coordinate', raw: token, lat: coord.lat, lon: coord.lon });
|
|
242
246
|
lastWaypointIdent = undefined;
|
|
247
|
+
lastPosition = { lat: coord.lat, lon: coord.lon };
|
|
243
248
|
i++;
|
|
244
249
|
continue;
|
|
245
250
|
}
|
|
@@ -267,6 +272,10 @@ export function createFlightplanResolver(options) {
|
|
|
267
272
|
});
|
|
268
273
|
// The exit fix is consumed as part of the airway; update last waypoint
|
|
269
274
|
lastWaypointIdent = exitFix;
|
|
275
|
+
const exitWaypoint = expansion.waypoints[expansion.waypoints.length - 1];
|
|
276
|
+
if (exitWaypoint) {
|
|
277
|
+
lastPosition = { lat: exitWaypoint.lat, lon: exitWaypoint.lon };
|
|
278
|
+
}
|
|
270
279
|
i += 2;
|
|
271
280
|
continue;
|
|
272
281
|
}
|
|
@@ -298,6 +307,7 @@ export function createFlightplanResolver(options) {
|
|
|
298
307
|
// airport and a navaid on V16).
|
|
299
308
|
lastWaypointIdent = token;
|
|
300
309
|
lastAirportIdent = airportElement.airport.icao ?? airportElement.airport.faaId;
|
|
310
|
+
lastPosition = { lat: airportElement.airport.lat, lon: airportElement.airport.lon };
|
|
301
311
|
i++;
|
|
302
312
|
continue;
|
|
303
313
|
}
|
|
@@ -308,9 +318,12 @@ export function createFlightplanResolver(options) {
|
|
|
308
318
|
// Update last waypoint to the last fix in the procedure expansion
|
|
309
319
|
// that actually terminates at a fix (some leg types have no fix).
|
|
310
320
|
for (let j = procElement.legs.length - 1; j >= 0; j--) {
|
|
311
|
-
const
|
|
312
|
-
if (
|
|
313
|
-
lastWaypointIdent =
|
|
321
|
+
const leg = procElement.legs[j];
|
|
322
|
+
if (leg?.fixIdentifier !== undefined) {
|
|
323
|
+
lastWaypointIdent = leg.fixIdentifier;
|
|
324
|
+
if (leg.lat !== undefined && leg.lon !== undefined) {
|
|
325
|
+
lastPosition = { lat: leg.lat, lon: leg.lon };
|
|
326
|
+
}
|
|
314
327
|
break;
|
|
315
328
|
}
|
|
316
329
|
}
|
|
@@ -318,18 +331,20 @@ export function createFlightplanResolver(options) {
|
|
|
318
331
|
continue;
|
|
319
332
|
}
|
|
320
333
|
// Try fix
|
|
321
|
-
const fixElement = tryFix(token, fixes);
|
|
334
|
+
const fixElement = tryFix(token, fixes, lastPosition);
|
|
322
335
|
if (fixElement) {
|
|
323
336
|
elements.push(fixElement);
|
|
324
337
|
lastWaypointIdent = token;
|
|
338
|
+
lastPosition = { lat: fixElement.lat, lon: fixElement.lon };
|
|
325
339
|
i++;
|
|
326
340
|
continue;
|
|
327
341
|
}
|
|
328
342
|
// Try navaid
|
|
329
|
-
const navaidElement = tryNavaid(token, navaids);
|
|
343
|
+
const navaidElement = tryNavaid(token, navaids, lastPosition);
|
|
330
344
|
if (navaidElement) {
|
|
331
345
|
elements.push(navaidElement);
|
|
332
346
|
lastWaypointIdent = token;
|
|
347
|
+
lastPosition = { lat: navaidElement.lat, lon: navaidElement.lon };
|
|
333
348
|
i++;
|
|
334
349
|
continue;
|
|
335
350
|
}
|
package/dist/route-distance.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Route distance and estimated time enroute computation for parsed flight
|
|
3
|
-
* plan routes.
|
|
4
|
-
*
|
|
3
|
+
* plan routes. Sums great-circle leg distances over the ordered geographic
|
|
4
|
+
* point sequence produced by the `route-geometry` module.
|
|
5
5
|
*/
|
|
6
6
|
import type { ParsedRoute, RouteElement } from './resolver.js';
|
|
7
7
|
/**
|
|
@@ -12,6 +12,14 @@ export interface RouteLeg {
|
|
|
12
12
|
from: string;
|
|
13
13
|
/** Identifier or raw token of the ending point. */
|
|
14
14
|
to: string;
|
|
15
|
+
/** Latitude of the starting point in decimal degrees, positive north. */
|
|
16
|
+
fromLat: number;
|
|
17
|
+
/** Longitude of the starting point in decimal degrees, positive east. */
|
|
18
|
+
fromLon: number;
|
|
19
|
+
/** Latitude of the ending point in decimal degrees, positive north. */
|
|
20
|
+
toLat: number;
|
|
21
|
+
/** Longitude of the ending point in decimal degrees, positive east. */
|
|
22
|
+
toLon: number;
|
|
15
23
|
/** Great-circle distance of this leg in nautical miles. */
|
|
16
24
|
distanceNm: number;
|
|
17
25
|
/** Cumulative distance from the route start through the end of this leg in nautical miles. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-distance.d.ts","sourceRoot":"","sources":["../src/route-distance.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"route-distance.d.ts","sourceRoot":"","sources":["../src/route-distance.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAO/D;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,EAAE,EAAE,MAAM,CAAC;IACX,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,8FAA8F;IAC9F,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,0DAA0D;IAC1D,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,2DAA2D;IAC3D,eAAe,EAAE,MAAM,CAAC;IACxB,uFAAuF;IACvF,uBAAuB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;;OAIG;IACH,kBAAkB,EAAE,YAAY,EAAE,CAAC;CACpC;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,WAAW,EAClB,aAAa,CAAC,EAAE,MAAM,GACrB,mBAAmB,CAsCrB"}
|
package/dist/route-distance.js
CHANGED
|
@@ -1,94 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Route distance and estimated time enroute computation for parsed flight
|
|
3
|
-
* plan routes.
|
|
4
|
-
*
|
|
3
|
+
* plan routes. Sums great-circle leg distances over the ordered geographic
|
|
4
|
+
* point sequence produced by the `route-geometry` module.
|
|
5
5
|
*/
|
|
6
6
|
import { greatCircle } from '@squawk/geo';
|
|
7
|
-
|
|
8
|
-
// Internal helpers
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
/** Epsilon for comparing coordinates to detect duplicate points. */
|
|
11
|
-
const COORD_EPSILON = 1e-9;
|
|
12
|
-
/**
|
|
13
|
-
* Returns true if two points share the same coordinates (within epsilon).
|
|
14
|
-
*/
|
|
15
|
-
function samePosition(a, b) {
|
|
16
|
-
return Math.abs(a.lat - b.lat) < COORD_EPSILON && Math.abs(a.lon - b.lon) < COORD_EPSILON;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Walks the route elements and extracts an ordered array of geographic
|
|
20
|
-
* points. Duplicate consecutive points (e.g. an airway entry fix that
|
|
21
|
-
* matches the preceding waypoint) are suppressed.
|
|
22
|
-
*
|
|
23
|
-
* Also collects all `unresolved` elements encountered during the walk.
|
|
24
|
-
*/
|
|
25
|
-
function extractGeoPoints(elements) {
|
|
26
|
-
const points = [];
|
|
27
|
-
const unresolvedElements = [];
|
|
28
|
-
function emit(point) {
|
|
29
|
-
if (points.length > 0 && samePosition(points[points.length - 1], point)) {
|
|
30
|
-
// When the duplicate carries a precomputed distance that the existing
|
|
31
|
-
// point lacks, adopt it. This happens when an airway's entry fix
|
|
32
|
-
// overlaps the preceding waypoint -- the airway waypoint has the
|
|
33
|
-
// published segment distance that would otherwise be lost.
|
|
34
|
-
const last = points[points.length - 1];
|
|
35
|
-
if (point.precomputedDistanceToNextNm !== undefined &&
|
|
36
|
-
last.precomputedDistanceToNextNm === undefined) {
|
|
37
|
-
last.precomputedDistanceToNextNm = point.precomputedDistanceToNextNm;
|
|
38
|
-
}
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
points.push(point);
|
|
42
|
-
}
|
|
43
|
-
for (const el of elements) {
|
|
44
|
-
switch (el.type) {
|
|
45
|
-
case 'airport':
|
|
46
|
-
emit({ label: el.raw, lat: el.airport.lat, lon: el.airport.lon });
|
|
47
|
-
break;
|
|
48
|
-
case 'waypoint':
|
|
49
|
-
emit({ label: el.raw, lat: el.lat, lon: el.lon });
|
|
50
|
-
break;
|
|
51
|
-
case 'coordinate':
|
|
52
|
-
emit({ label: el.raw, lat: el.lat, lon: el.lon });
|
|
53
|
-
break;
|
|
54
|
-
case 'airway':
|
|
55
|
-
for (let i = 0; i < el.waypoints.length; i++) {
|
|
56
|
-
const wp = el.waypoints[i];
|
|
57
|
-
const isLast = i === el.waypoints.length - 1;
|
|
58
|
-
const point = {
|
|
59
|
-
label: wp.identifier ?? wp.name,
|
|
60
|
-
lat: wp.lat,
|
|
61
|
-
lon: wp.lon,
|
|
62
|
-
};
|
|
63
|
-
// Only carry precomputed distance for non-last waypoints (the
|
|
64
|
-
// last waypoint's distanceToNextNm points beyond this segment).
|
|
65
|
-
if (!isLast && wp.distanceToNextNm !== undefined) {
|
|
66
|
-
point.precomputedDistanceToNextNm = wp.distanceToNextNm;
|
|
67
|
-
}
|
|
68
|
-
emit(point);
|
|
69
|
-
}
|
|
70
|
-
break;
|
|
71
|
-
case 'sid':
|
|
72
|
-
case 'star':
|
|
73
|
-
for (const leg of el.legs) {
|
|
74
|
-
if (leg.fixIdentifier === undefined || leg.lat === undefined || leg.lon === undefined) {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
emit({ label: leg.fixIdentifier, lat: leg.lat, lon: leg.lon });
|
|
78
|
-
}
|
|
79
|
-
break;
|
|
80
|
-
case 'unresolved':
|
|
81
|
-
unresolvedElements.push(el);
|
|
82
|
-
break;
|
|
83
|
-
// 'direct' and 'speedAltitude' are expected non-geographic markers
|
|
84
|
-
// and are silently skipped.
|
|
85
|
-
case 'direct':
|
|
86
|
-
case 'speedAltitude':
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return { points, unresolvedElements };
|
|
91
|
-
}
|
|
7
|
+
import { extractGeoPoints } from './route-geometry.js';
|
|
92
8
|
// ---------------------------------------------------------------------------
|
|
93
9
|
// Public API
|
|
94
10
|
// ---------------------------------------------------------------------------
|
|
@@ -135,6 +51,10 @@ export function computeRouteDistance(route, groundSpeedKt) {
|
|
|
135
51
|
legs.push({
|
|
136
52
|
from: from.label,
|
|
137
53
|
to: to.label,
|
|
54
|
+
fromLat: from.lat,
|
|
55
|
+
fromLon: from.lon,
|
|
56
|
+
toLat: to.lat,
|
|
57
|
+
toLon: to.lon,
|
|
138
58
|
distanceNm: legDistanceNm,
|
|
139
59
|
cumulativeDistanceNm: totalDistanceNm,
|
|
140
60
|
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drawable geometry extraction for parsed flight plan routes. Walks the
|
|
3
|
+
* {@link ParsedRoute} elements into an ordered point sequence - expanding
|
|
4
|
+
* airway waypoints, flattening SID/STAR legs, and suppressing consecutive
|
|
5
|
+
* duplicate points - and exposes that sequence both as plain
|
|
6
|
+
* {@link RoutePoint} values and as a GeoJSON `LineString` for map rendering.
|
|
7
|
+
*
|
|
8
|
+
* The same walk feeds `computeRouteDistance` in the `route-distance` module,
|
|
9
|
+
* so distance and geometry stay in agreement.
|
|
10
|
+
*/
|
|
11
|
+
import type { LineString } from 'geojson';
|
|
12
|
+
import type { ParsedRoute, RouteElement } from './resolver.js';
|
|
13
|
+
/**
|
|
14
|
+
* A single drawable geographic point along a parsed route, in route order.
|
|
15
|
+
*/
|
|
16
|
+
export interface RoutePoint {
|
|
17
|
+
/** Display label (identifier or raw token) for the point. */
|
|
18
|
+
label: string;
|
|
19
|
+
/** Latitude in decimal degrees, positive north. */
|
|
20
|
+
lat: number;
|
|
21
|
+
/** Longitude in decimal degrees, positive east. */
|
|
22
|
+
lon: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A geographic point extracted from a route element. Internal to the package;
|
|
26
|
+
* carries the precomputed airway segment distance that `computeRouteDistance`
|
|
27
|
+
* needs but that is not part of the public {@link RoutePoint} shape.
|
|
28
|
+
*
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
export interface GeoPoint {
|
|
32
|
+
/** Display label (identifier or raw token). */
|
|
33
|
+
label: string;
|
|
34
|
+
/** Latitude in decimal degrees, positive north. */
|
|
35
|
+
lat: number;
|
|
36
|
+
/** Longitude in decimal degrees, positive east. */
|
|
37
|
+
lon: number;
|
|
38
|
+
/**
|
|
39
|
+
* Pre-computed distance to the next point along an airway segment in
|
|
40
|
+
* nautical miles, if available from the source data.
|
|
41
|
+
*/
|
|
42
|
+
precomputedDistanceToNextNm?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Walks the route elements and extracts an ordered array of geographic
|
|
46
|
+
* points. Duplicate consecutive points (e.g. an airway entry fix that
|
|
47
|
+
* matches the preceding waypoint) are suppressed.
|
|
48
|
+
*
|
|
49
|
+
* Also collects all `unresolved` elements encountered during the walk.
|
|
50
|
+
*
|
|
51
|
+
* Internal to the package: `extractRoutePoints` and `routeToLineString`
|
|
52
|
+
* project this into public shapes, and `computeRouteDistance` consumes the
|
|
53
|
+
* precomputed segment distances directly.
|
|
54
|
+
*
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
export declare function extractGeoPoints(elements: RouteElement[]): {
|
|
58
|
+
points: GeoPoint[];
|
|
59
|
+
unresolvedElements: RouteElement[];
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Extracts the ordered sequence of drawable geographic points from a parsed
|
|
63
|
+
* flight plan route.
|
|
64
|
+
*
|
|
65
|
+
* Expands airway and SID/STAR segments into their constituent fixes and
|
|
66
|
+
* suppresses consecutive duplicate points (e.g. an airway entry fix that
|
|
67
|
+
* matches the preceding waypoint). Elements without coordinates (DCT,
|
|
68
|
+
* speed/altitude groups, unresolved tokens) contribute no points.
|
|
69
|
+
*
|
|
70
|
+
* This is the same walk that backs `computeRouteDistance`, so the point
|
|
71
|
+
* sequence here lines up with that result's legs.
|
|
72
|
+
*
|
|
73
|
+
* ```typescript
|
|
74
|
+
* import { createFlightplanResolver, extractRoutePoints } from '@squawk/flightplan';
|
|
75
|
+
*
|
|
76
|
+
* const resolver = createFlightplanResolver({ airports, navaids, fixes, airways });
|
|
77
|
+
* const route = resolver.parse('KJFK DCT MERIT J60 MARTN DCT KLAX');
|
|
78
|
+
* const points = extractRoutePoints(route);
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @param route - A parsed route from {@link FlightplanResolver.parse}.
|
|
82
|
+
* @returns Ordered drawable points along the route.
|
|
83
|
+
*/
|
|
84
|
+
export declare function extractRoutePoints(route: ParsedRoute): RoutePoint[];
|
|
85
|
+
/**
|
|
86
|
+
* Builds a GeoJSON `LineString` from a parsed flight plan route, ready to
|
|
87
|
+
* render as a polyline on a map (e.g. MapLibre, Leaflet).
|
|
88
|
+
*
|
|
89
|
+
* Coordinates follow the GeoJSON `[lon, lat]` ordering. Returns `undefined`
|
|
90
|
+
* when the route yields fewer than two drawable points, since a `LineString`
|
|
91
|
+
* requires at least two positions to be valid.
|
|
92
|
+
*
|
|
93
|
+
* ```typescript
|
|
94
|
+
* import { createFlightplanResolver, routeToLineString } from '@squawk/flightplan';
|
|
95
|
+
*
|
|
96
|
+
* const resolver = createFlightplanResolver({ airports, navaids, fixes, airways });
|
|
97
|
+
* const route = resolver.parse('KJFK DCT MERIT J60 MARTN DCT KLAX');
|
|
98
|
+
* const line = routeToLineString(route);
|
|
99
|
+
* if (line) {
|
|
100
|
+
* map.addSource('route', { type: 'geojson', data: line });
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @param route - A parsed route from {@link FlightplanResolver.parse}.
|
|
105
|
+
* @returns A GeoJSON `LineString`, or `undefined` if fewer than two points.
|
|
106
|
+
*/
|
|
107
|
+
export declare function routeToLineString(route: ParsedRoute): LineString | undefined;
|
|
108
|
+
//# sourceMappingURL=route-geometry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-geometry.d.ts","sourceRoot":"","sources":["../src/route-geometry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAM/D;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;IACZ,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;CACb;AAMD;;;;;;GAMG;AACH,MAAM,WAAW,QAAQ;IACvB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;IACZ,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,2BAA2B,CAAC,EAAE,MAAM,CAAC;CACtC;AAgBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG;IAC1D,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,kBAAkB,EAAE,YAAY,EAAE,CAAC;CACpC,CA6EA;AAMD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,UAAU,EAAE,CAGnE;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,GAAG,UAAU,GAAG,SAAS,CAS5E"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drawable geometry extraction for parsed flight plan routes. Walks the
|
|
3
|
+
* {@link ParsedRoute} elements into an ordered point sequence - expanding
|
|
4
|
+
* airway waypoints, flattening SID/STAR legs, and suppressing consecutive
|
|
5
|
+
* duplicate points - and exposes that sequence both as plain
|
|
6
|
+
* {@link RoutePoint} values and as a GeoJSON `LineString` for map rendering.
|
|
7
|
+
*
|
|
8
|
+
* The same walk feeds `computeRouteDistance` in the `route-distance` module,
|
|
9
|
+
* so distance and geometry stay in agreement.
|
|
10
|
+
*/
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Internal helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/** Epsilon for comparing coordinates to detect duplicate points. */
|
|
15
|
+
const COORD_EPSILON = 1e-9;
|
|
16
|
+
/**
|
|
17
|
+
* Returns true if two points share the same coordinates (within epsilon).
|
|
18
|
+
*/
|
|
19
|
+
function samePosition(a, b) {
|
|
20
|
+
return Math.abs(a.lat - b.lat) < COORD_EPSILON && Math.abs(a.lon - b.lon) < COORD_EPSILON;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Walks the route elements and extracts an ordered array of geographic
|
|
24
|
+
* points. Duplicate consecutive points (e.g. an airway entry fix that
|
|
25
|
+
* matches the preceding waypoint) are suppressed.
|
|
26
|
+
*
|
|
27
|
+
* Also collects all `unresolved` elements encountered during the walk.
|
|
28
|
+
*
|
|
29
|
+
* Internal to the package: `extractRoutePoints` and `routeToLineString`
|
|
30
|
+
* project this into public shapes, and `computeRouteDistance` consumes the
|
|
31
|
+
* precomputed segment distances directly.
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export function extractGeoPoints(elements) {
|
|
36
|
+
const points = [];
|
|
37
|
+
const unresolvedElements = [];
|
|
38
|
+
function emit(point) {
|
|
39
|
+
if (points.length > 0 && samePosition(points[points.length - 1], point)) {
|
|
40
|
+
// When the duplicate carries a precomputed distance that the existing
|
|
41
|
+
// point lacks, adopt it. This happens when an airway's entry fix
|
|
42
|
+
// overlaps the preceding waypoint -- the airway waypoint has the
|
|
43
|
+
// published segment distance that would otherwise be lost.
|
|
44
|
+
const last = points[points.length - 1];
|
|
45
|
+
if (point.precomputedDistanceToNextNm !== undefined &&
|
|
46
|
+
last.precomputedDistanceToNextNm === undefined) {
|
|
47
|
+
last.precomputedDistanceToNextNm = point.precomputedDistanceToNextNm;
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
points.push(point);
|
|
52
|
+
}
|
|
53
|
+
for (const el of elements) {
|
|
54
|
+
switch (el.type) {
|
|
55
|
+
case 'airport':
|
|
56
|
+
emit({ label: el.raw, lat: el.airport.lat, lon: el.airport.lon });
|
|
57
|
+
break;
|
|
58
|
+
case 'waypoint':
|
|
59
|
+
emit({ label: el.raw, lat: el.lat, lon: el.lon });
|
|
60
|
+
break;
|
|
61
|
+
case 'coordinate':
|
|
62
|
+
emit({ label: el.raw, lat: el.lat, lon: el.lon });
|
|
63
|
+
break;
|
|
64
|
+
case 'airway':
|
|
65
|
+
for (let i = 0; i < el.waypoints.length; i++) {
|
|
66
|
+
const wp = el.waypoints[i];
|
|
67
|
+
const isLast = i === el.waypoints.length - 1;
|
|
68
|
+
const point = {
|
|
69
|
+
label: wp.identifier ?? wp.name,
|
|
70
|
+
lat: wp.lat,
|
|
71
|
+
lon: wp.lon,
|
|
72
|
+
};
|
|
73
|
+
// Only carry precomputed distance for non-last waypoints (the
|
|
74
|
+
// last waypoint's distanceToNextNm points beyond this segment).
|
|
75
|
+
if (!isLast && wp.distanceToNextNm !== undefined) {
|
|
76
|
+
point.precomputedDistanceToNextNm = wp.distanceToNextNm;
|
|
77
|
+
}
|
|
78
|
+
emit(point);
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
case 'sid':
|
|
82
|
+
case 'star':
|
|
83
|
+
for (const leg of el.legs) {
|
|
84
|
+
if (leg.fixIdentifier === undefined || leg.lat === undefined || leg.lon === undefined) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
emit({ label: leg.fixIdentifier, lat: leg.lat, lon: leg.lon });
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
case 'unresolved':
|
|
91
|
+
unresolvedElements.push(el);
|
|
92
|
+
break;
|
|
93
|
+
// 'direct' and 'speedAltitude' are expected non-geographic markers
|
|
94
|
+
// and are silently skipped.
|
|
95
|
+
case 'direct':
|
|
96
|
+
case 'speedAltitude':
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { points, unresolvedElements };
|
|
101
|
+
}
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Public API
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
/**
|
|
106
|
+
* Extracts the ordered sequence of drawable geographic points from a parsed
|
|
107
|
+
* flight plan route.
|
|
108
|
+
*
|
|
109
|
+
* Expands airway and SID/STAR segments into their constituent fixes and
|
|
110
|
+
* suppresses consecutive duplicate points (e.g. an airway entry fix that
|
|
111
|
+
* matches the preceding waypoint). Elements without coordinates (DCT,
|
|
112
|
+
* speed/altitude groups, unresolved tokens) contribute no points.
|
|
113
|
+
*
|
|
114
|
+
* This is the same walk that backs `computeRouteDistance`, so the point
|
|
115
|
+
* sequence here lines up with that result's legs.
|
|
116
|
+
*
|
|
117
|
+
* ```typescript
|
|
118
|
+
* import { createFlightplanResolver, extractRoutePoints } from '@squawk/flightplan';
|
|
119
|
+
*
|
|
120
|
+
* const resolver = createFlightplanResolver({ airports, navaids, fixes, airways });
|
|
121
|
+
* const route = resolver.parse('KJFK DCT MERIT J60 MARTN DCT KLAX');
|
|
122
|
+
* const points = extractRoutePoints(route);
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* @param route - A parsed route from {@link FlightplanResolver.parse}.
|
|
126
|
+
* @returns Ordered drawable points along the route.
|
|
127
|
+
*/
|
|
128
|
+
export function extractRoutePoints(route) {
|
|
129
|
+
const { points } = extractGeoPoints(route.elements);
|
|
130
|
+
return points.map((p) => ({ label: p.label, lat: p.lat, lon: p.lon }));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Builds a GeoJSON `LineString` from a parsed flight plan route, ready to
|
|
134
|
+
* render as a polyline on a map (e.g. MapLibre, Leaflet).
|
|
135
|
+
*
|
|
136
|
+
* Coordinates follow the GeoJSON `[lon, lat]` ordering. Returns `undefined`
|
|
137
|
+
* when the route yields fewer than two drawable points, since a `LineString`
|
|
138
|
+
* requires at least two positions to be valid.
|
|
139
|
+
*
|
|
140
|
+
* ```typescript
|
|
141
|
+
* import { createFlightplanResolver, routeToLineString } from '@squawk/flightplan';
|
|
142
|
+
*
|
|
143
|
+
* const resolver = createFlightplanResolver({ airports, navaids, fixes, airways });
|
|
144
|
+
* const route = resolver.parse('KJFK DCT MERIT J60 MARTN DCT KLAX');
|
|
145
|
+
* const line = routeToLineString(route);
|
|
146
|
+
* if (line) {
|
|
147
|
+
* map.addSource('route', { type: 'geojson', data: line });
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* @param route - A parsed route from {@link FlightplanResolver.parse}.
|
|
152
|
+
* @returns A GeoJSON `LineString`, or `undefined` if fewer than two points.
|
|
153
|
+
*/
|
|
154
|
+
export function routeToLineString(route) {
|
|
155
|
+
const points = extractRoutePoints(route);
|
|
156
|
+
if (points.length < 2) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
type: 'LineString',
|
|
161
|
+
coordinates: points.map((p) => [p.lon, p.lat]),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wind-corrected route timing for parsed flight plan routes. Layers per-leg
|
|
3
|
+
* wind triangle solutions over the great-circle legs produced by the
|
|
4
|
+
* `route-distance` module: each leg's true course drives a heading, wind
|
|
5
|
+
* correction angle, and ground speed, which in turn yield estimated time
|
|
6
|
+
* enroute and optional fuel burn.
|
|
7
|
+
*
|
|
8
|
+
* Winds are supplied by the caller through a {@link WindProvider}, keeping this
|
|
9
|
+
* module independent of any particular weather source. A `@squawk/weather`
|
|
10
|
+
* winds-aloft forecast sampled at a chosen cruise altitude is one way to
|
|
11
|
+
* satisfy the provider.
|
|
12
|
+
*/
|
|
13
|
+
import type { WindVector } from '@squawk/flight-math';
|
|
14
|
+
import type { ParsedRoute, RouteElement } from './resolver.js';
|
|
15
|
+
/**
|
|
16
|
+
* Resolves the wind at a geographic point for route timing. Returns the wind
|
|
17
|
+
* vector to apply, or `undefined` when no wind data is available (or the wind
|
|
18
|
+
* is light and variable), in which case the leg is timed as if calm.
|
|
19
|
+
*
|
|
20
|
+
* The provider takes only a position; bake the sampling altitude into the
|
|
21
|
+
* closure so this module stays altitude-agnostic.
|
|
22
|
+
*
|
|
23
|
+
* @param lat - Latitude of the sample point in decimal degrees, positive north.
|
|
24
|
+
* @param lon - Longitude of the sample point in decimal degrees, positive east.
|
|
25
|
+
* @returns The wind vector to apply at the point, or `undefined` for calm.
|
|
26
|
+
*/
|
|
27
|
+
export type WindProvider = (lat: number, lon: number) => WindVector | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Options controlling a {@link computeRouteTiming} computation.
|
|
30
|
+
*/
|
|
31
|
+
export interface RouteTimingOptions {
|
|
32
|
+
/** True airspeed in knots flown on every leg. */
|
|
33
|
+
trueAirspeedKt: number;
|
|
34
|
+
/**
|
|
35
|
+
* Supplies the wind at each leg's midpoint. Omit to time every leg as calm
|
|
36
|
+
* (ground speed equals true airspeed).
|
|
37
|
+
*/
|
|
38
|
+
windProvider?: WindProvider;
|
|
39
|
+
/**
|
|
40
|
+
* Fuel burn rate per hour (any consistent unit). When provided, per-leg and
|
|
41
|
+
* total fuel are computed; the result fuel fields stay `undefined` otherwise.
|
|
42
|
+
*/
|
|
43
|
+
fuelBurnPerHr?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Fuel on board (same unit as `fuelBurnPerHr`). When provided together with
|
|
46
|
+
* `fuelBurnPerHr`, the result reports endurance and whether fuel is
|
|
47
|
+
* sufficient for the total estimated time enroute.
|
|
48
|
+
*/
|
|
49
|
+
fuelAvailable?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A single route leg with its wind-corrected timing solution.
|
|
53
|
+
*/
|
|
54
|
+
export interface RouteTimingLeg {
|
|
55
|
+
/** Identifier or raw token of the starting point. */
|
|
56
|
+
from: string;
|
|
57
|
+
/** Identifier or raw token of the ending point. */
|
|
58
|
+
to: string;
|
|
59
|
+
/** Latitude of the starting point in decimal degrees, positive north. */
|
|
60
|
+
fromLat: number;
|
|
61
|
+
/** Longitude of the starting point in decimal degrees, positive east. */
|
|
62
|
+
fromLon: number;
|
|
63
|
+
/** Latitude of the ending point in decimal degrees, positive north. */
|
|
64
|
+
toLat: number;
|
|
65
|
+
/** Longitude of the ending point in decimal degrees, positive east. */
|
|
66
|
+
toLon: number;
|
|
67
|
+
/** Great-circle distance of this leg in nautical miles. */
|
|
68
|
+
distanceNm: number;
|
|
69
|
+
/** Cumulative distance from the route start through the end of this leg in nautical miles. */
|
|
70
|
+
cumulativeDistanceNm: number;
|
|
71
|
+
/** Initial great-circle true course for the leg in degrees (0-360). */
|
|
72
|
+
trueCourseDeg: number;
|
|
73
|
+
/** True heading to fly to hold the course given the wind, in degrees (0-360). */
|
|
74
|
+
trueHeadingDeg: number;
|
|
75
|
+
/** Wind correction angle in degrees. Positive means crab right, negative means crab left. */
|
|
76
|
+
windCorrectionAngleDeg: number;
|
|
77
|
+
/** Ground speed in knots after wind correction. */
|
|
78
|
+
groundSpeedKt: number;
|
|
79
|
+
/** Wind applied to the leg, or `undefined` when the leg was timed as calm. */
|
|
80
|
+
wind: WindVector | undefined;
|
|
81
|
+
/** Estimated time enroute for the leg in hours, or `undefined` if ground speed is not positive. */
|
|
82
|
+
eteHrs: number | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Cumulative estimated time enroute from the route start through this leg in
|
|
85
|
+
* hours. `undefined` once any preceding leg could not be timed.
|
|
86
|
+
*/
|
|
87
|
+
cumulativeEteHrs: number | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Fuel required for the leg (same unit as `fuelBurnPerHr`), or `undefined`
|
|
90
|
+
* when no burn rate was given or the leg could not be timed.
|
|
91
|
+
*/
|
|
92
|
+
fuelRequired: number | undefined;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Result of computing wind-corrected timing across a parsed flight plan route.
|
|
96
|
+
*/
|
|
97
|
+
export interface RouteTimingResult {
|
|
98
|
+
/** Ordered legs with their wind-corrected timing. */
|
|
99
|
+
legs: RouteTimingLeg[];
|
|
100
|
+
/** Total great-circle route distance in nautical miles. */
|
|
101
|
+
totalDistanceNm: number;
|
|
102
|
+
/**
|
|
103
|
+
* Total estimated time enroute in hours, or `undefined` if any leg could not
|
|
104
|
+
* be timed (ground speed not positive).
|
|
105
|
+
*/
|
|
106
|
+
totalEteHrs: number | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* Total fuel required across all legs (same unit as `fuelBurnPerHr`), or
|
|
109
|
+
* `undefined` when no burn rate was given or any leg could not be timed.
|
|
110
|
+
*/
|
|
111
|
+
totalFuelRequired: number | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* Endurance in hours from `fuelAvailable` and `fuelBurnPerHr`, or `undefined`
|
|
114
|
+
* when either input was omitted.
|
|
115
|
+
*/
|
|
116
|
+
enduranceHrs: number | undefined;
|
|
117
|
+
/**
|
|
118
|
+
* Whether endurance covers the total estimated time enroute, or `undefined`
|
|
119
|
+
* when endurance or total ETE could not be computed.
|
|
120
|
+
*/
|
|
121
|
+
fuelSufficient: boolean | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* Route elements of type `unresolved` that could not contribute coordinates,
|
|
124
|
+
* passed through from the distance computation.
|
|
125
|
+
*/
|
|
126
|
+
unresolvedElements: RouteElement[];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Computes wind-corrected per-leg timing for a parsed flight plan route.
|
|
130
|
+
*
|
|
131
|
+
* Reuses {@link computeRouteDistance} for the leg geometry, then for each leg
|
|
132
|
+
* derives the initial true course, samples the wind at the leg midpoint via the
|
|
133
|
+
* optional {@link WindProvider}, and solves the wind triangle for heading, wind
|
|
134
|
+
* correction angle, and ground speed. Estimated time enroute is leg distance
|
|
135
|
+
* divided by ground speed, and fuel (when a burn rate is given) follows from
|
|
136
|
+
* the same ground speed.
|
|
137
|
+
*
|
|
138
|
+
* When the provider returns `undefined` for a leg, that leg is timed as calm:
|
|
139
|
+
* heading equals course, wind correction angle is zero, and ground speed equals
|
|
140
|
+
* true airspeed.
|
|
141
|
+
*
|
|
142
|
+
* Ground speed comes from {@link solveWindTriangle} as a magnitude, so it is
|
|
143
|
+
* never negative. A leg is left untimed (`eteHrs` `undefined`) only when ground
|
|
144
|
+
* speed is not positive, which occurs at the boundary where a pure headwind
|
|
145
|
+
* equals true airspeed. A headwind exceeding true airspeed still yields a
|
|
146
|
+
* positive magnitude and inherits the clamping behavior documented on
|
|
147
|
+
* {@link solveWindTriangle}; such legs are not separately flagged here. A single
|
|
148
|
+
* untimed leg makes `totalEteHrs` (and `totalFuelRequired`, when fuel is
|
|
149
|
+
* requested) `undefined`.
|
|
150
|
+
*
|
|
151
|
+
* ```typescript
|
|
152
|
+
* import { createFlightplanResolver, computeRouteTiming } from '@squawk/flightplan';
|
|
153
|
+
*
|
|
154
|
+
* const resolver = createFlightplanResolver({ airports, navaids, fixes, airways });
|
|
155
|
+
* const route = resolver.parse('KJFK DCT MERIT J60 MARTN DCT KLAX');
|
|
156
|
+
* const result = computeRouteTiming(route, {
|
|
157
|
+
* trueAirspeedKt: 450,
|
|
158
|
+
* windProvider: (lat, lon) => ({ directionDeg: 270, speedKt: 80 }),
|
|
159
|
+
* fuelBurnPerHr: 600,
|
|
160
|
+
* });
|
|
161
|
+
* console.log(result.totalEteHrs, result.totalFuelRequired);
|
|
162
|
+
* ```
|
|
163
|
+
*
|
|
164
|
+
* @param route - A parsed route from {@link FlightplanResolver.parse}.
|
|
165
|
+
* @param options - True airspeed, optional wind provider, and optional fuel inputs.
|
|
166
|
+
* @returns Per-leg wind-corrected timing with route totals.
|
|
167
|
+
*/
|
|
168
|
+
export declare function computeRouteTiming(route: ParsedRoute, options: RouteTimingOptions): RouteTimingResult;
|
|
169
|
+
//# sourceMappingURL=route-timing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-timing.d.ts","sourceRoot":"","sources":["../src/route-timing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGtD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAO/D;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAC;AAEhF;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,cAAc,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,EAAE,EAAE,MAAM,CAAC;IACX,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,8FAA8F;IAC9F,oBAAoB,EAAE,MAAM,CAAC;IAC7B,uEAAuE;IACvE,aAAa,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,cAAc,EAAE,MAAM,CAAC;IACvB,6FAA6F;IAC7F,sBAAsB,EAAE,MAAM,CAAC;IAC/B,mDAAmD;IACnD,aAAa,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,IAAI,EAAE,UAAU,GAAG,SAAS,CAAC;IAC7B,mGAAmG;IACnG,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B;;;OAGG;IACH,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC;;;OAGG;IACH,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,qDAAqD;IACrD,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,2DAA2D;IAC3D,eAAe,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC;;;OAGG;IACH,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC;;;OAGG;IACH,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;OAGG;IACH,cAAc,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC;;;OAGG;IACH,kBAAkB,EAAE,YAAY,EAAE,CAAC;CACpC;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,kBAAkB,GAC1B,iBAAiB,CAwFnB"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wind-corrected route timing for parsed flight plan routes. Layers per-leg
|
|
3
|
+
* wind triangle solutions over the great-circle legs produced by the
|
|
4
|
+
* `route-distance` module: each leg's true course drives a heading, wind
|
|
5
|
+
* correction angle, and ground speed, which in turn yield estimated time
|
|
6
|
+
* enroute and optional fuel burn.
|
|
7
|
+
*
|
|
8
|
+
* Winds are supplied by the caller through a {@link WindProvider}, keeping this
|
|
9
|
+
* module independent of any particular weather source. A `@squawk/weather`
|
|
10
|
+
* winds-aloft forecast sampled at a chosen cruise altitude is one way to
|
|
11
|
+
* satisfy the provider.
|
|
12
|
+
*/
|
|
13
|
+
import { planning, wind } from '@squawk/flight-math';
|
|
14
|
+
import { greatCircle } from '@squawk/geo';
|
|
15
|
+
import { computeRouteDistance } from './route-distance.js';
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Public API
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* Computes wind-corrected per-leg timing for a parsed flight plan route.
|
|
21
|
+
*
|
|
22
|
+
* Reuses {@link computeRouteDistance} for the leg geometry, then for each leg
|
|
23
|
+
* derives the initial true course, samples the wind at the leg midpoint via the
|
|
24
|
+
* optional {@link WindProvider}, and solves the wind triangle for heading, wind
|
|
25
|
+
* correction angle, and ground speed. Estimated time enroute is leg distance
|
|
26
|
+
* divided by ground speed, and fuel (when a burn rate is given) follows from
|
|
27
|
+
* the same ground speed.
|
|
28
|
+
*
|
|
29
|
+
* When the provider returns `undefined` for a leg, that leg is timed as calm:
|
|
30
|
+
* heading equals course, wind correction angle is zero, and ground speed equals
|
|
31
|
+
* true airspeed.
|
|
32
|
+
*
|
|
33
|
+
* Ground speed comes from {@link solveWindTriangle} as a magnitude, so it is
|
|
34
|
+
* never negative. A leg is left untimed (`eteHrs` `undefined`) only when ground
|
|
35
|
+
* speed is not positive, which occurs at the boundary where a pure headwind
|
|
36
|
+
* equals true airspeed. A headwind exceeding true airspeed still yields a
|
|
37
|
+
* positive magnitude and inherits the clamping behavior documented on
|
|
38
|
+
* {@link solveWindTriangle}; such legs are not separately flagged here. A single
|
|
39
|
+
* untimed leg makes `totalEteHrs` (and `totalFuelRequired`, when fuel is
|
|
40
|
+
* requested) `undefined`.
|
|
41
|
+
*
|
|
42
|
+
* ```typescript
|
|
43
|
+
* import { createFlightplanResolver, computeRouteTiming } from '@squawk/flightplan';
|
|
44
|
+
*
|
|
45
|
+
* const resolver = createFlightplanResolver({ airports, navaids, fixes, airways });
|
|
46
|
+
* const route = resolver.parse('KJFK DCT MERIT J60 MARTN DCT KLAX');
|
|
47
|
+
* const result = computeRouteTiming(route, {
|
|
48
|
+
* trueAirspeedKt: 450,
|
|
49
|
+
* windProvider: (lat, lon) => ({ directionDeg: 270, speedKt: 80 }),
|
|
50
|
+
* fuelBurnPerHr: 600,
|
|
51
|
+
* });
|
|
52
|
+
* console.log(result.totalEteHrs, result.totalFuelRequired);
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @param route - A parsed route from {@link FlightplanResolver.parse}.
|
|
56
|
+
* @param options - True airspeed, optional wind provider, and optional fuel inputs.
|
|
57
|
+
* @returns Per-leg wind-corrected timing with route totals.
|
|
58
|
+
*/
|
|
59
|
+
export function computeRouteTiming(route, options) {
|
|
60
|
+
const { trueAirspeedKt, windProvider, fuelBurnPerHr, fuelAvailable } = options;
|
|
61
|
+
const { legs: distanceLegs, totalDistanceNm, unresolvedElements } = computeRouteDistance(route);
|
|
62
|
+
const legs = [];
|
|
63
|
+
let cumulativeEteHrs = 0;
|
|
64
|
+
let totalFuelRequired = fuelBurnPerHr !== undefined ? 0 : undefined;
|
|
65
|
+
for (const leg of distanceLegs) {
|
|
66
|
+
const trueCourseDeg = greatCircle.bearing(leg.fromLat, leg.fromLon, leg.toLat, leg.toLon);
|
|
67
|
+
const mid = greatCircle.midpoint(leg.fromLat, leg.fromLon, leg.toLat, leg.toLon);
|
|
68
|
+
const legWind = windProvider?.(mid.lat, mid.lon);
|
|
69
|
+
const triangle = legWind !== undefined
|
|
70
|
+
? wind.solveWindTriangle(trueAirspeedKt, trueCourseDeg, legWind.directionDeg, legWind.speedKt)
|
|
71
|
+
: {
|
|
72
|
+
trueHeadingDeg: trueCourseDeg,
|
|
73
|
+
windCorrectionAngleDeg: 0,
|
|
74
|
+
groundSpeedKt: trueAirspeedKt,
|
|
75
|
+
};
|
|
76
|
+
const eteHrs = triangle.groundSpeedKt > 0 ? leg.distanceNm / triangle.groundSpeedKt : undefined;
|
|
77
|
+
if (eteHrs === undefined || cumulativeEteHrs === undefined) {
|
|
78
|
+
cumulativeEteHrs = undefined;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
cumulativeEteHrs += eteHrs;
|
|
82
|
+
}
|
|
83
|
+
const fuelRequired = fuelBurnPerHr !== undefined && eteHrs !== undefined
|
|
84
|
+
? planning.fuelRequired(leg.distanceNm, triangle.groundSpeedKt, fuelBurnPerHr)
|
|
85
|
+
: undefined;
|
|
86
|
+
if (fuelBurnPerHr !== undefined) {
|
|
87
|
+
if (fuelRequired === undefined || totalFuelRequired === undefined) {
|
|
88
|
+
totalFuelRequired = undefined;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
totalFuelRequired += fuelRequired;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
legs.push({
|
|
95
|
+
from: leg.from,
|
|
96
|
+
to: leg.to,
|
|
97
|
+
fromLat: leg.fromLat,
|
|
98
|
+
fromLon: leg.fromLon,
|
|
99
|
+
toLat: leg.toLat,
|
|
100
|
+
toLon: leg.toLon,
|
|
101
|
+
distanceNm: leg.distanceNm,
|
|
102
|
+
cumulativeDistanceNm: leg.cumulativeDistanceNm,
|
|
103
|
+
trueCourseDeg,
|
|
104
|
+
trueHeadingDeg: triangle.trueHeadingDeg,
|
|
105
|
+
windCorrectionAngleDeg: triangle.windCorrectionAngleDeg,
|
|
106
|
+
groundSpeedKt: triangle.groundSpeedKt,
|
|
107
|
+
wind: legWind,
|
|
108
|
+
eteHrs,
|
|
109
|
+
cumulativeEteHrs,
|
|
110
|
+
fuelRequired,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const totalEteHrs = legs.length > 0 ? legs[legs.length - 1].cumulativeEteHrs : 0;
|
|
114
|
+
const enduranceHrs = fuelAvailable !== undefined && fuelBurnPerHr !== undefined
|
|
115
|
+
? planning.endurance(fuelAvailable, fuelBurnPerHr)
|
|
116
|
+
: undefined;
|
|
117
|
+
const fuelSufficient = enduranceHrs !== undefined && totalEteHrs !== undefined
|
|
118
|
+
? enduranceHrs >= totalEteHrs
|
|
119
|
+
: undefined;
|
|
120
|
+
return {
|
|
121
|
+
legs,
|
|
122
|
+
totalDistanceNm,
|
|
123
|
+
totalEteHrs,
|
|
124
|
+
totalFuelRequired,
|
|
125
|
+
enduranceHrs,
|
|
126
|
+
fuelSufficient,
|
|
127
|
+
unresolvedElements,
|
|
128
|
+
};
|
|
129
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@squawk/flightplan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Flight plan route string parsing and resolution using composed navigation resolvers",
|
|
6
6
|
"author": "Neil Cochran",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"dist",
|
|
32
|
-
"!dist/**/*.spec.*"
|
|
32
|
+
"!dist/**/*.spec.*",
|
|
33
|
+
"!dist/test-utils.*"
|
|
33
34
|
],
|
|
34
35
|
"scripts": {
|
|
35
36
|
"build": "tsc",
|
|
@@ -41,20 +42,22 @@
|
|
|
41
42
|
"api:report": "api-extractor run --local --verbose"
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
45
|
+
"@squawk/flight-math": "^0.5.9",
|
|
44
46
|
"@squawk/geo": "^0.4.3",
|
|
45
|
-
"@squawk/types": "^0.8.0"
|
|
47
|
+
"@squawk/types": "^0.8.0",
|
|
48
|
+
"@types/geojson": "^7946.0.16"
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
48
51
|
"@squawk/airport-data": "^0.7.3",
|
|
49
|
-
"@squawk/airports": "^0.
|
|
52
|
+
"@squawk/airports": "^0.7.0",
|
|
50
53
|
"@squawk/airway-data": "^0.5.3",
|
|
51
|
-
"@squawk/airways": "^0.
|
|
54
|
+
"@squawk/airways": "^0.5.0",
|
|
52
55
|
"@squawk/fix-data": "^0.6.3",
|
|
53
|
-
"@squawk/fixes": "^0.
|
|
56
|
+
"@squawk/fixes": "^0.5.0",
|
|
54
57
|
"@squawk/navaid-data": "^0.6.3",
|
|
55
|
-
"@squawk/navaids": "^0.
|
|
58
|
+
"@squawk/navaids": "^0.6.0",
|
|
56
59
|
"@squawk/procedure-data": "^0.7.2",
|
|
57
|
-
"@squawk/procedures": "^0.
|
|
60
|
+
"@squawk/procedures": "^0.7.0",
|
|
58
61
|
"@types/node": "^25.9.3"
|
|
59
62
|
},
|
|
60
63
|
"keywords": [
|