flight-planner
Version:
Plan and route VFR flights
325 lines (324 loc) • 12.8 kB
JavaScript
import { WaypointVariant } from './waypoint.types.js';
import { isICAO } from './utils.js';
/**
* Abstract base class for service implementations that handle entity retrieval.
*
* @template T - The type of entity this service manages
*
* @remarks
* This class provides a standard interface for services that need to find entities
* by ICAO codes or geographic location. All concrete implementations must provide
* their own implementations of the abstract methods.
*
* Users of this library must implement this class to provide data fetching logic
* for their specific data sources (database, API, cache, etc.).
*
* @example
* ```typescript
* class MyAerodromeService extends ServiceBase<Aerodrome> {
* async findByICAO(icao: readonly ICAO[]): Promise<Aerodrome[]> {
* return await database.getAerodromesByICAO(icao);
* }
*
* async findByLocation(location: GeoJSON.Position, radius: number): Promise<Aerodrome[]> {
* return await database.getAerodromesNearLocation(location, radius);
* }
* }
* ```
*/
export class ServiceBase {
}
/**
* Default ICAO waypoint resolver that looks up aerodromes by ICAO code.
*
* @class ICAOResolver
* @implements {WaypointResolver}
*/
class ICAOResolver {
aerodromeService;
constructor(aerodromeService) {
this.aerodromeService = aerodromeService;
}
/**
* Resolves a route part if it matches a valid ICAO code pattern (4 letters).
*
* @param part - The route string part to resolve
* @returns The aerodrome if found, null if part is not a valid ICAO code
* @throws {Error} If the part is a valid ICAO code but no aerodrome is found
*/
async resolve(part) {
if (isICAO(part)) {
const airports = await this.aerodromeService.findByICAO([part]);
if (airports && airports.length > 0) {
return airports[0];
}
throw new Error(`Could not find aerodrome with ICAO code: ${part}`);
}
return null;
}
}
/**
* Default coordinate waypoint resolver that parses WP(lat,lng) format.
*
* @class CoordinateResolver
* @implements {WaypointResolver}
*/
class CoordinateResolver {
waypointRegex = /^WP\((-?\d+\.?\d*),(-?\d+\.?\d*)\)$/;
/**
* Resolves a route part if it matches the WP(lat,lng) coordinate format.
*
* @param part - The route string part to resolve
* @returns A waypoint with the parsed coordinates, or null if the part doesn't match the coordinate format
* @throws {Error} If the part matches the format but contains invalid numeric values
*/
async resolve(part) {
const waypointMatch = part.match(this.waypointRegex);
if (waypointMatch) {
const lat = parseFloat(waypointMatch[1]);
const lng = parseFloat(waypointMatch[2]);
if (isNaN(lat) || isNaN(lng)) {
throw new Error(`Invalid coordinates in waypoint: ${part}`);
}
const name = `WP-${lat.toFixed(2)},${lng.toFixed(2)}`;
return { name, coords: [lng, lat], waypointVariant: WaypointVariant.Waypoint };
}
return null;
}
}
/**
* Navaid resolver that looks up navigation aids (VOR, NDB, FIX, intersection)
* by their identifier. Handles alphanumeric identifiers that aren't 4-letter
* ICAO airport codes (those are handled by ICAOResolver).
*
* @class NavaidResolver
* @implements {WaypointResolver}
*/
class NavaidResolver {
navaidService;
identifierRegex = /^[A-Z0-9]{2,5}$/;
constructor(navaidService) {
this.navaidService = navaidService;
}
/**
* Resolves a route part if it matches a navaid identifier pattern.
* Skips 4-letter codes (deferred to ICAOResolver) and coordinate format.
*
* @param part - The route string part to resolve
* @returns The navaid if found, null if not a valid identifier pattern
* @throws {Error} If the identifier matches but no navaid is found
*/
async resolve(part) {
const normalized = part.toUpperCase();
if (!this.identifierRegex.test(normalized) || isICAO(normalized)) {
return null;
}
const navaids = await this.navaidService.findByICAO([normalized]);
if (navaids && navaids.length > 0) {
return navaids[0];
}
throw new Error(`Could not find navaid with identifier: ${part}`);
}
}
/**
* PlannerService class provides methods to parse and resolve flight route strings into waypoints.
* Uses a configurable chain of resolvers to support various waypoint formats.
*
* @class PlannerService
*
* @remarks
* **Typical Usage**: Use the `createDefaultPlannerService()` factory function to get a pre-configured
* instance with ICAO and coordinate resolvers.
*
* **Advanced Usage**: Use the constructor directly when you need full control over the resolver chain,
* such as when you want to exclude default resolvers or change their order.
*
* @example
* ```typescript
* // Recommended: Use factory function for standard use cases
* const planner = createDefaultPlannerService(aerodromeService);
*
* // Advanced: Direct construction for custom resolver chains
* const planner = new PlannerService([
* new MyCustomResolver(),
* new ICAOResolver(aerodromeService),
* ]);
* ```
*/
export class PlannerService {
resolvers;
/**
* Creates a new instance of the PlannerService class.
*
* @param resolvers - Array of waypoint resolvers that will be tried in sequence to resolve route parts
*
* @remarks
* Resolvers are tried in the order they appear in the array. The first resolver to return a
* non-null/non-undefined value wins. Consider using `createDefaultPlannerService()` instead
* for typical use cases.
*/
constructor(resolvers = []) {
this.resolvers = resolvers;
}
/**
* Adds a custom waypoint resolver to the resolver chain.
* The resolver will be added at the end of the chain and will be tried after existing resolvers.
*
* @param resolver - The waypoint resolver to add
* @returns void
*/
addResolver(resolver) {
this.resolvers.push(resolver);
}
/**
* Clears all waypoint resolvers from the resolver chain.
*
* @remarks
* WARNING: This removes ALL resolvers including the default ICAO and Coordinate resolvers.
* After calling this method, route parsing will fail unless new resolvers are added.
*
* @returns void
*/
clearResolvers() {
this.resolvers = [];
}
/**
* Parses a route string into an array of waypoints.
*
* This method accepts a route string containing various waypoint formats and converts them
* into standardized waypoint objects using a chain of resolvers.
*
* @param routeString - The route string to parse, containing waypoints separated by spaces, semicolons, or newlines
* @returns A promise that resolves to an array of successfully parsed waypoints
*
* @remarks
* Supported waypoint formats depend on the configured resolvers. The default factory function
* (`createDefaultPlannerService`) provides support for:
* - ICAO codes: 4-letter airport identifiers (e.g., "KJFK", "EGLL")
* - Coordinate waypoints: WP(latitude,longitude) format (e.g., "WP(40.7128,-74.0060)")
*
* Custom resolvers can be added to support additional formats such as:
* - IATA codes (e.g., "JFK", "LHR")
* - VOR/NDB navaids (e.g., "VOR123", "NDB456")
* - IFR waypoints (e.g., "BOSOX", "CRISY")
* - VFR reporting points (e.g., "VRP_XX")
*
* Processing steps:
* 1. Returns empty array if routeString is empty or falsy
* 2. Converts input to uppercase for consistency
* 3. Splits the route string by whitespace, semicolons, and newlines
* 4. Filters out empty parts
* 5. For each part, attempts to resolve using the chain of resolvers in order
* 6. Silently skips parts that cannot be resolved by any resolver
*
* **Error Handling**:
* - Parts that don't match any resolver pattern are **silently skipped**
* - Parts that match a resolver's pattern but fail to resolve (e.g., valid ICAO code but
* aerodrome not found in database) **will throw an error**
* - This means the returned array may have fewer waypoints than parts in the input string
*
* For example:
* - "KJFK UNKNOWN EGLL" where UNKNOWN is not a valid ICAO → returns 2 waypoints (KJFK, EGLL)
* - "KJFK XXXX EGLL" where XXXX is a valid ICAO pattern but not found → throws error
*
* @throws {Error} When a resolver matches a pattern but fails to find the entity
*
* @example
* ```typescript
* const waypoints = await planner.parseRouteString("KJFK EGLL WP(51.5,-0.1)");
* // Returns array with resolved waypoints, skipping any unresolvable parts
* ```
*/
async parseRouteString(routeString) {
if (!routeString)
return [];
const routeParts = routeString.toUpperCase().split(/[;\s\n]+/).filter(part => part.length > 0);
return await this.resolveRouteParts(routeParts);
}
/**
* Attempts to resolve multiple route parts into waypoints using the chain of resolvers.
*
* @param parts - An array of route string parts to resolve
* @returns A promise that resolves to an array of successfully resolved waypoints
*
* @remarks
* For each part, resolvers are tried in order until one successfully resolves it.
* Parts that cannot be resolved by any resolver are silently skipped.
* If a resolver throws an error for a matched pattern, that error will propagate and stop processing.
*/
async resolveRouteParts(parts) {
const waypoints = [];
for (const part of parts) {
for (const resolver of this.resolvers) {
const waypoint = await resolver.resolve(part);
if (waypoint) {
waypoints.push(waypoint);
break;
}
}
}
return waypoints;
}
/**
* Attempts to resolve a single route part into a waypoint using the chain of resolvers.
*
* @param part - The route string part to resolve
* @returns A promise that resolves to a WaypointType if any resolver successfully handles it, or null/undefined if no resolver can handle it
*
* @remarks
* Resolvers are tried in the order they exist in the resolvers array.
* Returns the result from the first resolver that returns a non-null/non-undefined value.
* If a resolver throws an error, that error will propagate.
*/
async resolveRoutePart(part) {
for (const resolver of this.resolvers) {
const waypoint = await resolver.resolve(part);
if (waypoint) {
return waypoint;
}
}
return null;
}
}
/**
* Creates a PlannerService instance with default waypoint resolvers.
*
* This factory function initializes a PlannerService with a standard resolver chain that includes:
* 1. ICAOResolver - for resolving 4-letter ICAO airport codes (tried first)
* 2. CoordinateResolver - for resolving WP(lat,lng) coordinate waypoints
* 3. Any custom resolvers provided (tried last)
*
* @param aerodromeService - The aerodrome service for looking up airports and aerodromes
* @param customResolvers - Optional array of custom waypoint resolvers that will be tried after the default resolvers
* @returns A configured PlannerService instance with the resolver chain
*
* @remarks
* Custom resolvers are placed at the end of the resolver chain, allowing you to extend
* the default behavior with additional formats. For example, you could add resolvers for
* IATA codes, VOR/NDB navaids, or IFR waypoints that would handle formats not recognized
* by the default resolvers.
*
* @example
* ```typescript
* const planner = createDefaultPlannerService(aerodromeService);
* // Creates a planner with ICAO and coordinate resolvers
* ```
*
* @example
* ```typescript
* const planner = createDefaultPlannerService(
* aerodromeService,
* [new IATAResolver(), new VORResolver()]
* );
* // Creates a planner that tries ICAO first, then coordinates, then custom resolvers
* ```
*/
export function createDefaultPlannerService(aerodromeService, options = {}) {
const { navaidService, customResolvers = [] } = options;
return new PlannerService([
new ICAOResolver(aerodromeService),
new CoordinateResolver(),
...(navaidService ? [new NavaidResolver(navaidService)] : []),
...customResolvers
]);
}