import { memo, useMemo, useState } from 'react'; import { Marker, Popup } from 'react-map-gl/maplibre'; import type { Airport } from '../data/airports'; const US_BASE_ICAOS = new Set([ 'OMAD', 'OTBH', 'OKAJ', 'LTAG', 'OEPS', 'ORAA', 'ORBD', 'OBBS', 'OMTH', 'HDCL', ]); function isUSBase(airport: Airport): boolean { return airport.type === 'military' && US_BASE_ICAOS.has(airport.icao); } const FLAG_EMOJI: Record = { IR: '\u{1F1EE}\u{1F1F7}', IQ: '\u{1F1EE}\u{1F1F6}', IL: '\u{1F1EE}\u{1F1F1}', AE: '\u{1F1E6}\u{1F1EA}', SA: '\u{1F1F8}\u{1F1E6}', QA: '\u{1F1F6}\u{1F1E6}', BH: '\u{1F1E7}\u{1F1ED}', KW: '\u{1F1F0}\u{1F1FC}', OM: '\u{1F1F4}\u{1F1F2}', TR: '\u{1F1F9}\u{1F1F7}', JO: '\u{1F1EF}\u{1F1F4}', LB: '\u{1F1F1}\u{1F1E7}', SY: '\u{1F1F8}\u{1F1FE}', EG: '\u{1F1EA}\u{1F1EC}', PK: '\u{1F1F5}\u{1F1F0}', DJ: '\u{1F1E9}\u{1F1EF}', YE: '\u{1F1FE}\u{1F1EA}', SO: '\u{1F1F8}\u{1F1F4}', }; const TYPE_LABELS: Record = { large: 'International Airport', medium: 'Airport', small: 'Regional Airport', military: 'Military Airbase', }; interface Props { airports: Airport[]; } const TYPE_PRIORITY: Record = { military: 3, large: 2, medium: 1, small: 0, }; // Keep one airport per area (~50km radius). Priority: military/US base > large > medium > small. function deduplicateByArea(airports: Airport[]): Airport[] { const sorted = [...airports].sort((a, b) => { const pa = TYPE_PRIORITY[a.type] + (isUSBase(a) ? 1 : 0); const pb = TYPE_PRIORITY[b.type] + (isUSBase(b) ? 1 : 0); return pb - pa; }); const kept: Airport[] = []; for (const ap of sorted) { const tooClose = kept.some( k => Math.abs(k.lat - ap.lat) < 0.8 && Math.abs(k.lng - ap.lng) < 0.8, ); if (!tooClose) kept.push(ap); } return kept; } export const AirportLayer = memo(function AirportLayer({ airports }: Props) { const filtered = useMemo(() => deduplicateByArea(airports), [airports]); return ( <> {filtered.map(ap => ( ))} ); }); function AirportMarker({ airport }: { airport: Airport }) { const [showPopup, setShowPopup] = useState(false); const isMil = airport.type === 'military'; const isUS = isUSBase(airport); const color = isUS ? '#3b82f6' : isMil ? '#ef4444' : '#f59e0b'; const size = airport.type === 'large' ? 18 : airport.type === 'small' ? 12 : 16; const flag = FLAG_EMOJI[airport.country] || ''; // Single circle with airplane inside (plane shifted down to center in circle) const plane = isMil ? : ; const icon = ( {plane} ); return ( <>
{ e.stopPropagation(); setShowPopup(true); }}> {icon}
{showPopup && ( setShowPopup(false)} closeOnClick={false} anchor="bottom" offset={[0, -size / 2]} maxWidth="280px" className="gl-popup">
{isUS ? {'\u{1F1FA}\u{1F1F8}'} : flag ? {flag} : null} {airport.name}
{airport.nameKo && (
{airport.nameKo}
)}
{isUS ? 'US Military Base' : TYPE_LABELS[airport.type]}
{airport.iata &&
IATA : {airport.iata}
}
ICAO : {airport.icao}
{airport.city &&
City : {airport.city}
}
Country : {airport.country}
{airport.lat.toFixed(4)}°{airport.lat >= 0 ? 'N' : 'S'}, {airport.lng.toFixed(4)}°{airport.lng >= 0 ? 'E' : 'W'}
{airport.iata && ( )}
)} ); }