flightcn
uiflightsource: ok · hit
Flight
Composable flight map components for routes, live tracking, weighted networks, geodesic ranges, aircraft trails, traffic flow, and a built-in 515-airport database.
npx shadcn add "https://flightcn.yencheng.dev/r/flight.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · light@turf/great-circle@turf/bearing
Registry dependencies
https://mapcn.dev/r/map.json
Files4 · source cached 10/14/2026, 11:22:16 AM
src/registry/flight.tsx
"use client";
import * as MapLibreGL from "maplibre-gl";
import {
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
useSyncExternalStore,
type ReactNode,
} from "react";
import { createPortal } from "react-dom";
import greatCircle from "@turf/great-circle";
import bearing from "@turf/bearing";
import {
useMap,
MapMarker,
MarkerContent,
MapPopup,
MarkerLabel,
} from "@/components/ui/map";
import { cn } from "@/lib/utils";
import { airports, type AirportInfo, type AirportRef } from "./flight-airports";
import { getAirportInfo, resolveAirport } from "./flight-airports-utils";
export type { AirportInfo, AirportRef } from "./flight-airports";
type FlightMapTheme = "light" | "dark";
function getDocumentTheme(): FlightMapTheme | null {
if (typeof document === "undefined") return null;
if (document.documentElement.classList.contains("dark")) return "dark";
if (document.documentElement.classList.contains("light")) return "light";
return null;
}
function getSystemTheme(): FlightMapTheme {
if (typeof window === "undefined") return "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
/**
* Resolves light/dark the same way as Map’s basemap: `html` class (e.g. next-themes)
* or `prefers-color-scheme` when unset.
*/
function useFlightMapTheme(): FlightMapTheme {
const [theme, setTheme] = useState<FlightMapTheme>(
() => getDocumentTheme() ?? getSystemTheme(),
);
useEffect(() => {
const observer = new MutationObserver(() => {
const docTheme = getDocumentTheme();
if (docTheme) setTheme(docTheme);
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const onSystem = (e: MediaQueryListEvent) => {
if (!getDocumentTheme()) setTheme(e.matches ? "dark" : "light");
};
mediaQuery.addEventListener("change", onSystem);
return () => {
observer.disconnect();
mediaQuery.removeEventListener("change", onSystem);
};
}, []);
return theme;
}
/** Default arc stroke on light basemap when `color` is omitted */
const FLIGHT_ROUTE_COLOR_LIGHT = "#0a0a0a";
/** Default arc stroke on dark basemap when `color` is omitted */
const FLIGHT_ROUTE_COLOR_DARK = "#e8e8e8";
function normalizeAirportRefKey(ref: AirportRef): string {
if (typeof ref === "string") {
return `code:${ref.toUpperCase()}`;
}
return `coord:${ref[0].toFixed(6)},${ref[1].toFixed(6)}`;
}
function safeResolveAirport(refKey: string): [number, number] | null {
try {
if (refKey.startsWith("code:")) {
return resolveAirport(refKey.slice(5));
}
const coordinates = refKey.slice(6).split(",");
if (coordinates.length !== 2) {
throw new Error(`Invalid airport coordinate key: "${refKey}"`);
}
const longitude = Number(coordinates[0]);
const latitude = Number(coordinates[1]);
if (Number.isNaN(longitude) || Number.isNaN(latitude)) {
throw new Error(`Invalid airport coordinate key: "${refKey}"`);
}
return [longitude, latitude];
} catch (error) {
console.warn(error);
return null;
}
}
type AirportMarkerDedupEntry = {
ownerId: string;
markerIds: Set<string>;
};
const airportMarkerDedupStores = new WeakMap<
MapLibreGL.Map,
Map<string, AirportMarkerDedupEntry>
>();
const airportMarkerDedupListeners = new WeakMap<
MapLibreGL.Map,
Set<() => void>
>();
function getAirportMarkerDedupStore(map: MapLibreGL.Map) {
let store = airportMarkerDedupStores.get(map);
if (!store) {
store = new Map<string, AirportMarkerDedupEntry>();
airportMarkerDedupStores.set(map, store);
}
return store;
}
function getAirportMarkerDedupListeners(map: MapLibreGL.Map) {
let listeners = airportMarkerDedupListeners.get(map);
if (!listeners) {
listeners = new Set<() => void>();
airportMarkerDedupListeners.set(map, listeners);
}
return listeners;
}
function notifyAirportMarkerDedupListeners(map: MapLibreGL.Map) {
getAirportMarkerDedupListeners(map).forEach((listener) => listener());
}
function subscribeAirportMarkerDedup(
map: MapLibreGL.Map | null,
listener: () => void,
) {
if (!map) return () => {};
const listeners = getAirportMarkerDedupListeners(map);
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
function getAirportMarkerOwnerId(
map: MapLibreGL.Map | null,
dedupeKey: string,
) {
if (!map) return null;
return getAirportMarkerDedupStore(map).get(dedupeKey)?.ownerId ?? null;
}
function claimAirportMarker(
map: MapLibreGL.Map | null,
dedupeKey: string,
markerId: string,
) {
if (!map) return () => {};
const store = getAirportMarkerDedupStore(map);
const existing = store.get(dedupeKey);
if (!existing) {
store.set(dedupeKey, {
ownerId: markerId,
markerIds: new Set([markerId]),
});
} else {
existing.markerIds.add(markerId);
}
notifyAirportMarkerDedupListeners(map);
return () => {
const entry = store.get(dedupeKey);
if (!entry) return;
entry.markerIds.delete(markerId);
if (entry.markerIds.size === 0) {
store.delete(dedupeKey);
} else if (entry.ownerId === markerId) {
entry.ownerId = entry.markerIds.values().next().value as string;
}
notifyAirportMarkerDedupListeners(map);
};
}
function useAirportMarkerVisibility(
map: MapLibreGL.Map | null,
dedupeKey?: string,
): boolean {
const markerId = useId();
const ownerId = useSyncExternalStore(
useCallback(
(onStoreChange) => subscribeAirportMarkerDedup(map, onStoreChange),
[map],
),
() => (dedupeKey ? getAirportMarkerOwnerId(map, dedupeKey) : null),
() => null,
);
useEffect(() => {
if (!dedupeKey) return;
return claimAirportMarker(map, dedupeKey, markerId);
}, [map, dedupeKey, markerId]);
if (!dedupeKey) return true;
return ownerId === markerId;
}
/** GeoJSON geometry for an arc — either a single line or multiple segments (antimeridian crossing) */
type ArcGeometry =
| { type: "LineString"; coordinates: [number, number][] }
| { type: "MultiLineString"; coordinates: [number, number][][] };
/**
* Generate great circle arc geometry between two points.
* Returns a GeoJSON geometry object (LineString or MultiLineString).
* MultiLineString is returned when the route crosses the antimeridian (180°).
*/
function generateArcGeometry(
from: [number, number],
to: [number, number],
npoints: number = 100,
): ArcGeometry {
// Handle identical points or very short distances
if (
(from[0] === to[0] && from[1] === to[1]) ||
(Math.abs(from[0] - to[0]) < 0.01 && Math.abs(from[1] - to[1]) < 0.01)
) {
return { type: "LineString", coordinates: [from, to] };
}
try {
const arc = greatCircle(from, to, { npoints });
const geometry = arc.geometry;
// Some arc versions split at sampled points without adding the seam
// intersection. Extend both segments to the same latitude at ±180°.
// Keep the split so Mercator never draws a line across the whole world.
if (geometry.type === "MultiLineString") {
const segments = geometry.coordinates.map((segment) =>
segment.map(([lng, lat]) => [lng, lat] as [number, number]),
);
for (let i = 1; i < segments.length; i++) {
const previous = segments[i - 1];
const next = segments[i];
const end = previous[previous.length - 1];
const start = next[0];
if (!end || !start || Math.abs(end[0] - start[0]) <= 180) continue;
const seam = end[0] > 0 ? 180 : -180;
const unwrappedStart = start[0] + (seam > 0 ? 360 : -360);
const span = unwrappedStart - end[0];
if (span === 0) continue;
const fraction = (seam - end[0]) / span;
const latitude = end[1] + fraction * (start[1] - end[1]);
if (end[0] !== seam) previous.push([seam, latitude]);
if (start[0] !== -seam) next.unshift([-seam, latitude]);
}
return { type: "MultiLineString", coordinates: segments };
}
return {
type: "LineString",
coordinates: geometry.coordinates as [number, number][],
};
} catch {
return { type: "LineString", coordinates: [from, to] };
}
}
/**
* Generate great circle arc coordinates between two points.
* Returns a flat array of [longitude, latitude] tuples.
* For routes crossing the antimeridian, longitudes are unwrapped to ensure
* continuity (values may exceed ±180°) so that linear interpolation produces
* correct intermediate positions instead of jumping across the map.
*/
function generateArcCoordinates(
from: [number, number],
to: [number, number],
npoints: number = 100,
): [number, number][] {
const geom = generateArcGeometry(from, to, npoints);
return unwrapArcCoordinates(geom);
}
/**
* Flatten an arc geometry into a continuous coordinate list,
* unwrapping longitudes across the antimeridian.
*/
function unwrapArcCoordinates(geometry: ArcGeometry): [number, number][] {
if (geometry.type === "LineString") {
return geometry.coordinates;
}
// Unwrap longitudes to ensure continuity across the antimeridian.
// Without unwrapping, linear interpolation between segments ending at ~180°
// and starting at ~-180° would incorrectly traverse through 0°.
const result: [number, number][] = [];
for (const segment of geometry.coordinates) {
for (const coord of segment) {
if (result.length === 0) {
result.push([coord[0], coord[1]]);
continue;
}
const prev = result[result.length - 1];
let lng = coord[0];
while (lng - prev[0] > 180) lng -= 360;
while (lng - prev[0] < -180) lng += 360;
result.push([lng, coord[1]]);
}
}
return result;
}
/**
* Normalize route geometry for the active projection.
* Mercator keeps the dateline split to avoid drawing a line across the full map.
* Globe should use one continuous line so the antimeridian seam does not show as a gap.
*/
function resolveArcGeometryForProjection(
geometry: ArcGeometry,
projectionType: string | null,
): ArcGeometry {
if (projectionType !== "globe" || geometry.type === "LineString") {
return geometry;
}
return {
type: "LineString",
coordinates: unwrapArcCoordinates(geometry),
};
}
function getProjectionType(
map: MapLibreGL.Map | null | undefined,
): string | null {
const projection = map?.getProjection();
return typeof projection?.type === "string" ? projection.type : null;
}
/**
* Compute the great-circle distance (in km) between two [lng, lat] points
* using the Haversine formula. Used for route distance calculation and
* to weight multi-leg animation durations proportionally.
*/
function haversineDistance(a: [number, number], b: [number, number]): number {
const toRad = (d: number) => (d * Math.PI) / 180;
const R = 6371; // Earth radius in km
const dLat = toRad(b[1] - a[1]);
const dLng = toRad(b[0] - a[0]);
const sinLat = Math.sin(dLat / 2);
const sinLng = Math.sin(dLng / 2);
const h =
sinLat * sinLat +
Math.cos(toRad(a[1])) * Math.cos(toRad(b[1])) * sinLng * sinLng;
return 2 * R * Math.asin(Math.sqrt(h));
}
type FlightAirportProps = {
/** IATA airport code (e.g. "TPE", "NRT") — mutually exclusive with longitude/latitude */
code?: string;
/** Longitude — use with latitude for custom coordinates */
longitude?: number;
/** Latitude — use with longitude for custom coordinates */
latitude?: number;
/** Display name override. When using code, auto-resolved from database. */
name?: string;
/** Whether to show the airport code/name label (default: false) */
showLabel?: boolean;
/** Label position relative to the marker (default: "top") */
labelPosition?: "top" | "bottom";
/** Additional CSS classes for the label */
labelClassName?: string;
/** Additional CSS classes for the MarkerContent container */
className?: string;
/**
* Custom marker content. When provided, replaces the default black dot marker.
* Accepts any ReactNode — use mapcn's marker patterns:
*
* - Colored dot: `<div className="size-4 rounded-full bg-red-500 border-2 border-white shadow-lg" />`
* - Icon in circle: `<div className="bg-emerald-500 rounded-full p-1.5 shadow-lg"><Plane className="size-3 text-white" /></div>`
* - Numbered: `<div className="size-5 rounded-full bg-blue-500 border-2 border-white shadow-lg flex items-center justify-center text-white text-xs font-semibold">1</div>`
* - Pulsing: `<div className="relative flex items-center justify-center"><div className="absolute size-6 rounded-full bg-cyan-500/20 animate-ping" /><div className="size-4 rounded-full bg-cyan-500 border-2 border-white shadow-lg" /></div>`
*
* When omitted, uses a theme-aware dot (h-4 w-4) for contrast on light/dark maps.
*/
markerContent?: ReactNode;
/** Callback when the airport marker is clicked */
onClick?: (
airport: AirportInfo | { longitude: number; latitude: number },
) => void;
/** Optional deduplication key to suppress duplicate markers rendered at the same airport */
dedupeKey?: string;
/** Custom content to render inside the marker popup (shown on click) */
children?: ReactNode;
};
function FlightAirport({
code,
longitude: lngProp,
latitude: latProp,
name: nameProp,
showLabel = false,
labelPosition = "top",
labelClassName,
className,
markerContent,
onClick,
dedupeKey,
children,
}: FlightAirportProps) {
const [isPopupOpen, setIsPopupOpen] = useState(false);
const { map } = useMap();
const flightMapTheme = useFlightMapTheme();
const isVisible = useAirportMarkerVisibility(map, dedupeKey);
// Resolve coordinates
const airportInfo = code ? getAirportInfo(code) : undefined;
const lng = lngProp ?? airportInfo?.longitude;
const lat = latProp ?? airportInfo?.latitude;
const displayName = nameProp ?? (airportInfo ? airportInfo.code : undefined);
if (lng === undefined || lat === undefined) {
if (code) {
console.warn(`FlightAirport: Unknown airport code "${code}"`);
} else {
console.warn(
"FlightAirport: Either code or longitude/latitude must be provided",
);
}
return null;
}
if (!isVisible) return null;
const handleClick = () => {
if (onClick) {
onClick(airportInfo ?? { longitude: lng, latitude: lat });
}
if (children) {
setIsPopupOpen(!isPopupOpen);
}
};
return (
<MapMarker longitude={lng} latitude={lat} onClick={handleClick}>
{/* When markerContent is provided, it replaces the default theme-aware dot */}
<MarkerContent className={className}>
{markerContent || (
<div
className={cn(
"relative h-4 w-4 rounded-full border-2 shadow-lg",
flightMapTheme === "dark"
? "border-neutral-700 bg-neutral-100"
: "border-white bg-neutral-950",
)}
/>
)}
{showLabel && displayName && (
<MarkerLabel
position={labelPosition}
className={cn(
flightMapTheme === "dark"
? "text-neutral-100"
: "text-neutral-950",
labelClassName,
)}
>
{displayName}
</MarkerLabel>
)}
</MarkerContent>
{children && isPopupOpen && (
<MapPopup
longitude={lng}
latitude={lat}
onClose={() => setIsPopupOpen(false)}
closeOnClick={false}
closeButton
>
{children}
</MapPopup>
)}
</MapMarker>
);
}
/** Animation configuration for FlightRoute */
type FlightRouteAnimateConfig = {
/**
* Animation duration in milliseconds (default: 4000).
* Ignored when `progress` is provided (controlled mode).
*/
duration?: number;
/**
* Manual progress value from 0 to 1. When provided, the component
* switches to controlled mode — no internal animation runs, and the
* airplane position is derived entirely from this value.
*/
progress?: number;
/**
* Whether the animation loops continuously (default: true).
* When false, the airplane disappears after reaching the destination.
*/
loop?: boolean;
/**
* Whether the airplane flies back to the origin after reaching the
* destination, creating a continuous round-trip animation (default: false).
* The total cycle time equals `duration` (half outbound, half return).
*/
roundTrip?: boolean;
/**
* Custom airplane icon. Defaults to a built-in plane SVG.
* The icon is auto-rotated via MapLibre `setRotation` to face the
* direction of travel. Design your icon pointing **up (↑ / north)**
* for correct alignment.
*/
icon?: ReactNode;
/** CSS class applied to the airplane icon wrapper */
iconClassName?: string;
/** Airplane icon size in pixels (default: 24) */
iconSize?: number;
/** Callback fired on every animation frame with current progress (0-1) */
onProgress?: (progress: number) => void;
/** Callback fired when the animation completes one full cycle */
onComplete?: () => void;
};
type FlightRouteProps = {
/** Origin airport: IATA code or [longitude, latitude] */
from: AirportRef;
/** Destination airport: IATA code or [longitude, latitude] */
to: AirportRef;
/** Optional unique identifier for this route layer */
id?: string;
/**
* Route line color. When omitted, picks a contrast color for the active map
* theme (same rules as Map basemap: `html` class or system preference).
*/
color?: string;
/** Route line width in pixels (default: 2) */
width?: number;
/** Route line opacity from 0 to 1 (default: 0.7) */
opacity?: number;
/**
* Line style preset (default: "solid")
* - "solid": continuous line
* - "dash": dashed line (— — —)
* - "dot": dotted line (· · ·)
*/
lineStyle?: "solid" | "dash" | "dot";
/** Number of points to interpolate along the arc (default: 100) */
npoints?: number;
/** Whether to render the origin/destination airport markers (default: false) */
showAirports?: boolean;
/** Whether to show the airport code label on markers (default: false). Only effective when showAirports is true. */
showLabel?: boolean;
/** Additional CSS classes for the airport label */
labelClassName?: string;
/**
* Custom marker content for auto-rendered airport markers.
* See FlightAirportProps.markerContent for examples.
*/
markerContent?: ReactNode;
/** Callback when the route line is clicked */
onClick?: () => void;
/** Callback when mouse enters the route line */
onMouseEnter?: () => void;
/** Callback when mouse leaves the route line */
onMouseLeave?: () => void;
/** Whether the route is interactive (default: true) */
interactive?: boolean;
/**
* Enable flight animation along the route.
* - `true`: animate with default settings
* - `FlightRouteAnimateConfig`: animate with custom settings
* - `false` / `undefined`: no animation (static route)
*/
animate?: boolean | FlightRouteAnimateConfig;
/**
* Whether to show hover effect on the route line (default: true).
* When enabled, hovering over the route line will:
* - Thicken the line for visual feedback
* - Show a tooltip with route information (origin, destination, distance,
* estimated flight time, and trip type)
*/
hoverEffect?: boolean;
/**
* Trip type for this route (default: "one-way").
* - "one-way": one-directional flight, tooltip shows →
* - "round-trip": bidirectional flight, tooltip shows ↔,
* and animation automatically uses roundTrip mode
*/
tripType?: "one-way" | "round-trip";
};
/** Resolve lineStyle preset to a dash array value */
function resolveDashArray(
lineStyle?: "solid" | "dash" | "dot",
): [number, number] | undefined {
switch (lineStyle) {
case "dash":
return [4, 3];
case "dot":
return [1, 2];
default:
return undefined;
}
}
type RouteHoverInfo = {
fromLabel: string;
toLabel: string;
distanceKm: number;
estimatedHours: number;
estimatedMinutes: number;
tripType: string;
isRoundTrip: boolean;
};
/** Build HTML content for the route hover tooltip using Tailwind classes only */
function buildRouteTooltipHTML(info: RouteHoverInfo): string {
const distance = info.distanceKm.toLocaleString("en-US", {
maximumFractionDigits: 0,
});
const time =
info.estimatedHours > 0
? `~${info.estimatedHours}h ${info.estimatedMinutes}m`
: `~${info.estimatedMinutes}m`;
const arrow = info.isRoundTrip ? "↔" : "→";
return `<div class="pointer-events-none min-w-[180px] rounded-md border border-border bg-popover px-3.5 py-2.5 text-xs leading-relaxed text-popover-foreground shadow-md">
<p class="mb-1 text-xs font-semibold">${info.fromLabel} ${arrow} ${info.toLabel}</p>
<div class="mt-1 border-t border-border pt-1.5">
<div class="flex items-center justify-between gap-4"><span class="text-muted-foreground">Distance</span><span>${distance} km</span></div>
<div class="flex items-center justify-between gap-4"><span class="text-muted-foreground">Est. Time</span><span>${time}</span></div>
<div class="flex items-center justify-between gap-4"><span class="text-muted-foreground">Type</span><span>${info.tripType}</span></div>
</div>
</div>`;
}
/** Compute hover info for a flight route */
function computeRouteHoverInfo(
from: AirportRef,
to: AirportRef,
fromCoords: [number, number],
toCoords: [number, number],
tripType?: "one-way" | "round-trip",
): RouteHoverInfo {
const fromInfo = typeof from === "string" ? getAirportInfo(from) : null;
const toInfo = typeof to === "string" ? getAirportInfo(to) : null;
const fromLabel = fromInfo
? `${fromInfo.city} (${fromInfo.code})`
: `${fromCoords[1].toFixed(2)}\u00b0, ${fromCoords[0].toFixed(2)}\u00b0`;
const toLabel = toInfo
? `${toInfo.city} (${toInfo.code})`
: `${toCoords[1].toFixed(2)}\u00b0, ${toCoords[0].toFixed(2)}\u00b0`;
const distanceKm = haversineDistance(fromCoords, toCoords);
const avgSpeed = 850; // km/h approximate cruising speed
const totalMinutes = Math.round((distanceKm / avgSpeed) * 60);
const estimatedHours = Math.floor(totalMinutes / 60);
const estimatedMinutes = totalMinutes % 60;
const isRoundTrip = tripType === "round-trip";
return {
fromLabel,
toLabel,
distanceKm,
estimatedHours,
estimatedMinutes,
tripType: isRoundTrip ? "Round Trip" : "One-way",
isRoundTrip,
};
}
function FlightRoute({
from,
to,
id: propId,
color,
width = 2,
opacity = 0.7,
lineStyle = "solid",
npoints = 100,
showAirports = false,
showLabel = false,
labelClassName,
markerContent,
onClick,
onMouseEnter,
onMouseLeave,
interactive = true,
animate,
hoverEffect = true,
tripType,
}: FlightRouteProps) {
const { map, isLoaded } = useMap();
const flightMapTheme = useFlightMapTheme();
const resolvedRouteColor =
color ??
(flightMapTheme === "dark"
? FLIGHT_ROUTE_COLOR_DARK
: FLIGHT_ROUTE_COLOR_LIGHT);
const autoId = useId();
const id = propId ?? autoId;
const sourceId = `flight-route-source-${id}`;
const layerId = `flight-route-layer-${id}`;
const projectionType = useSyncExternalStore<string | null>(
useCallback(
(onStoreChange) => {
if (!map) return () => {};
map.on("projectiontransition", onStoreChange);
map.on("styledata", onStoreChange);
return () => {
map.off("projectiontransition", onStoreChange);
map.off("styledata", onStoreChange);
};
},
[map],
),
() => getProjectionType(map),
() => null,
);
// Normalize animate prop — auto-inject roundTrip from tripType
const animateConfig = useMemo<FlightRouteAnimateConfig | null>(() => {
if (!animate) return null;
const base: FlightRouteAnimateConfig =
animate === true ? {} : { ...animate };
// tripType drives roundTrip unless explicitly overridden in animate config
if (tripType === "round-trip" && base.roundTrip === undefined) {
base.roundTrip = true;
}
return base;
}, [animate, tripType]);
// Resolve dash array from lineStyle preset
const resolvedDash = useMemo(() => resolveDashArray(lineStyle), [lineStyle]);
// Resolve coordinates — use value-based keys to avoid unnecessary recalculation
// when coordinate arrays are recreated with the same values on parent re-renders
const fromKey = normalizeAirportRefKey(from);
const fromCoords = useMemo(() => safeResolveAirport(fromKey), [fromKey]);
const toKey = normalizeAirportRefKey(to);
const toCoords = useMemo(() => safeResolveAirport(toKey), [toKey]);
const fromDedupeKey = fromKey;
const toDedupeKey = toKey;
// Generate the base arc geometry before projection-specific normalization.
const arcGeometry = useMemo(() => {
if (!fromCoords || !toCoords) return null;
return generateArcGeometry(fromCoords, toCoords, npoints);
}, [fromCoords, toCoords, npoints]);
const renderedArcGeometry = useMemo(() => {
if (!arcGeometry) return null;
return resolveArcGeometryForProjection(arcGeometry, projectionType);
}, [arcGeometry, projectionType]);
// Store callbacks in refs to avoid effect re-runs
const onClickRef = useRef(onClick);
const onMouseEnterRef = useRef(onMouseEnter);
const onMouseLeaveRef = useRef(onMouseLeave);
useEffect(() => {
onClickRef.current = onClick;
onMouseEnterRef.current = onMouseEnter;
onMouseLeaveRef.current = onMouseLeave;
});
// Compute route info for hover tooltip
const routeInfo = useMemo<RouteHoverInfo | null>(() => {
if (!fromCoords || !toCoords) return null;
return computeRouteHoverInfo(from, to, fromCoords, toCoords, tripType);
}, [fromCoords, toCoords, from, to, tripType]);
// Store mutable values in refs so the hover effect doesn't need to re-attach
const widthRef = useRef(width);
const opacityRef = useRef(opacity);
const routeInfoRef = useRef(routeInfo);
const hoverEffectRef = useRef(hoverEffect);
useEffect(() => {
widthRef.current = width;
opacityRef.current = opacity;
routeInfoRef.current = routeInfo;
hoverEffectRef.current = hoverEffect;
});
// Add source and layer on mount
useEffect(() => {
if (!isLoaded || !map) return;
map.addSource(sourceId, {
type: "geojson",
data: {
type: "Feature",
properties: {},
geometry: { type: "LineString", coordinates: [] },
},
});
const paint: Record<string, unknown> = {
"line-width": width,
"line-opacity": opacity,
"line-color": resolvedRouteColor,
};
if (resolvedDash) {
paint["line-dasharray"] = resolvedDash;
}
map.addLayer({
id: layerId,
type: "line",
source: sourceId,
layout: {
"line-join": "round",
"line-cap": resolvedDash ? "butt" : "round",
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
paint: paint as any,
});
return () => {
try {
if (map.getLayer(layerId)) map.removeLayer(layerId);
if (map.getSource(sourceId)) map.removeSource(sourceId);
} catch {
// ignore
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoaded, map]);
// Update coordinates when they change
useEffect(() => {
if (!isLoaded || !map || !renderedArcGeometry) return;
const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource;
if (source) {
source.setData({
type: "Feature",
properties: {},
geometry: renderedArcGeometry,
});
}
}, [isLoaded, map, renderedArcGeometry, sourceId]);
// Update paint properties when they change
useEffect(() => {
if (!isLoaded || !map || !map.getLayer(layerId)) return;
map.setPaintProperty(layerId, "line-color", resolvedRouteColor);
map.setPaintProperty(layerId, "line-dasharray", resolvedDash ?? undefined);
map.setPaintProperty(layerId, "line-width", width);
map.setPaintProperty(layerId, "line-opacity", opacity);
map.setLayoutProperty(layerId, "line-cap", resolvedDash ? "butt" : "round");
}, [
isLoaded,
map,
layerId,
resolvedRouteColor,
width,
opacity,
resolvedDash,
]);
// Handle click and hover events (with hover effect: line thickening + tooltip)
useEffect(() => {
if (!isLoaded || !map || !interactive) return;
// Create a reusable tooltip popup for this route
const tooltip = new MapLibreGL.Popup({
closeButton: false,
closeOnClick: false,
offset: 15,
className:
"[&_.maplibregl-popup-content]:!rounded-none [&_.maplibregl-popup-content]:!bg-transparent [&_.maplibregl-popup-content]:!p-0 [&_.maplibregl-popup-content]:!shadow-none [&_.maplibregl-popup-tip]:!hidden",
}).setMaxWidth("none");
const handleClick = () => onClickRef.current?.();
const handleMouseEnter = (e: MapLibreGL.MapLayerMouseEvent & object) => {
map.getCanvas().style.cursor = "pointer";
if (hoverEffectRef.current && map.getLayer(layerId)) {
// Thicken the line
map.setPaintProperty(
layerId,
"line-width",
Math.max(widthRef.current * 2.5, widthRef.current + 2),
);
map.setPaintProperty(layerId, "line-opacity", 1);
}
// Show tooltip with route info
const info = routeInfoRef.current;
if (hoverEffectRef.current && info) {
tooltip
.setLngLat(e.lngLat)
.setHTML(buildRouteTooltipHTML(info))
.addTo(map);
}
onMouseEnterRef.current?.();
};
const handleMouseMove = (e: MapLibreGL.MapLayerMouseEvent & object) => {
if (tooltip.isOpen()) {
tooltip.setLngLat(e.lngLat);
}
};
const handleMouseLeave = () => {
map.getCanvas().style.cursor = "";
if (hoverEffectRef.current && map.getLayer(layerId)) {
// Restore original line width and opacity
map.setPaintProperty(layerId, "line-width", widthRef.current);
map.setPaintProperty(layerId, "line-opacity", opacityRef.current);
}
// Hide tooltip
tooltip.remove();
onMouseLeaveRef.current?.();
};
map.on("click", layerId, handleClick);
map.on("mouseenter", layerId, handleMouseEnter);
map.on("mousemove", layerId, handleMouseMove);
map.on("mouseleave", layerId, handleMouseLeave);
return () => {
map.off("click", layerId, handleClick);
map.off("mouseenter", layerId, handleMouseEnter);
map.off("mousemove", layerId, handleMouseMove);
map.off("mouseleave", layerId, handleMouseLeave);
tooltip.remove();
};
}, [isLoaded, map, layerId, interactive]);
if (!fromCoords || !toCoords) return null;
return (
<>
{showAirports && (
<>
{typeof from === "string" ? (
<FlightAirport
code={from}
markerContent={markerContent}
showLabel={showLabel}
labelClassName={labelClassName}
dedupeKey={fromDedupeKey}
/>
) : (
<FlightAirport
longitude={from[0]}
latitude={from[1]}
markerContent={markerContent}
showLabel={false}
dedupeKey={fromDedupeKey}
/>
)}
{typeof to === "string" ? (
<FlightAirport
code={to}
markerContent={markerContent}
showLabel={showLabel}
labelClassName={labelClassName}
dedupeKey={toDedupeKey}
/>
) : (
<FlightAirport
longitude={to[0]}
latitude={to[1]}
markerContent={markerContent}
showLabel={false}
dedupeKey={toDedupeKey}
/>
)}
</>
)}
{animateConfig && fromCoords && toCoords && (
<FlightAnimationMarker
fromCoords={fromCoords}
toCoords={toCoords}
npoints={npoints}
config={animateConfig}
/>
)}
</>
);
}
type FlightRouteData = {
/** Origin airport: IATA code or [longitude, latitude] */
from: AirportRef;
/** Destination airport: IATA code or [longitude, latitude] */
to: AirportRef;
/** Route line color override */
color?: string;
/** Route line width override */
width?: number;
/** Route line opacity override */
opacity?: number;
/** Line style override: "solid", "dash", or "dot" */
lineStyle?: "solid" | "dash" | "dot";
/** Override hover effect for this specific route */
hoverEffect?: boolean;
/** Trip type for this specific route ("one-way" or "round-trip") */
tripType?: "one-way" | "round-trip";
/** Callback when this route line is clicked */
onClick?: () => void;
/** Callback when mouse enters this route line */
onMouseEnter?: () => void;
/** Callback when mouse leaves this route line */
onMouseLeave?: () => void;
/** Override interactive setting for this specific route */
interactive?: boolean;
/**
* Animation override for this specific route.
* - `true`: animate with default settings
* - `FlightRouteAnimateConfig`: animate with custom settings
* - `false`: disable animation for this route
* - `undefined`: use the parent FlightRoutes default
*/
animate?: boolean | FlightRouteAnimateConfig;
};
type FlightRoutesProps = {
/** Array of route data */
routes: readonly FlightRouteData[];
/** Default route line color; when omitted, uses theme-aware stroke (see FlightRoute `color`). */
color?: string;
/** Default route line width (default: 2) */
width?: number;
/** Default route line opacity (default: 0.7) */
opacity?: number;
/**
* Default line style for all routes (default: "solid")
* - "solid": continuous line
* - "dash": dashed line (— — —)
* - "dot": dotted line (· · ·)
*/
lineStyle?: "solid" | "dash" | "dot";
/** Number of arc interpolation points per route (default: 100) */
npoints?: number;
/** Whether to render airport markers at all endpoints (default: false) */
showAirports?: boolean;
/** Whether to show the airport code label on markers (default: false). Only effective when showAirports is true. */
showLabel?: boolean;
/** Additional CSS classes for the airport label */
labelClassName?: string;
/**
* Custom marker content for auto-rendered airport markers.
* When provided, replaces the default black dot. See FlightAirportProps.markerContent for examples.
*/
markerContent?: ReactNode;
/** Whether routes are interactive (default: true) */
interactive?: boolean;
/** Whether to show hover effect on route lines (default: true) */
hoverEffect?: boolean;
/**
* Default trip type for all routes (default: "one-way").
* Can be overridden per-route in the route data.
*/
tripType?: "one-way" | "round-trip";
/**
* Callback when any route line is clicked.
* Receives the route index and the route data object.
* Per-route `onClick` in `FlightRouteData` takes precedence when set.
*/
onClick?: (routeIndex: number, route: FlightRouteData) => void;
/**
* Callback when mouse enters any route line.
* Receives the route index and the route data object.
* Per-route `onMouseEnter` in `FlightRouteData` takes precedence when set.
*/
onMouseEnter?: (routeIndex: number, route: FlightRouteData) => void;
/**
* Callback when mouse leaves any route line.
* Receives the route index and the route data object.
* Per-route `onMouseLeave` in `FlightRouteData` takes precedence when set.
*/
onMouseLeave?: (routeIndex: number, route: FlightRouteData) => void;
/**
* Enable flight animation for all routes.
* - `true`: animate with default settings
* - `FlightRouteAnimateConfig`: animate with custom settings
* - `false` / `undefined`: no animation (static routes)
*/
animate?: boolean | FlightRouteAnimateConfig;
};
function FlightRoutes({
routes,
color,
width = 2,
opacity = 0.7,
lineStyle = "solid",
npoints = 100,
showAirports = false,
showLabel = false,
labelClassName,
markerContent,
interactive = true,
hoverEffect = true,
tripType = "one-way",
onClick,
onMouseEnter,
onMouseLeave,
animate,
}: FlightRoutesProps) {
// Collect unique airports for rendering when showAirports is true
const uniqueAirports = useMemo(() => {
if (!showAirports) return [];
const seen = new Set<string>();
const result: AirportRef[] = [];
for (const route of routes) {
const fromKey =
typeof route.from === "string" ? route.from : route.from.join(",");
const toKey =
typeof route.to === "string" ? route.to : route.to.join(",");
if (!seen.has(fromKey)) {
seen.add(fromKey);
result.push(route.from);
}
if (!seen.has(toKey)) {
seen.add(toKey);
result.push(route.to);
}
}
return result;
}, [routes, showAirports]);
return (
<>
{routes.map((route, index) => (
<FlightRoute
key={`${typeof route.from === "string" ? route.from : route.from.join(",")}-${typeof route.to === "string" ? route.to : route.to.join(",")}-${index}`}
from={route.from}
to={route.to}
color={route.color ?? color}
width={route.width ?? width}
opacity={route.opacity ?? opacity}
lineStyle={route.lineStyle ?? lineStyle}
npoints={npoints}
interactive={route.interactive ?? interactive}
hoverEffect={route.hoverEffect ?? hoverEffect}
tripType={route.tripType ?? tripType}
animate={route.animate ?? animate}
onClick={
route.onClick ?? (onClick ? () => onClick(index, route) : undefined)
}
onMouseEnter={
route.onMouseEnter ??
(onMouseEnter ? () => onMouseEnter(index, route) : undefined)
}
onMouseLeave={
route.onMouseLeave ??
(onMouseLeave ? () => onMouseLeave(index, route) : undefined)
}
showAirports={false}
/>
))}
{showAirports &&
uniqueAirports.map((airport) =>
typeof airport === "string" ? (
<FlightAirport
key={airport}
code={airport}
markerContent={markerContent}
showLabel={showLabel}
labelClassName={labelClassName}
dedupeKey={normalizeAirportRefKey(airport)}
/>
) : (
<FlightAirport
key={airport.join(",")}
longitude={airport[0]}
latitude={airport[1]}
markerContent={markerContent}
showLabel={false}
dedupeKey={normalizeAirportRefKey(airport)}
/>
),
)}
</>
);
}
type FlightMultiRouteProps = {
/**
* Ordered list of waypoints (airports) defining the multi-leg route.
* Must contain at least 2 entries.
* Example: `["TPE", "NRT", "LAX"]` renders TPE→NRT and NRT→LAX.
*/
waypoints: readonly AirportRef[];
/** Optional unique identifier prefix for route layers */
id?: string;
/** Route line color; when omitted, uses theme-aware stroke (see FlightRoute `color`). */
color?: string;
/** Route line width in pixels (default: 2) */
width?: number;
/** Route line opacity from 0 to 1 (default: 0.7) */
opacity?: number;
/**
* Line style preset (default: "solid")
* - "solid": continuous line
* - "dash": dashed line (— — —)
* - "dot": dotted line (· · ·)
*/
lineStyle?: "solid" | "dash" | "dot";
/** Number of arc interpolation points per leg (default: 100) */
npoints?: number;
/** Whether to render airport markers at each waypoint (default: false) */
showAirports?: boolean;
/** Whether to show the airport code label on markers (default: false). Only effective when showAirports is true. */
showLabel?: boolean;
/** Additional CSS classes for the airport label */
labelClassName?: string;
/**
* Custom marker content for auto-rendered airport markers.
* See FlightAirportProps.markerContent for examples.
*/
markerContent?: ReactNode;
/**
* Custom marker content for intermediate (stopover) airports.
* When provided, stopover airports use this marker instead of markerContent.
* Useful for visually distinguishing origin/destination from transfer points.
*/
stopoverMarkerContent?: ReactNode;
/** Callback when any leg line is clicked, receives leg index */
onLegClick?: (legIndex: number) => void;
/** Whether routes are interactive (default: true) */
interactive?: boolean;
/**
* Enable flight animation across all legs sequentially.
* A single airplane flies from the first waypoint through each intermediate
* stop to the last waypoint, with speed proportional to leg distance.
* - `true`: animate with default settings
* - `FlightRouteAnimateConfig`: animate with custom settings
* - `false` / `undefined`: no animation (static routes)
*/
animate?: boolean | FlightRouteAnimateConfig;
/** Whether to show hover effect on route lines (default: true) */
hoverEffect?: boolean;
/**
* Trip type for hover tooltip display on each leg (default: "one-way").
*/
tripType?: "one-way" | "round-trip";
};
function FlightMultiRoute({
waypoints,
id: propId,
color,
width = 2,
opacity = 0.7,
lineStyle = "solid",
npoints = 100,
showAirports = false,
showLabel = false,
labelClassName,
markerContent,
stopoverMarkerContent,
onLegClick,
interactive = true,
animate,
hoverEffect = true,
tripType = "one-way",
}: FlightMultiRouteProps) {
const autoId = useId();
const id = propId ?? autoId;
const flightMapTheme = useFlightMapTheme();
// Normalize animate prop
const animateConfig = useMemo<FlightRouteAnimateConfig | null>(() => {
if (!animate) return null;
if (animate === true) return {};
return animate;
}, [animate]);
// Resolve all waypoint coordinates for animation
// Use value-based key to avoid recalculation when arrays are recreated with same values
const waypointsKey = waypoints
.map((wp) => (typeof wp === "string" ? wp : `${wp[0]},${wp[1]}`))
.join("|");
const waypointCoords = useMemo(() => {
if (waypoints.length < 2) return null;
try {
return waypoints.map((wp) => resolveAirport(wp));
} catch (e) {
console.warn(e);
return null;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [waypointsKey]);
if (waypoints.length < 2) {
console.warn("FlightMultiRoute requires at least 2 waypoints");
return null;
}
// Build legs: pairs of consecutive waypoints
const legs: { from: AirportRef; to: AirportRef }[] = [];
for (let i = 0; i < waypoints.length - 1; i++) {
legs.push({ from: waypoints[i], to: waypoints[i + 1] });
}
return (
<>
{/* Render each leg as a FlightRoute */}
{legs.map((leg, index) => (
<FlightRoute
key={`${id}-leg-${index}`}
id={`${id}-leg-${index}`}
from={leg.from}
to={leg.to}
color={color}
width={width}
opacity={opacity}
lineStyle={lineStyle}
npoints={npoints}
interactive={interactive}
hoverEffect={hoverEffect}
tripType={tripType}
showAirports={false}
onClick={onLegClick ? () => onLegClick(index) : undefined}
/>
))}
{/* Render airport markers at each waypoint */}
{showAirports &&
waypoints.map((wp, index) => {
const isEndpoint = index === 0 || index === waypoints.length - 1;
const marker =
!isEndpoint && stopoverMarkerContent !== undefined ? (
stopoverMarkerContent
) : !isEndpoint ? (
<div
className={cn(
"relative h-2.5 w-2.5 rounded-full border-2 shadow-lg",
flightMapTheme === "dark"
? "border-neutral-700 bg-neutral-100"
: "border-white bg-neutral-950",
)}
/>
) : (
markerContent
);
return typeof wp === "string" ? (
<FlightAirport
key={`${id}-wp-${wp}-${index}`}
code={wp}
markerContent={marker}
showLabel={showLabel}
labelClassName={labelClassName}
dedupeKey={normalizeAirportRefKey(wp)}
/>
) : (
<FlightAirport
key={`${id}-wp-${wp.join(",")}-${index}`}
longitude={wp[0]}
latitude={wp[1]}
markerContent={marker}
showLabel={false}
dedupeKey={normalizeAirportRefKey(wp)}
/>
);
})}
{/* Animation marker across all legs */}
{animateConfig && waypointCoords && waypointCoords.length >= 2 && (
<FlightMultiLegAnimationMarker
waypointCoords={waypointCoords}
npoints={npoints}
config={animateConfig}
/>
)}
</>
);
}
/** Default airplane SVG icon (points north / ↑) */
function DefaultAirplaneIcon({ size = 24 }: { size?: number }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"
fill="currentColor"
/>
</svg>
);
}
/**
* Cached cumulative arc lengths, keyed by coordinate array identity.
* Coordinate arrays are memoized by the animation markers, so the cache is
* computed once per route and reused on every animation frame.
*/
const cumulativeArcLengths = new WeakMap<[number, number][], number[]>();
function getCumulativeArcLengths(coords: [number, number][]): number[] {
let lengths = cumulativeArcLengths.get(coords);
if (!lengths) {
lengths = new Array<number>(coords.length).fill(0);
for (let i = 1; i < coords.length; i += 1) {
lengths[i] = lengths[i - 1] + haversineDistance(coords[i - 1], coords[i]);
}
cumulativeArcLengths.set(coords, lengths);
}
return lengths;
}
/**
* Interpolate a position along coordinates at progress t (0-1).
* Parameterized by arc length so the aircraft moves at constant speed even
* when vertices are unevenly spaced — the antimeridian seam points inserted
* by generateArcGeometry create zero- and half-length segments that would
* otherwise make index-based interpolation visibly stall mid-flight.
*/
function interpolatePosition(
coords: [number, number][],
t: number,
): [number, number] {
if (coords.length === 0) return [0, 0];
if (t <= 0) return coords[0];
if (t >= 1) return coords[coords.length - 1];
const cumulative = getCumulativeArcLengths(coords);
const total = cumulative[cumulative.length - 1];
// Degenerate path (all points identical) — fall back to index spacing.
if (total <= Number.EPSILON) {
const exactIndex = t * (coords.length - 1);
const i = Math.min(Math.floor(exactIndex), coords.length - 2);
const frac = exactIndex - i;
const a = coords[i];
const b = coords[i + 1];
return [a[0] + (b[0] - a[0]) * frac, a[1] + (b[1] - a[1]) * frac];
}
const target = t * total;
// Binary search for the first vertex at or beyond the target distance.
// Because cumulative[low - 1] < target <= cumulative[low], the segment
// span is always positive — zero-length segments are skipped naturally.
let low = 1;
let high = coords.length - 1;
while (low < high) {
const mid = (low + high) >> 1;
if (cumulative[mid] < target) {
low = mid + 1;
} else {
high = mid;
}
}
const i = low - 1;
const span = cumulative[low] - cumulative[i];
const frac = span > 0 ? (target - cumulative[i]) / span : 0;
const a = coords[i];
const b = coords[low];
return [a[0] + (b[0] - a[0]) * frac, a[1] + (b[1] - a[1]) * frac];
}
/** Normalize longitude into the canonical [-180, 180] range for MapLibre and turf APIs. */
function normalizeLongitude(lng: number): number {
let normalized = ((((lng + 180) % 360) + 360) % 360) - 180;
if (normalized === -180 && lng > 0) {
normalized = 180;
}
return normalized;
}
/** Normalize a coordinate tuple before passing it to MapLibre or turf calculations. */
function normalizeLngLat(coord: [number, number]): [number, number] {
return [normalizeLongitude(coord[0]), coord[1]];
}
/**
* Calculate airplane rotation (degrees CW from screen-up, 0-360).
* Prefer screen-space tangent so the icon follows the visible arc under globe
* and other non-linear projections; fall back to geographic bearing.
*/
function calculateMarkerRotation(
map: MapLibreGL.Map | null,
from: [number, number],
to: [number, number],
): number {
const normalizedFrom = normalizeLngLat(from);
const normalizedTo = normalizeLngLat(to);
// Unwrap `to` relative to `from` so the pair stays continuous across the
// antimeridian — projecting independently normalized points puts them in
// different world copies and momentarily flips the computed heading.
let continuousToLng = normalizedTo[0];
while (continuousToLng - normalizedFrom[0] > 180) continuousToLng -= 360;
while (continuousToLng - normalizedFrom[0] < -180) continuousToLng += 360;
const continuousTo: [number, number] = [continuousToLng, normalizedTo[1]];
if (map) {
try {
const fromPoint = map.project(normalizedFrom);
const toPoint = map.project(continuousTo);
const dx = toPoint.x - fromPoint.x;
const dy = toPoint.y - fromPoint.y;
if (
Number.isFinite(dx) &&
Number.isFinite(dy) &&
(dx !== 0 || dy !== 0)
) {
const degrees = (Math.atan2(dx, -dy) * 180) / Math.PI;
return ((degrees % 360) + 360) % 360;
}
} catch {
// Fall back to geographic bearing when projection data is unavailable.
}
}
const b = bearing(normalizedFrom, normalizedTo);
return ((b % 360) + 360) % 360;
}
/**
* Internal component: renders the animated airplane marker along an arc.
* Used by FlightRoute when `animate` is enabled — not exported.
*/
function FlightAnimationMarker({
fromCoords,
toCoords,
npoints,
config,
}: {
fromCoords: [number, number];
toCoords: [number, number];
npoints: number;
config: FlightRouteAnimateConfig;
}) {
const { map } = useMap();
const markerRef = useRef<MapLibreGL.Marker | null>(null);
const markerElRef = useRef<HTMLDivElement | null>(null);
const animationRef = useRef<number | null>(null);
const startTimeRef = useRef<number | null>(null);
const [markerEl, setMarkerEl] = useState<HTMLDivElement | null>(null);
const {
duration = 4000,
progress: controlledProgress,
loop = true,
roundTrip = false,
icon,
iconClassName,
iconSize = 24,
onProgress,
onComplete,
} = config;
const isControlled = controlledProgress !== undefined;
const onProgressRef = useRef(onProgress);
const onCompleteRef = useRef(onComplete);
useEffect(() => {
onProgressRef.current = onProgress;
onCompleteRef.current = onComplete;
});
// Generate arc coordinates for the outbound path
const arcCoords = useMemo(
() => generateArcCoordinates(fromCoords, toCoords, npoints),
[fromCoords, toCoords, npoints],
);
// For roundTrip, pre-compute the reversed return path
const returnArcCoords = useMemo(() => {
if (!roundTrip) return null;
return generateArcCoordinates(toCoords, fromCoords, npoints);
}, [roundTrip, toCoords, fromCoords, npoints]);
// Update marker position & rotation for a given raw progress t (0-1)
const updateMarker = useCallback(
(t: number) => {
if (!arcCoords || arcCoords.length < 2 || !markerRef.current) return;
let coords: [number, number][];
let segmentT: number;
if (roundTrip && returnArcCoords && returnArcCoords.length >= 2) {
// First half (0-0.5): outbound, second half (0.5-1): return
if (t <= 0.5) {
coords = arcCoords;
segmentT = t * 2; // map 0-0.5 → 0-1
} else {
coords = returnArcCoords;
segmentT = (t - 0.5) * 2; // map 0.5-1 → 0-1
}
} else {
coords = arcCoords;
segmentT = t;
}
const pos = interpolatePosition(coords, segmentT);
markerRef.current.setLngLat(normalizeLngLat(pos));
// Look slightly ahead for bearing; fall back to looking behind at segment end
const lookAhead = Math.min(segmentT + 0.005, 1);
const nextPos = interpolatePosition(coords, lookAhead);
if (pos[0] !== nextPos[0] || pos[1] !== nextPos[1]) {
const deg = calculateMarkerRotation(map, pos, nextPos);
markerRef.current.setRotation(deg);
} else {
// At segment end, look backward to maintain correct heading
const lookBehind = Math.max(segmentT - 0.005, 0);
const prevPos = interpolatePosition(coords, lookBehind);
if (prevPos[0] !== pos[0] || prevPos[1] !== pos[1]) {
const deg = calculateMarkerRotation(map, prevPos, pos);
markerRef.current.setRotation(deg);
}
}
onProgressRef.current?.(t);
},
[arcCoords, returnArcCoords, roundTrip, map],
);
// Create / destroy MapLibre Marker
useEffect(() => {
if (!map || !arcCoords || arcCoords.length < 2) return;
const el = document.createElement("div");
markerElRef.current = el;
const marker = new MapLibreGL.Marker({
element: el,
anchor: "center",
rotationAlignment: "map",
pitchAlignment: "map",
})
.setLngLat(normalizeLngLat(arcCoords[0]))
.addTo(map);
markerRef.current = marker;
// Schedule state update outside the synchronous effect body to avoid
// react-hooks/set-state-in-effect (this is a legitimate portal container).
queueMicrotask(() => setMarkerEl(el));
return () => {
marker.remove();
markerRef.current = null;
markerElRef.current = null;
queueMicrotask(() => setMarkerEl(null));
};
}, [map, arcCoords]);
// Controlled mode
useEffect(() => {
if (!isControlled) return;
updateMarker(controlledProgress);
}, [isControlled, controlledProgress, updateMarker]);
// Auto-play mode
useEffect(() => {
if (isControlled || !arcCoords || arcCoords.length < 2) return;
startTimeRef.current = null;
const animate = (timestamp: number) => {
if (startTimeRef.current === null) {
startTimeRef.current = timestamp;
}
const elapsed = timestamp - startTimeRef.current;
let t = elapsed / duration;
if (t >= 1) {
onCompleteRef.current?.();
if (loop) {
// Carry over excess time for smooth loop transition instead of jumping to start
const remainder = elapsed % duration;
startTimeRef.current = timestamp - remainder;
t = remainder / duration;
} else {
// Non-loop: finish and hide marker
t = 1;
updateMarker(t);
// Hide the marker element so airplane disappears at destination
if (markerElRef.current) {
markerElRef.current.style.visibility = "hidden";
}
return;
}
}
// Ensure visible (in case restarted after being hidden)
if (markerElRef.current) {
markerElRef.current.style.visibility = "visible";
}
updateMarker(t);
animationRef.current = requestAnimationFrame(animate);
};
// Reset visibility when animation starts
if (markerElRef.current) {
markerElRef.current.style.visibility = "visible";
}
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current !== null) {
cancelAnimationFrame(animationRef.current);
animationRef.current = null;
}
};
}, [isControlled, duration, loop, arcCoords, updateMarker]);
if (!arcCoords || arcCoords.length < 2) return null;
return (
<>
{markerEl &&
createPortal(
<div
className={cn(
"flex items-center justify-center text-white drop-shadow-lg",
iconClassName,
)}
style={{ width: iconSize, height: iconSize }}
>
{icon || <DefaultAirplaneIcon size={iconSize} />}
</div>,
markerEl,
)}
</>
);
}
/**
* Internal component: renders a single animated airplane marker that flies
* sequentially across multiple legs (used by FlightMultiRoute).
*
* Each leg's share of the total progress is proportional to its geographic
* distance so the airplane appears to move at a constant speed.
*/
function FlightMultiLegAnimationMarker({
waypointCoords,
npoints,
config,
}: {
waypointCoords: [number, number][];
npoints: number;
config: FlightRouteAnimateConfig;
}) {
const { map } = useMap();
const markerRef = useRef<MapLibreGL.Marker | null>(null);
const markerElRef = useRef<HTMLDivElement | null>(null);
const animationRef = useRef<number | null>(null);
const startTimeRef = useRef<number | null>(null);
const [markerEl, setMarkerEl] = useState<HTMLDivElement | null>(null);
const {
duration = 4000,
progress: controlledProgress,
loop = true,
roundTrip = false,
icon,
iconClassName,
iconSize = 24,
onProgress,
onComplete,
} = config;
const isControlled = controlledProgress !== undefined;
const onProgressRef = useRef(onProgress);
const onCompleteRef = useRef(onComplete);
useEffect(() => {
onProgressRef.current = onProgress;
onCompleteRef.current = onComplete;
});
// Pre-compute all leg arc coordinates and cumulative distance breakpoints
const legData = useMemo(() => {
if (waypointCoords.length < 2) return null;
const legs: { coords: [number, number][]; distance: number }[] = [];
let totalDistance = 0;
for (let i = 0; i < waypointCoords.length - 1; i++) {
const from = waypointCoords[i];
const to = waypointCoords[i + 1];
const coords = generateArcCoordinates(from, to, npoints);
const dist = haversineDistance(from, to);
legs.push({ coords, distance: dist });
totalDistance += dist;
}
// Build cumulative breakpoints: breakpoints[i] is the progress value
// at which leg i starts (breakpoints[0] = 0, breakpoints[legs.length] = 1)
const breakpoints: number[] = [0];
let cumulative = 0;
for (const leg of legs) {
cumulative += leg.distance;
breakpoints.push(totalDistance > 0 ? cumulative / totalDistance : 0);
}
// Ensure last breakpoint is exactly 1
breakpoints[breakpoints.length - 1] = 1;
return { legs, breakpoints, totalDistance };
}, [waypointCoords, npoints]);
// For roundTrip, pre-compute the reversed leg data
const returnLegData = useMemo(() => {
if (!roundTrip || !legData) return null;
const reversedWaypoints = [...waypointCoords].reverse();
const legs: { coords: [number, number][]; distance: number }[] = [];
let totalDistance = 0;
for (let i = 0; i < reversedWaypoints.length - 1; i++) {
const from = reversedWaypoints[i];
const to = reversedWaypoints[i + 1];
const coords = generateArcCoordinates(from, to, npoints);
const dist = haversineDistance(from, to);
legs.push({ coords, distance: dist });
totalDistance += dist;
}
const breakpoints: number[] = [0];
let cumulative = 0;
for (const leg of legs) {
cumulative += leg.distance;
breakpoints.push(totalDistance > 0 ? cumulative / totalDistance : 0);
}
breakpoints[breakpoints.length - 1] = 1;
return { legs, breakpoints, totalDistance };
}, [roundTrip, waypointCoords, npoints, legData]);
// Given a progress t and leg data, compute position and bearing
const computePositionAndBearing = useCallback(
(
t: number,
data: { legs: { coords: [number, number][] }[]; breakpoints: number[] },
): { pos: [number, number]; bearing: number } => {
const { legs, breakpoints } = data;
// Find which leg we're in
let legIndex = 0;
for (let i = 0; i < legs.length; i++) {
if (t <= breakpoints[i + 1]) {
legIndex = i;
break;
}
if (i === legs.length - 1) {
legIndex = i;
}
}
const legStart = breakpoints[legIndex];
const legEnd = breakpoints[legIndex + 1];
const legRange = legEnd - legStart;
const legT = legRange > 0 ? (t - legStart) / legRange : 0;
const clampedLegT = Math.max(0, Math.min(1, legT));
const coords = legs[legIndex].coords;
const pos = interpolatePosition(coords, clampedLegT);
// Look ahead for bearing; fall back to looking behind at segment end
const lookAhead = Math.min(clampedLegT + 0.01, 1);
const nextPos = interpolatePosition(coords, lookAhead);
let deg = 0;
if (pos[0] !== nextPos[0] || pos[1] !== nextPos[1]) {
deg = calculateMarkerRotation(map, pos, nextPos);
} else {
// At segment end, look backward to maintain correct heading
const lookBehind = Math.max(clampedLegT - 0.01, 0);
const prevPos = interpolatePosition(coords, lookBehind);
if (prevPos[0] !== pos[0] || prevPos[1] !== pos[1]) {
deg = calculateMarkerRotation(map, prevPos, pos);
}
}
return { pos, bearing: deg };
},
[map],
);
// Update marker position & rotation for a given raw progress t (0-1)
const updateMarker = useCallback(
(t: number) => {
if (!legData || !markerRef.current) return;
let data: {
legs: { coords: [number, number][] }[];
breakpoints: number[];
};
let segmentT: number;
if (roundTrip && returnLegData) {
if (t <= 0.5) {
data = legData;
segmentT = t * 2;
} else {
data = returnLegData;
segmentT = (t - 0.5) * 2;
}
} else {
data = legData;
segmentT = t;
}
const { pos, bearing: deg } = computePositionAndBearing(segmentT, data);
markerRef.current.setLngLat(normalizeLngLat(pos));
markerRef.current.setRotation(deg);
onProgressRef.current?.(t);
},
[legData, returnLegData, roundTrip, computePositionAndBearing],
);
// Create / destroy MapLibre Marker
useEffect(() => {
if (!map || !legData) return;
const el = document.createElement("div");
markerElRef.current = el;
const firstCoord = legData.legs[0]?.coords[0];
if (!firstCoord) return;
const marker = new MapLibreGL.Marker({
element: el,
anchor: "center",
rotationAlignment: "map",
pitchAlignment: "map",
})
.setLngLat(normalizeLngLat(firstCoord))
.addTo(map);
markerRef.current = marker;
// Schedule state update outside the synchronous effect body to avoid
// react-hooks/set-state-in-effect (this is a legitimate portal container).
queueMicrotask(() => setMarkerEl(el));
return () => {
marker.remove();
markerRef.current = null;
markerElRef.current = null;
queueMicrotask(() => setMarkerEl(null));
};
}, [map, legData]);
// Controlled mode
useEffect(() => {
if (!isControlled) return;
updateMarker(controlledProgress);
}, [isControlled, controlledProgress, updateMarker]);
// Auto-play mode
useEffect(() => {
if (isControlled || !legData) return;
startTimeRef.current = null;
const animate = (timestamp: number) => {
if (startTimeRef.current === null) {
startTimeRef.current = timestamp;
}
const elapsed = timestamp - startTimeRef.current;
let t = elapsed / duration;
if (t >= 1) {
onCompleteRef.current?.();
if (loop) {
// Carry over excess time for smooth loop transition instead of jumping to start
const remainder = elapsed % duration;
startTimeRef.current = timestamp - remainder;
t = remainder / duration;
} else {
t = 1;
updateMarker(t);
if (markerElRef.current) {
markerElRef.current.style.visibility = "hidden";
}
return;
}
}
if (markerElRef.current) {
markerElRef.current.style.visibility = "visible";
}
updateMarker(t);
animationRef.current = requestAnimationFrame(animate);
};
if (markerElRef.current) {
markerElRef.current.style.visibility = "visible";
}
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current !== null) {
cancelAnimationFrame(animationRef.current);
animationRef.current = null;
}
};
}, [isControlled, duration, loop, legData, updateMarker]);
if (!legData) return null;
return (
<>
{markerEl &&
createPortal(
<div
className={cn(
"flex items-center justify-center text-white drop-shadow-lg",
iconClassName,
)}
style={{ width: iconSize, height: iconSize }}
>
{icon || <DefaultAirplaneIcon size={iconSize} />}
</div>,
markerEl,
)}
</>
);
}
/**
* Clamps the map's minimum zoom so a single world spans the viewport width,
* preventing repeated world copies when zooming out on a map rendered with
* `renderWorldCopies`. Render inside <Map>; set `enabled` to false to lift
* the clamp (e.g. while a globe projection is active).
*/
function SingleWorldZoomLimit({ enabled = true }: { enabled?: boolean }) {
const { map, isLoaded } = useMap();
useEffect(() => {
if (!map || !isLoaded) return;
if (!enabled) {
map.setMinZoom(0);
return;
}
const updateMinZoom = () => {
const width = map.getContainer().clientWidth;
if (width <= 0) return;
// MapLibre zoom is defined against a 512px world at zoom 0.
map.setMinZoom(Math.max(Math.log2(width / 512), 0));
};
updateMinZoom();
map.on("resize", updateMinZoom);
return () => {
map.off("resize", updateMinZoom);
map.setMinZoom(0);
};
}, [map, isLoaded, enabled]);
return null;
}
export {
FlightAirport,
FlightRoute,
FlightRoutes,
FlightMultiRoute,
SingleWorldZoomLimit,
airports,
resolveAirport,
getAirportInfo,
generateArcGeometry,
generateArcCoordinates,
};
export type {
FlightRouteAnimateConfig,
FlightRouteProps,
FlightAirportProps,
FlightRouteData,
FlightRoutesProps,
FlightMultiRouteProps,
};
export {
FlightTracker,
FlightRouteLabel,
FlightNetwork,
FlightRange,
AircraftTrail,
FlightFlow,
} from "./flight-visualizations";
export type {
FlightTrackerProps,
FlightRouteLabelAnimateConfig,
FlightRouteLabelMode,
FlightRouteLabelPosition,
FlightRouteLabelSize,
FlightRouteLabelProps,
FlightNetworkRoute,
FlightNetworkProps,
FlightRangeBand,
FlightRangeProps,
AircraftTrailAltitudeColorStop,
AircraftTrailPosition,
AircraftTrailProps,
FlightFlowRoute,
FlightFlowProps,
} from "./flight-visualizations";
src/registry/flight-airports.ts
/** Information about an airport */
export type AirportInfo = {
/** IATA airport code (e.g. "TPE") */
code: string;
/** Full airport name */
name: string;
/** City name */
city: string;
/** Country name */
country: string;
/** Latitude in degrees */
latitude: number;
/** Longitude in degrees */
longitude: number;
};
/** Reference to an airport: either an IATA code string or [longitude, latitude] tuple */
export type AirportRef = string | [number, number];
// prettier-ignore
export const airports: Record<string, AirportInfo> = {
HIR: { code: "HIR", name: "Honiara International Airport", city: "Honiara", country: "Solomon Islands", latitude: -9.428, longitude: 160.055 },
LAE: { code: "LAE", name: "Nadzab Tomodachi International Airport", city: "Lae", country: "Papua New Guinea", latitude: -6.568, longitude: 146.7265 },
POM: { code: "POM", name: "Port Moresby Jacksons International Airport", city: "Port Moresby", country: "Papua New Guinea", latitude: -9.4434, longitude: 147.22 },
GOH: { code: "GOH", name: "Nuuk International Airport", city: "Nuuk", country: "Greenland", latitude: 64.1911, longitude: -51.6791 },
SFJ: { code: "SFJ", name: "Kangerlussuaq International Airport", city: "Kangerlussuaq", country: "Greenland", latitude: 67.0104, longitude: -50.7153 },
THU: { code: "THU", name: "Pituffik Space Base", city: "Pituffik", country: "Greenland", latitude: 76.5306, longitude: -68.7005 },
AEY: { code: "AEY", name: "Akureyri International Airport", city: "Akureyri", country: "Iceland", latitude: 65.6566, longitude: -18.072 },
KEF: { code: "KEF", name: "Keflavik International Airport", city: "Reykjavík", country: "Iceland", latitude: 63.985, longitude: -22.6056 },
PRN: { code: "PRN", name: "Priština Adem Jashari International Airport", city: "Prishtina", country: "XK", latitude: 42.5728, longitude: 21.0358 },
YEG: { code: "YEG", name: "Edmonton International Airport", city: "Edmonton", country: "Canada", latitude: 53.3097, longitude: -113.58 },
YHZ: { code: "YHZ", name: "Halifax / Stanfield International Airport", city: "Halifax", country: "Canada", latitude: 44.8808, longitude: -63.5086 },
YLW: { code: "YLW", name: "Kelowna International Airport", city: "Kelowna", country: "Canada", latitude: 49.9561, longitude: -119.378 },
YOW: { code: "YOW", name: "Ottawa Macdonald-Cartier International Airport", city: "Ottawa", country: "Canada", latitude: 45.3225, longitude: -75.6692 },
YQB: { code: "YQB", name: "Quebec Jean Lesage International Airport", city: "Quebec", country: "Canada", latitude: 46.7911, longitude: -71.3933 },
YUL: { code: "YUL", name: "Montreal / Pierre Elliott Trudeau International Airport", city: "Montréal", country: "Canada", latitude: 45.4678, longitude: -73.7423 },
YVR: { code: "YVR", name: "Vancouver International Airport", city: "Vancouver", country: "Canada", latitude: 49.1939, longitude: -123.184 },
YWG: { code: "YWG", name: "Winnipeg / James Armstrong Richardson International Airport", city: "Winnipeg", country: "Canada", latitude: 49.91, longitude: -97.2399 },
YXE: { code: "YXE", name: "Saskatoon John G. Diefenbaker International Airport", city: "Saskatoon", country: "Canada", latitude: 52.1707, longitude: -106.7008 },
YYC: { code: "YYC", name: "Calgary International Airport", city: "Calgary", country: "Canada", latitude: 51.1188, longitude: -114.0099 },
YYJ: { code: "YYJ", name: "Victoria International Airport", city: "Victoria", country: "Canada", latitude: 48.6472, longitude: -123.4278 },
YYT: { code: "YYT", name: "St. John's International Airport", city: "St. John's", country: "Canada", latitude: 47.6186, longitude: -52.7519 },
YYZ: { code: "YYZ", name: "Toronto Pearson International Airport", city: "Toronto", country: "Canada", latitude: 43.6759, longitude: -79.6294 },
BJA: { code: "BJA", name: "Soummam–Abane Ramdane Airport", city: "Béjaïa", country: "Algeria", latitude: 36.7125, longitude: 5.0699 },
ALG: { code: "ALG", name: "Houari Boumediene Airport", city: "Algiers", country: "Algeria", latitude: 36.6939, longitude: 3.2145 },
DJG: { code: "DJG", name: "Tiska Djanet Airport", city: "Djanet", country: "Algeria", latitude: 24.2854, longitude: 9.4637 },
TMR: { code: "TMR", name: "Aguenar – Hadj Bey Akhamok Airport", city: "Tamanrasset", country: "Algeria", latitude: 22.811, longitude: 5.4508 },
GJL: { code: "GJL", name: "Jijel Ferhat Abbas Airport", city: "Tahir", country: "Algeria", latitude: 36.7941, longitude: 5.8737 },
AAE: { code: "AAE", name: "Annaba Rabah Bitat Airport", city: "Annaba", country: "Algeria", latitude: 36.8268, longitude: 7.8133 },
CZL: { code: "CZL", name: "Mohamed Boudiaf International Airport", city: "Constantine", country: "Algeria", latitude: 36.276, longitude: 6.6204 },
BLJ: { code: "BLJ", name: "Batna Mostefa Ben Boulaid Airport", city: "Batna", country: "Algeria", latitude: 35.7521, longitude: 6.3086 },
CFK: { code: "CFK", name: "Chlef Aboubakr Belkaid International Airport", city: "Chlef", country: "Algeria", latitude: 36.2166, longitude: 1.3411 },
TLM: { code: "TLM", name: "Zenata – Messali El Hadj Airport", city: "Zenata", country: "Algeria", latitude: 35.0127, longitude: -1.4571 },
ORN: { code: "ORN", name: "Oran Es-Sénia (Ahmed Ben Bella) International Airport", city: "Es-Sénia", country: "Algeria", latitude: 35.6206, longitude: -0.6225 },
BSK: { code: "BSK", name: "Biskra - Mohamed Khider Airport", city: "Biskra", country: "Algeria", latitude: 34.7932, longitude: 5.7389 },
COO: { code: "COO", name: "Cotonou Cadjehoun International Airport", city: "Cotonou", country: "Benin", latitude: 6.3572, longitude: 2.3843 },
OUA: { code: "OUA", name: "Ouagadougou Thomas Sankara International Airport", city: "Ouagadougou", country: "Burkina Faso", latitude: 12.3532, longitude: -1.5124 },
BOY: { code: "BOY", name: "Bobo Dioulasso Airport", city: "Bobo Dioulasso", country: "Burkina Faso", latitude: 11.1601, longitude: -4.331 },
ACC: { code: "ACC", name: "Kotoka International Airport", city: "Accra", country: "Ghana", latitude: 5.6052, longitude: -0.1668 },
TML: { code: "TML", name: "Yakubu Tali International Airport", city: "Tamale", country: "Ghana", latitude: 9.5539, longitude: -0.8661 },
KMS: { code: "KMS", name: "Prempeh I International Airport", city: "Kumasi", country: "Ghana", latitude: 6.7146, longitude: -1.5908 },
ABJ: { code: "ABJ", name: "Félix-Houphouët-Boigny International Airport", city: "Abidjan", country: "Côte d'Ivoire", latitude: 5.2614, longitude: -3.9263 },
ASK: { code: "ASK", name: "Yamoussoukro International Airport", city: "Yamoussoukro", country: "Côte d'Ivoire", latitude: 6.9032, longitude: -5.3656 },
ABV: { code: "ABV", name: "Nnamdi Azikiwe International Airport", city: "Abuja", country: "Nigeria", latitude: 9.0068, longitude: 7.2632 },
ABB: { code: "ABB", name: "Asaba International Airport", city: "Asaba", country: "Nigeria", latitude: 6.2042, longitude: 6.6653 },
BCU: { code: "BCU", name: "Sir Abubakar Tafawa Balewa Bauchi State International Airport", city: "Bauchi", country: "Nigeria", latitude: 10.4828, longitude: 9.744 },
ENU: { code: "ENU", name: "Akanu Ibiam International Airport", city: "Enegu", country: "Nigeria", latitude: 6.4737, longitude: 7.5605 },
ILR: { code: "ILR", name: "General Tunde Idiagbon International Airport", city: "Ilorin/Ogbomosho", country: "Nigeria", latitude: 8.4402, longitude: 4.4939 },
KAD: { code: "KAD", name: "Kaduna International Airport", city: "Kaduna", country: "Nigeria", latitude: 10.696, longitude: 7.3201 },
KAN: { code: "KAN", name: "Mallam Aminu Kano International Airport", city: "Kano", country: "Nigeria", latitude: 12.0456, longitude: 8.5236 },
MIU: { code: "MIU", name: "Maiduguri International Airport", city: "Maiduguri", country: "Nigeria", latitude: 11.8542, longitude: 13.0807 },
LOS: { code: "LOS", name: "Murtala Muhammed International Airport", city: "Lagos", country: "Nigeria", latitude: 6.5774, longitude: 3.3212 },
PHC: { code: "PHC", name: "Port Harcourt International Airport", city: "Port Harcourt", country: "Nigeria", latitude: 5.0155, longitude: 6.9496 },
SKO: { code: "SKO", name: "Sadiq Abubakar III International Airport", city: "Sokoto", country: "Nigeria", latitude: 12.9157, longitude: 5.2075 },
NIM: { code: "NIM", name: "Diori Hamani International Airport", city: "Niamey", country: "Niger", latitude: 13.4815, longitude: 2.1836 },
TUN: { code: "TUN", name: "Tunis Carthage International Airport", city: "Tunis", country: "Tunisia", latitude: 36.851, longitude: 10.2272 },
DJE: { code: "DJE", name: "Djerba Zarzis International Airport", city: "Mellita", country: "Tunisia", latitude: 33.8737, longitude: 10.7773 },
SFA: { code: "SFA", name: "Sfax Thyna International Airport", city: "Sfax", country: "Tunisia", latitude: 34.718, longitude: 10.691 },
LRL: { code: "LRL", name: "Niamtougou International Airport", city: "Niamtougou", country: "Togo", latitude: 9.7667, longitude: 1.0909 },
LFW: { code: "LFW", name: "Lomé–Tokoin International Airport", city: "Lomé", country: "Togo", latitude: 6.1656, longitude: 1.2545 },
BRU: { code: "BRU", name: "Brussels Airport", city: "Zaventem", country: "Belgium", latitude: 50.9014, longitude: 4.4844 },
CRL: { code: "CRL", name: "Brussels South Charleroi Airport", city: "Charleroi", country: "Belgium", latitude: 50.462, longitude: 4.4596 },
OST: { code: "OST", name: "Ostend-Bruges International Airport", city: "Oostende", country: "Belgium", latitude: 51.1998, longitude: 2.8747 },
BER: { code: "BER", name: "Berlin Brandenburg Airport", city: "Berlin", country: "Germany", latitude: 52.3617, longitude: 13.5023 },
DRS: { code: "DRS", name: "Dresden Airport", city: "Dresden", country: "Germany", latitude: 51.1341, longitude: 13.7678 },
ERF: { code: "ERF", name: "Erfurt-Weimar Airport", city: "Erfurt", country: "Germany", latitude: 50.9783, longitude: 10.9607 },
FRA: { code: "FRA", name: "Frankfurt Main Airport", city: "Frankfurt am Main", country: "Germany", latitude: 50.0267, longitude: 8.5584 },
FMO: { code: "FMO", name: "Münster Osnabrück Airport", city: "Greven", country: "Germany", latitude: 52.1338, longitude: 7.6885 },
HAM: { code: "HAM", name: "Hamburg Helmut Schmidt Airport", city: "Hamburg", country: "Germany", latitude: 53.6304, longitude: 9.9882 },
CGN: { code: "CGN", name: "Cologne Bonn Airport", city: "Köln (Cologne)", country: "Germany", latitude: 50.8659, longitude: 7.1427 },
DUS: { code: "DUS", name: "Düsseldorf Airport", city: "Düsseldorf", country: "Germany", latitude: 51.2895, longitude: 6.7668 },
MUC: { code: "MUC", name: "Munich Airport", city: "Munich", country: "Germany", latitude: 48.3538, longitude: 11.7861 },
NUE: { code: "NUE", name: "Nuremberg Airport", city: "Nuremberg", country: "Germany", latitude: 49.4987, longitude: 11.0781 },
LEJ: { code: "LEJ", name: "Leipzig/Halle Airport", city: "Schkeuditz", country: "Germany", latitude: 51.4207, longitude: 12.2327 },
STR: { code: "STR", name: "Stuttgart Airport", city: "Stuttgart", country: "Germany", latitude: 48.6899, longitude: 9.222 },
HAJ: { code: "HAJ", name: "Hannover Airport", city: "Hannover", country: "Germany", latitude: 52.4611, longitude: 9.6851 },
BRE: { code: "BRE", name: "Bremen Airport", city: "Bremen", country: "Germany", latitude: 53.0468, longitude: 8.7893 },
HHN: { code: "HHN", name: "Frankfurt-Hahn Airport", city: "Frankfurt am Main (Lautzenhausen)", country: "Germany", latitude: 49.9464, longitude: 7.2617 },
FMM: { code: "FMM", name: "Memmingen Allgau Airport", city: "Memmingen", country: "Germany", latitude: 47.9881, longitude: 10.2382 },
PAD: { code: "PAD", name: "Paderborn Lippstadt Airport", city: "Büren", country: "Germany", latitude: 51.6125, longitude: 8.6175 },
NRN: { code: "NRN", name: "Weeze (Niederrhein) Airport", city: "Weeze", country: "Germany", latitude: 51.6014, longitude: 6.1412 },
DTM: { code: "DTM", name: "Dortmund Airport", city: "Dortmund", country: "Germany", latitude: 51.5183, longitude: 7.6122 },
FDH: { code: "FDH", name: "Bodensee Airport Friedrichshafen", city: "Friedrichshafen", country: "Germany", latitude: 47.6713, longitude: 9.5115 },
FKB: { code: "FKB", name: "Karlsruhe Baden-Baden Airport", city: "Rheinmünster", country: "Germany", latitude: 48.7794, longitude: 8.0805 },
KSF: { code: "KSF", name: "Kassel Airport", city: "Calden", country: "Germany", latitude: 51.4184, longitude: 9.3916 },
EES: { code: "EES", name: "Berenice International Airport / Banas Cape Air Base", city: "Berenice Troglodytica", country: "Egypt", latitude: 23.9804, longitude: 35.4603 },
TLL: { code: "TLL", name: "Lennart Meri Tallinn Airport", city: "Tallinn", country: "Estonia", latitude: 59.4132, longitude: 24.8326 },
HEL: { code: "HEL", name: "Helsinki Vantaa Airport", city: "Helsinki (Vantaa)", country: "Finland", latitude: 60.3184, longitude: 24.9633 },
IVL: { code: "IVL", name: "Ivalo Airport", city: "Ivalo", country: "Finland", latitude: 68.6073, longitude: 27.4053 },
KTT: { code: "KTT", name: "Kittilä International Airport", city: "Kittilä", country: "Finland", latitude: 67.701, longitude: 24.8468 },
KUO: { code: "KUO", name: "Kuopio Airport", city: "Kuopio / Siilinjärvi", country: "Finland", latitude: 63.0071, longitude: 27.7978 },
LPP: { code: "LPP", name: "Lappeenranta Airport", city: "Lappeenranta", country: "Finland", latitude: 61.0446, longitude: 28.1447 },
MHQ: { code: "MHQ", name: "Mariehamn Airport", city: "Mariehamn", country: "Finland", latitude: 60.1222, longitude: 19.8982 },
OUL: { code: "OUL", name: "Oulu Airport", city: "Oulu / Oulunsalo", country: "Finland", latitude: 64.9301, longitude: 25.3546 },
RVN: { code: "RVN", name: "Rovaniemi Airport", city: "Rovaniemi", country: "Finland", latitude: 66.5633, longitude: 25.8298 },
TMP: { code: "TMP", name: "Tampere-Pirkkala Airport", city: "Tampere / Pirkkala", country: "Finland", latitude: 61.4141, longitude: 23.6044 },
TKU: { code: "TKU", name: "Turku Airport", city: "Turku", country: "Finland", latitude: 60.5141, longitude: 22.2628 },
VAA: { code: "VAA", name: "Vaasa Airport", city: "Vaasa", country: "Finland", latitude: 63.0502, longitude: 21.7625 },
BFS: { code: "BFS", name: "Belfast International Airport", city: "Belfast", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 54.6575, longitude: -6.2158 },
BHX: { code: "BHX", name: "Birmingham Airport", city: "Birmingham, West Midlands", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 52.4539, longitude: -1.748 },
MAN: { code: "MAN", name: "Manchester Airport", city: "Manchester, Greater Manchester", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 53.3494, longitude: -2.2795 },
CWL: { code: "CWL", name: "Cardiff International Airport", city: "Cardiff", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 51.3967, longitude: -3.3433 },
BRS: { code: "BRS", name: "Bristol Airport", city: "Bristol", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 51.3823, longitude: -2.7165 },
LPL: { code: "LPL", name: "Liverpool John Lennon Airport", city: "Liverpool", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 53.3349, longitude: -2.8496 },
LTN: { code: "LTN", name: "London Luton Airport", city: "Luton, Luton", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 51.8747, longitude: -0.3683 },
LGW: { code: "LGW", name: "London Gatwick Airport", city: "London", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 51.1487, longitude: -0.1857 },
LHR: { code: "LHR", name: "London Heathrow Airport", city: "London", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 51.4707, longitude: -0.4599 },
LBA: { code: "LBA", name: "Leeds Bradford Airport", city: "Leeds, West Yorkshire", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 53.8659, longitude: -1.6606 },
IOM: { code: "IOM", name: "Isle of Man Airport", city: "Castletown", country: "Isle of Man", latitude: 54.0831, longitude: -4.6239 },
NCL: { code: "NCL", name: "Newcastle International Airport", city: "Newcastle upon Tyne, Tyne and Wear", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 55.038, longitude: -1.6896 },
EMA: { code: "EMA", name: "East Midlands Airport", city: "Nottingham, Leicestershire", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 52.8311, longitude: -1.3281 },
ABZ: { code: "ABZ", name: "Aberdeen International Airport", city: "Aberdeen", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 57.2019, longitude: -2.1978 },
GLA: { code: "GLA", name: "Glasgow Airport", city: "Glasgow", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 55.8719, longitude: -4.4331 },
EDI: { code: "EDI", name: "Edinburgh Airport", city: "Edinburgh", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 55.9501, longitude: -3.3723 },
PIK: { code: "PIK", name: "Glasgow Prestwick Airport", city: "Prestwick, South Ayrshire", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 55.5015, longitude: -4.5772 },
STN: { code: "STN", name: "London Stansted Airport", city: "London, Essex", country: "United Kingdom of Great Britain and Northern Ireland", latitude: 51.885, longitude: 0.235 },
MPN: { code: "MPN", name: "Mount Pleasant Airport / RAF Mount Pleasant", city: "Mount Pleasant", country: "Falkland Islands (Malvinas)", latitude: -51.8226, longitude: -58.4458 },
AMS: { code: "AMS", name: "Amsterdam Airport Schiphol", city: "Amsterdam", country: "Netherlands, Kingdom of the", latitude: 52.3086, longitude: 4.7639 },
MST: { code: "MST", name: "Maastricht Aachen Airport", city: "Maastricht", country: "Netherlands, Kingdom of the", latitude: 50.9111, longitude: 5.7694 },
EIN: { code: "EIN", name: "Eindhoven Airport", city: "Eindhoven", country: "Netherlands, Kingdom of the", latitude: 51.4501, longitude: 5.3745 },
GRQ: { code: "GRQ", name: "Groningen Airport Eelde", city: "Groningen", country: "Netherlands, Kingdom of the", latitude: 53.1191, longitude: 6.5777 },
RTM: { code: "RTM", name: "Rotterdam The Hague Airport", city: "Rotterdam", country: "Netherlands, Kingdom of the", latitude: 51.9569, longitude: 4.4372 },
ORK: { code: "ORK", name: "Cork International Airport", city: "Cork", country: "Ireland", latitude: 51.8413, longitude: -8.4911 },
DUB: { code: "DUB", name: "Dublin Airport", city: "Dublin", country: "Ireland", latitude: 53.4287, longitude: -6.2621 },
NOC: { code: "NOC", name: "Ireland West Airport Knock", city: "Charlestown", country: "Ireland", latitude: 53.9104, longitude: -8.817 },
SNN: { code: "SNN", name: "Shannon Airport", city: "Shannon", country: "Ireland", latitude: 52.702, longitude: -8.9248 },
AAR: { code: "AAR", name: "Aarhus Airport", city: "Aarhus", country: "Denmark", latitude: 56.3033, longitude: 10.6183 },
BLL: { code: "BLL", name: "Billund Airport", city: "Billund", country: "Denmark", latitude: 55.7403, longitude: 9.157 },
CPH: { code: "CPH", name: "Copenhagen Kastrup Airport", city: "Copenhagen", country: "Denmark", latitude: 55.6179, longitude: 12.656 },
ODE: { code: "ODE", name: "Odense Hans Christian Andersen Airport", city: "Odense", country: "Denmark", latitude: 55.4753, longitude: 10.3272 },
FAE: { code: "FAE", name: "Vágar Airport", city: "Vágar", country: "Faroe Islands", latitude: 62.0633, longitude: -7.2758 },
AAL: { code: "AAL", name: "Aalborg Airport", city: "Aalborg", country: "Denmark", latitude: 57.0948, longitude: 9.8499 },
LUX: { code: "LUX", name: "Luxembourg-Findel International Airport", city: "Luxembourg", country: "Luxembourg", latitude: 49.6268, longitude: 6.2121 },
AES: { code: "AES", name: "Ålesund Airport", city: "Ålesund", country: "Norway", latitude: 62.5604, longitude: 6.1108 },
BOO: { code: "BOO", name: "Bodø Airport", city: "Bodø", country: "Norway", latitude: 67.2692, longitude: 14.3653 },
BGO: { code: "BGO", name: "Bergen Airport, Flesland", city: "Bergen", country: "Norway", latitude: 60.2934, longitude: 5.2181 },
KRS: { code: "KRS", name: "Kristiansand Airport", city: "Kristiansand(Kjevik)", country: "Norway", latitude: 58.2042, longitude: 8.0854 },
EVE: { code: "EVE", name: "Harstad/Narvik Airport", city: "Evenes", country: "Norway", latitude: 68.4913, longitude: 16.6781 },
OSL: { code: "OSL", name: "Oslo-Gardermoen International Airport", city: "Oslo (Gardermoen)", country: "Norway", latitude: 60.1939, longitude: 11.1004 },
TOS: { code: "TOS", name: "Tromsø Airport", city: "Tromsø", country: "Norway", latitude: 69.6833, longitude: 18.9189 },
TRF: { code: "TRF", name: "Sandefjord Airport, Torp", city: "Sandefjord(Torp)", country: "Norway", latitude: 59.1867, longitude: 10.2586 },
TRD: { code: "TRD", name: "Trondheim Airport, Værnes", city: "Trondheim", country: "Norway", latitude: 63.4578, longitude: 10.924 },
SVG: { code: "SVG", name: "Stavanger Airport, Sola", city: "Stavanger", country: "Norway", latitude: 58.8767, longitude: 5.6378 },
GDN: { code: "GDN", name: "Gdańsk Lech Wałęsa Airport", city: "Gdańsk", country: "Poland", latitude: 54.3776, longitude: 18.4662 },
KRK: { code: "KRK", name: "Kraków John Paul II International Airport", city: "Balice", country: "Poland", latitude: 50.0777, longitude: 19.7848 },
KTW: { code: "KTW", name: "Katowice Wojciech Korfanty International Airport", city: "Katowice", country: "Poland", latitude: 50.476, longitude: 19.0807 },
LUZ: { code: "LUZ", name: "Lublin Airport", city: "Lublin", country: "Poland", latitude: 51.2402, longitude: 22.7135 },
LCJ: { code: "LCJ", name: "Łódź Władysław Reymont Airport", city: "Łódź", country: "Poland", latitude: 51.7219, longitude: 19.3981 },
WMI: { code: "WMI", name: "Warsaw Modlin Airport", city: "Nowy Dwór Mazowiecki", country: "Poland", latitude: 52.4511, longitude: 20.6518 },
POZ: { code: "POZ", name: "Poznań-Ławica Airport", city: "Poznań", country: "Poland", latitude: 52.4216, longitude: 16.8234 },
RZE: { code: "RZE", name: "Rzeszów-Jasionka Airport", city: "Jasionka", country: "Poland", latitude: 50.1098, longitude: 22.0242 },
SZZ: { code: "SZZ", name: "Solidarity Szczecin–Goleniów Airport", city: "Szczecin(Glewice)", country: "Poland", latitude: 53.5847, longitude: 14.9022 },
WAW: { code: "WAW", name: "Warsaw Chopin Airport", city: "Warsaw", country: "Poland", latitude: 52.1657, longitude: 20.9671 },
WRO: { code: "WRO", name: "Copernicus Wrocław Airport", city: "Wrocław", country: "Poland", latitude: 51.1037, longitude: 16.8821 },
GOT: { code: "GOT", name: "Göteborg Landvetter Airport", city: "Göteborg", country: "Sweden", latitude: 57.6628, longitude: 12.2798 },
NYO: { code: "NYO", name: "Stockholm Skavsta Airport", city: "Nyköping", country: "Sweden", latitude: 58.7897, longitude: 16.9115 },
SCR: { code: "SCR", name: "Scandinavian Mountains Airport", city: "Malung-Sälen", country: "Sweden", latitude: 61.1651, longitude: 12.8335 },
MMX: { code: "MMX", name: "Malmö Sturup Airport", city: "Malmö", country: "Sweden", latitude: 55.5356, longitude: 13.3763 },
KRN: { code: "KRN", name: "Kiruna Airport", city: "Kiruna", country: "Sweden", latitude: 67.822, longitude: 20.3368 },
UME: { code: "UME", name: "Umeå Airport", city: "Umeå", country: "Sweden", latitude: 63.7918, longitude: 20.2828 },
OSD: { code: "OSD", name: "Åre Östersund Airport", city: "Östersund", country: "Sweden", latitude: 63.1935, longitude: 14.5042 },
VST: { code: "VST", name: "Stockholm Västerås Airport", city: "Stockholm / Västerås", country: "Sweden", latitude: 59.5894, longitude: 16.6336 },
LLA: { code: "LLA", name: "Luleå Airport", city: "Luleå", country: "Sweden", latitude: 65.5438, longitude: 22.122 },
ARN: { code: "ARN", name: "Stockholm-Arlanda Airport", city: "Stockholm", country: "Sweden", latitude: 59.6485, longitude: 17.9288 },
LPI: { code: "LPI", name: "Linköping City Airport", city: "Linköping", country: "Sweden", latitude: 58.4049, longitude: 15.6845 },
VBY: { code: "VBY", name: "Visby Airport", city: "Visby", country: "Sweden", latitude: 57.6628, longitude: 18.3462 },
LPX: { code: "LPX", name: "Liepāja International Airport", city: "Liepāja", country: "Latvia", latitude: 56.5175, longitude: 21.0969 },
RIX: { code: "RIX", name: "Riga International Airport", city: "Riga", country: "Latvia", latitude: 56.9208, longitude: 23.9707 },
KUN: { code: "KUN", name: "Kaunas International Airport", city: "Kaunas", country: "Lithuania", latitude: 54.964, longitude: 24.0858 },
PLQ: { code: "PLQ", name: "Palanga International Airport", city: "Palanga", country: "Lithuania", latitude: 55.9732, longitude: 21.0939 },
VNO: { code: "VNO", name: "Vilnius International Airport", city: "Vilnius", country: "Lithuania", latitude: 54.6341, longitude: 25.2858 },
BFN: { code: "BFN", name: "Bram Fischer International Airport", city: "Bloemfontein", country: "South Africa", latitude: -29.0927, longitude: 26.3024 },
CPT: { code: "CPT", name: "Cape Town International Airport", city: "Cape Town", country: "South Africa", latitude: -33.974, longitude: 18.6043 },
ELS: { code: "ELS", name: "King Phalo Airport", city: "East London", country: "South Africa", latitude: -33.0356, longitude: 27.8259 },
GRJ: { code: "GRJ", name: "George Airport", city: "George", country: "South Africa", latitude: -34.0056, longitude: 22.3789 },
KIM: { code: "KIM", name: "Kimberley Airport", city: "Kimberley", country: "South Africa", latitude: -28.8054, longitude: 24.7649 },
MQP: { code: "MQP", name: "Kruger Mpumalanga International Airport", city: "Mbombela", country: "South Africa", latitude: -25.3833, longitude: 31.1053 },
HLA: { code: "HLA", name: "Lanseria International Airport", city: "Johannesburg", country: "South Africa", latitude: -25.939, longitude: 27.9266 },
DUR: { code: "DUR", name: "King Shaka International Airport", city: "Durban", country: "South Africa", latitude: -29.6144, longitude: 31.1197 },
JNB: { code: "JNB", name: "O.R. Tambo International Airport", city: "Johannesburg", country: "South Africa", latitude: -26.1401, longitude: 28.2468 },
PLZ: { code: "PLZ", name: "Chief Dawid Stuurman International Airport", city: "Gqeberha (Port Elizabeth)", country: "South Africa", latitude: -33.9897, longitude: 25.6174 },
PTG: { code: "PTG", name: "Polokwane International Airport", city: "Polokwane", country: "South Africa", latitude: -23.8453, longitude: 29.4586 },
FRW: { code: "FRW", name: "Phillip Gaonwe Matante International Airport", city: "Francistown", country: "Botswana", latitude: -21.1592, longitude: 27.4688 },
BBK: { code: "BBK", name: "Kasane International Airport", city: "Kasane", country: "Botswana", latitude: -17.8317, longitude: 25.1662 },
MUB: { code: "MUB", name: "Maun International Airport", city: "Maun", country: "Botswana", latitude: -19.9705, longitude: 23.4314 },
GBE: { code: "GBE", name: "Sir Seretse Khama International Airport", city: "Gaborone", country: "Botswana", latitude: -24.5552, longitude: 25.9182 },
BZV: { code: "BZV", name: "Maya-Maya International Airport", city: "Brazzaville", country: "Congo", latitude: -4.2517, longitude: 15.253 },
PNR: { code: "PNR", name: "Antonio Agostinho-Neto International Airport", city: "Pointe Noire", country: "Congo", latitude: -4.816, longitude: 11.8866 },
MTS: { code: "MTS", name: "Matsapha International Airport", city: "Manzini", country: "Eswatini", latitude: -26.5289, longitude: 31.3076 },
SHO: { code: "SHO", name: "King Mswati III International Airport", city: "Mpaka", country: "Eswatini", latitude: -26.3586, longitude: 31.7169 },
BGF: { code: "BGF", name: "Bangui M'Poko International Airport", city: "Bangui", country: "Central African Republic", latitude: 4.3985, longitude: 18.5188 },
BSG: { code: "BSG", name: "Bata International Airport", city: "Bata", country: "Equatorial Guinea", latitude: 1.9055, longitude: 9.8057 },
GEM: { code: "GEM", name: "President Obiang Nguema International Airport", city: "Mengomeyén", country: "Equatorial Guinea", latitude: 1.6764, longitude: 11.0249 },
SSG: { code: "SSG", name: "Malabo International Airport", city: "Malabo", country: "Equatorial Guinea", latitude: 3.7553, longitude: 8.7087 },
MRU: { code: "MRU", name: "Sir Seewoosagur Ramgoolam International Airport", city: "Plaine Magnien", country: "Mauritius", latitude: -20.4302, longitude: 57.6836 },
DLA: { code: "DLA", name: "Douala International Airport", city: "Douala", country: "Cameroon", latitude: 4.0061, longitude: 9.7195 },
GOU: { code: "GOU", name: "Garoua International Airport", city: "Garoua", country: "Cameroon", latitude: 9.3348, longitude: 13.3721 },
NSI: { code: "NSI", name: "Yaoundé Nsimalen International Airport", city: "Yaoundé", country: "Cameroon", latitude: 3.7226, longitude: 11.5533 },
LVI: { code: "LVI", name: "Harry Mwanga Nkumbula International Airport", city: "Livingstone", country: "Zambia", latitude: -17.8215, longitude: 25.8196 },
LUN: { code: "LUN", name: "Kenneth Kaunda International Airport", city: "Lusaka", country: "Zambia", latitude: -15.3308, longitude: 28.4527 },
MFU: { code: "MFU", name: "Mfuwe International Airport", city: "Mfuwe", country: "Zambia", latitude: -13.2589, longitude: 31.9366 },
NLA: { code: "NLA", name: "Simon Mwansa Kapwepwe International Airport", city: "Ndola", country: "Zambia", latitude: -12.9651, longitude: 28.5156 },
HAH: { code: "HAH", name: "Prince Said Ibrahim International Airport", city: "Moroni", country: "Comoros", latitude: -11.5337, longitude: 43.2719 },
DZA: { code: "DZA", name: "Dzaoudzi Pamandzi International Airport", city: "Dzaoudzi", country: "Mayotte", latitude: -12.8093, longitude: 45.2818 },
RUN: { code: "RUN", name: "Roland Garros Airport", city: "Sainte-Marie", country: "Réunion", latitude: -20.8901, longitude: 55.5189 },
ZSE: { code: "ZSE", name: "Saint-Pierre Pierrefonds Airport", city: "Saint-Pierre", country: "Réunion", latitude: -21.3194, longitude: 55.4225 },
TNR: { code: "TNR", name: "Ivato International Airport", city: "Antananarivo", country: "Madagascar", latitude: -18.7969, longitude: 47.4788 },
TMM: { code: "TMM", name: "Toamasina Ambalamanasy Airport", city: "Toamasina", country: "Madagascar", latitude: -18.1135, longitude: 49.3923 },
MJN: { code: "MJN", name: "Amborovy Airport", city: "Mahajanga", country: "Madagascar", latitude: -15.6668, longitude: 46.3512 },
NBJ: { code: "NBJ", name: "Dr. Antonio Agostinho Neto International Airport", city: "Luanda (Ícolo e Bengo)", country: "Angola", latitude: -9.0507, longitude: 13.4991 },
LAD: { code: "LAD", name: "Quatro de Fevereiro International Airport", city: "Luanda", country: "Angola", latitude: -8.8584, longitude: 13.2312 },
POG: { code: "POG", name: "Port Gentil International Airport", city: "Port Gentil", country: "Gabon", latitude: -0.7117, longitude: 8.7544 },
LBV: { code: "LBV", name: "Libreville Leon M'ba International Airport", city: "Libreville", country: "Gabon", latitude: 0.459, longitude: 9.4121 },
MVB: { code: "MVB", name: "M'Vengue El Hadj Omar Bongo Ondimba International Airport", city: "Franceville", country: "Gabon", latitude: -1.6562, longitude: 13.438 },
TMS: { code: "TMS", name: "São Tomé International Airport", city: "São Tomé", country: "Sao Tome and Principe", latitude: 0.3782, longitude: 6.7122 },
BEW: { code: "BEW", name: "Beira International Airport", city: "Beira", country: "Mozambique", latitude: -19.7964, longitude: 34.9076 },
MPM: { code: "MPM", name: "Maputo Airport", city: "Maputo", country: "Mozambique", latitude: -25.9208, longitude: 32.5726 },
APL: { code: "APL", name: "Nampula Airport", city: "Nampula", country: "Mozambique", latitude: -15.1056, longitude: 39.2818 },
TET: { code: "TET", name: "Tete Airport", city: "Tete", country: "Mozambique", latitude: -16.1048, longitude: 33.6402 },
SEZ: { code: "SEZ", name: "Seychelles International Airport", city: "Victoria", country: "Seychelles", latitude: -4.6743, longitude: 55.5218 },
NDJ: { code: "NDJ", name: "N'Djamena International Airport", city: "N'Djamena", country: "Chad", latitude: 12.1337, longitude: 15.034 },
BUQ: { code: "BUQ", name: "Joshua Mqabuko Nkomo International Airport", city: "Bulawayo", country: "Zimbabwe", latitude: -20.0163, longitude: 28.6229 },
VFA: { code: "VFA", name: "Victoria Falls International Airport", city: "Victoria Falls", country: "Zimbabwe", latitude: -18.0974, longitude: 25.8369 },
HRE: { code: "HRE", name: "Robert Gabriel Mugabe International Airport", city: "Harare", country: "Zimbabwe", latitude: -17.9318, longitude: 31.0928 },
BLZ: { code: "BLZ", name: "Chileka International Airport", city: "Blantyre", country: "Malawi", latitude: -15.6772, longitude: 34.9723 },
LLW: { code: "LLW", name: "Kamuzu International Airport", city: "Lumbadzi", country: "Malawi", latitude: -13.7894, longitude: 33.781 },
MSU: { code: "MSU", name: "Moshoeshoe I International Airport", city: "Maseru(Mazenod)", country: "Lesotho", latitude: -29.4563, longitude: 27.5545 },
WVB: { code: "WVB", name: "Walvis Bay International Airport", city: "Walvis Bay(Rooikop)", country: "Namibia", latitude: -22.9793, longitude: 14.6471 },
WDH: { code: "WDH", name: "Hosea Kutako International Airport", city: "Windhoek", country: "Namibia", latitude: -22.4799, longitude: 17.4709 },
FIH: { code: "FIH", name: "Ndjili International Airport", city: "Kinshasa", country: "Congo, Democratic Republic of the", latitude: -4.3857, longitude: 15.4446 },
FKI: { code: "FKI", name: "Bangoka International Airport", city: "Kisangani", country: "Congo, Democratic Republic of the", latitude: 0.4816, longitude: 25.338 },
GOM: { code: "GOM", name: "Goma International Airport", city: "Goma", country: "Congo, Democratic Republic of the", latitude: -1.6668, longitude: 29.238 },
FBM: { code: "FBM", name: "Lubumbashi International Airport", city: "Lubumbashi", country: "Congo, Democratic Republic of the", latitude: -11.5915, longitude: 27.5308 },
BKO: { code: "BKO", name: "Modibo Keita International Airport", city: "Bamako", country: "Mali", latitude: 12.5335, longitude: -7.9499 },
TOM: { code: "TOM", name: "Tombouktou Airport", city: "Timbuktu", country: "Mali", latitude: 16.7305, longitude: -3.0076 },
BJL: { code: "BJL", name: "Banjul International Airport", city: "Yundum", country: "Gambia", latitude: 13.338, longitude: -16.6522 },
FUE: { code: "FUE", name: "Fuerteventura Airport", city: "El Matorral", country: "Spain", latitude: 28.4527, longitude: -13.8638 },
LPA: { code: "LPA", name: "Gran Canaria Airport", city: "Gran Canaria Island", country: "Spain", latitude: 27.9319, longitude: -15.3866 },
ACE: { code: "ACE", name: "César Manrique-Lanzarote Airport", city: "San Bartolomé", country: "Spain", latitude: 28.9455, longitude: -13.6052 },
TFS: { code: "TFS", name: "Tenerife Sur Airport", city: "Tenerife", country: "Spain", latitude: 28.0445, longitude: -16.5725 },
TFN: { code: "TFN", name: "Tenerife Norte-Ciudad de La Laguna Airport", city: "Tenerife", country: "Spain", latitude: 28.4828, longitude: -16.3417 },
FNA: { code: "FNA", name: "Lungi International Airport", city: "Freetown (Lungi-Town)", country: "Sierra Leone", latitude: 8.6164, longitude: -13.1955 },
OXB: { code: "OXB", name: "Osvaldo Vieira International Airport", city: "Bissau", country: "Guinea-Bissau", latitude: 11.8943, longitude: -15.6536 },
ROB: { code: "ROB", name: "Roberts International Airport", city: "Monrovia", country: "Liberia", latitude: 6.2338, longitude: -10.3623 },
AGA: { code: "AGA", name: "Al Massira Airport", city: "Agadir (Temsia)", country: "Morocco", latitude: 30.3225, longitude: -9.412 },
OZG: { code: "OZG", name: "Zagora Airport", city: "Zagora", country: "Morocco", latitude: 30.2658, longitude: -5.8608 },
FEZ: { code: "FEZ", name: "Fes Saïss International Airport", city: "Saïss", country: "Morocco", latitude: 33.9273, longitude: -4.978 },
OUD: { code: "OUD", name: "Oujda Angads Airport", city: "Ahl Angad", country: "Morocco", latitude: 34.7896, longitude: -1.926 },
SMW: { code: "SMW", name: "Smara Airport", city: "Smara", country: "Western Sahara", latitude: 26.732, longitude: -11.6837 },
BEM: { code: "BEM", name: "Beni Mellal Airport", city: "Oulad Yaich", country: "Morocco", latitude: 32.4019, longitude: -6.3159 },
RBA: { code: "RBA", name: "Rabat-Salé Airport", city: "Rabat", country: "Morocco", latitude: 34.0515, longitude: -6.7515 },
VIL: { code: "VIL", name: "Dakhla Airport", city: "Dakhla", country: "Western Sahara", latitude: 23.7183, longitude: -15.932 },
EUN: { code: "EUN", name: "Laayoune Hassan I International Airport", city: "El Aaiún", country: "Western Sahara", latitude: 27.1425, longitude: -13.2249 },
CMN: { code: "CMN", name: "Mohammed V International Airport", city: "Casablanca", country: "Morocco", latitude: 33.3675, longitude: -7.59 },
NDR: { code: "NDR", name: "Nador Al Aaroui International Airport", city: "Al Aaroui", country: "Morocco", latitude: 34.9888, longitude: -3.0282 },
RAK: { code: "RAK", name: "Marrakesh Menara Airport", city: "Marrakesh", country: "Morocco", latitude: 31.6048, longitude: -8.0358 },
OZZ: { code: "OZZ", name: "Ouarzazate International Airport", city: "Ouarzazate", country: "Morocco", latitude: 30.9391, longitude: -6.9094 },
TTU: { code: "TTU", name: "Sania Ramel Airport", city: "Tétouan", country: "Morocco", latitude: 35.5943, longitude: -5.32 },
TNG: { code: "TNG", name: "Tangier Ibn Battuta Airport", city: "Tangier", country: "Morocco", latitude: 35.7317, longitude: -5.9215 },
DSS: { code: "DSS", name: "Blaise Diagne International Airport", city: "Dakar", country: "Senegal", latitude: 14.6709, longitude: -17.0728 },
DKR: { code: "DKR", name: "Léopold Sédar Senghor International Airport", city: "Dakar", country: "Senegal", latitude: 14.7423, longitude: -17.4792 },
NKC: { code: "NKC", name: "Nouakchott–Oumtounsy International Airport", city: "Nouakchott", country: "Mauritania", latitude: 18.31, longitude: -15.9697 },
ATR: { code: "ATR", name: "Atar International Airport", city: "Atar", country: "Mauritania", latitude: 20.5058, longitude: -13.0437 },
NDB: { code: "NDB", name: "Nouadhibou International Airport", city: "Nouadhibou", country: "Mauritania", latitude: 20.9324, longitude: -17.0302 },
CKY: { code: "CKY", name: "Ahmed Sékou Touré International Airport", city: "Conakry", country: "Guinea", latitude: 9.5769, longitude: -13.612 },
SID: { code: "SID", name: "Amílcar Cabral International Airport", city: "Espargos", country: "Cabo Verde", latitude: 16.7414, longitude: -22.9494 },
BVC: { code: "BVC", name: "Aristides Pereira International Airport", city: "Rabil", country: "Cabo Verde", latitude: 16.1365, longitude: -22.8889 },
RAI: { code: "RAI", name: "Nelson Mandela International Airport", city: "Praia", country: "Cabo Verde", latitude: 14.9411, longitude: -23.4847 },
VXE: { code: "VXE", name: "Cesaria Evora International Airport", city: "São Pedro", country: "Cabo Verde", latitude: 16.8334, longitude: -25.0553 },
ADD: { code: "ADD", name: "Addis Ababa Bole International Airport", city: "Addis Ababa", country: "Ethiopia", latitude: 8.9779, longitude: 38.7993 },
DIR: { code: "DIR", name: "Aba Tenna Dejazmach Yilma International Airport", city: "Dire Dawa", country: "Ethiopia", latitude: 9.6235, longitude: 41.855 },
JIJ: { code: "JIJ", name: "Gerad Wilwal International Airport", city: "Jijiga", country: "Ethiopia", latitude: 9.3319, longitude: 42.9118 },
AWA: { code: "AWA", name: "Hawassa International Airport", city: "Hawassa", country: "Ethiopia", latitude: 7.1006, longitude: 38.3965 },
BJM: { code: "BJM", name: "Bujumbura Melchior Ndadaye International Airport", city: "Bujumbura", country: "Burundi", latitude: -3.324, longitude: 29.3185 },
BSA: { code: "BSA", name: "Bender Qassim International Airport", city: "Bosaso", country: "Somalia", latitude: 11.2752, longitude: 49.1392 },
HGA: { code: "HGA", name: "Egal International Airport", city: "Hargeisa", country: "Somalia", latitude: 9.5141, longitude: 44.0835 },
MGQ: { code: "MGQ", name: "Aden Adde International Airport", city: "Mogadishu", country: "Somalia", latitude: 2.0144, longitude: 45.3047 },
JIB: { code: "JIB", name: "Djibouti-Ambouli Airport", city: "Djibouti City", country: "Djibouti", latitude: 11.5473, longitude: 43.1595 },
RDL: { code: "RDL", name: "Bardawil International Airport", city: "El Hassana", country: "Egypt", latitude: 30.4107, longitude: 33.1553 },
DBB: { code: "DBB", name: "El Alamein International Airport", city: "El Alamein", country: "Egypt", latitude: 30.9243, longitude: 28.4616 },
AAC: { code: "AAC", name: "El Arish International Airport", city: "El Arish", country: "Egypt", latitude: 31.0553, longitude: 33.828 },
ATZ: { code: "ATZ", name: "Asyut International Airport", city: "Asyut", country: "Egypt", latitude: 27.046, longitude: 31.0128 },
HBE: { code: "HBE", name: "Alexandria International Airport", city: "Alexandria", country: "Egypt", latitude: 30.9325, longitude: 29.6964 },
CAI: { code: "CAI", name: "Cairo International Airport", city: "Cairo", country: "Egypt", latitude: 30.1115, longitude: 31.3967 },
CCE: { code: "CCE", name: "Capital International Airport", city: "New Cairo", country: "Egypt", latitude: 30.0647, longitude: 31.84 },
HRG: { code: "HRG", name: "Hurghada International Airport", city: "Hurghada", country: "Egypt", latitude: 27.1768, longitude: 33.7967 },
LXR: { code: "LXR", name: "Luxor International Airport", city: "Luxor", country: "Egypt", latitude: 25.671, longitude: 32.7064 },
RMF: { code: "RMF", name: "Marsa Alam International Airport", city: "Marsa Alam", country: "Egypt", latitude: 25.5555, longitude: 34.5924 },
MUH: { code: "MUH", name: "Mersa Matruh International Airport", city: "Marsa Matruh", country: "Egypt", latitude: 31.3243, longitude: 27.2223 },
PSD: { code: "PSD", name: "Port Said International Airport", city: "Port Said", country: "Egypt", latitude: 31.2793, longitude: 32.2406 },
SKV: { code: "SKV", name: "Saint Catherine International Airport", city: "Saint Catherine", country: "Egypt", latitude: 28.6843, longitude: 34.0644 },
HMB: { code: "HMB", name: "Sohag International Airport", city: "Suhaj", country: "Egypt", latitude: 26.3425, longitude: 31.743 },
SSH: { code: "SSH", name: "Sharm El Sheikh International Airport", city: "Sharm El Sheikh", country: "Egypt", latitude: 27.9773, longitude: 34.3947 },
ASW: { code: "ASW", name: "Aswan International Airport", city: "Aswan", country: "Egypt", latitude: 23.9611, longitude: 32.8204 },
SPX: { code: "SPX", name: "Sphinx International Airport", city: "Al Jiza", country: "Egypt", latitude: 30.1082, longitude: 30.8957 },
TCP: { code: "TCP", name: "Taba International Airport", city: "Taba", country: "Egypt", latitude: 29.5945, longitude: 34.7758 },
ASM: { code: "ASM", name: "Asmara International Airport", city: "Asmara", country: "Eritrea", latitude: 15.2919, longitude: 38.9107 },
JUB: { code: "JUB", name: "Juba International Airport", city: "Juba", country: "South Sudan", latitude: 4.872, longitude: 31.6011 },
EDL: { code: "EDL", name: "Eldoret International Airport", city: "Eldoret", country: "Kenya", latitude: 0.4045, longitude: 35.2389 },
NBO: { code: "NBO", name: "Jomo Kenyatta International Airport", city: "Nairobi", country: "Kenya", latitude: -1.3189, longitude: 36.9282 },
KIS: { code: "KIS", name: "Kisumu International Airport", city: "Kisumu", country: "Kenya", latitude: -0.0861, longitude: 34.7289 },
MBA: { code: "MBA", name: "Moi International Airport", city: "Mombasa", country: "Kenya", latitude: -4.0348, longitude: 39.5942 },
SRX: { code: "SRX", name: "Sirt International Airport / Ghardabiya Airbase", city: "Sirt", country: "Libya", latitude: 31.0586, longitude: 16.5971 },
BEN: { code: "BEN", name: "Benina International Airport", city: "Benina", country: "Libya", latitude: 32.0968, longitude: 20.2695 },
MJI: { code: "MJI", name: "Mitiga International Airport", city: "Tripoli", country: "Libya", latitude: 32.8918, longitude: 13.2879 },
LAQ: { code: "LAQ", name: "Al Abraq International Airport", city: "Al Albraq", country: "Libya", latitude: 32.789, longitude: 21.9549 },
KGL: { code: "KGL", name: "Kigali International Airport", city: "Kigali", country: "Rwanda", latitude: -1.9686, longitude: 30.1395 },
PZU: { code: "PZU", name: "Port Sudan New International Airport", city: "Port Sudan", country: "Sudan", latitude: 19.4346, longitude: 37.2341 },
KRT: { code: "KRT", name: "Khartoum International Airport", city: "Khartoum", country: "Sudan", latitude: 15.5895, longitude: 32.5532 },
DAR: { code: "DAR", name: "Julius Nyerere International Airport", city: "Dar es Salaam", country: "Tanzania, United Republic of", latitude: -6.8735, longitude: 39.2073 },
JRO: { code: "JRO", name: "Kilimanjaro International Airport", city: "Arusha", country: "Tanzania, United Republic of", latitude: -3.427, longitude: 37.0735 },
MWZ: { code: "MWZ", name: "Mwanza International Airport", city: "Mwanza", country: "Tanzania, United Republic of", latitude: -2.4466, longitude: 32.936 },
ZNZ: { code: "ZNZ", name: "Abeid Amani Karume International Airport", city: "Zanzibar", country: "Tanzania, United Republic of", latitude: -6.222, longitude: 39.2249 },
EBB: { code: "EBB", name: "Entebbe International Airport", city: "Entebbe", country: "Uganda", latitude: 0.0424, longitude: 32.4435 },
DHX: { code: "DHX", name: "Dhoho International Airport", city: "Kediri", country: "Indonesia", latitude: -7.7503, longitude: 111.9472 },
NMI: { code: "NMI", name: "Navi Mumbai International Airport", city: "Navi Mumbai", country: "India", latitude: 18.9846, longitude: 73.0653 },
DXN: { code: "DXN", name: "Noida International Airport", city: "Gautam Buddha Nagar", country: "India", latitude: 28.1799, longitude: 77.6118 },
ABQ: { code: "ABQ", name: "Albuquerque International Sunport", city: "Albuquerque", country: "United States of America", latitude: 35.04, longitude: -106.6089 },
ALB: { code: "ALB", name: "Albany International Airport", city: "Albany", country: "United States of America", latitude: 42.7483, longitude: -73.8017 },
ATL: { code: "ATL", name: "Hartsfield Jackson Atlanta International Airport", city: "Atlanta", country: "United States of America", latitude: 33.6367, longitude: -84.4281 },
AUS: { code: "AUS", name: "Austin Bergstrom International Airport", city: "Austin", country: "United States of America", latitude: 30.1975, longitude: -97.662 },
BDL: { code: "BDL", name: "Bradley International Airport", city: "Hartford", country: "United States of America", latitude: 41.9386, longitude: -72.688 },
BHM: { code: "BHM", name: "Birmingham-Shuttlesworth International Airport", city: "Birmingham", country: "United States of America", latitude: 33.5629, longitude: -86.7507 },
BNA: { code: "BNA", name: "Nashville International Airport", city: "Nashville", country: "United States of America", latitude: 36.1245, longitude: -86.6782 },
BOI: { code: "BOI", name: "Boise Air Terminal/Gowen Field", city: "Boise", country: "United States of America", latitude: 43.5644, longitude: -116.223 },
BOS: { code: "BOS", name: "Boston Logan International Airport", city: "Boston", country: "United States of America", latitude: 42.362, longitude: -71.0079 },
BUF: { code: "BUF", name: "Buffalo Niagara International Airport", city: "Buffalo", country: "United States of America", latitude: 42.9405, longitude: -78.7322 },
BUR: { code: "BUR", name: "Hollywood Burbank Airport", city: "Burbank", country: "United States of America", latitude: 34.2028, longitude: -118.3581 },
BWI: { code: "BWI", name: "Baltimore/Washington International Thurgood Marshall Airport", city: "Baltimore", country: "United States of America", latitude: 39.1754, longitude: -76.6683 },
CHS: { code: "CHS", name: "Charleston International Airport", city: "Charleston", country: "United States of America", latitude: 32.8962, longitude: -80.0382 },
CLE: { code: "CLE", name: "Cleveland Hopkins International Airport", city: "Cleveland", country: "United States of America", latitude: 41.4117, longitude: -81.8498 },
CLT: { code: "CLT", name: "Charlotte Douglas International Airport", city: "Charlotte", country: "United States of America", latitude: 35.214, longitude: -80.9431 },
CMH: { code: "CMH", name: "John Glenn Columbus International Airport", city: "Columbus", country: "United States of America", latitude: 39.998, longitude: -82.8919 },
COS: { code: "COS", name: "City of Colorado Springs Municipal Airport", city: "Colorado Springs", country: "United States of America", latitude: 38.8058, longitude: -104.701 },
CVG: { code: "CVG", name: "Cincinnati Northern Kentucky International Airport", city: "Cincinnati / Covington", country: "United States of America", latitude: 39.0488, longitude: -84.6678 },
DAL: { code: "DAL", name: "Dallas Love Field", city: "Dallas", country: "United States of America", latitude: 32.8448, longitude: -96.8477 },
DCA: { code: "DCA", name: "Ronald Reagan Washington National Airport", city: "Washington", country: "United States of America", latitude: 38.8521, longitude: -77.0377 },
DEN: { code: "DEN", name: "Denver International Airport", city: "Denver", country: "United States of America", latitude: 39.86, longitude: -104.6738 },
DFW: { code: "DFW", name: "Dallas Fort Worth International Airport", city: "Dallas-Fort Worth", country: "United States of America", latitude: 32.8968, longitude: -97.038 },
DSM: { code: "DSM", name: "Des Moines International Airport", city: "Des Moines", country: "United States of America", latitude: 41.534, longitude: -93.6567 },
DTW: { code: "DTW", name: "Detroit Metropolitan Wayne County Airport", city: "Detroit", country: "United States of America", latitude: 42.2138, longitude: -83.3538 },
ELP: { code: "ELP", name: "El Paso International Airport", city: "El Paso", country: "United States of America", latitude: 31.8099, longitude: -106.3756 },
EWR: { code: "EWR", name: "Newark Liberty International Airport", city: "Newark", country: "United States of America", latitude: 40.6894, longitude: -74.1705 },
FAT: { code: "FAT", name: "Fresno Yosemite International Airport", city: "Fresno", country: "United States of America", latitude: 36.7758, longitude: -119.718 },
FLL: { code: "FLL", name: "Fort Lauderdale Hollywood International Airport", city: "Fort Lauderdale", country: "United States of America", latitude: 26.0726, longitude: -80.1527 },
GEG: { code: "GEG", name: "Spokane International Airport", city: "Spokane", country: "United States of America", latitude: 47.6199, longitude: -117.534 },
GRR: { code: "GRR", name: "Gerald R. Ford International Airport", city: "Grand Rapids", country: "United States of America", latitude: 42.8808, longitude: -85.5228 },
GSO: { code: "GSO", name: "Piedmont Triad International Airport", city: "Greensboro", country: "United States of America", latitude: 36.0994, longitude: -79.9373 },
DSY: { code: "DSY", name: "Dara Sakor International Airport", city: "Ta Noun", country: "Cambodia", latitude: 10.9142, longitude: 103.2267 },
KTI: { code: "KTI", name: "Techo International Airport", city: "Phnom Penh (Boeng Khyang)", country: "Cambodia", latitude: 11.36, longitude: 104.9213 },
HOU: { code: "HOU", name: "William P. Hobby Airport", city: "Houston", country: "United States of America", latitude: 29.6453, longitude: -95.2768 },
IAD: { code: "IAD", name: "Washington Dulles International Airport", city: "Dulles", country: "United States of America", latitude: 38.9445, longitude: -77.4558 },
IAH: { code: "IAH", name: "George Bush Intercontinental Airport", city: "Houston", country: "United States of America", latitude: 29.9844, longitude: -95.3414 },
IND: { code: "IND", name: "Indianapolis International Airport", city: "Indianapolis", country: "United States of America", latitude: 39.7173, longitude: -86.2944 },
JAX: { code: "JAX", name: "Jacksonville International Airport", city: "Jacksonville", country: "United States of America", latitude: 30.4925, longitude: -81.6878 },
JFK: { code: "JFK", name: "John F. Kennedy International Airport", city: "New York", country: "United States of America", latitude: 40.6394, longitude: -73.7793 },
LAS: { code: "LAS", name: "Harry Reid International Airport", city: "Las Vegas", country: "United States of America", latitude: 36.0834, longitude: -115.1518 },
LAX: { code: "LAX", name: "Los Angeles International Airport", city: "Los Angeles", country: "United States of America", latitude: 33.9425, longitude: -118.408 },
LGA: { code: "LGA", name: "LaGuardia Airport", city: "New York", country: "United States of America", latitude: 40.7772, longitude: -73.8726 },
LGB: { code: "LGB", name: "Long Beach International Airport", city: "Long Beach", country: "United States of America", latitude: 33.8165, longitude: -118.1499 },
MCI: { code: "MCI", name: "Kansas City International Airport", city: "Kansas City", country: "United States of America", latitude: 39.3017, longitude: -94.7139 },
MCO: { code: "MCO", name: "Orlando International Airport", city: "Orlando", country: "United States of America", latitude: 28.4294, longitude: -81.309 },
MDW: { code: "MDW", name: "Chicago Midway International Airport", city: "Chicago", country: "United States of America", latitude: 41.786, longitude: -87.7524 },
MEM: { code: "MEM", name: "Memphis International Airport", city: "Memphis", country: "United States of America", latitude: 35.0438, longitude: -89.9763 },
MIA: { code: "MIA", name: "Miami International Airport", city: "Miami", country: "United States of America", latitude: 25.796, longitude: -80.2898 },
MKE: { code: "MKE", name: "General Mitchell International Airport", city: "Milwaukee", country: "United States of America", latitude: 42.9472, longitude: -87.8966 },
MSP: { code: "MSP", name: "Minneapolis–Saint Paul International Airport / Wold–Chamberlain Field", city: "Minneapolis", country: "United States of America", latitude: 44.8801, longitude: -93.2217 },
MSY: { code: "MSY", name: "Louis Armstrong New Orleans International Airport", city: "New Orleans", country: "United States of America", latitude: 29.9934, longitude: -90.2647 },
MYR: { code: "MYR", name: "Myrtle Beach International Airport", city: "Myrtle Beach", country: "United States of America", latitude: 33.6797, longitude: -78.9283 },
OAK: { code: "OAK", name: "San Francisco Bay Oakland International Airport", city: "Oakland", country: "United States of America", latitude: 37.7201, longitude: -122.2212 },
OKC: { code: "OKC", name: "OKC Will Rogers World Airport", city: "Oklahoma City", country: "United States of America", latitude: 35.3934, longitude: -97.5982 },
OMA: { code: "OMA", name: "Eppley Airfield", city: "Omaha", country: "United States of America", latitude: 41.3032, longitude: -95.8941 },
ONT: { code: "ONT", name: "Ontario International Airport", city: "Ontario", country: "United States of America", latitude: 34.056, longitude: -117.601 },
ORD: { code: "ORD", name: "Chicago O'Hare International Airport", city: "Chicago", country: "United States of America", latitude: 41.9786, longitude: -87.9048 },
ORF: { code: "ORF", name: "Norfolk International Airport", city: "Norfolk", country: "United States of America", latitude: 36.8953, longitude: -76.201 },
PBI: { code: "PBI", name: "Palm Beach International Airport", city: "West Palm Beach", country: "United States of America", latitude: 26.6832, longitude: -80.0956 },
PDX: { code: "PDX", name: "Portland International Airport", city: "Portland", country: "United States of America", latitude: 45.5887, longitude: -122.598 },
PHL: { code: "PHL", name: "Philadelphia International Airport", city: "Philadelphia", country: "United States of America", latitude: 39.8719, longitude: -75.2411 },
PHX: { code: "PHX", name: "Phoenix Sky Harbor International Airport", city: "Phoenix", country: "United States of America", latitude: 33.4353, longitude: -112.0059 },
PIE: { code: "PIE", name: "St. Petersburg Clearwater International Airport", city: "Pinellas Park", country: "United States of America", latitude: 27.9102, longitude: -82.6874 },
PIT: { code: "PIT", name: "Pittsburgh International Airport", city: "Pittsburgh", country: "United States of America", latitude: 40.4915, longitude: -80.2329 },
PNS: { code: "PNS", name: "Pensacola International Airport", city: "Pensacola", country: "United States of America", latitude: 30.4727, longitude: -87.1866 },
PSP: { code: "PSP", name: "Palm Springs International Airport", city: "Palm Springs", country: "United States of America", latitude: 33.8297, longitude: -116.507 },
PVD: { code: "PVD", name: "Rhode Island T. F. Green International Airport", city: "Providence/Warwick", country: "United States of America", latitude: 41.725, longitude: -71.4257 },
PWM: { code: "PWM", name: "Portland International Jetport", city: "Portland", country: "United States of America", latitude: 43.6462, longitude: -70.3093 },
RDU: { code: "RDU", name: "Raleigh-Durham International Airport", city: "Raleigh/Durham", country: "United States of America", latitude: 35.8787, longitude: -78.7873 },
RIC: { code: "RIC", name: "Richmond International Airport", city: "Richmond", country: "United States of America", latitude: 37.5052, longitude: -77.3197 },
RNO: { code: "RNO", name: "Reno Tahoe International Airport", city: "Reno", country: "United States of America", latitude: 39.4991, longitude: -119.768 },
ROC: { code: "ROC", name: "Frederick Douglass Greater Rochester International Airport", city: "Rochester", country: "United States of America", latitude: 43.1189, longitude: -77.6724 },
RSW: { code: "RSW", name: "Southwest Florida International Airport", city: "Fort Myers", country: "United States of America", latitude: 26.5347, longitude: -81.7528 },
SAN: { code: "SAN", name: "San Diego International Airport", city: "San Diego", country: "United States of America", latitude: 32.7336, longitude: -117.19 },
SAT: { code: "SAT", name: "San Antonio International Airport", city: "San Antonio", country: "United States of America", latitude: 29.5337, longitude: -98.4698 },
SAV: { code: "SAV", name: "Savannah Hilton Head International Airport", city: "Savannah", country: "United States of America", latitude: 32.1266, longitude: -81.2 },
SBD: { code: "SBD", name: "San Bernardino International Airport", city: "San Bernardino", country: "United States of America", latitude: 34.0967, longitude: -117.2366 },
SDF: { code: "SDF", name: "Louisville Muhammad Ali International Airport", city: "Louisville", country: "United States of America", latitude: 38.1706, longitude: -85.7351 },
SEA: { code: "SEA", name: "Seattle–Tacoma International Airport", city: "Seattle", country: "United States of America", latitude: 47.4479, longitude: -122.3103 },
SFB: { code: "SFB", name: "Orlando Sanford International Airport", city: "Orlando", country: "United States of America", latitude: 28.7743, longitude: -81.2346 },
SFO: { code: "SFO", name: "San Francisco International Airport", city: "San Francisco", country: "United States of America", latitude: 37.6198, longitude: -122.3748 },
SJC: { code: "SJC", name: "Norman Y. Mineta San Jose International Airport", city: "San Jose", country: "United States of America", latitude: 37.3625, longitude: -121.9292 },
SLC: { code: "SLC", name: "Salt Lake City International Airport", city: "Salt Lake City", country: "United States of America", latitude: 40.7889, longitude: -111.9799 },
SMF: { code: "SMF", name: "Sacramento International Airport", city: "Sacramento", country: "United States of America", latitude: 38.6954, longitude: -121.591 },
SNA: { code: "SNA", name: "John Wayne Orange County International Airport", city: "Santa Ana", country: "United States of America", latitude: 33.6751, longitude: -117.8693 },
SRQ: { code: "SRQ", name: "Sarasota Bradenton International Airport", city: "Sarasota/Bradenton", country: "United States of America", latitude: 27.3946, longitude: -82.5544 },
STL: { code: "STL", name: "St. Louis Lambert International Airport", city: "St Louis", country: "United States of America", latitude: 38.7487, longitude: -90.37 },
SYR: { code: "SYR", name: "Syracuse Hancock International Airport", city: "Syracuse", country: "United States of America", latitude: 43.1112, longitude: -76.1063 },
TPA: { code: "TPA", name: "Tampa International Airport", city: "Tampa", country: "United States of America", latitude: 27.9755, longitude: -82.5332 },
TUL: { code: "TUL", name: "Tulsa International Airport", city: "Tulsa", country: "United States of America", latitude: 36.1971, longitude: -95.8862 },
TUS: { code: "TUS", name: "Tucson International Airport", city: "Tucson", country: "United States of America", latitude: 32.115, longitude: -110.9381 },
TYS: { code: "TYS", name: "McGhee Tyson Airport", city: "Knoxville/Maryville", country: "United States of America", latitude: 35.811, longitude: -83.994 },
VNY: { code: "VNY", name: "Van Nuys Airport", city: "Van Nuys", country: "United States of America", latitude: 34.2098, longitude: -118.49 },
TIA: { code: "TIA", name: "Tirana International Airport Mother Teresa", city: "Rinas", country: "Albania", latitude: 41.4147, longitude: 19.7206 },
BOJ: { code: "BOJ", name: "Burgas Airport", city: "Burgas", country: "Bulgaria", latitude: 42.5699, longitude: 27.5152 },
PDV: { code: "PDV", name: "Plovdiv International Airport", city: "Plovdiv", country: "Bulgaria", latitude: 42.0678, longitude: 24.8508 },
SOF: { code: "SOF", name: "Sofia Airport", city: "Sofia", country: "Bulgaria", latitude: 42.6964, longitude: 23.4177 },
VAR: { code: "VAR", name: "Varna Airport", city: "Varna", country: "Bulgaria", latitude: 43.2321, longitude: 27.8251 },
ECN: { code: "ECN", name: "Ercan International Airport", city: "Tymbou (Kirklar)", country: "Cyprus", latitude: 35.1531, longitude: 33.5074 },
LCA: { code: "LCA", name: "Larnaca International Airport", city: "Larnaca", country: "Cyprus", latitude: 34.8751, longitude: 33.6249 },
PFO: { code: "PFO", name: "Paphos International Airport", city: "Paphos", country: "Cyprus", latitude: 34.718, longitude: 32.4857 },
DBV: { code: "DBV", name: "Dubrovnik Ruđer Bošković Airport", city: "Dubrovnik", country: "Croatia", latitude: 42.5622, longitude: 18.2655 },
PUY: { code: "PUY", name: "Pula Airport", city: "Pula", country: "Croatia", latitude: 44.8935, longitude: 13.9222 },
RJK: { code: "RJK", name: "Rijeka Airport", city: "Rijeka(Omišalj)", country: "Croatia", latitude: 45.2164, longitude: 14.5709 },
SPU: { code: "SPU", name: "Split Saint Jerome Airport", city: "Split", country: "Croatia", latitude: 43.5389, longitude: 16.298 },
ZAG: { code: "ZAG", name: "Zagreb Franjo Tuđman International Airport", city: "Velika Gorica", country: "Croatia", latitude: 45.7429, longitude: 16.0688 },
ZAD: { code: "ZAD", name: "Zadar Airport", city: "Zadar", country: "Croatia", latitude: 44.097, longitude: 15.3536 },
ALC: { code: "ALC", name: "Alicante-Elche Miguel Hernández Airport", city: "Alicante", country: "Spain", latitude: 38.2822, longitude: -0.5582 },
OVD: { code: "OVD", name: "Asturias Airport", city: "Ranón", country: "Spain", latitude: 43.5636, longitude: -6.0346 },
BIO: { code: "BIO", name: "Bilbao Airport", city: "Bilbao", country: "Spain", latitude: 43.3011, longitude: -2.9106 },
BCN: { code: "BCN", name: "Josep Tarradellas Barcelona-El Prat Airport", city: "Barcelona", country: "Spain", latitude: 41.2971, longitude: 2.0785 },
GRO: { code: "GRO", name: "Girona-Costa Brava Airport", city: "Girona", country: "Spain", latitude: 41.9046, longitude: 2.7618 },
IBZ: { code: "IBZ", name: "Ibiza Airport", city: "Ibiza (Eivissa)", country: "Spain", latitude: 38.8729, longitude: 1.3731 },
LEN: { code: "LEN", name: "León Int'l Airport", city: "La Virgen Del Camino", country: "Spain", latitude: 42.5907, longitude: -5.6534 },
MAD: { code: "MAD", name: "Adolfo Suárez Madrid–Barajas Airport", city: "Madrid", country: "Spain", latitude: 40.4934, longitude: -3.5722 },
AGP: { code: "AGP", name: "Málaga-Costa del Sol Airport", city: "Málaga", country: "Spain", latitude: 36.6749, longitude: -4.4991 },
MAH: { code: "MAH", name: "Menorca Airport", city: "Mahón (Maó)", country: "Spain", latitude: 39.8626, longitude: 4.2187 },
RMU: { code: "RMU", name: "Region of Murcia International Airport", city: "Corvera", country: "Spain", latitude: 37.8029, longitude: -1.1249 },
PMI: { code: "PMI", name: "Palma de Mallorca Airport", city: "Palma de Mallorca", country: "Spain", latitude: 39.5517, longitude: 2.7388 },
REU: { code: "REU", name: "Reus Airport", city: "Reus", country: "Spain", latitude: 41.1475, longitude: 1.1684 },
SCQ: { code: "SCQ", name: "Santiago-Rosalía de Castro Airport", city: "Santiago de Compostela", country: "Spain", latitude: 42.8963, longitude: -8.4151 },
VLC: { code: "VLC", name: "Valencia Airport", city: "Valencia", country: "Spain", latitude: 39.4892, longitude: -0.481 },
ZAZ: { code: "ZAZ", name: "Zaragoza Airport", city: "Zaragoza", country: "Spain", latitude: 41.6662, longitude: -1.0415 },
SVQ: { code: "SVQ", name: "Seville Airport", city: "Seville", country: "Spain", latitude: 37.418, longitude: -5.8931 },
BOD: { code: "BOD", name: "Bordeaux–Mérignac Airport", city: "Bordeaux", country: "France", latitude: 44.8287, longitude: -0.7154 },
TLS: { code: "TLS", name: "Toulouse-Blagnac Airport", city: "Toulouse/Blagnac", country: "France", latitude: 43.6291, longitude: 1.3638 },
BIA: { code: "BIA", name: "Bastia-Poretta International airport", city: "Bastia", country: "France", latitude: 42.5527, longitude: 9.4837 },
FSC: { code: "FSC", name: "Figari Sud-Corse Airport", city: "Figari", country: "France", latitude: 41.5018, longitude: 9.0971 },
CFE: { code: "CFE", name: "Clermont-Ferrand Auvergne airport", city: "Clermont-Ferrand", country: "France", latitude: 45.7867, longitude: 3.1692 },
LYS: { code: "LYS", name: "Lyon Saint-Exupéry Airport", city: "Colombier-Saugnieu, Rhône", country: "France", latitude: 45.726, longitude: 5.0901 },
MRS: { code: "MRS", name: "Marseille Provence Airport", city: "Marignane, Bouches-du-Rhône", country: "France", latitude: 43.4381, longitude: 5.2125 },
NCE: { code: "NCE", name: "Nice-Côte d'Azur Airport", city: "Nice, Alpes-Maritimes", country: "France", latitude: 43.6584, longitude: 7.2159 },
BVA: { code: "BVA", name: "Beauvais-Tillé airport", city: "Beauvais", country: "France", latitude: 49.4544, longitude: 2.1128 },
LBG: { code: "LBG", name: "Paris-Le Bourget International Airport", city: "Paris", country: "France", latitude: 48.9623, longitude: 2.4365 },
CDG: { code: "CDG", name: "Charles de Gaulle International Airport", city: "Paris (Roissy-en-France, Val-d'Oise)", country: "France", latitude: 49.009, longitude: 2.5541 },
ORY: { code: "ORY", name: "Paris-Orly Airport", city: "Paris (Orly, Val-de-Marne)", country: "France", latitude: 48.7233, longitude: 2.3794 },
LIL: { code: "LIL", name: "Lille Airport", city: "Lesquin", country: "France", latitude: 50.5666, longitude: 3.1024 },
BES: { code: "BES", name: "Brest Bretagne airport", city: "Brest", country: "France", latitude: 48.4479, longitude: -4.4185 },
BSL: { code: "BSL", name: "EuroAirport Basel–Mulhouse–Freiburg", city: "Bâle / Mulhouse", country: "France", latitude: 47.6007, longitude: 7.5211 },
SXB: { code: "SXB", name: "Strasbourg Airport", city: "Strasbourg", country: "France", latitude: 48.5383, longitude: 7.6282 },
ATH: { code: "ATH", name: "Athens Eleftherios Venizelos International Airport", city: "Spata-Artemida", country: "Greece", latitude: 37.9364, longitude: 23.9445 },
HER: { code: "HER", name: "Heraklion International Nikos Kazantzakis Airport", city: "Heraklion", country: "Greece", latitude: 35.3397, longitude: 25.1803 },
KGS: { code: "KGS", name: "Kos International Airport \"Ippokratis\"", city: "Kos Island", country: "Greece", latitude: 36.7945, longitude: 27.0911 },
CFU: { code: "CFU", name: "Corfu Ioannis Kapodistrias International Airport", city: "Kerkyra (Corfu)", country: "Greece", latitude: 39.6014, longitude: 19.9122 },
KVA: { code: "KVA", name: "Kavala Alexander the Great International Airport", city: "Kavala", country: "Greece", latitude: 40.9133, longitude: 24.6192 },
RHO: { code: "RHO", name: "Rhodes International Airport \"Diagoras\"", city: "Rhodes", country: "Greece", latitude: 36.4054, longitude: 28.0862 },
CHQ: { code: "CHQ", name: "Chania International Airport", city: "Souda", country: "Greece", latitude: 35.5312, longitude: 24.1507 },
JTR: { code: "JTR", name: "Santorini International Airport", city: "Santorini Island", country: "Greece", latitude: 36.4, longitude: 25.4786 },
SKG: { code: "SKG", name: "Thessaloniki Macedonia International Airport", city: "Thessaloniki", country: "Greece", latitude: 40.5193, longitude: 22.97 },
BUD: { code: "BUD", name: "Budapest Liszt Ferenc International Airport", city: "Budapest", country: "Hungary", latitude: 47.4302, longitude: 19.2624 },
DEB: { code: "DEB", name: "Debrecen International Airport", city: "Debrecen", country: "Hungary", latitude: 47.4895, longitude: 21.6163 },
PEV: { code: "PEV", name: "Pécs-Pogány International Airport", city: "Pécs", country: "Hungary", latitude: 45.9889, longitude: 18.242 },
BRI: { code: "BRI", name: "Bari Karol Wojtyła International Airport", city: "Bari", country: "Italy", latitude: 41.1389, longitude: 16.7606 },
PSR: { code: "PSR", name: "Abruzzo Airport", city: "Pescara", country: "Italy", latitude: 42.4311, longitude: 14.183 },
BDS: { code: "BDS", name: "Brindisi Airport", city: "Brindisi", country: "Italy", latitude: 40.6576, longitude: 17.947 },
SUF: { code: "SUF", name: "Lamezia Terme Sant'Eufemia International Airport", city: "Lamezia Terme (CZ)", country: "Italy", latitude: 38.9062, longitude: 16.246 },
CTA: { code: "CTA", name: "Catania-Fontanarossa Airport", city: "Catania", country: "Italy", latitude: 37.4668, longitude: 15.0664 },
PMO: { code: "PMO", name: "Falcone–Borsellino Airport", city: "Palermo", country: "Italy", latitude: 38.176, longitude: 13.091 },
CAG: { code: "CAG", name: "Cagliari Elmas Airport", city: "Cagliari", country: "Italy", latitude: 39.2515, longitude: 9.0543 },
OLB: { code: "OLB", name: "Olbia Costa Smeralda Airport", city: "Olbia (SS)", country: "Italy", latitude: 40.899, longitude: 9.5185 },
MXP: { code: "MXP", name: "Milan Malpensa International Airport", city: "Ferno (VA)", country: "Italy", latitude: 45.6306, longitude: 8.7281 },
BGY: { code: "BGY", name: "Il Caravaggio International Airport", city: "Orio al Serio (BG)", country: "Italy", latitude: 45.6694, longitude: 9.7089 },
TRN: { code: "TRN", name: "Turin Airport", city: "Caselle Torinese (TO)", country: "Italy", latitude: 45.2008, longitude: 7.6496 },
GOA: { code: "GOA", name: "Genoa Cristoforo Colombo Airport", city: "Genova (GE)", country: "Italy", latitude: 44.412, longitude: 8.8407 },
LIN: { code: "LIN", name: "Milano Linate Airport", city: "Segrate (MI)", country: "Italy", latitude: 45.4451, longitude: 9.2767 },
BLQ: { code: "BLQ", name: "Bologna Guglielmo Marconi Airport", city: "Bologna", country: "Italy", latitude: 44.5354, longitude: 11.2887 },
TSF: { code: "TSF", name: "Treviso Airport", city: "Treviso (TV)", country: "Italy", latitude: 45.6484, longitude: 12.1944 },
TRS: { code: "TRS", name: "Trieste Airport", city: "Ronchi dei Legionari/Trieste", country: "Italy", latitude: 45.8275, longitude: 13.4722 },
RMI: { code: "RMI", name: "Federico Fellini International Airport", city: "Rimini (RN)", country: "Italy", latitude: 44.02, longitude: 12.6122 },
VRN: { code: "VRN", name: "Verona Villafranca Valerio Catullo Airport", city: "Caselle (VR)", country: "Italy", latitude: 45.395, longitude: 10.8873 },
VCE: { code: "VCE", name: "Venice Marco Polo Airport", city: "Venezia (VE)", country: "Italy", latitude: 45.5053, longitude: 12.3519 },
CIA: { code: "CIA", name: "Ciampino–G. B. Pastine International Airport", city: "Rome", country: "Italy", latitude: 41.7988, longitude: 12.5953 },
FCO: { code: "FCO", name: "Rome–Fiumicino Leonardo da Vinci International Airport", city: "Rome", country: "Italy", latitude: 41.8045, longitude: 12.252 },
NAP: { code: "NAP", name: "Naples International Airport", city: "Napoli", country: "Italy", latitude: 40.886, longitude: 14.2908 },
PSA: { code: "PSA", name: "Pisa International Airport", city: "Pisa (PI)", country: "Italy", latitude: 43.6839, longitude: 10.3927 },
FLR: { code: "FLR", name: "Florence Airport, Peretola", city: "Firenze (FI)", country: "Italy", latitude: 43.8086, longitude: 11.2028 },
PEG: { code: "PEG", name: "Perugia San Francesco d'Assisi – Umbria International Airport", city: "Perugia (PG)", country: "Italy", latitude: 43.0959, longitude: 12.5132 },
LJU: { code: "LJU", name: "Ljubljana Jože Pučnik Airport", city: "Zgornji Brnik", country: "Slovenia", latitude: 46.2237, longitude: 14.4576 },
JCL: { code: "JCL", name: "České Budějovice South Bohemian Airport", city: "České Budějovice", country: "Czechia", latitude: 48.9482, longitude: 14.4283 },
KLV: { code: "KLV", name: "Karlovy Vary Airport", city: "Karlovy Vary", country: "Czechia", latitude: 50.203, longitude: 12.915 },
OSR: { code: "OSR", name: "Leoš Janáček Airport Ostrava", city: "Mošnov", country: "Czechia", latitude: 49.6963, longitude: 18.1111 },
PED: { code: "PED", name: "Pardubice Airport", city: "Pardubice", country: "Czechia", latitude: 50.015, longitude: 15.7398 },
PRG: { code: "PRG", name: "Václav Havel Airport Prague", city: "Prague", country: "Czechia", latitude: 50.1009, longitude: 14.2599 },
TLV: { code: "TLV", name: "Ben Gurion International Airport", city: "Tel Aviv", country: "Israel", latitude: 32.0114, longitude: 34.8867 },
ETM: { code: "ETM", name: "Ramon International Airport", city: "Eilat", country: "Israel", latitude: 29.727, longitude: 35.0141 },
MLA: { code: "MLA", name: "Malta International Airport", city: "Valletta", country: "Malta", latitude: 35.8459, longitude: 14.4915 },
GRZ: { code: "GRZ", name: "Graz Airport", city: "Feldkirchen bei Graz", country: "Austria", latitude: 46.9911, longitude: 15.4396 },
INN: { code: "INN", name: "Innsbruck Airport", city: "Innsbruck", country: "Austria", latitude: 47.2602, longitude: 11.344 },
KLU: { code: "KLU", name: "Klagenfurt Airport", city: "Klagenfurt am Wörthersee", country: "Austria", latitude: 46.6425, longitude: 14.3377 },
LNZ: { code: "LNZ", name: "Linz-Hörsching Airport", city: "Linz", country: "Austria", latitude: 48.2354, longitude: 14.1881 },
SZG: { code: "SZG", name: "Salzburg Airport", city: "Salzburg", country: "Austria", latitude: 47.7933, longitude: 13.0043 },
VIE: { code: "VIE", name: "Vienna International Airport", city: "Vienna", country: "Austria", latitude: 48.1103, longitude: 16.5697 },
FAO: { code: "FAO", name: "Faro - Gago Coutinho International Airport", city: "Faro", country: "Portugal", latitude: 37.0159, longitude: -7.9709 },
FNC: { code: "FNC", name: "Cristiano Ronaldo International Airport", city: "Funchal", country: "Portugal", latitude: 32.6978, longitude: -16.7746 },
PDL: { code: "PDL", name: "João Paulo II Airport", city: "Ponta Delgada", country: "Portugal", latitude: 37.7412, longitude: -25.6979 },
OPO: { code: "OPO", name: "Francisco de Sá Carneiro Airport", city: "Porto", country: "Portugal", latitude: 41.2481, longitude: -8.6814 },
LIS: { code: "LIS", name: "Lisbon Humberto Delgado Airport", city: "Lisbon", country: "Portugal", latitude: 38.7813, longitude: -9.1359 },
BNX: { code: "BNX", name: "Banja Luka International Airport", city: "Mahovljani", country: "Bosnia and Herzegovina", latitude: 44.9414, longitude: 17.2975 },
OMO: { code: "OMO", name: "Mostar International Airport", city: "Mostar", country: "Bosnia and Herzegovina", latitude: 43.2825, longitude: 17.8461 },
SJJ: { code: "SJJ", name: "Sarajevo International Airport", city: "Sarajevo", country: "Bosnia and Herzegovina", latitude: 43.8246, longitude: 18.3315 },
TZL: { code: "TZL", name: "Tuzla International Airport", city: "Dubrave Gornje", country: "Bosnia and Herzegovina", latitude: 44.4599, longitude: 18.7236 },
BCM: { code: "BCM", name: "Bacău George Enescu International Airport", city: "Bacău", country: "Romania", latitude: 46.5219, longitude: 26.9103 },
BBU: { code: "BBU", name: "Bucharest Băneasa Aurel Vlaicu International Airport", city: "Bucharest", country: "Romania", latitude: 44.5031, longitude: 26.1029 },
GHV: { code: "GHV", name: "Brașov-Ghimbav International Airport", city: "Brașov (Ghimbav)", country: "Romania", latitude: 45.7056, longitude: 25.5229 },
CND: { code: "CND", name: "Mihail Kogălniceanu International Airport", city: "Constanța", country: "Romania", latitude: 44.3622, longitude: 28.4883 },
CLJ: { code: "CLJ", name: "Avram Iancu Cluj International Airport", city: "Cluj-Napoca", country: "Romania", latitude: 46.786, longitude: 23.6857 },
CRA: { code: "CRA", name: "Craiova International Airport", city: "Craiova", country: "Romania", latitude: 44.3181, longitude: 23.8886 },
IAS: { code: "IAS", name: "Iaşi International Airport", city: "Iaşi", country: "Romania", latitude: 47.1796, longitude: 27.6214 },
OMR: { code: "OMR", name: "Oradea International Airport", city: "Oradea", country: "Romania", latitude: 47.0253, longitude: 21.9025 },
OTP: { code: "OTP", name: "Bucharest Henri Coandă International Airport", city: "Otopeni", country: "Romania", latitude: 44.5718, longitude: 26.1033 },
SBZ: { code: "SBZ", name: "Sibiu International Airport", city: "Sibiu", country: "Romania", latitude: 45.7858, longitude: 24.0867 },
SCV: { code: "SCV", name: "Suceava Ștefan cel Mare International Airport", city: "Suceava", country: "Romania", latitude: 47.6875, longitude: 26.3541 },
TSR: { code: "TSR", name: "Timișoara Traian Vuia International Airport", city: "Timişoara", country: "Romania", latitude: 45.8099, longitude: 21.3379 },
GVA: { code: "GVA", name: "Geneva Cointrin International Airport", city: "Geneva", country: "Switzerland", latitude: 46.2381, longitude: 6.109 },
ZRH: { code: "ZRH", name: "Zürich Airport", city: "Zurich", country: "Switzerland", latitude: 47.4581, longitude: 8.5481 },
ESB: { code: "ESB", name: "Esenboğa International Airport", city: "Ankara", country: "Türkiye", latitude: 40.1281, longitude: 32.9951 },
ADA: { code: "ADA", name: "Adana Şakirpaşa Airport", city: "Seyhan", country: "Türkiye", latitude: 36.9822, longitude: 35.2804 },
AYT: { code: "AYT", name: "Antalya International Airport", city: "Antalya", country: "Türkiye", latitude: 36.8987, longitude: 30.8005 },
GZT: { code: "GZT", name: "Gaziantep Oğuzeli International Airport", city: "Gaziantep", country: "Türkiye", latitude: 36.9472, longitude: 37.4787 },
KYA: { code: "KYA", name: "Konya Airport", city: "Konya", country: "Türkiye", latitude: 37.979, longitude: 32.5619 },
ASR: { code: "ASR", name: "Kayseri Erkilet International Airport", city: "Kayseri", country: "Türkiye", latitude: 38.7704, longitude: 35.4954 },
NAV: { code: "NAV", name: "Nevşehir Kapadokya Airport", city: "Nevşehir", country: "Türkiye", latitude: 38.7719, longitude: 34.5345 },
ISL: { code: "ISL", name: "İstanbul Atatürk Airport", city: "Istanbul(Bakırköy)", country: "Türkiye", latitude: 40.9719, longitude: 28.8237 },
ADB: { code: "ADB", name: "Adnan Menderes International Airport", city: "Gaziemir", country: "Türkiye", latitude: 38.2924, longitude: 27.157 },
DLM: { code: "DLM", name: "Dalaman International Airport", city: "Dalaman", country: "Türkiye", latitude: 36.7131, longitude: 28.7925 },
AOE: { code: "AOE", name: "Hasan Polatkan Airport", city: "Eskişehir", country: "Türkiye", latitude: 39.8116, longitude: 30.5193 },
GNY: { code: "GNY", name: "Şanlıurfa GAP Airport", city: "Şanlıurfa", country: "Türkiye", latitude: 37.4457, longitude: 38.8956 },
COV: { code: "COV", name: "Çukurova International Airport", city: "Tarsus", country: "Türkiye", latitude: 36.8915, longitude: 35.0712 },
EDO: { code: "EDO", name: "Balıkesir Koca Seyit Airport", city: "Edremit", country: "Türkiye", latitude: 39.5525, longitude: 27.0102 },
BJV: { code: "BJV", name: "Milas Bodrum International Airport", city: "Bodrum", country: "Türkiye", latitude: 37.2493, longitude: 27.664 },
SAW: { code: "SAW", name: "Istanbul Sabiha Gökçen International Airport", city: "Pendik, Istanbul", country: "Türkiye", latitude: 40.8986, longitude: 29.3092 },
IST: { code: "IST", name: "İstanbul Airport", city: "Istanbul", country: "Türkiye", latitude: 41.2749, longitude: 28.7321 },
RZV: { code: "RZV", name: "Rize–Artvin Airport", city: "Rize", country: "Türkiye", latitude: 41.1798, longitude: 40.8488 },
RMO: { code: "RMO", name: "Chişinău International Airport", city: "Chişinău", country: "Moldova, Republic of", latitude: 46.9277, longitude: 28.9317 },
OHD: { code: "OHD", name: "Ohrid St. Paul the Apostle Airport", city: "Ohrid", country: "North Macedonia", latitude: 41.18, longitude: 20.7423 },
SKP: { code: "SKP", name: "Skopje International Airport", city: "Ilinden", country: "North Macedonia", latitude: 41.9581, longitude: 21.6226 },
GIB: { code: "GIB", name: "Gibraltar Airport", city: "Gibraltar", country: "Gibraltar", latitude: 36.1517, longitude: -5.3498 },
BEG: { code: "BEG", name: "Belgrade Nikola Tesla Airport", city: "Belgrade", country: "Serbia", latitude: 44.8184, longitude: 20.3091 },
INI: { code: "INI", name: "Niš Constantine the Great Airport", city: "Niš", country: "Serbia", latitude: 43.3365, longitude: 21.8562 },
TGD: { code: "TGD", name: "Podgorica Airport / Podgorica Golubovci Airbase", city: "Podgorica", country: "Montenegro", latitude: 42.3594, longitude: 19.2519 },
BTS: { code: "BTS", name: "M. R. Štefánik Airport", city: "Bratislava", country: "Slovakia", latitude: 48.1702, longitude: 17.2127 },
PLS: { code: "PLS", name: "Providenciales International Airport", city: "Providenciales", country: "Turks and Caicos Islands", latitude: 21.7737, longitude: -72.2683 },
LRM: { code: "LRM", name: "Casa De Campo International Airport", city: "La Romana", country: "Dominican Republic", latitude: 18.4522, longitude: -68.9111 },
PUJ: { code: "PUJ", name: "Punta Cana International Airport", city: "Punta Cana", country: "Dominican Republic", latitude: 18.5671, longitude: -68.3646 },
SDQ: { code: "SDQ", name: "Las Américas International Airport", city: "Santo Domingo", country: "Dominican Republic", latitude: 18.4297, longitude: -69.6689 },
STI: { code: "STI", name: "Cibao International Airport", city: "Santiago", country: "Dominican Republic", latitude: 19.4041, longitude: -70.6044 },
GUA: { code: "GUA", name: "La Aurora International Airport", city: "Guatemala City", country: "Guatemala", latitude: 14.5829, longitude: -90.5275 },
SAP: { code: "SAP", name: "Ramón Villeda Morales International Airport", city: "San Pedro Sula", country: "Honduras", latitude: 15.4526, longitude: -87.9236 },
RTB: { code: "RTB", name: "Juan Manuel Gálvez International Airport", city: "Coxen Hole", country: "Honduras", latitude: 16.3168, longitude: -86.523 },
XPL: { code: "XPL", name: "Palmerola International Airport", city: "Palmerola", country: "Honduras", latitude: 14.3824, longitude: -87.6212 },
KIN: { code: "KIN", name: "Norman Manley International Airport", city: "Kingston", country: "Jamaica", latitude: 17.9357, longitude: -76.7875 },
MBJ: { code: "MBJ", name: "Sangster International Airport", city: "Montego Bay", country: "Jamaica", latitude: 18.5034, longitude: -77.9132 },
ACA: { code: "ACA", name: "General Juan N. Álvarez International Airport", city: "Acapulco", country: "Mexico", latitude: 16.7571, longitude: -99.7531 },
AGU: { code: "AGU", name: "Aguascalientes International Airport", city: "Aguascalientes", country: "Mexico", latitude: 21.6996, longitude: -102.3184 },
HUX: { code: "HUX", name: "Bahías de Huatulco International Airport", city: "Huatulco", country: "Mexico", latitude: 15.7754, longitude: -96.2605 },
CUL: { code: "CUL", name: "Bachigualato Federal International Airport", city: "Culiacán", country: "Mexico", latitude: 24.765, longitude: -107.4752 },
CJS: { code: "CJS", name: "Abraham González International Airport", city: "Ciudad Juárez", country: "Mexico", latitude: 31.6367, longitude: -106.4285 },
CUU: { code: "CUU", name: "General Roberto Fierro Villalobos International Airport", city: "Chihuahua", country: "Mexico", latitude: 28.7026, longitude: -105.9638 },
CZM: { code: "CZM", name: "Cozumel International Airport", city: "Cozumel", country: "Mexico", latitude: 20.5149, longitude: -86.9285 },
GDL: { code: "GDL", name: "Guadalajara International Airport", city: "Guadalajara", country: "Mexico", latitude: 20.5233, longitude: -103.3101 },
HMO: { code: "HMO", name: "General Ignacio L. Pesqueira International Airport", city: "Hermosillo", country: "Mexico", latitude: 29.0928, longitude: -111.053 },
BJX: { code: "BJX", name: "Guanajuato International Airport", city: "Silao", country: "Mexico", latitude: 20.9927, longitude: -101.4803 },
LTO: { code: "LTO", name: "Loreto International Airport", city: "Loreto", country: "Mexico", latitude: 25.9895, longitude: -111.3484 },
MID: { code: "MID", name: "Manuel Crescencio Rejón International Airport", city: "Mérida", country: "Mexico", latitude: 20.9305, longitude: -89.6455 },
MLM: { code: "MLM", name: "General Francisco J. Mujica International Airport", city: "Morelia", country: "Mexico", latitude: 19.8499, longitude: -101.025 },
MEX: { code: "MEX", name: "Mexico City Benito Juárez International Airport", city: "Mexico City", country: "Mexico", latitude: 19.4358, longitude: -99.0703 },
MTY: { code: "MTY", name: "Monterrey International Airport", city: "Monterrey", country: "Mexico", latitude: 25.7785, longitude: -100.107 },
MZT: { code: "MZT", name: "General Rafael Buelna International Airport", city: "Mazatlàn", country: "Mexico", latitude: 23.1628, longitude: -106.2645 },
OAX: { code: "OAX", name: "Xoxocotlán International Airport", city: "Oaxaca", country: "Mexico", latitude: 16.9988, longitude: -96.7261 },
PBC: { code: "PBC", name: "Hermanos Serdán International Airport", city: "Puebla", country: "Mexico", latitude: 19.1585, longitude: -98.3716 },
PVR: { code: "PVR", name: "Puerto Vallarta International Airport", city: "Puerto Vallarta", country: "Mexico", latitude: 20.6799, longitude: -105.2544 },
QRO: { code: "QRO", name: "Querétaro Intercontinental Airport", city: "Querétaro", country: "Mexico", latitude: 20.6188, longitude: -100.1864 },
SJD: { code: "SJD", name: "Los Cabos International Airport", city: "San José del Cabo", country: "Mexico", latitude: 23.1519, longitude: -109.7207 },
NLU: { code: "NLU", name: "Felipe Ángeles International Airport", city: "Mexico City", country: "Mexico", latitude: 19.7438, longitude: -99.0151 },
TIJ: { code: "TIJ", name: "General Abelardo L. Rodriguez International Airport", city: "Tijuana", country: "Mexico", latitude: 32.541, longitude: -116.97 },
TQO: { code: "TQO", name: "Felipe Carrillo Puerto International Airport Tulum", city: "Tulum", country: "Mexico", latitude: 20.1721, longitude: -87.6603 },
TLC: { code: "TLC", name: "Adolfo López Mateos International Airport", city: "Toluca", country: "Mexico", latitude: 19.3369, longitude: -99.5658 },
CUN: { code: "CUN", name: "Cancún International Airport", city: "Cancún", country: "Mexico", latitude: 21.0408, longitude: -86.8735 },
VSA: { code: "VSA", name: "Carlos Rovirosa Pérez International Airport", city: "Villahermosa", country: "Mexico", latitude: 17.9943, longitude: -92.8182 },
VER: { code: "VER", name: "General Heriberto Jara International Airport", city: "Veracruz", country: "Mexico", latitude: 19.1396, longitude: -96.1886 },
ZIH: { code: "ZIH", name: "Ixtapa-Zihuatanejo International Airport", city: "Ixtapa", country: "Mexico", latitude: 17.6018, longitude: -101.4606 },
MGA: { code: "MGA", name: "Augusto C. Sandino (Managua) International Airport", city: "Managua", country: "Nicaragua", latitude: 12.1415, longitude: -86.1682 },
PTY: { code: "PTY", name: "Tocumen International Airport", city: "Tocumen", country: "Panama", latitude: 9.0714, longitude: -79.3835 },
LIR: { code: "LIR", name: "Daniel Oduber Quirós International Airport", city: "Liberia", country: "Costa Rica", latitude: 10.5933, longitude: -85.5444 },
SJO: { code: "SJO", name: "Juan Santamaría International Airport", city: "San José (Alajuela)", country: "Costa Rica", latitude: 9.9939, longitude: -84.2088 },
SAL: { code: "SAL", name: "El Salvador International Airport Saint Óscar Arnulfo Romero y Galdámez", city: "San Salvador (San Luis Talpa)", country: "El Salvador", latitude: 13.4445, longitude: -89.0558 },
CAP: { code: "CAP", name: "Cap Haitien International Airport", city: "Cap Haitien", country: "Haiti", latitude: 19.7255, longitude: -72.2007 },
PAP: { code: "PAP", name: "Toussaint Louverture International Airport", city: "Port-au-Prince", country: "Haiti", latitude: 18.58, longitude: -72.2926 },
CMW: { code: "CMW", name: "Ignacio Agramonte International Airport", city: "Camaguey", country: "Cuba", latitude: 21.4199, longitude: -77.848 },
SCU: { code: "SCU", name: "Antonio Maceo International Airport", city: "Santiago", country: "Cuba", latitude: 19.9747, longitude: -75.8355 },
HAV: { code: "HAV", name: "José Martí International Airport", city: "Havana", country: "Cuba", latitude: 22.9892, longitude: -82.4091 },
HOG: { code: "HOG", name: "Frank Pais International Airport", city: "Holguin", country: "Cuba", latitude: 20.7851, longitude: -76.3155 },
SNU: { code: "SNU", name: "Abel Santamaria International Airport", city: "Santa Clara", country: "Cuba", latitude: 22.4922, longitude: -79.9431 },
VRA: { code: "VRA", name: "Juan Gualberto Gomez International Airport", city: "Matanzas", country: "Cuba", latitude: 23.0344, longitude: -81.4353 },
GCM: { code: "GCM", name: "Owen Roberts International Airport", city: "George Town", country: "Cayman Islands", latitude: 19.2928, longitude: -81.3577 },
FPO: { code: "FPO", name: "Grand Bahama International Airport", city: "Freeport", country: "Bahamas", latitude: 26.558, longitude: -78.6956 },
NAS: { code: "NAS", name: "Lynden Pindling International Airport", city: "Nassau", country: "Bahamas", latitude: 25.039, longitude: -77.4662 },
ZSA: { code: "ZSA", name: "San Salvador International Airport", city: "San Salvador", country: "Bahamas", latitude: 24.063, longitude: -74.5232 },
BZE: { code: "BZE", name: "Philip S. W. Goldson International Airport", city: "Belize City", country: "Belize", latitude: 17.54, longitude: -88.3036 },
RAR: { code: "RAR", name: "Rarotonga International Airport", city: "Avarua", country: "Cook Islands", latitude: -21.2027, longitude: -159.806 },
NAN: { code: "NAN", name: "Nadi International Airport", city: "Nadi", country: "Fiji", latitude: -17.7618, longitude: 177.4378 },
SUV: { code: "SUV", name: "Nausori International Airport", city: "Nausori", country: "Fiji", latitude: -18.0442, longitude: 178.5615 },
TBU: { code: "TBU", name: "Fua'amotu International Airport", city: "Nuku'alofa", country: "Tonga", latitude: -21.2414, longitude: -175.1492 },
VAV: { code: "VAV", name: "Vava'u International Airport", city: "Vava'u Island", country: "Tonga", latitude: -18.5853, longitude: -173.962 },
TRW: { code: "TRW", name: "Bonriki International Airport", city: "South Tarawa", country: "Kiribati", latitude: 1.3816, longitude: 173.147 },
WLS: { code: "WLS", name: "Hihifo Airport", city: "Wallis Island", country: "Wallis and Futuna", latitude: -13.2394, longitude: -176.1986 },
PHH: { code: "PHH", name: "Pokhara International Airport", city: "Pokhara", country: "Nepal", latitude: 28.1838, longitude: 84.0147 },
APW: { code: "APW", name: "Faleolo International Airport", city: "Apia", country: "Samoa", latitude: -13.83, longitude: -172.008 },
PPG: { code: "PPG", name: "Pago Pago International Airport", city: "Pago Pago", country: "American Samoa", latitude: -14.331, longitude: -170.71 },
PPT: { code: "PPT", name: "Fa'a'ā International Airport", city: "Papeete", country: "French Polynesia", latitude: -17.5535, longitude: -149.6069 },
VLI: { code: "VLI", name: "Bauerfield International Airport", city: "Port Vila", country: "Vanuatu", latitude: -17.6993, longitude: 168.32 },
NOU: { code: "NOU", name: "La Tontouta International Airport", city: "Nouméa (La Tontouta)", country: "New Caledonia", latitude: -22.0146, longitude: 166.213 },
AKL: { code: "AKL", name: "Auckland International Airport", city: "Auckland", country: "New Zealand", latitude: -37.012, longitude: 174.7863 },
CHC: { code: "CHC", name: "Christchurch International Airport", city: "Christchurch", country: "New Zealand", latitude: -43.489, longitude: 172.5321 },
ZQN: { code: "ZQN", name: "Queenstown Airport", city: "Queenstown", country: "New Zealand", latitude: -45.0192, longitude: 168.7464 },
WLG: { code: "WLG", name: "Wellington International Airport", city: "Wellington", country: "New Zealand", latitude: -41.3268, longitude: 174.8069 },
HEA: { code: "HEA", name: "Herat - Khwaja Abdullah Ansari International Airport", city: "Guzara", country: "Afghanistan", latitude: 34.21, longitude: 62.2283 },
KBL: { code: "KBL", name: "Kabul International Airport", city: "Kabul", country: "Afghanistan", latitude: 34.5659, longitude: 69.2123 },
KDH: { code: "KDH", name: "Ahmad Shah Baba International Airport", city: "Kandahar", country: "Afghanistan", latitude: 31.5058, longitude: 65.848 },
MZR: { code: "MZR", name: "Mazar-i-Sharif International Airport", city: "Mazar-i-Sharif", country: "Afghanistan", latitude: 36.7041, longitude: 67.2105 },
BAH: { code: "BAH", name: "Bahrain International Airport", city: "Manama", country: "Bahrain", latitude: 26.2673, longitude: 50.6376 },
OCS: { code: "OCS", name: "Corisco International Airport", city: "Corisco Island", country: "Equatorial Guinea", latitude: 0.9109, longitude: 9.3303 },
AHB: { code: "AHB", name: "Abha International Airport", city: "Abha", country: "Saudi Arabia", latitude: 18.2404, longitude: 42.6566 },
HOF: { code: "HOF", name: "Al-Ahsa International Airport", city: "Hofuf", country: "Saudi Arabia", latitude: 25.2853, longitude: 49.4852 },
DMM: { code: "DMM", name: "King Fahd International Airport", city: "Ad Dammam", country: "Saudi Arabia", latitude: 26.4691, longitude: 49.7982 },
DHA: { code: "DHA", name: "King Abdulaziz Air Base", city: "Dhahran", country: "Saudi Arabia", latitude: 26.2654, longitude: 50.152 },
ELQ: { code: "ELQ", name: "Prince Naif bin Abdulaziz International Airport", city: "Qassim", country: "Saudi Arabia", latitude: 26.3028, longitude: 43.7744 },
JED: { code: "JED", name: "King Abdulaziz International Airport", city: "Jeddah", country: "Saudi Arabia", latitude: 21.6802, longitude: 39.1574 },
MED: { code: "MED", name: "Prince Mohammad Bin Abdulaziz Airport", city: "Medina", country: "Saudi Arabia", latitude: 24.5534, longitude: 39.7051 },
NUM: { code: "NUM", name: "Neom Bay Airport", city: "Sharma", country: "Saudi Arabia", latitude: 27.9243, longitude: 35.2936 },
RUH: { code: "RUH", name: "King Khalid International Airport", city: "Riyadh", country: "Saudi Arabia", latitude: 24.9576, longitude: 46.6988 },
AJF: { code: "AJF", name: "Al-Jawf International Airport", city: "Al-Jawf", country: "Saudi Arabia", latitude: 29.7833, longitude: 40.1009 },
TUU: { code: "TUU", name: "Prince Sultan bin Abdulaziz International Airport", city: "Tabuk", country: "Saudi Arabia", latitude: 28.3711, longitude: 36.6249 },
TIF: { code: "TIF", name: "Taif International Airport", city: "Taif", country: "Saudi Arabia", latitude: 21.4847, longitude: 40.5441 },
YNB: { code: "YNB", name: "Prince Abdulmohsen Bin Abdulaziz International Airport", city: "Yanbu", country: "Saudi Arabia", latitude: 24.1442, longitude: 38.0634 },
ABD: { code: "ABD", name: "Abadan Ayatollah Jami International Airport", city: "Abadan", country: "Iran, Islamic Republic of", latitude: 30.3679, longitude: 48.2301 },
AWZ: { code: "AWZ", name: "Qasem Soleimani International Airport", city: "Ahvaz", country: "Iran, Islamic Republic of", latitude: 31.3364, longitude: 48.7638 },
KIH: { code: "KIH", name: "Kish International Airport", city: "Kish Island", country: "Iran, Islamic Republic of", latitude: 26.5254, longitude: 53.9805 },
IFN: { code: "IFN", name: "Isfahan Shahid Beheshti International Airport", city: "Isfahan", country: "Iran, Islamic Republic of", latitude: 32.7551, longitude: 51.8839 },
IKA: { code: "IKA", name: "Imam Khomeini International Airport", city: "Tehran", country: "Iran, Islamic Republic of", latitude: 35.4161, longitude: 51.1522 },
THR: { code: "THR", name: "Mehrabad International Airport", city: "Tehran", country: "Iran, Islamic Republic of", latitude: 35.6892, longitude: 51.3144 },
PYK: { code: "PYK", name: "Payam International Airport", city: "Karaj", country: "Iran, Islamic Republic of", latitude: 35.7761, longitude: 50.8267 },
BND: { code: "BND", name: "Bandar Abbas International Airport", city: "Bandar Abbas", country: "Iran, Islamic Republic of", latitude: 27.2183, longitude: 56.3778 },
KER: { code: "KER", name: "Ayatollah Hashemi Rafsanjani International Airport", city: "Kerman", country: "Iran, Islamic Republic of", latitude: 30.2713, longitude: 56.9497 },
GSM: { code: "GSM", name: "Qeshm International Airport", city: "Qeshm(Dayrestan)", country: "Iran, Islamic Republic of", latitude: 26.7546, longitude: 55.9024 },
XBJ: { code: "XBJ", name: "Birjand International Airport", city: "Birjand", country: "Iran, Islamic Republic of", latitude: 32.8965, longitude: 59.2813 },
MHD: { code: "MHD", name: "Mashhad International Airport", city: "Mashhad", country: "Iran, Islamic Republic of", latitude: 36.2348, longitude: 59.6429 },
SYZ: { code: "SYZ", name: "Shiraz Shahid Dastghaib International Airport", city: "Shiraz", country: "Iran, Islamic Republic of", latitude: 29.5392, longitude: 52.5898 },
ZAH: { code: "ZAH", name: "Zahedan International Airport", city: "Zahedan", country: "Iran, Islamic Republic of", latitude: 29.4757, longitude: 60.9062 },
AMM: { code: "AMM", name: "Queen Alia International Airport", city: "Amman", country: "Jordan", latitude: 31.7226, longitude: 35.9932 },
ADJ: { code: "ADJ", name: "Marka International (Amman Civil) Airport", city: "Amman", country: "Jordan", latitude: 31.9727, longitude: 35.9916 },
AQJ: { code: "AQJ", name: "King Hussein International Airport", city: "Aqaba", country: "Jordan", latitude: 29.6116, longitude: 35.0181 },
KWI: { code: "KWI", name: "Kuwait International Airport", city: "Kuwait City", country: "Kuwait", latitude: 29.2245, longitude: 47.9698 },
BEY: { code: "BEY", name: "Beirut Rafic Hariri International Airport", city: "Beirut", country: "Lebanon", latitude: 33.8198, longitude: 35.4874 },
DQM: { code: "DQM", name: "Duqm International Airport", city: "Duqm", country: "Oman", latitude: 19.5019, longitude: 57.6342 },
AUH: { code: "AUH", name: "Zayed International Airport", city: "Abu Dhabi", country: "United Arab Emirates", latitude: 24.441, longitude: 54.6492 },
AZI: { code: "AZI", name: "Al Bateen Executive Airport", city: "Abu Dhabi", country: "United Arab Emirates
[truncated — showing 100000 of 177411 characters]src/registry/flight-airports-utils.ts
import { airports, type AirportInfo, type AirportRef } from "./flight-airports";
/**
* Resolve an airport reference to [longitude, latitude] coordinates.
* Accepts either an IATA code string or a [lng, lat] tuple.
*/
export function resolveAirport(ref: AirportRef): [number, number] {
if (Array.isArray(ref)) return ref;
const airport = airports[ref.toUpperCase()];
if (!airport) {
throw new Error(
`Unknown airport code: "${ref}". Use a valid IATA code or pass [longitude, latitude] directly.`,
);
}
return [airport.longitude, airport.latitude];
}
/**
* Look up full airport info by IATA code.
* Returns undefined if the code is not found.
*/
export function getAirportInfo(code: string): AirportInfo | undefined {
return airports[code.toUpperCase()];
}
export type { AirportInfo, AirportRef };
export { airports };
src/registry/flight-visualizations.tsx
"use client";
import bearing from "@turf/bearing";
import greatCircle from "@turf/great-circle";
import * as MapLibreGL from "maplibre-gl";
import {
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
type ReactNode,
} from "react";
import { createPortal } from "react-dom";
import {
MapMarker,
MarkerContent,
MarkerLabel,
useMap,
} from "@/components/ui/map";
import type { AirportRef } from "./flight-airports";
import { resolveAirport } from "./flight-airports-utils";
type Coordinates = [number, number];
type RouteGeometry =
| { type: "LineString"; coordinates: Coordinates[] }
| { type: "MultiLineString"; coordinates: Coordinates[][] };
type GeoJsonFeature = {
type: "Feature";
properties: Record<string, unknown>;
geometry:
| RouteGeometry
| { type: "Point"; coordinates: Coordinates }
| { type: "Polygon"; coordinates: Coordinates[][] }
| { type: "MultiPolygon"; coordinates: Coordinates[][][] };
};
type GeoJsonCollection = {
type: "FeatureCollection";
features: GeoJsonFeature[];
};
type OverlayLayer = {
id: string;
source: string;
type: "line" | "circle" | "symbol" | "fill";
minzoom?: number;
maxzoom?: number;
layout?: Record<string, unknown>;
paint?: Record<string, unknown>;
filter?: MapLibreGL.FilterSpecification;
};
type OverlayImageResource = {
id: string;
signature: string;
create: () => ImageData;
pixelRatio?: number;
};
// Overlay layers keep paint/layout as open-ended records, while maplibre-gl v6
// types the setters per property key; derive the parameter types so dynamic
// updates compile against both v5 and v6.
type SetPaintPropertyArgs = Parameters<MapLibreGL.Map["setPaintProperty"]>;
type SetLayoutPropertyArgs = Parameters<MapLibreGL.Map["setLayoutProperty"]>;
const EMPTY_COLLECTION: GeoJsonCollection = {
type: "FeatureCollection",
features: [],
};
function clamp(value: number, min = 0, max = 1) {
return Math.min(max, Math.max(min, value));
}
function colorWithAlpha(color: string, alpha: number) {
const normalized = color.trim();
const shortHex = /^#([\da-f])([\da-f])([\da-f])$/i.exec(normalized);
const longHex = /^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(normalized);
const channels = shortHex
? shortHex.slice(1).map((channel) => Number.parseInt(channel + channel, 16))
: longHex
? longHex.slice(1).map((channel) => Number.parseInt(channel, 16))
: [15, 23, 42];
return `rgba(${channels[0]},${channels[1]},${channels[2]},${clamp(alpha)})`;
}
function normalizeLongitude(longitude: number) {
return ((((longitude + 180) % 360) + 360) % 360) - 180;
}
function resolveRef(ref: AirportRef): Coordinates | null {
try {
return resolveAirport(ref);
} catch (error) {
console.warn(error);
return null;
}
}
function normalizeRefKey(ref: AirportRef) {
return typeof ref === "string"
? `code:${ref.toUpperCase()}`
: `coordinate:${ref[0]},${ref[1]}`;
}
function resolveRefKey(key: string): Coordinates | null {
if (key.startsWith("code:")) return resolveRef(key.slice(5));
const [longitude, latitude] = key.slice(11).split(",").map(Number);
return resolveRef([longitude, latitude]);
}
function routeLabel(ref: AirportRef) {
if (typeof ref === "string") return ref.toUpperCase();
return `${ref[1].toFixed(2)}°, ${ref[0].toFixed(2)}°`;
}
function makeArcGeometry(
from: Coordinates,
to: Coordinates,
npoints = 100,
): RouteGeometry {
if (from[0] === to[0] && from[1] === to[1]) {
return { type: "LineString", coordinates: [from, to] };
}
try {
const geometry = greatCircle(from, to, { npoints }).geometry;
if (geometry.type === "MultiLineString") {
return {
type: "MultiLineString",
coordinates: geometry.coordinates as Coordinates[][],
};
}
return {
type: "LineString",
coordinates: geometry.coordinates as Coordinates[],
};
} catch {
return { type: "LineString", coordinates: [from, to] };
}
}
function flattenGeometry(geometry: RouteGeometry): Coordinates[] {
const segments =
geometry.type === "LineString"
? [geometry.coordinates]
: geometry.coordinates;
const result: Coordinates[] = [];
for (const segment of segments) {
for (const coordinate of segment) {
if (result.length === 0) {
result.push(coordinate);
continue;
}
const previous = result[result.length - 1];
let longitude = coordinate[0];
while (longitude - previous[0] > 180) longitude -= 360;
while (longitude - previous[0] < -180) longitude += 360;
result.push([longitude, coordinate[1]]);
}
}
return result;
}
function makeArcCoordinates(from: Coordinates, to: Coordinates, npoints = 100) {
return flattenGeometry(makeArcGeometry(from, to, npoints));
}
function positionAlong(
coordinates: Coordinates[],
progress: number,
): { coordinate: Coordinates; heading: number } {
if (coordinates.length === 0) {
return { coordinate: [0, 0], heading: 0 };
}
if (coordinates.length === 1) {
return {
coordinate: [normalizeLongitude(coordinates[0][0]), coordinates[0][1]],
heading: 0,
};
}
const scaled = clamp(progress) * (coordinates.length - 1);
const index = Math.min(Math.floor(scaled), coordinates.length - 2);
const localProgress = scaled - index;
const current = coordinates[index];
const next = coordinates[index + 1];
const coordinate: Coordinates = [
normalizeLongitude(current[0] + (next[0] - current[0]) * localProgress),
current[1] + (next[1] - current[1]) * localProgress,
];
const heading = bearing(
[normalizeLongitude(current[0]), current[1]],
[normalizeLongitude(next[0]), next[1]],
);
return { coordinate, heading };
}
function coordinatesToGeometry(coordinates: Coordinates[]): RouteGeometry {
if (coordinates.length < 2) {
return {
type: "LineString",
coordinates:
coordinates.length === 1 ? [coordinates[0], coordinates[0]] : [],
};
}
const segments: Coordinates[][] = [[]];
for (const coordinate of coordinates) {
const normalized: Coordinates = [
normalizeLongitude(coordinate[0]),
coordinate[1],
];
const segment = segments[segments.length - 1];
const previous = segment[segment.length - 1];
if (previous && Math.abs(normalized[0] - previous[0]) > 180) {
// Interpolate the latitude where the path crosses the antimeridian and
// extend both segments to ±180° so the split leaves no visible gap.
const seam = previous[0] > 0 ? 180 : -180;
const unwrapped = normalized[0] + (seam > 0 ? 360 : -360);
const span = unwrapped - previous[0];
const seamLatitude =
span === 0
? previous[1]
: previous[1] +
((seam - previous[0]) / span) * (normalized[1] - previous[1]);
if (previous[0] !== seam) segment.push([seam, seamLatitude]);
segments.push(
normalized[0] === -seam
? [normalized]
: [[-seam, seamLatitude], normalized],
);
} else {
segment.push(normalized);
}
}
const validSegments = segments.filter((segment) => segment.length >= 2);
if (validSegments.length <= 1) {
return {
type: "LineString",
coordinates: validSegments[0] ?? coordinates.slice(0, 2),
};
}
return { type: "MultiLineString", coordinates: validSegments };
}
function splitArc(
coordinates: Coordinates[],
progress: number,
): { completed: RouteGeometry; remaining: RouteGeometry } {
const scaled = clamp(progress) * Math.max(0, coordinates.length - 1);
const index = Math.min(
Math.floor(scaled),
Math.max(0, coordinates.length - 2),
);
const current = coordinates[index] ?? coordinates[0] ?? [0, 0];
const next = coordinates[index + 1] ?? current;
const local = scaled - index;
const splitPoint: Coordinates = [
current[0] + (next[0] - current[0]) * local,
current[1] + (next[1] - current[1]) * local,
];
const completed = [...coordinates.slice(0, index + 1), splitPoint];
const remaining = [splitPoint, ...coordinates.slice(index + 1)];
return {
completed: coordinatesToGeometry(completed),
remaining: coordinatesToGeometry(remaining),
};
}
function useGeoJsonOverlay(
sourceId: string,
data: GeoJsonCollection,
layers: readonly OverlayLayer[],
sourceOptions?: Partial<MapLibreGL.GeoJSONSourceSpecification>,
imageResource?: OverlayImageResource,
) {
const { map, isLoaded } = useMap();
const layerIds = layers.map((layer) => layer.id).join("|");
const imageId = imageResource?.id;
const imagePixelRatio = imageResource?.pixelRatio;
useEffect(() => {
if (!map || !isLoaded || map.getSource(sourceId)) return;
if (imageResource && !map.hasImage(imageResource.id)) {
map.addImage(imageResource.id, imageResource.create(), {
pixelRatio: imageResource.pixelRatio,
});
}
map.addSource(sourceId, {
type: "geojson",
data: data as MapLibreGL.GeoJSONSourceSpecification["data"],
...sourceOptions,
});
for (const layer of layers) {
map.addLayer(layer as unknown as MapLibreGL.AddLayerObject);
}
return () => {
try {
for (const layerId of layerIds.split("|").reverse()) {
if (map.getLayer(layerId)) map.removeLayer(layerId);
}
if (map.getSource(sourceId)) map.removeSource(sourceId);
if (imageId && map.hasImage(imageId)) map.removeImage(imageId);
} catch {
// The map or its style may already be disposed.
}
};
// Layer definitions are updated by the effects below without remounting.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [map, isLoaded, sourceId, layerIds, imageId, imagePixelRatio]);
useEffect(() => {
if (!map || !isLoaded || !imageResource) return;
const image = imageResource.create();
if (map.hasImage(imageResource.id)) {
map.updateImage(imageResource.id, image);
} else {
map.addImage(imageResource.id, image, {
pixelRatio: imageResource.pixelRatio,
});
}
// The signature intentionally controls image regeneration.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [imageResource?.signature, isLoaded, map]);
useEffect(() => {
if (!map || !isLoaded) return;
const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource;
source?.setData(data as MapLibreGL.GeoJSONSourceSpecification["data"]);
}, [data, isLoaded, map, sourceId]);
useEffect(() => {
if (!map || !isLoaded) return;
for (const layer of layers) {
if (!map.getLayer(layer.id)) continue;
if (layer.paint) {
for (const [property, value] of Object.entries(layer.paint)) {
map.setPaintProperty(
layer.id,
property as SetPaintPropertyArgs[1],
value as SetPaintPropertyArgs[2],
);
}
}
if (layer.layout) {
for (const [property, value] of Object.entries(layer.layout)) {
map.setLayoutProperty(
layer.id,
property as SetLayoutPropertyArgs[1],
value as SetLayoutPropertyArgs[2],
);
}
}
if (layer.filter) map.setFilter(layer.id, layer.filter);
}
}, [isLoaded, layers, map]);
}
function PlaneGlyph({ size = 24 }: { size?: number }) {
return (
<svg
aria-hidden="true"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
>
<path
d="M12 2.5c.75 0 1.35.6 1.35 1.35v5.1l6.1 3.65v1.75l-6.1-1.85v4.2l2.1 1.55v1.35L12 18.7l-3.45.9v-1.35l2.1-1.55v-4.2l-6.1 1.85V12.6l6.1-3.65v-5.1c0-.75.6-1.35 1.35-1.35Z"
fill="currentColor"
/>
</svg>
);
}
function EndpointMarker({
refValue,
showLabel,
}: {
refValue: AirportRef;
showLabel: boolean;
}) {
const coordinates = resolveRef(refValue);
if (!coordinates) return null;
return (
<MapMarker longitude={coordinates[0]} latitude={coordinates[1]}>
<MarkerContent>
<div className="size-3 rounded-full border-2 border-white bg-slate-950 shadow-md" />
{showLabel ? <MarkerLabel>{routeLabel(refValue)}</MarkerLabel> : null}
</MarkerContent>
</MapMarker>
);
}
export type FlightTrackerProps = {
from: AirportRef;
to: AirportRef;
/** Controlled flight progress from 0 to 1. */
progress: number;
id?: string;
completedColor?: string;
remainingColor?: string;
width?: number;
showAirports?: boolean;
showLabel?: boolean;
altitude?: number;
speed?: number;
showInfo?: boolean;
/** Custom content shown in the aircraft info card beside progress. */
children?: ReactNode;
icon?: ReactNode;
iconSize?: number;
npoints?: number;
};
/** A controlled, single-flight progress overlay with completed/remaining paths. */
function FlightTracker({
from,
to,
progress,
id: propId,
completedColor = "#0f172a",
remainingColor = "#94a3b8",
width = 3,
showAirports = true,
showLabel = true,
altitude,
speed,
showInfo = true,
children,
icon,
iconSize = 26,
npoints = 140,
}: FlightTrackerProps) {
const autoId = useId();
const id = propId ?? autoId;
const sourceId = `flight-tracker-source-${id}`;
const layerId = `flight-tracker-layer-${id}`;
const fromKey = normalizeRefKey(from);
const toKey = normalizeRefKey(to);
const fromCoordinates = useMemo(() => resolveRefKey(fromKey), [fromKey]);
const toCoordinates = useMemo(() => resolveRefKey(toKey), [toKey]);
const coordinates = useMemo(
() =>
fromCoordinates && toCoordinates
? makeArcCoordinates(fromCoordinates, toCoordinates, npoints)
: [],
[fromCoordinates, toCoordinates, npoints],
);
const safeProgress = clamp(progress);
const position = useMemo(
() => positionAlong(coordinates, safeProgress),
[coordinates, safeProgress],
);
const data = useMemo<GeoJsonCollection>(() => {
if (coordinates.length < 2) return EMPTY_COLLECTION;
const arc = splitArc(coordinates, safeProgress);
return {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: { segment: "remaining" },
geometry: arc.remaining,
},
{
type: "Feature",
properties: { segment: "completed" },
geometry: arc.completed,
},
],
};
}, [coordinates, safeProgress]);
const layers = useMemo<OverlayLayer[]>(
() => [
{
id: layerId,
source: sourceId,
type: "line",
layout: { "line-cap": "round", "line-join": "round" },
paint: {
"line-color": [
"match",
["get", "segment"],
"completed",
completedColor,
remainingColor,
],
"line-opacity": ["match", ["get", "segment"], "completed", 1, 0.5],
"line-width": width,
},
},
],
[completedColor, layerId, remainingColor, sourceId, width],
);
useGeoJsonOverlay(sourceId, data, layers);
if (!fromCoordinates || !toCoordinates || coordinates.length < 2) return null;
return (
<>
{showAirports ? (
<>
<EndpointMarker refValue={from} showLabel={showLabel} />
<EndpointMarker refValue={to} showLabel={showLabel} />
</>
) : null}
<MapMarker
longitude={position.coordinate[0]}
latitude={position.coordinate[1]}
>
<MarkerContent className="cursor-default">
<div
className="text-slate-950 drop-shadow-[0_2px_3px_rgba(255,255,255,0.95)]"
style={{ transform: `rotate(${position.heading}deg)` }}
>
{icon ?? <PlaneGlyph size={iconSize} />}
</div>
{showInfo ? (
<div className="absolute top-full left-1/2 mt-2 min-w-40 -translate-x-1/2 rounded-xl border border-black/10 bg-white/95 px-3 py-2 text-[10px] whitespace-nowrap text-slate-600 shadow-lg backdrop-blur">
<div className="flex items-center justify-between gap-3 font-semibold text-slate-950">
<div>
{children ?? `${routeLabel(from)} → ${routeLabel(to)}`}
</div>
<span>{Math.round(safeProgress * 100)}%</span>
</div>
{altitude !== undefined || speed !== undefined ? (
<div className="mt-1 flex items-center gap-2">
{altitude !== undefined ? (
<span>{altitude.toLocaleString()} ft</span>
) : null}
{speed !== undefined ? <span>{speed} kt</span> : null}
</div>
) : null}
</div>
) : null}
</MarkerContent>
</MapMarker>
</>
);
}
export type FlightRouteLabelSize = "sm" | "md" | "lg";
export type FlightRouteLabelMode = "route" | "aircraft";
export type FlightRouteLabelPosition = "top" | "right" | "bottom" | "left";
export type FlightRouteLabelAnimateConfig = {
/** Milliseconds for a complete route traversal (default: 7200). */
duration?: number;
/** Restart at the beginning after reaching the destination (default: true). */
loop?: boolean;
};
const FLIGHT_ROUTE_LABEL_SIZE_CLASSES: Record<FlightRouteLabelSize, string> = {
sm: "rounded px-1.5 py-0.5 text-[9px]",
md: "rounded-md px-2 py-1 text-[10px]",
lg: "rounded-lg px-3 py-1.5 text-xs",
};
const FLIGHT_ROUTE_LABEL_POSITION_CLASSES: Record<
FlightRouteLabelPosition,
string
> = {
top: "bottom-full left-1/2 mb-2 -translate-x-1/2",
right: "top-1/2 left-full ml-2 -translate-y-1/2",
bottom: "top-full left-1/2 mt-2 -translate-x-1/2",
left: "top-1/2 right-full mr-2 -translate-y-1/2",
};
export type FlightRouteLabelProps = {
from: AirportRef;
to: AirportRef;
children: ReactNode;
/** Fixed route annotation or an aircraft-following label (default: "route"). */
mode?: FlightRouteLabelMode;
position?: number;
rotate?: boolean;
offset?: [number, number];
/** Controls the label type scale and spacing (default: "md"). */
size?: FlightRouteLabelSize;
/** Label placement around the aircraft in aircraft mode (default: "right"). */
labelPosition?: FlightRouteLabelPosition;
/** Enables aircraft movement; only used in aircraft mode. */
animate?: boolean | FlightRouteLabelAnimateConfig;
/** Custom aircraft icon; only used in aircraft mode. */
icon?: ReactNode;
/** Aircraft icon size in pixels (default: 22). */
iconSize?: number;
className?: string;
npoints?: number;
};
function FlightRouteLabelCard({
children,
size,
className,
}: {
children: ReactNode;
size: FlightRouteLabelSize;
className?: string;
}) {
return (
<div
className={`border border-black/10 bg-white/94 font-semibold whitespace-nowrap text-slate-800 shadow-[0_4px_14px_rgba(15,23,42,0.12)] backdrop-blur ${FLIGHT_ROUTE_LABEL_SIZE_CLASSES[size]} ${className ?? ""}`}
>
{children}
</div>
);
}
function MovingFlightRouteLabel({
coordinates,
children,
position,
offset,
size,
labelPosition,
animate,
icon,
iconSize,
className,
}: {
coordinates: Coordinates[];
children: ReactNode;
position: number;
offset: [number, number];
size: FlightRouteLabelSize;
labelPosition: FlightRouteLabelPosition;
animate?: boolean | FlightRouteLabelAnimateConfig;
icon?: ReactNode;
iconSize: number;
className?: string;
}) {
const { map } = useMap();
const markerRef = useRef<MapLibreGL.Marker | null>(null);
const planeRef = useRef<HTMLDivElement | null>(null);
const frameRef = useRef<number | null>(null);
const [markerElement, setMarkerElement] = useState<HTMLDivElement | null>(
null,
);
const isAnimated = Boolean(animate);
const duration =
typeof animate === "object"
? Math.max(250, animate.duration ?? 7200)
: 7200;
const loop = typeof animate === "object" ? (animate.loop ?? true) : true;
const startProgress = clamp(position) >= 1 ? 0 : clamp(position);
const initialHeading = positionAlong(
coordinates,
isAnimated ? startProgress : clamp(position),
).heading;
const updateMarker = useCallback(
(progress: number) => {
const marker = markerRef.current;
if (!marker || coordinates.length < 2) return;
const current = positionAlong(coordinates, progress);
marker.setLngLat(current.coordinate);
if (planeRef.current) {
planeRef.current.style.transform = `rotate(${current.heading}deg)`;
}
},
[coordinates],
);
useEffect(() => {
if (!map || coordinates.length < 2) return;
let mounted = true;
const element = document.createElement("div");
const initial = positionAlong(coordinates, 0);
const marker = new MapLibreGL.Marker({
element,
anchor: "center",
rotationAlignment: "map",
pitchAlignment: "map",
})
.setLngLat(initial.coordinate)
.addTo(map);
markerRef.current = marker;
queueMicrotask(() => {
if (mounted) setMarkerElement(element);
});
return () => {
mounted = false;
marker.remove();
markerRef.current = null;
planeRef.current = null;
};
}, [coordinates, map]);
useEffect(() => {
if (isAnimated) return;
updateMarker(clamp(position));
}, [isAnimated, position, updateMarker]);
useEffect(() => {
if (!isAnimated || coordinates.length < 2) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
updateMarker(startProgress);
return;
}
const startedAt = performance.now();
const remainingSpan = Math.max(0.000001, 1 - startProgress);
const update = (now: number) => {
const elapsedProgress = (now - startedAt) / duration;
const rawProgress = startProgress + elapsedProgress;
if (rawProgress >= 1 && !loop) {
updateMarker(1);
return;
}
const progress = loop
? startProgress + ((rawProgress - startProgress) % remainingSpan)
: rawProgress;
updateMarker(progress);
frameRef.current = requestAnimationFrame(update);
};
frameRef.current = requestAnimationFrame(update);
return () => {
if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);
frameRef.current = null;
};
}, [coordinates, duration, isAnimated, loop, startProgress, updateMarker]);
if (coordinates.length < 2 || !markerElement) return null;
return createPortal(
<div
className="relative flex items-center justify-center text-slate-950"
style={{ width: iconSize, height: iconSize }}
>
<div
ref={planeRef}
className="flex items-center justify-center drop-shadow-[0_1px_2px_rgba(255,255,255,0.95)] will-change-transform"
style={{
width: iconSize,
height: iconSize,
transform: `rotate(${initialHeading}deg)`,
}}
>
{icon ?? <PlaneGlyph size={iconSize} />}
</div>
<div
className={`absolute ${FLIGHT_ROUTE_LABEL_POSITION_CLASSES[labelPosition]}`}
>
<div style={{ transform: `translate(${offset[0]}px, ${offset[1]}px)` }}>
<FlightRouteLabelCard size={size} className={className}>
{children}
</FlightRouteLabelCard>
</div>
</div>
</div>,
markerElement,
);
}
/** Places a fixed route annotation or a label that follows an aircraft. */
function FlightRouteLabel({
from,
to,
children,
mode = "route",
position = 0.5,
rotate = false,
offset = [0, 0],
size = "md",
labelPosition = "right",
animate,
icon,
iconSize = 22,
className,
npoints = 100,
}: FlightRouteLabelProps) {
const fromKey = normalizeRefKey(from);
const toKey = normalizeRefKey(to);
const coordinates = useMemo(() => {
const fromCoordinates = resolveRefKey(fromKey);
const toCoordinates = resolveRefKey(toKey);
return fromCoordinates && toCoordinates
? makeArcCoordinates(fromCoordinates, toCoordinates, npoints)
: [];
}, [fromKey, npoints, toKey]);
const routePosition = useMemo(
() =>
coordinates.length >= 2 ? positionAlong(coordinates, position) : null,
[coordinates, position],
);
if (mode === "aircraft") {
return (
<MovingFlightRouteLabel
coordinates={coordinates}
position={position}
offset={offset}
size={size}
labelPosition={labelPosition}
animate={animate}
icon={icon}
iconSize={iconSize}
className={className}
>
{children}
</MovingFlightRouteLabel>
);
}
if (!routePosition) return null;
return (
<MapMarker
longitude={routePosition.coordinate[0]}
latitude={routePosition.coordinate[1]}
offset={offset}
rotation={rotate ? routePosition.heading - 90 : 0}
rotationAlignment="map"
>
<MarkerContent className="cursor-default">
<FlightRouteLabelCard size={size} className={className}>
{children}
</FlightRouteLabelCard>
</MarkerContent>
</MapMarker>
);
}
export type FlightNetworkRoute = {
/** Route origin as an IATA code or [longitude, latitude]. */
from: AirportRef;
/** Route destination as an IATA code or [longitude, latitude]. */
to: AirportRef;
/**
* Relative non-negative route weight (default: 1). Higher values produce a
* thicker route and contribute more to the size of both connected nodes.
*/
value?: number;
};
export type FlightNetworkProps = {
/** Weighted origin/destination routes used to size routes and airport nodes. */
routes: readonly FlightNetworkRoute[];
id?: string;
color?: string;
highlightColor?: string;
minRouteWidth?: number;
maxRouteWidth?: number;
minNodeSize?: number;
maxNodeSize?: number;
showLabels?: boolean;
selectedAirport?: string | null;
onAirportSelect?: (airport: string | null) => void;
npoints?: number;
};
/** Weighted route network with linked-route highlighting on airport focus. */
function FlightNetwork({
routes,
id: propId,
color = "#64748b",
highlightColor = "#0f172a",
minRouteWidth = 0.75,
maxRouteWidth = 2.75,
minNodeSize = 3,
maxNodeSize = 7.5,
showLabels = true,
selectedAirport,
onAirportSelect,
npoints = 100,
}: FlightNetworkProps) {
const autoId = useId();
const id = propId ?? autoId;
const sourceId = `flight-network-source-${id}`;
const routeLayerId = `flight-network-routes-${id}`;
const nodeLayerId = `flight-network-nodes-${id}`;
const labelLayerId = `flight-network-labels-${id}`;
const [hoveredAirport, setHoveredAirport] = useState<string | null>(null);
const focus = hoveredAirport ?? selectedAirport ?? null;
const routeSignature = JSON.stringify(routes);
const networkData = useMemo(() => {
const nodeValues = new Map<
string,
{ coordinate: Coordinates; value: number }
>();
const routeFeatures: GeoJsonFeature[] = [];
for (const route of routes) {
const fromCoordinates = resolveRef(route.from);
const toCoordinates = resolveRef(route.to);
if (!fromCoordinates || !toCoordinates) continue;
const from = routeLabel(route.from);
const to = routeLabel(route.to);
const value = Math.max(0, route.value ?? 1);
routeFeatures.push({
type: "Feature",
properties: {
kind: "route",
from,
to,
value,
focused: focus !== null,
active: focus !== null && (focus === from || focus === to),
},
geometry: makeArcGeometry(fromCoordinates, toCoordinates, npoints),
});
for (const [key, coordinate] of [
[from, fromCoordinates],
[to, toCoordinates],
] as const) {
const current = nodeValues.get(key);
nodeValues.set(key, {
coordinate,
value: (current?.value ?? 0) + value,
});
}
}
const nodeFeatures: GeoJsonFeature[] = Array.from(nodeValues).map(
([key, node]) => ({
type: "Feature",
properties: {
kind: "airport",
key,
label: key,
value: node.value,
focused: focus !== null,
active: focus === key,
},
geometry: { type: "Point", coordinates: node.coordinate },
}),
);
return {
collection: {
type: "FeatureCollection",
features: [...routeFeatures, ...nodeFeatures],
} satisfies GeoJsonCollection,
maxRouteValue: Math.max(
1,
...routeFeatures.map((feature) => Number(feature.properties.value)),
),
maxNodeValue: Math.max(
1,
...nodeFeatures.map((feature) => Number(feature.properties.value)),
),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [routeSignature, focus, npoints]);
const data = networkData.collection;
const maxValue = networkData.maxRouteValue;
const nodeMaxValue = networkData.maxNodeValue;
const layers = useMemo<OverlayLayer[]>(
() => [
{
id: routeLayerId,
source: sourceId,
type: "line",
filter: ["==", ["get", "kind"], "route"],
layout: { "line-cap": "round", "line-join": "round" },
paint: {
"line-color": [
"case",
["all", ["get", "focused"], ["get", "active"]],
highlightColor,
color,
],
"line-opacity": [
"case",
["!", ["get", "focused"]],
0.46,
["get", "active"],
0.95,
0.08,
],
"line-width": [
"interpolate",
["linear"],
["sqrt", ["get", "value"]],
0,
minRouteWidth,
Math.sqrt(maxValue),
maxRouteWidth,
],
},
},
{
id: nodeLayerId,
source: sourceId,
type: "circle",
filter: ["==", ["get", "kind"], "airport"],
paint: {
"circle-color": [
"case",
["all", ["get", "focused"], ["get", "active"]],
highlightColor,
color,
],
"circle-opacity": [
"case",
["!", ["get", "focused"]],
0.95,
["get", "active"],
1,
0.2,
],
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 1.5,
"circle-radius": [
"interpolate",
["linear"],
["sqrt", ["get", "value"]],
0,
minNodeSize,
Math.sqrt(nodeMaxValue),
maxNodeSize,
],
},
},
{
id: labelLayerId,
source: sourceId,
type: "symbol",
filter: ["==", ["get", "kind"], "airport"],
layout: {
visibility: showLabels ? "visible" : "none",
"text-field": ["get", "label"],
"text-size": 9.5,
"text-offset": [0, 1.15],
"text-anchor": "top",
"text-allow-overlap": false,
},
paint: {
"text-color": "#0f172a",
"text-halo-color": "#ffffff",
"text-halo-width": 1.25,
},
},
],
[
color,
highlightColor,
labelLayerId,
maxRouteWidth,
maxValue,
minNodeSize,
minRouteWidth,
nodeLayerId,
nodeMaxValue,
maxNodeSize,
routeLayerId,
showLabels,
sourceId,
],
);
const { map, isLoaded } = useMap();
useGeoJsonOverlay(sourceId, data, layers);
useEffect(() => {
if (!map || !isLoaded || !map.getLayer(nodeLayerId)) return;
const onEnter = (event: MapLibreGL.MapLayerMouseEvent) => {
map.getCanvas().style.cursor = "pointer";
const key = event.features?.[0]?.properties?.key;
setHoveredAirport(typeof key === "string" ? key : null);
};
const onLeave = () => {
map.getCanvas().style.cursor = "";
setHoveredAirport(null);
};
const onClick = (event: MapLibreGL.MapLayerMouseEvent) => {
const key = event.features?.[0]?.properties?.key;
onAirportSelect?.(typeof key === "string" ? key : null);
};
map.on("mouseenter", nodeLayerId, onEnter);
map.on("mouseleave", nodeLayerId, onLeave);
map.on("click", nodeLayerId, onClick);
return () => {
map.off("mouseenter", nodeLayerId, onEnter);
map.off("mouseleave", nodeLayerId, onLeave);
map.off("click", nodeLayerId, onClick);
};
}, [isLoaded, map, nodeLayerId, onAirportSelect]);
return null;
}
export type FlightRangeBand = {
distance: number;
color?: string;
opacity?: number;
};
export type FlightRangeProps = {
origin: AirportRef;
ranges: readonly FlightRangeBand[];
id?: string;
outlineWidth?: number;
showOrigin?: boolean;
showLabel?: boolean;
steps?: number;
};
const EARTH_RADIUS_KM = 6371;
/**
* Raw geodesic circle vertices with longitudes unwrapped for continuity
* (values may exceed ±180°). The ring is open — the first point is not
* repeated at the end.
*/
function geodesicCirclePoints(
center: Coordinates,
distanceKm: number,
steps: number,
): Coordinates[] {
const angularDistance = distanceKm / EARTH_RADIUS_KM;
const latitude = (center[1] * Math.PI) / 180;
const longitude = (center[0] * Math.PI) / 180;
const coordinates: Coordinates[] = [];
for (let index = 0; index < steps; index += 1) {
const angle = (index / steps) * Math.PI * 2;
const destinationLatitude = Math.asin(
Math.sin(latitude) * Math.cos(angularDistance) +
Math.cos(latitude) * Math.sin(angularDistance) * Math.cos(angle),
);
const destinationLongitude =
longitude +
Math.atan2(
Math.sin(angle) * Math.sin(angularDistance) * Math.cos(latitude),
Math.cos(angularDistance) -
Math.sin(latitude) * Math.sin(destinationLatitude),
);
let lng = (destinationLongitude * 180) / Math.PI;
const previous = coordinates[coordinates.length - 1];
if (previous) {
while (lng - previous[0] > 180) lng -= 360;
while (lng - previous[0] < -180) lng += 360;
} else {
lng = normalizeLongitude(lng);
}
coordinates.push([lng, (destinationLatitude * 180) / Math.PI]);
}
return coordinates;
}
/** Sutherland–Hodgman clip of a closed ring against a vertical half-plane. */
function clipRingByLongitude(
ring: Coordinates[],
seam: number,
keepWestOfSeam: boolean,
): Coordinates[] {
const inside = (lng: number) => (keepWestOfSeam ? lng <= seam : lng >= seam);
const clipped: Coordinates[] = [];
for (let index = 0; index < ring.length; index += 1) {
const current = ring[index];
const next = ring[(index + 1) % ring.length];
const currentInside = inside(current[0]);
if (currentInside) clipped.push(current);
if (currentInside !== inside(next[0]) && next[0] !== current[0]) {
const t = (seam - current[0]) / (next[0] - current[0]);
clipped.push([seam, current[1] + (next[1] - current[1]) * t]);
}
}
return clipped;
}
function closeRing(ring: Coordinates[]): Coordinates[] {
if (ring.length === 0) return ring;
const first = ring[0];
const last = ring[ring.length - 1];
if (first[0] === last[0] && first[1] === last[1]) return ring;
return [...ring, first];
}
/**
* Split a ring with unwrapped longitudes into closed rings that stay within
* the canonical [-180, 180] range, cutting at the antimeridian when needed.
*/
function splitRingAtAntimeridian(ring: Coordinates[]): Coordinates[][] {
const longitudes = ring.map((point) => point[0]);
const minLongitude = Math.min(...longitudes);
const maxLongitude = Math.max(...longitudes);
if (maxLongitude <= 180 && minLongitude >= -180) return [closeRing(ring)];
const seam = maxLongitude > 180 ? 180 : -180;
const shift = seam > 0 ? -360 : 360;
const mainRing = clipRingByLongitude(ring, seam, seam > 0);
const overflowRing = clipRingByLongitude(ring, seam, seam < 0).map(
(point): Coordinates => [point[0] + shift, point[1]],
);
const rings: Coordinates[][] = [];
if (mainRing.length >= 3) rings.push(closeRing(mainRing));
if (overflowRing.length >= 3) rings.push(closeRing(overflowRing));
return rings;
}
/**
* Build render-safe geometry for a geodesic circle: fill polygons split at
* the antimeridian (or closed over an enclosed pole) plus an outline that
* follows only the circle itself — never the artificial seam edges.
*/
function geodesicCircleGeometry(
center: Coordinates,
distanceKm: number,
steps: number,
): {
polygons: Coordinates[][][];
outline: RouteGeometry;
} {
// Just under half the circumference — an antipodal circle degenerates.
const maxDistanceKm = Math.PI * EARTH_RADIUS_KM * 0.995;
const distance = Math.min(distanceKm, maxDistanceKm);
const ring = geodesicCirclePoints(center, distance, steps);
const distanceToNorthPole =
((90 - center[1]) * Math.PI * EARTH_RADIUS_KM) / 180;
const distanceToSouthPole =
((90 + center[1]) * Math.PI * EARTH_RADIUS_KM) / 180;
if (distance > distanceToNorthPole && distance > distanceToSouthPole) {
// Both poles are within range: the reachable region is the whole globe
// minus a cap around the antipode. Build two hemispheric polygons that
// meet along the cap's centre meridian so the cap becomes a hole.
const capRadiusKm = Math.PI * EARTH_RADIUS_KM - distance;
const capCenter: Coordinates = [
normalizeLongitude(center[0] + 180),
-center[1],
];
const capRing = geodesicCirclePoints(capCenter, capRadiusKm, steps);
const half = Math.floor(capRing.length / 2);
const top = capRing[0];
const bottom = capRing[half];
const eastRing: Coordinates[] = [
[top[0], 90],
...capRing.slice(0, half + 1),
[bottom[0], -90],
[top[0] + 180, -90],
[top[0] + 180, 90],
];
const westRing: Coordinates[] = [
[bottom[0], -90],
...capRing.slice(half),
top,
[top[0], 90],
[top[0] - 180, 90],
[top[0] - 180, -90],
];
return {
polygons: [eastRing, westRing].flatMap((ring) =>
splitRingAtAntimeridian(ring).map((part) => [part]),
),
outline: coordinatesToGeometry(closeRing(capRing)),
};
}
const enclosedPoleLatitude =
distance > distanceToNorthPole
? 90
: distance > distanceToSouthPole
? -90
: null;
if (enclosedPoleLatitude !== null) {
// The circle wraps every meridian — close the region over the pole.
const sorted = ring
.map((point): Coordinates => [normalizeLongitude(point[0]), point[1]])
.sort((a, b) => a[0] - b[0]);
const edgeLatitude = (sorted[0][1] + sorted[sorted.length - 1][1]) / 2;
const boundary: Coordinates[] = [
[-180, edgeLatitude],
...sorted,
[180, edgeLatitude],
];
const polygonRing = closeRing([
...boundary,
[180, enclosedPoleLatitude],
[-180, enclosedPoleLatitude],
]);
return {
polygons: [[polygonRing]],
outline: { type: "LineString", coordinates: boundary },
};
}
return {
polygons: splitRingAtAntimeridian(ring).map((part) => [part]),
outline: coordinatesToGeometry(closeRing(ring)),
};
}
/** True geodesic range bands measured in kilometres from an airport or point. */
function FlightRange({
origin,
ranges,
id: propId,
outlineWidth = 1.5,
showOrigin = true,
showLabel = true,
steps = 128,
}: FlightRangeProps) {
const autoId = useId();
const id = propId ?? autoId;
const sourceId = `flight-range-source-${id}`;
const fillLayerId = `flight-range-fill-${id}`;
const outlineLayerId = `flight-range-outline-${id}`;
const originKey = normalizeRefKey(origin);
const originCoordinates = useMemo(
() => resolveRefKey(originKey),
[originKey],
);
const rangeSignature = JSON.stringify(ranges);
const data = useMemo<GeoJsonCollection>(() => {
if (!originCoordinates) return EMPTY_COLLECTION;
const sorted = [...ranges].sort((a, b) => b.distance - a.distance);
return {
type: "FeatureCollection",
features: sorted.flatMap((range, index): GeoJsonFeature[] => {
const color =
range.color ?? ["#cbd5e1", "#94a3b8", "#475569"][index % 3];
const circle = geodesicCircleGeometry(
originCoordinates,
range.distance,
steps,
);
return [
{
type: "Feature",
properties: {
kind: "band",
color,
opacity: range.opacity ?? 0.065,
distance: range.distance,
},
geometry: { type: "MultiPolygon", coordinates: circle.polygons },
},
{
type: "Feature",
properties: { kind: "outline", color, distance: range.distance },
geometry: circle.outline,
},
];
}),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [originCoordinates, rangeSignature, steps]);
const layers = useMemo<OverlayLayer[]>(
() => [
{
id: fillLayerId,
source: sourceId,
type: "fill",
filter: ["==", ["get", "kind"], "band"],
paint: {
"fill-color": ["get", "color"],
"fill-opacity": ["get", "opacity"],
},
},
{
id: outlineLayerId,
source: sourceId,
type: "line",
filter: ["==", ["get", "kind"], "outline"],
paint: {
"line-color": ["get", "color"],
"line-opacity": 0.72,
"line-width": outlineWidth,
},
},
],
[fillLayerId, outlineLayerId, outlineWidth, sourceId],
);
useGeoJsonOverlay(sourceId, data, layers);
return showOrigin && originCoordinates ? (
<EndpointMarker refValue={origin} showLabel={showLabel} />
) : null;
}
export type AircraftTrailPosition = {
longitude: number;
latitude: number;
timestamp?: number | string;
/** Altitude value used by `altitudeColorStops` (typically feet). */
altitude?: number;
};
export type AircraftTrailAltitudeColorStop = {
altitude: number;
color: string;
};
export type AircraftTrailProps = {
positions: readonly AircraftTrailPosition[];
/** Optional destination used to generate a dashed great-circle continuation. */
to?: AirportRef;
/**
* Explicit future route positions. When provided, these take precedence over
* the generated `to` route and are connected to the latest observed point.
*/
plannedPositions?: readonly AircraftTrailPosition[];
id?: string;
color?: string;
/**
* Maps recorded position altitudes to a smooth route gradient. Stops are
* sorted by altitude; missing altitude samples are interpolated from their
* nearest known neighbours.
*/
altitudeColorStops?: readonly AircraftTrailAltitudeColorStop[];
width?: number;
startOpacity?: number;
endOpacity?: number;
/** Adds a soft halo beneath the recorded track (default: true). */
showGlow?: boolean;
showAircraft?: boolean;
icon?: ReactNode;
iconSize?: number;
plannedColor?: string;
plannedWidth?: number;
plannedOpacity?: number;
plannedDashArray?: readonly [number, number];
/** Lateral bend for an automatically generated destination route. */
plannedCurvature?: number;
npoints?: number;
};
const DEFAULT_AIRCRAFT_TRAIL_DASH_ARRAY = [2, 2] as const;
function coordinateDistance(a: Coordinates, b: Coordinates) {
const toRadians = Math.PI / 180;
const latitudeA = a[1] * toRadians;
const latitudeB = b[1] * toRadians;
const latitudeDelta = (b[1] - a[1]) * toRadians;
const longitudeDelta = (b[0] - a[0]) * toRadians;
const sinLatitude = Math.sin(latitudeDelta / 2);
const sinLongitude = Math.sin(longitudeDelta / 2);
const haversine =
sinLatitude * sinLatitude +
Math.cos(latitudeA) * Math.cos(latitudeB) * sinLongitude * sinLongitude;
const safeHaversine = clamp(haversine);
return 2 * Math.atan2(Math.sqrt(safeHaversine), Math.sqrt(1 - safeHaversine));
}
function pathProgressValues(coordinates: readonly Coordinates[]) {
if (coordinates.length <= 1) return coordinates.map(() => 0);
const progress = new Array<number>(coordinates.length).fill(0);
let totalDistance = 0;
for (let index = 1; index < coordinates.length; index += 1) {
totalDistance += coordinateDistance(
coordinates[index - 1],
coordinates[index],
);
progress[index] = totalDistance;
}
if (totalDistance <= Number.EPSILON) {
return progress.map((_, index) => index / (coordinates.length - 1));
}
for (let index = 1; index < progress.length; index += 1) {
progress[index] /= totalDistance;
}
return progress;
}
function curvePlannedCoordinates(
coordinates: readonly Coordinates[],
curvature: number,
): Coordinates[] {
if (coordinates.length < 3 || Math.abs(curvature) <= Number.EPSILON) {
return [...coordinates];
}
const start = coordinates[0];
const end = coordinates[coordinates.length - 1];
const referenceLatitude = ((start[1] + end[1]) / 2) * (Math.PI / 180);
const longitudeScale = Math.max(0.15, Math.cos(referenceLatitude));
const deltaX = (end[0] - start[0]) * longitudeScale;
const deltaY = end[1] - start[1];
const distance = Math.hypot(deltaX, deltaY);
if (distance <= Number.EPSILON) return [...coordinates];
const normalX = -deltaY / distance;
const normalY = deltaX / distance;
const bend = clamp(curvature, -0.5, 0.5);
const progress = pathProgressValues(coordinates);
return coordinates.map((coordinate, index) => {
if (index === 0 || index === coordinates.length - 1) return coordinate;
const offset = distance * bend * Math.sin(Math.PI * progress[index]);
const pointLongitudeScale = Math.max(
0.15,
Math.cos(coordinate[1] * (Math.PI / 180)),
);
return [
coordinate[0] + (normalX * offset) / pointLongitudeScale,
coordinate[1] + normalY * offset,
];
});
}
function resolveTrailAltitudes(
positions: readonly AircraftTrailPosition[],
progress: readonly number[],
) {
const knownIndexes: number[] = [];
for (let index = 0; index < positions.length; index += 1) {
if (Number.isFinite(positions[index].altitude)) knownIndexes.push(index);
}
if (knownIndexes.length === 0) return null;
const resolved = new Array<number>(positions.length);
let nextKnownPointer = 0;
for (let index = 0; index < positions.length; index += 1) {
const altitude = positions[index].altitude;
if (Number.isFinite(altitude)) {
resolved[index] = altitude as number;
if (knownIndexes[nextKnownPointer] === index) nextKnownPointer += 1;
continue;
}
const previousIndex = knownIndexes[Math.max(0, nextKnownPointer - 1)];
const nextIndex =
knownIndexes[Math.min(knownIndexes.length - 1, nextKnownPointer)];
if (previousIndex === nextIndex || index < previousIndex) {
resolved[index] = positions[nextIndex].altitude as number;
continue;
}
if (index > nextIndex) {
resolved[index] = positions[previousIndex].altitude as number;
continue;
}
const span = progress[nextIndex] - progress[previousIndex];
const ratio =
span > Number.EPSILON
? (progress[index] - progress[previousIndex]) / span
: (index - previousIndex) / (nextIndex - previousIndex);
const previousAltitude = positions[previousIndex].altitude as number;
const nextAltitude = positions[nextIndex].altitude as number;
resolved[index] =
previousAltitude + (nextAltitude - previousAltitude) * clamp(ratio);
}
return resolved;
}
function altitudeColorExpression(
altitude: number,
stops: readonly AircraftTrailAltitudeColorStop[],
): unknown {
if (stops.length === 1) return stops[0].color;
const expression: unknown[] = ["interpolate", ["linear"], altitude];
for (const stop of stops) expression.push(stop.altitude, stop.color);
return expression;
}
function colorOpacityExpression(color: unknown, opacity: number) {
return [
"rgba",
["at", 0, ["to-rgba", color]],
["at", 1, ["to-rgba", color]],
["at", 2, ["to-rgba", color]],
clamp(opacity),
];
}
function normalizeAltitudeColorStops(
colorStops: readonly AircraftTrailAltitudeColorStop[],
) {
const validStops = [...colorStops]
.filter(
(stop) => Number.isFinite(stop.altitude) && stop.color.trim().length > 0,
)
.sort((a, b) => a.altitude - b.altitude);
const sortedStops: AircraftTrailAltitudeColorStop[] = [];
for (const stop of validStops) {
const previous = sortedStops[sortedStops.length - 1];
if (previous?.altitude === stop.altitude) {
previous.color = stop.color;
} else {
sortedStops.push({ ...stop });
}
}
return sortedStops;
}
function makeAltitudeTrailGradient(
positions: readonly AircraftTrailPosition[],
coordinates: readonly Coordinates[],
colorStops: readonly AircraftTrailAltitudeColorStop[],
startOpacity: number,
endOpacity: number,
): unknown[] | null {
if (positions.length < 2 || positions.length !== coordinates.length)
return null;
const sortedStops = normalizeAltitudeColorStops(colorStops);
if (sortedStops.length === 0) return null;
const progress = pathProgressValues(coordinates);
const altitudes = resolveTrailAltitudes(positions, progress);
if (!altitudes) return null;
const samples: { progress: number; color: unknown }[] = [];
for (let index = 0; index < positions.length; index += 1) {
const sampleProgress = clamp(progress[index]);
const opacity =
clamp(startOpacity) +
(clamp(endOpacity) - clamp(startOpacity)) * sampleProgress;
const color = colorOpacityExpression(
altitudeColorExpression(altitudes[index], sortedStops),
opacity,
);
const previous = samples[samples.length - 1];
if (previous && sampleProgress <= previous.progress + Number.EPSILON) {
previous.color = color;
} else {
samples.push({ progress: sampleProgress, color });
}
}
if (samples.length < 2) return null;
samples[0].progress = 0;
samples[samples.length - 1].progress = 1;
const gradient: unknown[] = ["interpolate", ["linear"], ["line-progress"]];
for (const sample of samples) gradient.push(sample.progress, sample.color);
return gradient;
}
/** Renders the actual recorded aircraft path with a recent-position emphasis. */
function AircraftTrail({
positions,
to,
plannedPositions,
id: propId,
color = "#0f172a",
altitudeColorStops,
width = 2.5,
startOpacity = 0.04,
endOpacity = 1,
showGlow = true,
showAircraft = true,
icon,
iconSize = 24,
plannedColor,
plannedWidth,
plannedOpacity = 0.62,
plannedDashArray = DEFAULT_AIRCRAFT_TRAIL_DASH_ARRAY,
plannedCurvature = 0.14,
npoints = 72,
}: AircraftTrailProps) {
const autoId = useId();
const id = propId ?? autoId;
const sourceId = `aircraft-trail-source-${id}`;
const glowLayerId = `aircraft-trail-glow-${id}`;
const layerId = `aircraft-trail-layer-${id}`;
const plannedSourceId = `aircraft-trail-planned-source-${id}`;
const plannedLayerId = `aircraft-trail-planned-layer-${id}`;
const signature = JSON.stringify(positions);
const altitudeColorStopsSignature = JSON.stringify(altitudeColorStops ?? []);
const plannedSignature = JSON.stringify(plannedPositions ?? []);
const toKey = to ? normalizeRefKey(to) : null;
const coordinates = useMemo<Coordinates[]>(
() => positions.map((point) => [point.longitude, point.latitude]),
// eslint-disable-next-line react-hooks/exhaustive-deps
[signature],
);
const data = useMemo<GeoJsonCollection>(
() => ({
type: "FeatureCollection",
features:
coordinates.length >= 2
? [
{
type: "Feature",
properties: {},
geometry: coordinatesToGeometry(coordinates),
},
]
: [],
}),
[coordinates],
);
const transparentColor = colorWithAlpha(color, clamp(startOpacity));
const glowStartColor = colorWithAlpha(color, 0);
const glowEndColor = colorWithAlpha(color, 0.2);
const altitudeGradients = useMemo(() => {
if (!altitudeColorStops || altitudeColorStops.length === 0) return null;
return {
trail: makeAltitudeTrailGradient(
positions,
coordinates,
altitudeColorStops,
startOpacity,
endOpacity,
),
glow: makeAltitudeTrailGradient(
positions,
coordinates,
altitudeColorStops,
0,
0.2,
),
};
// Value signatures intentionally avoid recalculating for equivalent arrays.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
altitudeColorStopsSignature,
coordinates,
endOpacity,
signature,
startOpacity,
]);
const hasAltitudeGradient = Boolean(altitudeGradients?.trail);
const layers = useMemo<OverlayLayer[]>(
() => [
{
id: glowLayerId,
source: sourceId,
type: "line",
layout: { "line-cap": "round", "line-join": "round" },
paint: {
"line-width": width + 5,
"line-gradient": altitudeGradients?.glow ?? [
"interpolate",
["linear"],
["line-progress"],
0,
glowStartColor,
1,
glowEndColor,
],
"line-opacity": showGlow ? 1 : 0,
"line-blur": 3,
},
},
{
id: layerId,
source: sourceId,
type: "line",
layout: { "line-cap": "round", "line-join": "round" },
paint: {
"line-width": width,
"line-gradient": altitudeGradients?.trail ?? [
"interpolate",
["linear"],
["line-progress"],
0,
transparentColor,
1,
color,
],
"line-opacity": hasAltitudeGradient ? 1 : endOpacity,
},
},
],
[
color,
altitudeGradients,
endOpacity,
glowEndColor,
glowLayerId,
glowStartColor,
layerId,
hasAltitudeGradient,
showGlow,
sourceId,
transparentColor,
width,
],
);
useGeoJsonOverlay(sourceId, data, layers, { lineMetrics: true });
const latestPosition = coordinates[coordinates.length - 1];
const plannedCoordinates = useMemo<Coordinates[]>(() => {
if (!latestPosition) return [];
if (plannedPositions && plannedPositions.length > 0) {
const futureCoordinates = plannedPositions.map(
(point): Coordinates => [point.longitude, point.latitude],
);
// Accept paths touching the latest position at either end. A reversed
// path (destination → latest position) keeps the dash phase anchored at
// the fixed destination, so the dashes stay visually static while the
// aircraft advances.
const touchesLatestPosition = (coordinate: Coordinates) =>
Math.abs(coordinate[0] - latestPosition[0]) < 0.000001 &&
Math.abs(coordinate[1] - latestPosition[1]) < 0.000001;
return touchesLatestPosition(futureCoordinates[0]) ||
touchesLatestPosition(futureCoordinates[futureCoordinates.length - 1])
? futureCoordinates
: [latestPosition, ...futureCoordinates];
}
if (!toKey) return [];
const destination = resolveRefKey(toKey);
return destination
? curvePlannedCoordinates(
makeArcCoordinates(latestPosition, destination, npoints),
plannedCurvature,
)
: [];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [latestPosition, npoints, plannedCurvature, plannedSignature, toKey]);
const plannedData = useMemo<GeoJsonCollection>(
() => ({
type: "FeatureCollection",
features:
plannedCoordinates.length >= 2
? [
{
type: "Feature",
properties: {},
geometry: coordinatesToGeometry(plannedCoordinates),
},
]
: [],
}),
[plannedCoordinates],
);
const resolvedPlannedColor = plannedColor ?? color;
const resolvedPlannedWidth = plannedWidth ?? Math.max(1, width * 0.82);
const plannedLayers = useMemo<OverlayLayer[]>(
() => [
{
id: plannedLayerId,
source: plannedSourceId,
type: "line",
layout: { "line-cap": "butt", "line-join": "round" },
paint: {
"line-color": resolvedPlannedColor,
"line-width": resolvedPlannedWidth,
"line-opacity": clamp(plannedOpacity),
"line-dasharray": [plannedDashArray[0], plannedDashArray[1]],
},
},
],
[
plannedDashArray,
plannedLayerId,
plannedOpacity,
plannedSourceId,
resolvedPlannedColor,
resolvedPlannedWidth,
],
);
useGeoJsonOverlay(plannedSourceId, plannedData, plannedLayers);
const heading = useMemo(() => {
if (coordinates.length < 2) return 0;
return bearing(
coordinates[coordinates.length - 2],
coordinates[coordinates.length - 1],
);
}, [coordinates]);
if (!showAircraft || !latestPosition) return null;
return (
<MapMarker longitude={latestPosition[0]} latitude={latestPosition[1]}>
<MarkerContent>
<div
className="text-slate-950 drop-shadow-[0_1px_2px_rgba(255,255,255,0.95)]"
style={{ transform: `rotate(${heading}deg)` }}
>
{icon ?? <PlaneGlyph size={iconSize} />}
</div>
</MarkerContent>
</MapMarker>
);
}
const FLIGHT_FLOW_AIRCRAFT_IMAGE_SIZE = 32;
const FLIGHT_FLOW_AIRCRAFT_IMAGE_PIXEL_RATIO = 3;
function makeFlightFlowAircraftImage(color: string) {
const canvas = document.createElement("canvas");
const canvasSize =
FLIGHT_FLOW_AIRCRAFT_IMAGE_SIZE * FLIGHT_FLOW_AIRCRAFT_IMAGE_PIXEL_RATIO;
canvas.width = canvasSize;
canvas.height = canvasSize;
const context = canvas.getContext("2d");
if (!context) return new ImageData(canvasSize, canvasSize);
const path = new Path2D(
"M12 2.5c.75 0 1.35.6 1.35 1.35v5.1l6.1 3.65v1.75l-6.1-1.85v4.2l2.1 1.55v1.35L12 18.7l-3.45.9v-1.35l2.1-1.55v-4.2l-6.1 1.85V12.6l6.1-3.65v-5.1c0-.75.6-1.35 1.35-1.35Z",
);
context.scale(canvasSize / 24, canvasSize / 24);
context.lineJoin = "round";
context.shadowColor = "rgba(15, 23, 42, 0.38)";
context.shadowBlur = 1.4;
context.shadowOffsetY = 0.6;
context.strokeStyle = "rgba(255, 255, 255, 0.96)";
context.lineWidth = 1.25;
context.stroke(path);
context.fillStyle = color;
context.fill(path);
return context.getImageData(0, 0, canvasSize, canvasSize);
}
export type FlightFlowRoute = FlightNetworkRoute & {
/** Exact aircraft count for this route; overrides weighted distribution. */
aircraftCount?: number;
};
export type FlightFlowProps = {
routes: readonly FlightFlowRoute[];
id?: string;
/** Aircraft icon color. */
color?: string;
/** Whether to show the optional guide routes beneath the aircraft. */
showRoutes?: boolean;
routeColor?: string;
routeOpacity?: number;
routeWidth?: number;
/** Animate aircraft along each route; false keeps them evenly distributed. */
animate?: boolean;
/** Approximate total aircraft count distributed by route value. */
aircraftCount?: number;
/** Aircraft icon size in pixels. */
aircraftSize?: number;
/** @deprecated Use `aircraftCount` instead. */
particleCount?: number;
/** @deprecated Use `aircraftSize` instead. */
particleSize?: number;
/** @deprecated Particle tails are no longer rendered. */
tailLength?: number;
duration?: number;
npoints?: number;
};
/** Renders weighted aircraft traffic in animated or static mode. */
function FlightFlow({
routes,
id: propId,
color = "#f59e0b",
showRoutes = false,
routeColor = "#94a3b8",
routeOpacity = 0.16,
routeWidth = 1,
animate = true,
aircraftCount,
aircraftSize,
particleCount,
particleSize,
duration = 12000,
npoints = 100,
}: FlightFlowProps) {
const autoId = useId();
const id = propId ?? autoId;
const routeSourceId = `flight-flow-routes-source-${id}`;
const routeLayerId = `flight-flow-routes-layer-${id}`;
const aircraftSourceId = `flight-flow-aircraft-source-${id}`;
const aircraftLayerId = `flight-flow-aircraft-layer-${id}`;
const aircraftImageId = `flight-flow-aircraft-image-${id}`;
const resolvedAircraftCount = Math.round(
clamp(aircraftCount ?? particleCount ?? 24, 1, 200),
);
const resolvedAircraftSize = clamp(
aircraftSize ?? (particleSize ? particleSize * 7 : 18),
8,
48,
);
const signature = JSON.stringify(routes);
const samples = useMemo(() => {
const resolved: {
coordinates: Coordinates[];
value: number;
aircraftCount: number | null;
}[] = [];
for (const route of routes) {
const from = resolveRef(route.from);
const to = resolveRef(route.to);
if (!from || !to) continue;
resolved.push({
coordinates: makeArcCoordinates(from, to, npoints),
value: Math.max(0, route.value ?? 1),
aircraftCount: Number.isFinite(route.aircraftCount)
? Math.round(clamp(route.aircraftCount ?? 0, 0, 200))
: null,
});
}
return resolved;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [signature, npoints]);
const maxSampleValue = useMemo(
() => Math.max(1, ...samples.map((sample) => sample.value)),
[samples],
);
const routeData = useMemo<GeoJsonCollection>(
() => ({
type: "FeatureCollection",
features: samples.map((sample) => ({
type: "Feature",
properties: { value: sample.value },
geometry: coordinatesToGeometry(sample.coordinates),
})),
}),
[samples],
);
const routeLayers = useMemo<OverlayLayer[]>(
() => [
{
id: routeLayerId,
source: routeSourceId,
type: "line",
layout: { "line-cap": "round" },
paint: {
"line-color": routeColor,
"line-opacity": showRoutes ? routeOpacity : 0,
"line-width": [
"interpolate",
["linear"],
["sqrt", ["get", "value"]],
0,
routeWidth * 0.65,
Math.sqrt(maxSampleValue),
routeWidth * 1.35,
],
},
},
],
[
maxSampleValue,
routeLayerId,
routeColor,
routeOpacity,
routeSourceId,
routeWidth,
showRoutes,
],
);
useGeoJsonOverlay(routeSourceId, routeData, routeLayers);
const trafficSamples = useMemo(() => {
let totalWeight = 0;
for (const sample of samples) {
if (sample.aircraftCount === null) totalWeight += sample.value;
}
const resolved: { coordinates: Coordinates[]; count: number }[] = [];
for (const sample of samples) {
const count =
sample.aircraftCount ??
(totalWeight > 0 && sample.value > 0
? Math.max(
1,
Math.round(resolvedAircraftCount * (sample.value / totalWeight)),
)
: 0);
if (count > 0) resolved.push({ coordinates: sample.coordinates, count });
}
return resolved;
}, [resolvedAircraftCount, samples]);
const makeAircraftData = useCallback(
(elapsed: number, moving: boolean): GeoJsonCollection => {
const features: GeoJsonFeature[] = [];
for (
let routeIndex = 0;
routeIndex < trafficSamples.length;
routeIndex += 1
) {
const sample = trafficSamples[routeIndex];
const routeDuration =
Math.max(1000, duration) * (0.92 + (routeIndex % 4) * 0.06);
for (
let aircraftIndex = 0;
aircraftIndex < sample.count;
aircraftIndex += 1
) {
const initialProgress =
(aircraftIndex + 0.5) / sample.count + routeIndex * 0.071;
const progress =
(initialProgress + (moving ? elapsed / routeDuration : 0)) % 1;
const position = positionAlong(sample.coordinates, progress);
features.push({
type: "Feature",
properties: { heading: position.heading },
geometry: { type: "Point", coordinates: position.coordinate },
});
}
}
return { type: "FeatureCollection", features };
},
[duration, trafficSamples],
);
const initialAircraftData = useMemo(
() => makeAircraftData(0, false),
[makeAircraftData],
);
const aircraftImage = useMemo<OverlayImageResource>(
() => ({
id: aircraftImageId,
signature: color,
create: () => makeFlightFlowAircraftImage(color),
pixelRatio: FLIGHT_FLOW_AIRCRAFT_IMAGE_PIXEL_RATIO,
}),
[aircraftImageId, color],
);
const aircraftLayers = useMemo<OverlayLayer[]>(
() => [
{
id: aircraftLayerId,
source: aircraftSourceId,
type: "symbol",
layout: {
"icon-image": aircraftImageId,
"icon-size": resolvedAircraftSize / FLIGHT_FLOW_AIRCRAFT_IMAGE_SIZE,
"icon-allow-overlap": true,
"icon-ignore-placement": true,
"icon-rotate": ["get", "heading"],
"icon-rotation-alignment": "map",
"icon-pitch-alignment": "map",
},
paint: {
"icon-opacity": 0.96,
},
},
],
[aircraftImageId, aircraftLayerId, aircraftSourceId, resolvedAircraftSize],
);
useGeoJsonOverlay(
aircraftSourceId,
initialAircraftData,
aircraftLayers,
undefined,
aircraftImage,
);
const { map, isLoaded } = useMap();
const frameRef = useRef<number | null>(null);
const updateAircraft = useCallback(
(elapsed: number, moving: boolean) => {
if (!map || trafficSamples.length === 0) return;
const source = map.getSource(
aircraftSourceId,
) as MapLibreGL.GeoJSONSource;
source?.setData(
makeAircraftData(
elapsed,
moving,
) as MapLibreGL.GeoJSONSourceSpecification["data"],
);
},
[aircraftSourceId, makeAircraftData, map, trafficSamples.length],
);
useEffect(() => {
if (!map || !isLoaded) return;
const reducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
if (!animate || reducedMotion) {
updateAircraft(0, false);
return;
}
const startedAt = performance.now();
let lastUpdate = 0;
const tick = (now: number) => {
if (now - lastUpdate >= 1000 / 30) {
updateAircraft(now - startedAt, true);
lastUpdate = now;
}
frameRef.current = requestAnimationFrame(tick);
};
frameRef.current = requestAnimationFrame(tick);
return () => {
if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);
frameRef.current = null;
};
}, [animate, isLoaded, map, updateAircraft]);
return null;
}
export {
FlightTracker,
FlightRouteLabel,
FlightNetwork,
FlightRange,
AircraftTrail,
FlightFlow,
};