flight-planner
Version:
Plan and route VFR flights
136 lines (135 loc) • 5.54 kB
JavaScript
import { isICAO, normalizeICAO } from "./utils.js";
import { point, nearestPoint } from "@turf/turf";
import { featureCollection } from '@turf/helpers';
/**
* AerodromeService class provides methods to manage and retrieve aerodrome data.
*
* @class AerodromeService
* @property {Map<ICAO, Aerodrome>} aerodromes - A map of ICAO codes to Aerodrome objects.
* @property {RepositoryBase<Aerodrome>} [repository] - Optional repository for fetching aerodrome data.
* @property {WeatherService} [weatherService] - Optional weather service for fetching METAR data.
*/
export class AerodromeService {
aerodromes;
repository;
weatherService;
/**
* Creates a new instance of the AerodromeService class.
*
* @param options - An object containing optional properties for initializing the service.
* @returns An instance of the AerodromeService class.
*/
constructor(options = {}) {
this.repository = options.repository;
this.weatherService = options.weatherService;
this.aerodromes = options.aerodromes
? new Map(options.aerodromes.map(aerodrome => [normalizeICAO(aerodrome.ICAO), aerodrome]))
: new Map();
}
/**
* Returns an array of ICAO codes for the aerodromes.
*
* @returns An array of ICAO codes.
*/
keys() {
return Array.from(this.aerodromes.keys());
}
/**
* Returns an array of aerodromes.
*
* @returns An array of Aerodrome objects.
*/
values() {
return Array.from(this.aerodromes.values());
}
/**
* Adds aerodromes to the service.
*
* @param aerodromes - An array of Aerodrome objects or a single Aerodrome object to add.
*/
async add(aerodromes) {
let aerodromeArray = [];
if (Array.isArray(aerodromes)) {
aerodromeArray = aerodromes;
}
else {
aerodromeArray = [aerodromes];
}
const aerodromeWithoutMetar = [];
aerodromeArray.forEach(aerodrome => {
const normalizedICAO = normalizeICAO(aerodrome.ICAO);
if (isICAO(normalizedICAO)) {
this.aerodromes.set(normalizedICAO, aerodrome);
// TODO: calculate geohash
if (!aerodrome.metarStation) {
aerodromeWithoutMetar.push(aerodrome);
}
}
});
if (this.weatherService && aerodromeWithoutMetar.length > 0) {
for (const aerodrome of aerodromeWithoutMetar) {
if (aerodrome.location && aerodrome.location.geometry && aerodrome.location.geometry.coordinates) {
const nearestMetar = await this.weatherService.nearest(aerodrome.location.geometry.coordinates);
if (nearestMetar) {
aerodrome.metarStation = nearestMetar;
this.aerodromes.set(normalizeICAO(aerodrome.ICAO), aerodrome);
}
}
}
}
}
// TODO: Check if isICAO
/**
* Finds an aerodrome by its ICAO code.
*
* @param icao - The ICAO code of the aerodrome.
* @returns A promise that resolves to the aerodrome, or undefined if not found.
*/
async get(icao) {
const normalizedIcao = normalizeICAO(icao);
const aerodrome = this.aerodromes.get(normalizedIcao);
if (aerodrome) {
if (this.weatherService && aerodrome.metarStation) {
const metar = await this.weatherService.get(aerodrome.metarStation.station);
if (metar && metar.length > 0) {
aerodrome.metarStation = metar[0];
}
}
return aerodrome;
}
else if (this.repository) {
const result = await this.repository.fetchByICAO([normalizedIcao]);
await this.add(result);
}
return this.aerodromes.get(normalizedIcao);
}
/**
* Finds the nearest aerodrome to the given location.
*
* @param location - The geographical location to find the nearest aerodrome to.
* @param radius - The search radius in kilometers (default is 100 km).
* @param exclude - An optional array of ICAO codes to exclude from the search.
* @returns A promise that resolves to the nearest aerodrome, or undefined if not found.
* @throws Error if no aerodromes are available and the repository doesn't support radius search.
*/
async nearest(location, radius = 100, exclude = []) {
if (!this.repository || !this.repository.fetchByRadius) {
throw new Error('Repository not set or does not support fetchByRadius');
}
const radiusRange = Math.min(1000, Math.max(1, radius));
const result = await this.repository.fetchByRadius(location, radiusRange);
await this.add(result);
if (this.aerodromes.size === 0) {
return undefined;
}
const normalizedExclude = exclude.map(icao => normalizeICAO(icao));
const aerodromeCandidates = this.values().filter(airport => !normalizedExclude.includes(normalizeICAO(airport.ICAO)));
if (aerodromeCandidates.length === 0) {
return undefined;
}
const nearest = nearestPoint(location, featureCollection(aerodromeCandidates.map(airport => {
return point(airport.location.geometry.coordinates, { icao: airport.ICAO });
})));
return this.get(nearest.properties.icao);
}
}