shadimage
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 ? "&harr;" : "&rarr;";

  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,
};

More from flightcn

Browse all components in this library →