From 0f8012361fbf796f3274fb7b93678b6ebf74d4e4 Mon Sep 17 00:00:00 2001 From: Kewonit <108450560+kewonit@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:39:17 +0530 Subject: [PATCH] Update city radius values and add Miami; refine map styles and improve OpenSky code readability --- docs.txt | 1010 +++++++ src/components/flight-tracker.tsx | 155 +- src/components/map/airport-layer.tsx | 199 ++ src/components/map/camera-controller.tsx | 111 + src/components/map/flight-layers.tsx | 27 +- src/components/map/map.tsx | 93 +- src/components/ui/control-panel.tsx | 142 +- src/hooks/use-flights.ts | 8 +- src/hooks/use-trail-history.ts | 7 +- src/lib/airports.ts | 3048 ++++++++++++++++++++++ src/lib/cities.ts | 30 +- src/lib/map-styles.ts | 4 +- src/lib/opensky.ts | 9 +- 13 files changed, 4647 insertions(+), 196 deletions(-) create mode 100644 docs.txt create mode 100644 src/components/map/airport-layer.tsx create mode 100644 src/components/map/camera-controller.tsx create mode 100644 src/lib/airports.ts diff --git a/docs.txt b/docs.txt new file mode 100644 index 0000000..c3f01b6 --- /dev/null +++ b/docs.txt @@ -0,0 +1,1010 @@ + OpenSky REST API +OpenSky REST API¶ +The root URL of our REST API is: + +https://opensky-network.org/api +There are several functions available to retrieve state vectors, flights and tracks for the whole network, a particular sensor, or a particular aircraft. Note that the functions to retrieve state vectors of sensors other than your own are rate limited (see Limitations). + +All State Vectors¶ +The following API call can be used to retrieve any state vector of the OpenSky. Please note that rate limits apply for this call (see Limitations). For API calls without rate limitation, see Own State Vectors. + +Operation¶ +GET /states/all + +Request¶ +You can (optionally) request state vectors for particular airplanes or times using the following request parameters: + +Property + +Type + +Description + +time + +integer + +The time in seconds since epoch (Unix time stamp to retrieve states for. Current time will be used if omitted. + +icao24 + +string + +One or more ICAO24 transponder addresses represented by a hex string (e.g. abc9f3). To filter multiple ICAO24 append the property once for each address. If omitted, the state vectors of all aircraft are returned. + +In addition to that, it is possible to query a certain area defined by a bounding box of WGS84 coordinates. For this purpose, add all of the following parameters: + +Property + +Type + +Description + +lamin + +float + +lower bound for the latitude in decimal degrees + +lomin + +float + +lower bound for the longitude in decimal degrees + +lamax + +float + +upper bound for the latitude in decimal degrees + +lomax + +float + +upper bound for the longitude in decimal degrees + +Lastly, you can request the category of aircraft by adding the following request parameter: + +Property + +Type + +Description + +extended + +integer + +Set to 1 if required + +Example query with time and aircraft: https://opensky-network.org/api/states/all?time=1458564121&icao24=3c6444 + +Example query with bounding box covering Switzerland: https://opensky-network.org/api/states/all?lamin=45.8389&lomin=5.9962&lamax=47.8229&lomax=10.5226 + +Response¶ +The response is a JSON object with the following properties + +Property + +Type + +Description + +time + +integer + +The time which the state vectors in this response are associated with. All vectors represent the state of a vehicle with the interval [𝑡⁢𝑖⁢𝑚⁢𝑒 −1,𝑡⁢𝑖⁢𝑚⁢𝑒]. + +states + +array + +The state vectors. + +The states property is a two-dimensional array. Each row represents a state vector and contains the following fields: + +Index + +Property + +Type + +Description + +0 + +icao24 + +string + +Unique ICAO 24-bit address of the transponder in hex string representation. + +1 + +callsign + +string + +Callsign of the vehicle (8 chars). Can be null if no callsign has been received. + +2 + +origin_country + +string + +Country name inferred from the ICAO 24-bit address. + +3 + +time_position + +int + +Unix timestamp (seconds) for the last position update. Can be null if no position report was received by OpenSky within the past 15s. + +4 + +last_contact + +int + +Unix timestamp (seconds) for the last update in general. This field is updated for any new, valid message received from the transponder. + +5 + +longitude + +float + +WGS-84 longitude in decimal degrees. Can be null. + +6 + +latitude + +float + +WGS-84 latitude in decimal degrees. Can be null. + +7 + +baro_altitude + +float + +Barometric altitude in meters. Can be null. + +8 + +on_ground + +boolean + +Boolean value which indicates if the position was retrieved from a surface position report. + +9 + +velocity + +float + +Velocity over ground in m/s. Can be null. + +10 + +true_track + +float + +True track in decimal degrees clockwise from north (north=0°). Can be null. + +11 + +vertical_rate + +float + +Vertical rate in m/s. A positive value indicates that the airplane is climbing, a negative value indicates that it descends. Can be null. + +12 + +sensors + +int[] + +IDs of the receivers which contributed to this state vector. Is null if no filtering for sensor was used in the request. + +13 + +geo_altitude + +float + +Geometric altitude in meters. Can be null. + +14 + +squawk + +string + +The transponder code aka Squawk. Can be null. + +15 + +spi + +boolean + +Whether flight status indicates special purpose indicator. + +16 + +position_source + +int + +Origin of this state’s position. + +0 = ADS-B + +1 = ASTERIX + +2 = MLAT + +3 = FLARM + +17 + +category + +int + +Aircraft category. + +0 = No information at all + +1 = No ADS-B Emitter Category Information + +2 = Light (< 15500 lbs) + +3 = Small (15500 to 75000 lbs) + +4 = Large (75000 to 300000 lbs) + +5 = High Vortex Large (aircraft such as B-757) + +6 = Heavy (> 300000 lbs) + +7 = High Performance (> 5g acceleration and 400 kts) + +8 = Rotorcraft + +9 = Glider / sailplane + +10 = Lighter-than-air + +11 = Parachutist / Skydiver + +12 = Ultralight / hang-glider / paraglider + +13 = Reserved + +14 = Unmanned Aerial Vehicle + +15 = Space / Trans-atmospheric vehicle + +16 = Surface Vehicle – Emergency Vehicle + +17 = Surface Vehicle – Service Vehicle + +18 = Point Obstacle (includes tethered balloons) + +19 = Cluster Obstacle + +20 = Line Obstacle + +Limitations¶ +Limitiations for anonymous (unauthenticated) users¶ +Anonymous are those users who access the API without using credentials. The limitations for anonymous users are: + +Anonymous users can only get the most recent state vectors, i.e. the time parameter will be ignored. + +Anonymous users can only retrieve data with a time resolution of 10 seconds. That means, the API will return state vectors for time 𝑛⁢𝑜⁢𝑤 −(𝑛⁢𝑜⁢𝑤 𝑚⁢𝑜⁢𝑑 10). + +Anonymous users get 400 API credits per day (see credit usage below). + +Limitations for OpenSky users¶ +Note + +IMPORTANT: Legacy accounts can continue using the API as before; however, basic authentication using your username and password is being deprecated and will only be supported for a limited time. Accounts created on the new website since mid-March 2025 do not have additional privileges and will receive an Unauthorized response. If you have a new account, follow the instructions in the section below on using the OAuth2 client credentials flow. + +An OpenSky user is anybody who uses a valid OpenSky account or corresponding API client to access the API. The rate limitations for OpenSky users are: + +OpenSky users clients can retrieve data of up to 1 hour in the past. If the time parameter has a value 𝑡 <𝑛⁢𝑜⁢𝑤 −3600 the API will return 400 Bad Request. + +OpenSky users can retrieve data with a time resolution of 5 seconds. That means, if the time parameter was set to 𝑡, the API will return state vectors for time 𝑡 −(𝑡 𝑚⁢𝑜⁢𝑑 5). + +OpenSky users get 4000 API credits per day. This is also true for the default privileges when using the API client. For higher request loads please contact OpenSky. + +Active contributing OpenSky users get a total of 8000 API credits per day. An active user is a user which has an ADS-B receiver that is at least 30% online (measured over the current month). + +Note + +If you are feeding and using the API client it will take 50+ requests before your credit allowance is increased to 8000. This new credit allowance is dynamic and not tied to any role so you will still see the default role with 4000 credits in the API client info. To verirfy you are getting 8000 credits inspect the x-rate-limit-remaining response header. If at times (like the start of the day) it is greater than 4000 then you will be getting the 8000 credit allowance. This is exactly the same as how things work with basic authentication. + +Note + +You can retrieve all state vectors received by your receivers without any restrictions. See Own State Vectors. Before the request limit is reached the header X-Rate-Limit-Remaining indicates the amount of remaining credits. After the rate limit is reached the status code 429 - Too Many Requests is returned and the header X-Rate-Limit-Retry-After-Seconds indicates how many seconds until credits/request become available again. + +This is currently not working for the API client and is in the process of being fixed. + +OAuth2 Client Credentials Flow¶ +To authenticate using a modern and secure method, OpenSky now supports the OAuth2 client credentials flow. This is required for all accounts created since mid-March 2025 and is recommended for all programmatic access to the API. + +To get started: + +Log in to your OpenSky account and visit the Account page. + +Create a new API client and retrieve your client_id and client_secret. + +Use these credentials to obtain an access token from the OpenSky authentication server. + +Here is an example using curl to obtain an access token: + +export CLIENT_ID=your_client_id +export CLIENT_SECRET=your_client_secret + +export TOKEN=$(curl -X POST "https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=$CLIENT_ID" \ + -d "client_secret=$CLIENT_SECRET" | jq -r .access_token) +Once you have an access token, include it in the Authorization header of your API requests: + +curl -H "Authorization: Bearer $TOKEN" https://opensky-network.org/api/states/all | jq . +Note + +IMPORTANT: When using the API client replace -u “USERNAME:PASSWORD” with -H “Authorization: Bearer $TOKEN in all following example requests. + +The token will expire after 30 minutes. You can repeat the above request to obtain a new token as needed. If a request returns a 401 Unauthorized response, it likely means the token has expired or is invalid. + +/states/all and other authenticated endpoints require this token-based authentication for non-legacy accounts using your API client. + +API credit usage¶ +API credits are now used for all endpoints except /states/own. Credit usage is lower in general for restricted/smaller areas (/states/all) and shorter time frames (/flights and /tracks). For /states/all the credit calculation is done by square degrees. The area can be restricted by using the lamin, lamax, lomin, lomax query parameters. The area square deg column in the table below indicates the square degree limit - e.g. a box extending over latitude 10 degress and longitude 5 degrees, would equal 50 square degrees: + +Area square deg + +Credits + +Example + +0 - 25 (<500x500km) + +1 + +/api/states/all?lamin=49.7&lamax=50.5&lomin=3.2&lomax=4.6 + +25 - 100 (<1000x1000km) + +2 + +/api/states/all?lamin=46.5&lamax=49.9&lomin=-1.4&lomax=6.8 + +100 - 400 (<2000x2000km) + +3 + +/api/states/all?lamin=42.2&lamax=49.8&lomin=-4.7&lomax=10.9 + +over 400 or all (>2000x2000km) + +4 + +/api/states/all + +For /flights and /tracks the credit usage is calculated by partitions used by the query, which corresponds roughly to number of days queried. + +Examples¶ +Retrieve all states as an anonymous user: + +$ curl -s "https://opensky-network.org/api/states/all" | python -m json.tool +Retrieve all states as an authenticated OpenSky user: + +$ curl -u "USERNAME:PASSWORD" -s "https://opensky-network.org/api/states/all" | python -m json.tool +Retrieve states of two particular airplanes: + +$ curl -s "https://opensky-network.org/api/states/all?icao24=3c6444&icao24=3e1bf9" | python -m json.tool +Own State Vectors¶ +The following API call can be used to retrieve state vectors for your own sensors without rate limitations. Note that authentication is required for this operation, otherwise you will get a 403 - Forbidden. + +Operation¶ +GET /states/own + +Request¶ +Pass one of the following (optional) properties as request parameters to the GET request. + +Property + +Type + +Description + +time + +integer + +The time in seconds since epoch (Unix timestamp to retrieve states for. Current time will be used if omitted. + +icao24 + +string + +One or more ICAO24 transponder addresses represented by a hex string (e.g. abc9f3). To filter multiple ICAO24 append the property once for each address. If omitted, the state vectors of all aircraft are returned. + +serials + +integer + +Retrieve only states of a subset of your receivers. You can pass this argument several time to filter state of more than one of your receivers. In this case, the API returns all states of aircraft that are visible to at least one of the given receivers. + +Response¶ +The response is a JSON object with the following properties + +Property + +Type + +Description + +time + +integer + +The time which the state vectors in this response are associated with. All vectors represent the state of a vehicle with the interval [𝑡⁢𝑖⁢𝑚⁢𝑒 −1,𝑡⁢𝑖⁢𝑚⁢𝑒]. + +states + +array + +The state vectors. + +The states property is a two-dimensional array. Each row represents a state vector and contains the following fields: + +Index + +Property + +Type + +Description + +0 + +icao24 + +string + +Unique ICAO 24-bit address of the transponder in hex string representation. + +1 + +callsign + +string + +Callsign of the vehicle (8 chars). Can be null if no callsign has been received. + +2 + +origin_country + +string + +Country name inferred from the ICAO 24-bit address. + +3 + +time_position + +int + +Unix timestamp (seconds) for the last position update. Can be null if no position report was received by OpenSky within the past 15s. + +4 + +last_contact + +int + +Unix timestamp (seconds) for the last update in general. This field is updated for any new, valid message received from the transponder. + +5 + +longitude + +float + +WGS-84 longitude in decimal degrees. Can be null. + +6 + +latitude + +float + +WGS-84 latitude in decimal degrees. Can be null. + +7 + +baro_altitude + +float + +Barometric altitude in meters. Can be null. + +8 + +on_ground + +boolean + +Boolean value which indicates if the position was retrieved from a surface position report. + +9 + +velocity + +float + +Velocity over ground in m/s. Can be null. + +10 + +true_track + +float + +True track in decimal degrees clockwise from north (north=0°). Can be null. + +11 + +vertical_rate + +float + +Vertical rate in m/s. A positive value indicates that the airplane is climbing, a negative value indicates that it descends. Can be null. + +12 + +sensors + +int[] + +IDs of the receivers which contributed to this state vector. Is null if no filtering for sensor was used in the request. + +13 + +geo_altitude + +float + +Geometric altitude in meters. Can be null. + +14 + +squawk + +string + +The transponder code aka Squawk. Can be null. + +15 + +spi + +boolean + +Whether flight status indicates special purpose indicator. + +16 + +position_source + +int + +Origin of this state’s position. + +0 = ADS-B + +1 = ASTERIX + +2 = MLAT + +3 = FLARM + +17 + +category + +int + +Aircraft category. + +0 = No information at all + +1 = No ADS-B Emitter Category Information + +2 = Light (< 15500 lbs) + +3 = Small (15500 to 75000 lbs) + +4 = Large (75000 to 300000 lbs) + +5 = High Vortex Large (aircraft such as B-757) + +6 = Heavy (> 300000 lbs) + +7 = High Performance (> 5g acceleration and 400 kts) + +8 = Rotorcraft + +9 = Glider / sailplane + +10 = Lighter-than-air + +11 = Parachutist / Skydiver + +12 = Ultralight / hang-glider / paraglider + +13 = Reserved + +14 = Unmanned Aerial Vehicle + +15 = Space / Trans-atmospheric vehicle + +16 = Surface Vehicle – Emergency Vehicle + +17 = Surface Vehicle – Service Vehicle + +18 = Point Obstacle (includes tethered balloons) + +19 = Cluster Obstacle + +20 = Line Obstacle + +Examples¶ +Retrieve states for all sensors that belong to you: + +$ curl -u "USERNAME:PASSWORD" -s "https://opensky-network.org/api/states/own" | python -m json.tool +Retrieve states as seen by a specific sensor with serial 123456 + +$ curl -u "USERNAME:PASSWORD" -s "https://opensky-network.org/api/states/own?serials=123456" | python -m json.tool +Retrieve states for several receivers: + +$ curl -u "USERNAME:PASSWORD" -s "https://opensky-network.org/api/states/own?serials=123456&serials=98765" | python -m json.tool +Flights in Time Interval¶ +This API call retrieves flights for a certain time interval [begin, end]. If no flights are found for the given time period, HTTP status 404 - Not found is returned with an empty response body. + +Operation¶ +GET /flights/all + +Request¶ +These are the required request parameters: + +Property + +Type + +Description + +begin + +integer + +Start of time interval to retrieve flights for as Unix time (seconds since epoch) + +end + +integer + +End of time interval to retrieve flights for as Unix time (seconds since epoch) + +The given time interval must not be larger than two hours! + +Response¶ +The response is a JSON array of flights where each flight is an object with the following properties: + +Examples¶ +Get flights from 12pm to 1pm on Jan 29 2018: + +$ curl -u "USERNAME:PASSWORD" -s "https://opensky-network.org/api/flights/all?begin=1517227200&end=1517230800" | python -m json.tool +Flights by Aircraft¶ +This API call retrieves flights for a particular aircraft within a certain time interval. Resulting flights departed and arrived within [begin, end]. If no flights are found for the given period, HTTP stats 404 - Not found is returned with an empty response body. + +Note + +Flights are updated by a batch process at night, i.e., only flights from the previous day or earlier are available using this endpoint. + +Operation¶ +GET /flights/aircraft + +Request¶ +These are the required request parameters: + +Property + +Type + +Description + +icao24 + +string + +Unique ICAO 24-bit address of the transponder in hex string representation. All letters need to be lower case + +begin + +integer + +Start of time interval to retrieve flights for as Unix time (seconds since epoch) + +end + +integer + +End of time interval to retrieve flights for as Unix time (seconds since epoch) + +The given time interval must not be larger than 2 days! + +Response¶ +The response is a JSON array of flights where each flight is an object with the following properties: + +Examples¶ +Get flights for D-AIZZ (3c675a) on Jan 29 2018: + +$ curl -u "USERNAME:PASSWORD" -s "https://opensky-network.org/api/flights/aircraft?icao24=3c675a&begin=1517184000&end=1517270400" | python -m json.tool +Arrivals by Airport¶ +Retrieve flights for a certain airport which arrived within a given time interval [begin, end]. If no flights are found for the given period, HTTP stats 404 - Not found is returned with an empty response body. + +Note + +Similar to flights, arrivals are updated by a batch process at night, i.e., only arrivals from the previous day or earlier are available using this endpoint. + +Operation¶ +GET /flights/arrival + +Request¶ +These are the required request parameters: + +Property + +Type + +Description + +airport + +string + +ICAO identier for the airport + +begin + +integer + +Start of time interval to retrieve flights for as Unix time (seconds since epoch) + +end + +integer + +End of time interval to retrieve flights for as Unix time (seconds since epoch) + +The given time interval must not be larger than two days! + +Response¶ +The response is a JSON array of flights where each flight is an object with the following properties: + +Examples¶ +Get all flights arriving at Frankfurt International Airport (EDDF) from 12pm to 1pm on Jan 29 2018: + +$ curl -u "USERNAME:PASSWORD" -s "https://opensky-network.org/api/flights/arrival?airport=EDDF&begin=1517227200&end=1517230800" | python -m json.tool +Departures by Airport¶ +Retrieve flights for a certain airport which departed within a given time interval [begin, end]. If no flights are found for the given period, HTTP stats 404 - Not found is returned with an empty response body. + +Operation¶ +GET /flights/departure + +Request¶ +These are the required request parameters: + +Property + +Type + +Description + +airport + +string + +ICAO identier for the airport (usually upper case) + +begin + +integer + +Start of time interval to retrieve flights for as Unix time (seconds since epoch) + +end + +integer + +End of time interval to retrieve flights for as Unix time (seconds since epoch) + +The given time interval must cover more than two days (UTC)! + +Response¶ +The response is a JSON array of flights where each flight is an object with the following properties + +Examples¶ +Get all flights departing at Frankfurt International Airport (EDDF) from 12pm to 1pm on Jan 29 2018: + +$ curl -u "USERNAME:PASSWORD" -s "https://opensky-network.org/api/flights/departure?airport=EDDF&begin=1517227200&end=1517230800" | python -m json.tool +Track by Aircraft¶ +Note + +The tracks endpoint is purely experimental. You can use the flights endpoint for historical data: Flights in Time Interval. + +Retrieve the trajectory for a certain aircraft at a given time. The trajectory is a list of waypoints containing position, barometric altitude, true track and an on-ground flag. + +In contrast to state vectors, trajectories do not contain all information we have about the flight, but rather show the aircraft’s general movement pattern. For this reason, waypoints are selected among available state vectors given the following set of rules: + +The first point is set immediately after the the aircraft’s expected departure, or after the network received the first poisition when the aircraft entered its reception range. + +The last point is set right before the aircraft’s expected arrival, or the aircraft left the networks reception range. + +There is a waypoint at least every 15 minutes when the aircraft is in-flight. + +A waypoint is added if the aircraft changes its track more than 2.5°. + +A waypoint is added if the aircraft changes altitude by more than 100m (~330ft). + +A waypoint is added if the on-ground state changes. + +Tracks are strongly related to flights. Internally, we compute flights and tracks within the same processing step. As such, it may be benificial to retrieve a list of flights with the API methods from above, and use these results with the given time stamps to retrieve detailed track information. + +Operation¶ +GET /tracks + +Request¶ +Property + +Type + +Description + +icao24 + +string + +Unique ICAO 24-bit address of the transponder in hex string representation. All letters need to be lower case + +time + +integer + +Unix time in seconds since epoch. It can be any time betwee start and end of a known flight. If time = 0, get the live track if there is any flight ongoing for the given aircraft. + +Response¶ +This endpoint is experimental and can be out of order at any time. + +The response is a JSON object with the following properties: + +Property + +Type + +Description + +icao24 + +string + +Unique ICAO 24-bit address of the transponder in lower case hex string representation. + +startTime + +integer + +Time of the first waypoint in seconds since epoch (Unix time). + +endTime + +integer + +Time of the last waypoint in seconds since epoch (Unix time). + +calllsign + +string + +Callsign (8 characters) that holds for the whole track. Can be null. + +path + +array + +Waypoints of the trajectory (description below). + +Waypoints are represented as JSON arrays to save bandwidth. Each point contains the following information: + +Index + +Property + +Type + +Description + +0 + +time + +integer + +Time which the given waypoint is associated with in seconds since epoch (Unix time). + +1 + +latitude + +float + +WGS-84 latitude in decimal degrees. Can be null. + +2 + +longitude + +float + +WGS-84 longitude in decimal degrees. Can be null. + +3 + +baro_altitude + +float + +Barometric altitude in meters. Can be null. + +4 + +true_track + +float + +True track in decimal degrees clockwise from north (north=0°). Can be null. + +5 + +on_ground + +boolean + +Boolean value which indicates if the position was retrieved from a surface position report. + +Limitations¶ +It is not possible to access flight tracks from more than 30 days in the past. + +Examples¶ +Get the live track for aircraft with transponder address 3c4b26 (D-ABYF) + +$ curl -u "USERNAME:PASSWORD" -s "https://opensky-network.org/api/tracks/all?icao24=3c4b26&time=0" \ No newline at end of file diff --git a/src/components/flight-tracker.tsx b/src/components/flight-tracker.tsx index de62904..79b9610 100644 --- a/src/components/flight-tracker.tsx +++ b/src/components/flight-tracker.tsx @@ -1,14 +1,10 @@ "use client"; -import { - useState, - useCallback, - useRef, - useEffect, - useSyncExternalStore, -} from "react"; +import { useState, useCallback, useSyncExternalStore } from "react"; import { ErrorBoundary } from "@/components/error-boundary"; -import { Map, useMap } from "@/components/map/map"; +import { Map } from "@/components/map/map"; +import { CameraController } from "@/components/map/camera-controller"; +import { AirportLayer } from "@/components/map/airport-layer"; import { FlightLayers } from "@/components/map/flight-layers"; import { FlightCard } from "@/components/ui/flight-card"; import { ControlPanel } from "@/components/ui/control-panel"; @@ -19,28 +15,47 @@ import { useFlights } from "@/hooks/use-flights"; import { useTrailHistory } from "@/hooks/use-trail-history"; import { MAP_STYLES, DEFAULT_STYLE, type MapStyle } from "@/lib/map-styles"; import { CITIES, type City } from "@/lib/cities"; +import { findByIata, airportToCity } from "@/lib/airports"; import type { FlightState } from "@/lib/opensky"; import type { PickingInfo } from "@deck.gl/core"; -const IDLE_TIMEOUT_MS = 5_000; -const DEFAULT_CITY_ID = "sfo"; +const DEFAULT_CITY_ID = "mia"; const STYLE_STORAGE_KEY = "aeris:mapStyle"; const DEFAULT_CITY = CITIES.find((c) => c.id === DEFAULT_CITY_ID) ?? CITIES[0]; const subscribeNoop = () => () => {}; +let _cachedInitialCity: City | null = null; + function resolveInitialCity(): City { + if (_cachedInitialCity) return _cachedInitialCity; try { const params = new URLSearchParams(window.location.search); const code = params.get("city")?.trim().toUpperCase(); - if (!code) return DEFAULT_CITY; - return ( - CITIES.find( - (c) => c.iata.toUpperCase() === code || c.id === code.toLowerCase(), - ) ?? DEFAULT_CITY + if (!code) { + _cachedInitialCity = DEFAULT_CITY; + return DEFAULT_CITY; + } + + const preset = CITIES.find( + (c) => c.iata.toUpperCase() === code || c.id === code.toLowerCase(), ); + if (preset) { + _cachedInitialCity = preset; + return preset; + } + + const airport = findByIata(code); + if (airport) { + _cachedInitialCity = airportToCity(airport); + return _cachedInitialCity; + } + + _cachedInitialCity = DEFAULT_CITY; + return DEFAULT_CITY; } catch { + _cachedInitialCity = DEFAULT_CITY; return DEFAULT_CITY; } } @@ -75,109 +90,6 @@ function saveMapStyle(style: MapStyle): void { } } -function CameraController({ city }: { city: City }) { - const { map, isLoaded } = useMap(); - const { settings } = useSettings(); - const prevCityRef = useRef(null); - const idleTimerRef = useRef | null>(null); - const orbitFrameRef = useRef(null); - const isInteractingRef = useRef(false); - - useEffect(() => { - if (!map || !isLoaded || !city) return; - if (city.id === prevCityRef.current) return; - - prevCityRef.current = city.id; - map.flyTo({ - center: city.coordinates, - zoom: 9.2, - pitch: 49, - bearing: 27.4, - duration: 2800, - essential: true, - }); - }, [map, isLoaded, city]); - - useEffect(() => { - if (!map || !isLoaded || !city || !settings.autoOrbit) { - if (orbitFrameRef.current) cancelAnimationFrame(orbitFrameRef.current); - if (idleTimerRef.current) clearTimeout(idleTimerRef.current); - return; - } - - const prefersReducedMotion = - window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false; - if (prefersReducedMotion) return; - - const directionMultiplier = - settings.orbitDirection === "clockwise" ? 1 : -1; - const speed = settings.orbitSpeed * directionMultiplier; - - function startOrbit() { - if (!map || isInteractingRef.current) return; - - function tick() { - if (!map || isInteractingRef.current) return; - const bearing = map.getBearing() + speed; - map.setBearing(bearing % 360); - orbitFrameRef.current = requestAnimationFrame(tick); - } - - orbitFrameRef.current = requestAnimationFrame(tick); - } - - function stopOrbit() { - if (orbitFrameRef.current) { - cancelAnimationFrame(orbitFrameRef.current); - orbitFrameRef.current = null; - } - } - - function resetIdleTimer() { - isInteractingRef.current = true; - stopOrbit(); - - if (idleTimerRef.current) clearTimeout(idleTimerRef.current); - idleTimerRef.current = setTimeout(() => { - isInteractingRef.current = false; - startOrbit(); - }, IDLE_TIMEOUT_MS); - } - - const events = ["mousedown", "wheel", "touchstart"] as const; - const container = map.getContainer(); - events.forEach((e) => - container.addEventListener(e, resetIdleTimer, { passive: true }), - ); - - const onMoveStart = () => { - if (isInteractingRef.current) stopOrbit(); - }; - map.on("movestart", onMoveStart); - - idleTimerRef.current = setTimeout(() => { - isInteractingRef.current = false; - startOrbit(); - }, IDLE_TIMEOUT_MS); - - return () => { - stopOrbit(); - if (idleTimerRef.current) clearTimeout(idleTimerRef.current); - events.forEach((e) => container.removeEventListener(e, resetIdleTimer)); - map.off("movestart", onMoveStart); - }; - }, [ - map, - isLoaded, - city, - settings.autoOrbit, - settings.orbitSpeed, - settings.orbitDirection, - ]); - - return null; -} - function FlightTrackerInner() { const hydratedCity = useSyncExternalStore( subscribeNoop, @@ -228,8 +140,13 @@ function FlightTrackerInner() { return (
- + + void; + isDark: boolean; +}; + +const airportGeoJson: GeoJSON.FeatureCollection = { + type: "FeatureCollection", + features: AIRPORTS.map((a) => ({ + type: "Feature" as const, + geometry: { type: "Point" as const, coordinates: [a.lng, a.lat] }, + properties: { + iata: a.iata, + name: a.name, + city: a.city, + country: a.country, + }, + })), +}; + +const LAYER_CSS = ` +.airport-beacon{position:relative;width:20px;height:20px;pointer-events:none} +.airport-beacon-core{position:absolute;inset:7px;border-radius:50%;background:rgba(255,255,255,0.3);box-shadow:0 0 6px rgba(255,255,255,0.1)} +.airport-beacon-ring{position:absolute;inset:2px;border-radius:50%;border:1px solid rgba(255,255,255,0.12);animation:ab-pulse 6s ease-out infinite} +.airport-beacon-ring:nth-child(2){animation-delay:2s} +.airport-beacon-ring:nth-child(3){animation-delay:4s} +@keyframes ab-pulse{0%{transform:scale(1);opacity:0.3}100%{transform:scale(2.5);opacity:0}} +.airport-popup .maplibregl-popup-content{background:rgba(12,12,14,0.9);color:rgba(255,255,255,0.8);font:500 11px/1.4 system-ui,sans-serif;padding:5px 10px;border-radius:8px;border:1px solid rgba(255,255,255,0.06);backdrop-filter:blur(12px);box-shadow:0 4px 20px rgba(0,0,0,0.4)} +.airport-popup .maplibregl-popup-tip{border-top-color:rgba(12,12,14,0.9)} +`; + +let _cssInjected = false; +function injectCSS() { + if (_cssInjected) return; + const el = document.createElement("style"); + el.textContent = LAYER_CSS; + document.head.appendChild(el); + _cssInjected = true; +} + +function resolveCity(iata: string): City { + const preset = CITIES.find((c) => c.iata === iata); + if (preset) return preset; + const airport = AIRPORTS.find((a) => a.iata === iata); + if (airport) return airportToCity(airport); + return CITIES[0]; +} + +export function AirportLayer({ + activeCity, + onSelectAirport, + isDark, +}: AirportLayerProps) { + const { map, isLoaded } = useMap(); + const markerRef = useRef(null); + const popupRef = useRef(null); + const callbackRef = useRef(onSelectAirport); + useEffect(() => { + callbackRef.current = onSelectAirport; + }); + + useEffect(() => { + if (!map || !isLoaded) return; + injectCSS(); + const m = map; + + const dotColor = isDark ? "rgba(74,222,128,0.6)" : "rgba(22,163,74,0.55)"; + const strokeColor = isDark ? "rgba(74,222,128,0.8)" : "rgba(22,163,74,0.7)"; + + function addSourceAndLayers() { + if (m.getSource(SOURCE_ID)) return; + + m.addSource(SOURCE_ID, { type: "geojson", data: airportGeoJson }); + + m.addLayer({ + id: DOTS_LAYER, + type: "circle", + source: SOURCE_ID, + paint: { + "circle-radius": [ + "interpolate", + ["linear"], + ["zoom"], + 3, + 2, + 6, + 3, + 10, + 4.5, + 14, + 7, + ], + "circle-color": dotColor, + "circle-stroke-width": 1, + "circle-stroke-color": strokeColor, + }, + }); + } + + addSourceAndLayers(); + m.on("style.load", addSourceAndLayers); + + const popup = new maplibregl.Popup({ + closeButton: false, + closeOnClick: false, + className: "airport-popup", + offset: 10, + }); + popupRef.current = popup; + + function onMouseEnter( + e: maplibregl.MapMouseEvent & { + features?: maplibregl.MapGeoJSONFeature[]; + }, + ) { + m.getCanvas().style.cursor = "pointer"; + const f = e.features?.[0]; + if (f?.properties) { + popup + .setLngLat(e.lngLat) + .setHTML( + `${f.properties.iata} · ${f.properties.city}`, + ) + .addTo(m); + } + } + + function onMouseLeave() { + m.getCanvas().style.cursor = ""; + popup.remove(); + } + + function onClick( + e: maplibregl.MapMouseEvent & { + features?: maplibregl.MapGeoJSONFeature[]; + }, + ) { + const f = e.features?.[0]; + if (f?.properties?.iata) { + const city = resolveCity(f.properties.iata as string); + callbackRef.current(city); + } + } + + m.on("mouseenter", DOTS_LAYER, onMouseEnter); + m.on("mouseleave", DOTS_LAYER, onMouseLeave); + m.on("click", DOTS_LAYER, onClick); + + return () => { + m.off("style.load", addSourceAndLayers); + m.off("mouseenter", DOTS_LAYER, onMouseEnter); + m.off("mouseleave", DOTS_LAYER, onMouseLeave); + m.off("click", DOTS_LAYER, onClick); + popup.remove(); + try { + if (m.getLayer(DOTS_LAYER)) m.removeLayer(DOTS_LAYER); + if (m.getSource(SOURCE_ID)) m.removeSource(SOURCE_ID); + } catch { + /* already cleaned up */ + } + }; + }, [map, isLoaded, isDark]); + + useEffect(() => { + if (!map || !isLoaded) return; + injectCSS(); + + const el = document.createElement("div"); + el.className = "airport-beacon"; + el.innerHTML = + '
' + + '
' + + '
' + + '
'; + + const marker = new maplibregl.Marker({ element: el }) + .setLngLat(activeCity.coordinates) + .addTo(map); + markerRef.current = marker; + + return () => { + marker.remove(); + markerRef.current = null; + }; + }, [map, isLoaded, activeCity]); + + return null; +} diff --git a/src/components/map/camera-controller.tsx b/src/components/map/camera-controller.tsx new file mode 100644 index 0000000..2a5936e --- /dev/null +++ b/src/components/map/camera-controller.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { useMap } from "./map"; +import { useSettings } from "@/hooks/use-settings"; +import type { City } from "@/lib/cities"; + +const IDLE_TIMEOUT_MS = 5_000; + +export function CameraController({ city }: { city: City }) { + const { map, isLoaded } = useMap(); + const { settings } = useSettings(); + const prevCityRef = useRef(null); + const idleTimerRef = useRef | null>(null); + const orbitFrameRef = useRef(null); + const isInteractingRef = useRef(false); + + useEffect(() => { + if (!map || !isLoaded || !city) return; + if (city.id === prevCityRef.current) return; + + prevCityRef.current = city.id; + map.flyTo({ + center: city.coordinates, + zoom: 9.2, + pitch: 49, + bearing: 27.4, + duration: 2800, + essential: true, + }); + }, [map, isLoaded, city]); + + useEffect(() => { + if (!map || !isLoaded || !city || !settings.autoOrbit) { + if (orbitFrameRef.current) cancelAnimationFrame(orbitFrameRef.current); + if (idleTimerRef.current) clearTimeout(idleTimerRef.current); + return; + } + + const prefersReducedMotion = + window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false; + if (prefersReducedMotion) return; + + const directionMultiplier = + settings.orbitDirection === "clockwise" ? 1 : -1; + const speed = settings.orbitSpeed * directionMultiplier; + + function startOrbit() { + if (!map || isInteractingRef.current) return; + + function tick() { + if (!map || isInteractingRef.current) return; + const bearing = map.getBearing() + speed; + map.setBearing(bearing % 360); + orbitFrameRef.current = requestAnimationFrame(tick); + } + + orbitFrameRef.current = requestAnimationFrame(tick); + } + + function stopOrbit() { + if (orbitFrameRef.current) { + cancelAnimationFrame(orbitFrameRef.current); + orbitFrameRef.current = null; + } + } + + function resetIdleTimer() { + isInteractingRef.current = true; + stopOrbit(); + + if (idleTimerRef.current) clearTimeout(idleTimerRef.current); + idleTimerRef.current = setTimeout(() => { + isInteractingRef.current = false; + startOrbit(); + }, IDLE_TIMEOUT_MS); + } + + const events = ["mousedown", "wheel", "touchstart"] as const; + const container = map.getContainer(); + events.forEach((e) => + container.addEventListener(e, resetIdleTimer, { passive: true }), + ); + + const onMoveStart = () => { + if (isInteractingRef.current) stopOrbit(); + }; + map.on("movestart", onMoveStart); + + idleTimerRef.current = setTimeout(() => { + isInteractingRef.current = false; + startOrbit(); + }, IDLE_TIMEOUT_MS); + + return () => { + stopOrbit(); + if (idleTimerRef.current) clearTimeout(idleTimerRef.current); + events.forEach((e) => container.removeEventListener(e, resetIdleTimer)); + map.off("movestart", onMoveStart); + }; + }, [ + map, + isLoaded, + city, + settings.autoOrbit, + settings.orbitSpeed, + settings.orbitDirection, + ]); + + return null; +} diff --git a/src/components/map/flight-layers.tsx b/src/components/map/flight-layers.tsx index fd95c5c..856ad7e 100644 --- a/src/components/map/flight-layers.tsx +++ b/src/components/map/flight-layers.tsx @@ -318,24 +318,17 @@ export function FlightLayers({ const ax = animFlight.longitude; const ay = animFlight.latitude; - const curr = currSnapshotsRef.current.get(d.icao24); - const prev = prevSnapshotsRef.current.get(d.icao24); + const heading = ((animFlight.trueTrack ?? 0) * Math.PI) / 180; + const fdx = Math.sin(heading); + const fdy = Math.cos(heading); - if (curr && prev) { - // Direction from prev → curr - const fdx = curr.lng - prev.lng; - const fdy = curr.lat - prev.lat; - - // Walk backward; collapse points that are ahead of the - // animated position (positive projection along flight dir) - for (let i = basePath.length - 1; i >= 0; i--) { - const vx = basePath[i][0] - ax; - const vy = basePath[i][1] - ay; - if (vx * fdx + vy * fdy > 0) { - basePath[i] = [ax, ay, alt]; - } else { - break; - } + for (let i = basePath.length - 1; i >= 0; i--) { + const vx = basePath[i][0] - ax; + const vy = basePath[i][1] - ay; + if (vx * fdx + vy * fdy > 0) { + basePath[i] = [ax, ay, alt]; + } else { + break; } } basePath[basePath.length - 1] = [ax, ay, alt]; diff --git a/src/components/map/map.tsx b/src/components/map/map.tsx index 89ea5d2..a9c56cb 100644 --- a/src/components/map/map.tsx +++ b/src/components/map/map.tsx @@ -34,6 +34,7 @@ type MapProps = { children?: ReactNode; className?: string; mapStyle?: MapStyleSpec; + isDark?: boolean; center?: [number, number]; zoom?: number; pitch?: number; @@ -49,6 +50,7 @@ export const Map = forwardRef(function Map( children, className, mapStyle = DEFAULT_STYLE.style, + isDark = true, center = [0, 20], zoom = 2.5, pitch = 49, @@ -92,11 +94,14 @@ export const Map = forwardRef(function Map( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const isDarkRef = useRef(isDark); + isDarkRef.current = isDark; + useEffect(() => { if (!mapInstance || !isLoaded) return; mapInstance.setStyle(mapStyle as maplibregl.StyleSpecification | string); - const applyTerrain = () => { + const onStyleLoad = () => { if (typeof mapStyle === "object" && "terrain" in mapStyle) { const spec = mapStyle as Record; try { @@ -113,11 +118,13 @@ export const Map = forwardRef(function Map( /* no terrain to remove */ } } + + addAerowayLayers(mapInstance, isDarkRef.current); }; - mapInstance.once("style.load", applyTerrain); + mapInstance.once("style.load", onStyleLoad); return () => { - mapInstance.off("style.load", applyTerrain); + mapInstance.off("style.load", onStyleLoad); }; }, [mapInstance, isLoaded, mapStyle]); @@ -139,3 +146,83 @@ export const Map = forwardRef(function Map( }); Map.displayName = "Map"; + +function findVectorSource(map: maplibregl.Map): string | null { + const style = map.getStyle(); + if (!style?.sources) return null; + for (const [name, source] of Object.entries(style.sources)) { + if ( + source && + typeof source === "object" && + "type" in source && + source.type === "vector" + ) { + return name; + } + } + return null; +} + +function addAerowayLayers(map: maplibregl.Map, dark: boolean): void { + const source = findVectorSource(map); + if (!source) return; + + const runwayColor = dark ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.1)"; + const taxiwayColor = dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.06)"; + + try { + if (!map.getLayer("aeroway-runway")) { + map.addLayer({ + id: "aeroway-runway", + type: "line", + source, + "source-layer": "aeroway", + filter: ["==", "class", "runway"], + minzoom: 10, + layout: { "line-cap": "round" }, + paint: { + "line-color": runwayColor, + "line-width": [ + "interpolate", + ["exponential", 1.5], + ["zoom"], + 10, + 1, + 14, + 30, + 18, + 100, + ], + }, + }); + } + + if (!map.getLayer("aeroway-taxiway")) { + map.addLayer({ + id: "aeroway-taxiway", + type: "line", + source, + "source-layer": "aeroway", + filter: ["==", "class", "taxiway"], + minzoom: 12, + layout: { "line-cap": "round" }, + paint: { + "line-color": taxiwayColor, + "line-width": [ + "interpolate", + ["exponential", 1.5], + ["zoom"], + 12, + 0.5, + 14, + 6, + 18, + 20, + ], + }, + }); + } + } catch { + /* aeroway source-layer may not exist in this tileset */ + } +} diff --git a/src/components/ui/control-panel.tsx b/src/components/ui/control-panel.tsx index b3cab61..3d58a6d 100644 --- a/src/components/ui/control-panel.tsx +++ b/src/components/ui/control-panel.tsx @@ -19,6 +19,7 @@ import { Github, } from "lucide-react"; import { CITIES, type City } from "@/lib/cities"; +import { searchAirports, airportToCity } from "@/lib/airports"; import { MAP_STYLES, type MapStyle } from "@/lib/map-styles"; import { useSettings, type OrbitDirection } from "@/hooks/use-settings"; import { ScrollArea } from "@/components/ui/scroll-area"; @@ -375,16 +376,32 @@ function SearchContent({ requestAnimationFrame(() => inputRef.current?.focus()); }, []); - const filtered = useMemo(() => { - const q = query.toLowerCase(); - return CITIES.filter( + const { featured, airports } = useMemo(() => { + const q = query.trim().toLowerCase(); + + if (!q) + return { + featured: CITIES, + airports: [] as ReturnType, + }; + + const featured = CITIES.filter( (c) => c.name.toLowerCase().includes(q) || c.iata.toLowerCase().includes(q) || c.country.toLowerCase().includes(q), ); + + const featuredIatas = new Set(CITIES.map((c) => c.iata)); + const airports = searchAirports(q).filter( + (a) => !featuredIatas.has(a.iata), + ); + + return { featured, airports }; }, [query]); + const hasResults = featured.length > 0 || airports.length > 0; + return (
@@ -393,48 +410,111 @@ function SearchContent({ ref={inputRef} value={query} onChange={(e) => setQuery(e.target.value)} - placeholder="Search airspace..." - aria-label="Search cities by name, IATA code, or country" + placeholder="Search airports..." + aria-label="Search airports by name, IATA code, city, or country" className="flex-1 bg-transparent text-[14px] font-medium text-white/90 placeholder:text-white/20 outline-none" /> + {query && ( + + )}
- {filtered.length === 0 && ( + {!hasResults && (

- No cities found + No airports found

)} - {filtered.map((city) => ( - - ))} + )} + {featured.map((city) => ( + onSelect(city)} + /> + ))} + + )} + + {airports.length > 0 && ( + <> +

0 ? "pt-3" : "pt-2" + }`} + > + Airports +

+ {airports.map((airport) => ( + onSelect(airportToCity(airport))} + /> + ))} + + )} + + {!query && ( +

+ Search 400+ airports worldwide +

+ )}
); } +function LocationRow({ + name, + detail, + isActive, + onClick, +}: { + name: string; + detail: string; + isActive: boolean; + onClick: () => void; +}) { + return ( + + ); +} + function StyleContent({ activeStyle, onSelect, @@ -457,8 +537,8 @@ function StyleContent({

- Satellite \u00a9 Esri \u00b7 Terrain \u00a9 OpenTopoMap \u00b7 Base maps \u00a9 - CARTO + Satellite \u00a9 Esri \u00b7 Terrain \u00a9 OpenTopoMap \u00b7 Base + maps \u00a9 CARTO

diff --git a/src/hooks/use-flights.ts b/src/hooks/use-flights.ts index 0eb69e4..2da2617 100644 --- a/src/hooks/use-flights.ts +++ b/src/hooks/use-flights.ts @@ -34,6 +34,7 @@ export function useFlights(city: City | null) { const [error, setError] = useState(null); const [rateLimited, setRateLimited] = useState(false); const [retryIn, setRetryIn] = useState(0); + const [creditsRemaining, setCreditsRemaining] = useState(null); const timerRef = useRef | null>(null); const countdownRef = useRef | null>(null); @@ -111,6 +112,7 @@ export function useFlights(city: City | null) { if (result.creditsRemaining !== null) { creditsRef.current = result.creditsRemaining; + setCreditsRemaining(result.creditsRemaining); } const nextInterval = adaptiveInterval(creditsRef.current); @@ -169,14 +171,16 @@ export function useFlights(city: City | null) { setRateLimited(false); clearCountdown(); - fetchData(city); + + const deferred = setTimeout(() => fetchData(city), 0); return () => { + clearTimeout(deferred); clearSchedule(); abortRef.current?.abort(); clearCountdown(); }; }, [city, fetchData, clearCountdown, clearSchedule]); - return { flights, loading, error, rateLimited, retryIn }; + return { flights, loading, error, rateLimited, retryIn, creditsRemaining }; } diff --git a/src/hooks/use-trail-history.ts b/src/hooks/use-trail-history.ts index 87345aa..9fc1ef3 100644 --- a/src/hooks/use-trail-history.ts +++ b/src/hooks/use-trail-history.ts @@ -1,6 +1,6 @@ "use client"; -import { useRef, useMemo } from "react"; +import { useState, useMemo } from "react"; import type { FlightState } from "@/lib/opensky"; type Position = [lng: number, lat: number]; @@ -141,7 +141,6 @@ class TrailStore { } export function useTrailHistory(flights: FlightState[]): TrailEntry[] { - const storeRef = useRef(null); - if (!storeRef.current) storeRef.current = new TrailStore(); - return useMemo(() => storeRef.current!.update(flights), [flights]); + const [store] = useState(() => new TrailStore()); + return useMemo(() => store.update(flights), [flights, store]); } diff --git a/src/lib/airports.ts b/src/lib/airports.ts new file mode 100644 index 0000000..c7ef0e2 --- /dev/null +++ b/src/lib/airports.ts @@ -0,0 +1,3048 @@ +import type { City } from "./cities"; + +export type Airport = { + iata: string; + name: string; + city: string; + country: string; + lat: number; + lng: number; +}; + +const DEFAULT_RADIUS = 2.5; + +export const AIRPORTS: Airport[] = [ + { + iata: "AAL", + name: "Aalborg", + city: "Aalborg", + country: "DK", + lat: 57.0926, + lng: 9.8492, + }, + { + iata: "ABJ", + name: "Félix-Houphouët-Boigny", + city: "Abidjan", + country: "CI", + lat: 5.2614, + lng: -3.9262, + }, + { + iata: "ABQ", + name: "Albuquerque Intl Sunport", + city: "Albuquerque", + country: "US", + lat: 35.0402, + lng: -106.6091, + }, + { + iata: "ABV", + name: "Nnamdi Azikiwe", + city: "Abuja", + country: "NG", + lat: 9.0065, + lng: 7.2632, + }, + { + iata: "ABZ", + name: "Aberdeen", + city: "Aberdeen", + country: "GB", + lat: 57.2019, + lng: -2.1978, + }, + { + iata: "ACC", + name: "Kotoka", + city: "Accra", + country: "GH", + lat: 5.6052, + lng: -0.1668, + }, + { + iata: "ADB", + name: "Adnan Menderes", + city: "Izmir", + country: "TR", + lat: 38.2924, + lng: 27.157, + }, + { + iata: "ADD", + name: "Bole", + city: "Addis Ababa", + country: "ET", + lat: 8.9779, + lng: 38.7993, + }, + { + iata: "ADL", + name: "Adelaide", + city: "Adelaide", + country: "AU", + lat: -34.945, + lng: 138.5306, + }, + { + iata: "AEP", + name: "Aeroparque Jorge Newbery", + city: "Buenos Aires", + country: "AR", + lat: -34.5592, + lng: -58.4156, + }, + { + iata: "AER", + name: "Sochi", + city: "Sochi", + country: "RU", + lat: 43.4499, + lng: 39.9566, + }, + { + iata: "AGP", + name: "Málaga-Costa del Sol", + city: "Málaga", + country: "ES", + lat: 36.675, + lng: -4.499, + }, + { + iata: "AKL", + name: "Auckland", + city: "Auckland", + country: "NZ", + lat: -37.0082, + lng: 174.7917, + }, + { + iata: "ALA", + name: "Almaty", + city: "Almaty", + country: "KZ", + lat: 43.3521, + lng: 77.0405, + }, + { + iata: "ALC", + name: "Alicante-Elche", + city: "Alicante", + country: "ES", + lat: 38.2822, + lng: -0.5582, + }, + { + iata: "ALG", + name: "Houari Boumediene", + city: "Algiers", + country: "DZ", + lat: 36.691, + lng: 3.2154, + }, + { + iata: "AMS", + name: "Schiphol", + city: "Amsterdam", + country: "NL", + lat: 52.3086, + lng: 4.7639, + }, + { + iata: "ANC", + name: "Ted Stevens", + city: "Anchorage", + country: "US", + lat: 61.1743, + lng: -149.9962, + }, + { + iata: "ARN", + name: "Arlanda", + city: "Stockholm", + country: "SE", + lat: 59.6519, + lng: 17.9186, + }, + { + iata: "ATH", + name: "Eleftherios Venizelos", + city: "Athens", + country: "GR", + lat: 37.9364, + lng: 23.9445, + }, + { + iata: "ATL", + name: "Hartsfield-Jackson", + city: "Atlanta", + country: "US", + lat: 33.6407, + lng: -84.4277, + }, + { + iata: "AUA", + name: "Queen Beatrix", + city: "Oranjestad", + country: "AW", + lat: 12.5014, + lng: -70.0152, + }, + { + iata: "AUH", + name: "Zayed International", + city: "Abu Dhabi", + country: "AE", + lat: 24.433, + lng: 54.6511, + }, + { + iata: "AUS", + name: "Austin-Bergstrom", + city: "Austin", + country: "US", + lat: 30.1975, + lng: -97.6664, + }, + { + iata: "AYT", + name: "Antalya", + city: "Antalya", + country: "TR", + lat: 36.8987, + lng: 30.8005, + }, + { + iata: "BAH", + name: "Bahrain", + city: "Manama", + country: "BH", + lat: 26.2708, + lng: 50.6336, + }, + { + iata: "BBI", + name: "Biju Patnaik", + city: "Bhubaneswar", + country: "IN", + lat: 20.2444, + lng: 85.8178, + }, + { + iata: "BCN", + name: "El Prat", + city: "Barcelona", + country: "ES", + lat: 41.2971, + lng: 2.0785, + }, + { + iata: "BEG", + name: "Nikola Tesla", + city: "Belgrade", + country: "RS", + lat: 44.8184, + lng: 20.309, + }, + { + iata: "BEY", + name: "Rafic Hariri", + city: "Beirut", + country: "LB", + lat: 33.8209, + lng: 35.4884, + }, + { + iata: "BGO", + name: "Flesland", + city: "Bergen", + country: "NO", + lat: 60.2934, + lng: 5.2181, + }, + { + iata: "BHD", + name: "George Best Belfast City", + city: "Belfast", + country: "GB", + lat: 54.618, + lng: -5.8725, + }, + { + iata: "BHX", + name: "Birmingham", + city: "Birmingham", + country: "GB", + lat: 52.4539, + lng: -1.748, + }, + { + iata: "BIO", + name: "Bilbao", + city: "Bilbao", + country: "ES", + lat: 43.3011, + lng: -2.9106, + }, + { + iata: "BKI", + name: "Kota Kinabalu", + city: "Kota Kinabalu", + country: "MY", + lat: 5.9372, + lng: 116.0515, + }, + { + iata: "BKK", + name: "Suvarnabhumi", + city: "Bangkok", + country: "TH", + lat: 13.6899, + lng: 100.7501, + }, + { + iata: "BLQ", + name: "Guglielmo Marconi", + city: "Bologna", + country: "IT", + lat: 44.5354, + lng: 11.2887, + }, + { + iata: "BLR", + name: "Kempegowda", + city: "Bengaluru", + country: "IN", + lat: 13.1986, + lng: 77.7066, + }, + { + iata: "BNA", + name: "Nashville", + city: "Nashville", + country: "US", + lat: 36.1246, + lng: -86.6782, + }, + { + iata: "BNE", + name: "Brisbane", + city: "Brisbane", + country: "AU", + lat: -27.3842, + lng: 153.1175, + }, + { + iata: "BOD", + name: "Mérignac", + city: "Bordeaux", + country: "FR", + lat: 44.8283, + lng: -0.7156, + }, + { + iata: "BOG", + name: "El Dorado", + city: "Bogotá", + country: "CO", + lat: 4.7016, + lng: -74.1469, + }, + { + iata: "BOM", + name: "Chhatrapati Shivaji Maharaj", + city: "Mumbai", + country: "IN", + lat: 19.0896, + lng: 72.8656, + }, + { + iata: "BOS", + name: "Logan", + city: "Boston", + country: "US", + lat: 42.3656, + lng: -71.0096, + }, + { + iata: "BRU", + name: "Brussels", + city: "Brussels", + country: "BE", + lat: 50.9014, + lng: 4.4844, + }, + { + iata: "BSB", + name: "Brasília", + city: "Brasília", + country: "BR", + lat: -15.8711, + lng: -47.9186, + }, + { + iata: "BTS", + name: "M. R. Štefánik", + city: "Bratislava", + country: "SK", + lat: 48.1702, + lng: 17.2127, + }, + { + iata: "BUD", + name: "Ferenc Liszt", + city: "Budapest", + country: "HU", + lat: 47.4369, + lng: 19.2556, + }, + { + iata: "BUF", + name: "Buffalo Niagara", + city: "Buffalo", + country: "US", + lat: 42.9405, + lng: -78.7322, + }, + { + iata: "BWI", + name: "Baltimore/Washington", + city: "Baltimore", + country: "US", + lat: 39.1754, + lng: -76.6684, + }, + { + iata: "CAG", + name: "Cagliari Elmas", + city: "Cagliari", + country: "IT", + lat: 39.2515, + lng: 9.0543, + }, + { + iata: "CAI", + name: "Cairo", + city: "Cairo", + country: "EG", + lat: 30.1219, + lng: 31.4056, + }, + { + iata: "CAN", + name: "Baiyun", + city: "Guangzhou", + country: "CN", + lat: 23.3924, + lng: 113.299, + }, + { + iata: "CCS", + name: "Simón Bolívar", + city: "Caracas", + country: "VE", + lat: 10.6012, + lng: -66.9912, + }, + { + iata: "CCU", + name: "Netaji Subhas Chandra Bose", + city: "Kolkata", + country: "IN", + lat: 22.6547, + lng: 88.4467, + }, + { + iata: "CDG", + name: "Charles de Gaulle", + city: "Paris", + country: "FR", + lat: 49.0097, + lng: 2.5479, + }, + { + iata: "CEB", + name: "Mactan-Cebu", + city: "Cebu", + country: "PH", + lat: 10.3075, + lng: 123.9794, + }, + { + iata: "CFU", + name: "Ioannis Kapodistrias", + city: "Corfu", + country: "GR", + lat: 39.6019, + lng: 19.9117, + }, + { + iata: "CGK", + name: "Soekarno-Hatta", + city: "Jakarta", + country: "ID", + lat: -6.1256, + lng: 106.6558, + }, + { + iata: "CGN", + name: "Cologne Bonn", + city: "Cologne", + country: "DE", + lat: 50.8659, + lng: 7.1427, + }, + { + iata: "CGO", + name: "Xinzheng", + city: "Zhengzhou", + country: "CN", + lat: 34.5197, + lng: 113.8409, + }, + { + iata: "CHC", + name: "Christchurch", + city: "Christchurch", + country: "NZ", + lat: -43.4894, + lng: 172.5322, + }, + { + iata: "CJU", + name: "Jeju", + city: "Jeju", + country: "KR", + lat: 33.5114, + lng: 126.4929, + }, + { + iata: "CKG", + name: "Jiangbei", + city: "Chongqing", + country: "CN", + lat: 29.7192, + lng: 106.6417, + }, + { + iata: "CLJ", + name: "Cluj-Napoca", + city: "Cluj-Napoca", + country: "RO", + lat: 46.7852, + lng: 23.6862, + }, + { + iata: "CLT", + name: "Charlotte Douglas", + city: "Charlotte", + country: "US", + lat: 35.214, + lng: -80.9431, + }, + { + iata: "CMB", + name: "Bandaranaike", + city: "Colombo", + country: "LK", + lat: 7.1808, + lng: 79.8841, + }, + { + iata: "CMH", + name: "John Glenn Columbus", + city: "Columbus", + country: "US", + lat: 39.998, + lng: -82.8919, + }, + { + iata: "CMN", + name: "Mohammed V", + city: "Casablanca", + country: "MA", + lat: 33.3675, + lng: -7.5898, + }, + { + iata: "CNS", + name: "Cairns", + city: "Cairns", + country: "AU", + lat: -16.8858, + lng: 145.7554, + }, + { + iata: "CNX", + name: "Chiang Mai", + city: "Chiang Mai", + country: "TH", + lat: 18.7668, + lng: 98.9626, + }, + { + iata: "COK", + name: "Cochin", + city: "Kochi", + country: "IN", + lat: 10.152, + lng: 76.4019, + }, + { + iata: "CPH", + name: "Kastrup", + city: "Copenhagen", + country: "DK", + lat: 55.618, + lng: 12.656, + }, + { + iata: "CPT", + name: "Cape Town", + city: "Cape Town", + country: "ZA", + lat: -33.9649, + lng: 18.6017, + }, + { + iata: "CRK", + name: "Clark", + city: "Angeles", + country: "PH", + lat: 15.186, + lng: 120.5604, + }, + { + iata: "CSX", + name: "Huanghua", + city: "Changsha", + country: "CN", + lat: 28.1892, + lng: 113.2199, + }, + { + iata: "CTA", + name: "Fontanarossa", + city: "Catania", + country: "IT", + lat: 37.4668, + lng: 15.0664, + }, + { + iata: "CTG", + name: "Rafael Núñez", + city: "Cartagena", + country: "CO", + lat: 10.4424, + lng: -75.513, + }, + { + iata: "CTS", + name: "New Chitose", + city: "Sapporo", + country: "JP", + lat: 42.7752, + lng: 141.6922, + }, + { + iata: "CTU", + name: "Shuangliu", + city: "Chengdu", + country: "CN", + lat: 30.5785, + lng: 103.9471, + }, + { + iata: "CUN", + name: "Cancún", + city: "Cancún", + country: "MX", + lat: 21.0365, + lng: -86.877, + }, + { + iata: "CVG", + name: "Cincinnati/Northern Kentucky", + city: "Cincinnati", + country: "US", + lat: 39.0489, + lng: -84.6678, + }, + { + iata: "DAC", + name: "Hazrat Shahjalal", + city: "Dhaka", + country: "BD", + lat: 23.8432, + lng: 90.3977, + }, + { + iata: "DAD", + name: "Da Nang", + city: "Da Nang", + country: "VN", + lat: 16.0439, + lng: 108.1992, + }, + { + iata: "DAR", + name: "Julius Nyerere", + city: "Dar es Salaam", + country: "TZ", + lat: -6.8781, + lng: 39.2026, + }, + { + iata: "DBV", + name: "Dubrovnik", + city: "Dubrovnik", + country: "HR", + lat: 42.5614, + lng: 18.2682, + }, + { + iata: "DCA", + name: "Ronald Reagan Washington", + city: "Washington", + country: "US", + lat: 38.8521, + lng: -77.0377, + }, + { + iata: "DEL", + name: "Indira Gandhi", + city: "Delhi", + country: "IN", + lat: 28.5562, + lng: 77.1001, + }, + { + iata: "DEN", + name: "Denver", + city: "Denver", + country: "US", + lat: 39.8561, + lng: -104.6737, + }, + { + iata: "DFW", + name: "Dallas/Fort Worth", + city: "Dallas", + country: "US", + lat: 32.8998, + lng: -97.0403, + }, + { + iata: "DKR", + name: "Blaise Diagne", + city: "Dakar", + country: "SN", + lat: 14.6697, + lng: -17.0734, + }, + { + iata: "DLC", + name: "Zhoushuizi", + city: "Dalian", + country: "CN", + lat: 38.9657, + lng: 121.5386, + }, + { + iata: "DME", + name: "Domodedovo", + city: "Moscow", + country: "RU", + lat: 55.4088, + lng: 37.9063, + }, + { + iata: "DMK", + name: "Don Mueang", + city: "Bangkok", + country: "TH", + lat: 13.9126, + lng: 100.607, + }, + { + iata: "DMM", + name: "King Fahd", + city: "Dammam", + country: "SA", + lat: 26.4712, + lng: 49.7979, + }, + { + iata: "DOH", + name: "Hamad", + city: "Doha", + country: "QA", + lat: 25.2731, + lng: 51.6081, + }, + { + iata: "DPS", + name: "Ngurah Rai", + city: "Bali", + country: "ID", + lat: -8.7482, + lng: 115.1672, + }, + { + iata: "DRS", + name: "Dresden", + city: "Dresden", + country: "DE", + lat: 51.1328, + lng: 13.7672, + }, + { + iata: "DTW", + name: "Detroit Metropolitan Wayne County", + city: "Detroit", + country: "US", + lat: 42.2124, + lng: -83.3534, + }, + { + iata: "DUB", + name: "Dublin", + city: "Dublin", + country: "IE", + lat: 53.4213, + lng: -6.2701, + }, + { + iata: "DUR", + name: "King Shaka", + city: "Durban", + country: "ZA", + lat: -29.6144, + lng: 31.1197, + }, + { + iata: "DUS", + name: "Düsseldorf", + city: "Düsseldorf", + country: "DE", + lat: 51.2895, + lng: 6.7668, + }, + { + iata: "DWC", + name: "Al Maktoum", + city: "Dubai", + country: "AE", + lat: 24.8966, + lng: 55.1614, + }, + { + iata: "DXB", + name: "Dubai", + city: "Dubai", + country: "AE", + lat: 25.2532, + lng: 55.3657, + }, + { + iata: "EBB", + name: "Entebbe", + city: "Entebbe", + country: "UG", + lat: 0.0424, + lng: 32.4435, + }, + { + iata: "EDI", + name: "Edinburgh", + city: "Edinburgh", + country: "GB", + lat: 55.9508, + lng: -3.3726, + }, + { + iata: "EMA", + name: "East Midlands", + city: "Nottingham", + country: "GB", + lat: 52.8311, + lng: -1.3281, + }, + { + iata: "ESB", + name: "Esenboğa", + city: "Ankara", + country: "TR", + lat: 40.1281, + lng: 32.9951, + }, + { + iata: "EVN", + name: "Zvartnots", + city: "Yerevan", + country: "AM", + lat: 40.1473, + lng: 44.3959, + }, + { + iata: "EWR", + name: "Newark Liberty", + city: "Newark", + country: "US", + lat: 40.6925, + lng: -74.1687, + }, + { + iata: "EZE", + name: "Ministro Pistarini", + city: "Buenos Aires", + country: "AR", + lat: -34.8222, + lng: -58.5358, + }, + { + iata: "FAO", + name: "Faro", + city: "Faro", + country: "PT", + lat: 37.0144, + lng: -7.9659, + }, + { + iata: "FCO", + name: "Leonardo da Vinci–Fiumicino", + city: "Rome", + country: "IT", + lat: 41.8003, + lng: 12.2389, + }, + { + iata: "FLL", + name: "Fort Lauderdale-Hollywood", + city: "Fort Lauderdale", + country: "US", + lat: 26.0726, + lng: -80.1527, + }, + { + iata: "FLN", + name: "Hercílio Luz", + city: "Florianópolis", + country: "BR", + lat: -27.6703, + lng: -48.5478, + }, + { + iata: "FLR", + name: "Amerigo Vespucci", + city: "Florence", + country: "IT", + lat: 43.81, + lng: 11.2051, + }, + { + iata: "FNC", + name: "Cristiano Ronaldo", + city: "Funchal", + country: "PT", + lat: 32.6942, + lng: -16.7781, + }, + { + iata: "FOC", + name: "Changle", + city: "Fuzhou", + country: "CN", + lat: 25.9348, + lng: 119.6631, + }, + { + iata: "FRA", + name: "Frankfurt", + city: "Frankfurt", + country: "DE", + lat: 50.0379, + lng: 8.5622, + }, + { + iata: "FRU", + name: "Manas", + city: "Bishkek", + country: "KG", + lat: 43.0613, + lng: 74.4776, + }, + { + iata: "FUK", + name: "Fukuoka", + city: "Fukuoka", + country: "JP", + lat: 33.5859, + lng: 130.4507, + }, + { + iata: "GDL", + name: "Miguel Hidalgo y Costilla", + city: "Guadalajara", + country: "MX", + lat: 20.5218, + lng: -103.3114, + }, + { + iata: "GDN", + name: "Lech Wałęsa", + city: "Gdańsk", + country: "PL", + lat: 54.3776, + lng: 18.4662, + }, + { + iata: "GIG", + name: "Galeão", + city: "Rio de Janeiro", + country: "BR", + lat: -22.8099, + lng: -43.2506, + }, + { + iata: "GLA", + name: "Glasgow", + city: "Glasgow", + country: "GB", + lat: 55.8717, + lng: -4.4331, + }, + { + iata: "GMP", + name: "Gimpo", + city: "Seoul", + country: "KR", + lat: 37.5582, + lng: 126.7906, + }, + { + iata: "GND", + name: "Maurice Bishop", + city: "St. George's", + country: "GD", + lat: 12.0042, + lng: -61.7862, + }, + { + iata: "GOI", + name: "Dabolim", + city: "Goa", + country: "IN", + lat: 15.3808, + lng: 73.8314, + }, + { + iata: "GOT", + name: "Landvetter", + city: "Gothenburg", + country: "SE", + lat: 57.6628, + lng: 12.2798, + }, + { + iata: "GRU", + name: "Guarulhos", + city: "São Paulo", + country: "BR", + lat: -23.4356, + lng: -46.4731, + }, + { + iata: "GRZ", + name: "Graz", + city: "Graz", + country: "AT", + lat: 46.9911, + lng: 15.4396, + }, + { + iata: "GVA", + name: "Geneva", + city: "Geneva", + country: "CH", + lat: 46.238, + lng: 6.1089, + }, + { + iata: "GYD", + name: "Heydar Aliyev", + city: "Baku", + country: "AZ", + lat: 40.4675, + lng: 50.0467, + }, + { + iata: "HAJ", + name: "Hannover-Langenhagen", + city: "Hannover", + country: "DE", + lat: 52.4611, + lng: 9.6851, + }, + { + iata: "HAK", + name: "Meilan", + city: "Haikou", + country: "CN", + lat: 19.9349, + lng: 110.459, + }, + { + iata: "HAM", + name: "Hamburg", + city: "Hamburg", + country: "DE", + lat: 53.6304, + lng: 9.9882, + }, + { + iata: "HAN", + name: "Noi Bai", + city: "Hanoi", + country: "VN", + lat: 21.2212, + lng: 105.807, + }, + { + iata: "HAV", + name: "José Martí", + city: "Havana", + country: "CU", + lat: 22.9892, + lng: -82.4091, + }, + { + iata: "HEL", + name: "Helsinki-Vantaa", + city: "Helsinki", + country: "FI", + lat: 60.3172, + lng: 24.9633, + }, + { + iata: "HER", + name: "Heraklion", + city: "Heraklion", + country: "GR", + lat: 35.3397, + lng: 25.1803, + }, + { + iata: "HGH", + name: "Xiaoshan", + city: "Hangzhou", + country: "CN", + lat: 30.2295, + lng: 120.4344, + }, + { + iata: "HIJ", + name: "Hiroshima", + city: "Hiroshima", + country: "JP", + lat: 34.4361, + lng: 132.9194, + }, + { + iata: "HKG", + name: "Hong Kong", + city: "Hong Kong", + country: "HK", + lat: 22.3089, + lng: 113.9185, + }, + { + iata: "HKT", + name: "Phuket", + city: "Phuket", + country: "TH", + lat: 8.1132, + lng: 98.3169, + }, + { + iata: "HND", + name: "Haneda", + city: "Tokyo", + country: "JP", + lat: 35.5494, + lng: 139.7798, + }, + { + iata: "HNL", + name: "Daniel K. Inouye", + city: "Honolulu", + country: "US", + lat: 21.3187, + lng: -157.9225, + }, + { + iata: "HOU", + name: "William P. Hobby", + city: "Houston", + country: "US", + lat: 29.6454, + lng: -95.2789, + }, + { + iata: "HRB", + name: "Taiping", + city: "Harbin", + country: "CN", + lat: 45.6234, + lng: 126.25, + }, + { + iata: "HRG", + name: "Hurghada", + city: "Hurghada", + country: "EG", + lat: 27.1784, + lng: 33.7994, + }, + { + iata: "HYD", + name: "Rajiv Gandhi", + city: "Hyderabad", + country: "IN", + lat: 17.2313, + lng: 78.4299, + }, + { + iata: "IAD", + name: "Dulles", + city: "Washington", + country: "US", + lat: 38.9445, + lng: -77.4558, + }, + { + iata: "IAH", + name: "George Bush Intercontinental", + city: "Houston", + country: "US", + lat: 29.9844, + lng: -95.3414, + }, + { + iata: "IBZ", + name: "Ibiza", + city: "Ibiza", + country: "ES", + lat: 38.8729, + lng: 1.3731, + }, + { + iata: "ICN", + name: "Incheon", + city: "Seoul", + country: "KR", + lat: 37.4602, + lng: 126.4407, + }, + { + iata: "IKA", + name: "Imam Khomeini", + city: "Tehran", + country: "IR", + lat: 35.4161, + lng: 51.1522, + }, + { + iata: "IND", + name: "Indianapolis", + city: "Indianapolis", + country: "US", + lat: 39.7173, + lng: -86.2944, + }, + { + iata: "ISB", + name: "Islamabad", + city: "Islamabad", + country: "PK", + lat: 33.5605, + lng: 72.8496, + }, + { + iata: "IST", + name: "Istanbul", + city: "Istanbul", + country: "TR", + lat: 41.2753, + lng: 28.7519, + }, + { + iata: "JAX", + name: "Jacksonville", + city: "Jacksonville", + country: "US", + lat: 30.4941, + lng: -81.6879, + }, + { + iata: "JED", + name: "King Abdulaziz", + city: "Jeddah", + country: "SA", + lat: 21.6796, + lng: 39.1565, + }, + { + iata: "JFK", + name: "John F. Kennedy", + city: "New York", + country: "US", + lat: 40.6413, + lng: -73.7781, + }, + { + iata: "JMK", + name: "Mykonos", + city: "Mykonos", + country: "GR", + lat: 37.4351, + lng: 25.3481, + }, + { + iata: "JNB", + name: "O. R. Tambo", + city: "Johannesburg", + country: "ZA", + lat: -26.1392, + lng: 28.246, + }, + { + iata: "JRO", + name: "Kilimanjaro", + city: "Arusha", + country: "TZ", + lat: -3.4294, + lng: 37.0745, + }, + { + iata: "JTR", + name: "Santorini", + city: "Santorini", + country: "GR", + lat: 36.3992, + lng: 25.4793, + }, + { + iata: "KBP", + name: "Boryspil", + city: "Kyiv", + country: "UA", + lat: 50.345, + lng: 30.8947, + }, + { + iata: "KCH", + name: "Kuching", + city: "Kuching", + country: "MY", + lat: 1.4847, + lng: 110.3472, + }, + { + iata: "KEF", + name: "Keflavík", + city: "Reykjavík", + country: "IS", + lat: 63.985, + lng: -22.6056, + }, + { + iata: "KGL", + name: "Kigali", + city: "Kigali", + country: "RW", + lat: -1.9686, + lng: 30.1395, + }, + { + iata: "KHH", + name: "Kaohsiung", + city: "Kaohsiung", + country: "TW", + lat: 22.5771, + lng: 120.3501, + }, + { + iata: "KHI", + name: "Jinnah", + city: "Karachi", + country: "PK", + lat: 24.9065, + lng: 67.1609, + }, + { + iata: "KIN", + name: "Norman Manley", + city: "Kingston", + country: "JM", + lat: 17.9357, + lng: -76.7875, + }, + { + iata: "KIX", + name: "Kansai", + city: "Osaka", + country: "JP", + lat: 34.4273, + lng: 135.244, + }, + { + iata: "KMG", + name: "Changshui", + city: "Kunming", + country: "CN", + lat: 24.9924, + lng: 102.7432, + }, + { + iata: "KOA", + name: "Ellison Onizuka Kona", + city: "Kona", + country: "US", + lat: 19.7388, + lng: -156.0456, + }, + { + iata: "KOS", + name: "Sihanoukville", + city: "Sihanoukville", + country: "KH", + lat: 10.5797, + lng: 103.637, + }, + { + iata: "KRK", + name: "John Paul II", + city: "Kraków", + country: "PL", + lat: 50.0777, + lng: 19.7848, + }, + { + iata: "KRT", + name: "Khartoum", + city: "Khartoum", + country: "SD", + lat: 15.5895, + lng: 32.5532, + }, + { + iata: "KTM", + name: "Tribhuvan", + city: "Kathmandu", + country: "NP", + lat: 27.6966, + lng: 85.3591, + }, + { + iata: "KUL", + name: "Kuala Lumpur", + city: "Kuala Lumpur", + country: "MY", + lat: 2.7456, + lng: 101.7099, + }, + { + iata: "KWI", + name: "Kuwait", + city: "Kuwait City", + country: "KW", + lat: 29.2266, + lng: 47.9689, + }, + { + iata: "KWL", + name: "Liangjiang", + city: "Guilin", + country: "CN", + lat: 25.2181, + lng: 110.0392, + }, + { + iata: "LAD", + name: "Quatro de Fevereiro", + city: "Luanda", + country: "AO", + lat: -8.8584, + lng: 13.2312, + }, + { + iata: "LAS", + name: "Harry Reid", + city: "Las Vegas", + country: "US", + lat: 36.084, + lng: -115.1537, + }, + { + iata: "LAX", + name: "Los Angeles", + city: "Los Angeles", + country: "US", + lat: 33.9425, + lng: -118.4081, + }, + { + iata: "LBA", + name: "Leeds Bradford", + city: "Leeds", + country: "GB", + lat: 53.8659, + lng: -1.6606, + }, + { + iata: "LBV", + name: "Léon-Mba", + city: "Libreville", + country: "GA", + lat: 0.4586, + lng: 9.4123, + }, + { + iata: "LCA", + name: "Larnaca", + city: "Larnaca", + country: "CY", + lat: 34.8751, + lng: 33.6249, + }, + { + iata: "LCY", + name: "London City", + city: "London", + country: "GB", + lat: 51.5053, + lng: 0.0553, + }, + { + iata: "LED", + name: "Pulkovo", + city: "St. Petersburg", + country: "RU", + lat: 59.8003, + lng: 30.2625, + }, + { + iata: "LEJ", + name: "Leipzig/Halle", + city: "Leipzig", + country: "DE", + lat: 51.4324, + lng: 12.2416, + }, + { + iata: "LGA", + name: "LaGuardia", + city: "New York", + country: "US", + lat: 40.7772, + lng: -73.8726, + }, + { + iata: "LGW", + name: "Gatwick", + city: "London", + country: "GB", + lat: 51.1537, + lng: -0.1821, + }, + { + iata: "LHE", + name: "Allama Iqbal", + city: "Lahore", + country: "PK", + lat: 31.5216, + lng: 74.4036, + }, + { + iata: "LHR", + name: "Heathrow", + city: "London", + country: "GB", + lat: 51.4706, + lng: -0.4619, + }, + { + iata: "LIM", + name: "Jorge Chávez", + city: "Lima", + country: "PE", + lat: -12.0219, + lng: -77.1143, + }, + { + iata: "LIN", + name: "Linate", + city: "Milan", + country: "IT", + lat: 45.4491, + lng: 9.2782, + }, + { + iata: "LIS", + name: "Humberto Delgado", + city: "Lisbon", + country: "PT", + lat: 38.7742, + lng: -9.1342, + }, + { + iata: "LJU", + name: "Jože Pučnik", + city: "Ljubljana", + country: "SI", + lat: 46.2237, + lng: 14.4576, + }, + { + iata: "LOS", + name: "Murtala Muhammed", + city: "Lagos", + country: "NG", + lat: 6.5774, + lng: 3.3211, + }, + { + iata: "LPA", + name: "Gran Canaria", + city: "Las Palmas", + country: "ES", + lat: 27.9319, + lng: -15.3866, + }, + { + iata: "LPB", + name: "El Alto", + city: "La Paz", + country: "BO", + lat: -16.5133, + lng: -68.1923, + }, + { + iata: "LPQ", + name: "Luang Prabang", + city: "Luang Prabang", + country: "LA", + lat: 19.8973, + lng: 102.1611, + }, + { + iata: "LTN", + name: "Luton", + city: "London", + country: "GB", + lat: 51.8747, + lng: -0.3684, + }, + { + iata: "LUN", + name: "Kenneth Kaunda", + city: "Lusaka", + country: "ZM", + lat: -15.3308, + lng: 28.4526, + }, + { + iata: "LUX", + name: "Luxembourg", + city: "Luxembourg", + country: "LU", + lat: 49.6233, + lng: 6.2044, + }, + { + iata: "LYS", + name: "Lyon-Saint Exupéry", + city: "Lyon", + country: "FR", + lat: 45.7256, + lng: 5.0811, + }, + { + iata: "MAA", + name: "Chennai", + city: "Chennai", + country: "IN", + lat: 12.9941, + lng: 80.1709, + }, + { + iata: "MAD", + name: "Adolfo Suárez Madrid-Barajas", + city: "Madrid", + country: "ES", + lat: 40.4983, + lng: -3.5676, + }, + { + iata: "MAN", + name: "Manchester", + city: "Manchester", + country: "GB", + lat: 53.3537, + lng: -2.275, + }, + { + iata: "MBA", + name: "Moi", + city: "Mombasa", + country: "KE", + lat: -4.0348, + lng: 39.5942, + }, + { + iata: "MBJ", + name: "Sangster", + city: "Montego Bay", + country: "JM", + lat: 18.5037, + lng: -77.9134, + }, + { + iata: "MCI", + name: "Kansas City", + city: "Kansas City", + country: "US", + lat: 39.2976, + lng: -94.7139, + }, + { + iata: "MCO", + name: "Orlando", + city: "Orlando", + country: "US", + lat: 28.4312, + lng: -81.3081, + }, + { + iata: "MCT", + name: "Muscat", + city: "Muscat", + country: "OM", + lat: 23.5933, + lng: 58.2844, + }, + { + iata: "MDE", + name: "José María Córdova", + city: "Medellín", + country: "CO", + lat: 6.1645, + lng: -75.4231, + }, + { + iata: "MDW", + name: "Midway", + city: "Chicago", + country: "US", + lat: 41.786, + lng: -87.7524, + }, + { + iata: "MED", + name: "Prince Mohammad bin Abdulaziz", + city: "Medina", + country: "SA", + lat: 24.5534, + lng: 39.7051, + }, + { + iata: "MEL", + name: "Melbourne", + city: "Melbourne", + country: "AU", + lat: -37.6733, + lng: 144.8433, + }, + { + iata: "MEM", + name: "Memphis", + city: "Memphis", + country: "US", + lat: 35.0424, + lng: -89.9767, + }, + { + iata: "MEX", + name: "Benito Juárez", + city: "Mexico City", + country: "MX", + lat: 19.4363, + lng: -99.0721, + }, + { + iata: "MFM", + name: "Macau", + city: "Macau", + country: "MO", + lat: 22.1496, + lng: 113.5914, + }, + { + iata: "MHD", + name: "Shahid Hashemi Nejad", + city: "Mashhad", + country: "IR", + lat: 36.2352, + lng: 59.641, + }, + { + iata: "MIA", + name: "Miami", + city: "Miami", + country: "US", + lat: 25.7959, + lng: -80.287, + }, + { + iata: "MKE", + name: "General Mitchell", + city: "Milwaukee", + country: "US", + lat: 42.9472, + lng: -87.8966, + }, + { + iata: "MLA", + name: "Malta", + city: "Luqa", + country: "MT", + lat: 35.8575, + lng: 14.4775, + }, + { + iata: "MLE", + name: "Velana", + city: "Malé", + country: "MV", + lat: 4.1918, + lng: 73.5291, + }, + { + iata: "MNL", + name: "Ninoy Aquino", + city: "Manila", + country: "PH", + lat: 14.5086, + lng: 121.0198, + }, + { + iata: "MPM", + name: "Maputo", + city: "Maputo", + country: "MZ", + lat: -25.9208, + lng: 32.5726, + }, + { + iata: "MRS", + name: "Marseille Provence", + city: "Marseille", + country: "FR", + lat: 43.4393, + lng: 5.2214, + }, + { + iata: "MRU", + name: "Sir Seewoosagur Ramgoolam", + city: "Mauritius", + country: "MU", + lat: -20.4302, + lng: 57.6836, + }, + { + iata: "MSP", + name: "Minneapolis-Saint Paul", + city: "Minneapolis", + country: "US", + lat: 44.882, + lng: -93.2218, + }, + { + iata: "MSY", + name: "Louis Armstrong New Orleans", + city: "New Orleans", + country: "US", + lat: 29.9934, + lng: -90.258, + }, + { + iata: "MUC", + name: "Franz Josef Strauss", + city: "Munich", + country: "DE", + lat: 48.3538, + lng: 11.7861, + }, + { + iata: "MVD", + name: "Carrasco", + city: "Montevideo", + country: "UY", + lat: -34.8384, + lng: -56.0308, + }, + { + iata: "MXP", + name: "Malpensa", + city: "Milan", + country: "IT", + lat: 45.6306, + lng: 8.7281, + }, + { + iata: "NAN", + name: "Nadi", + city: "Nadi", + country: "FJ", + lat: -17.7554, + lng: 177.4431, + }, + { + iata: "NAP", + name: "Naples", + city: "Naples", + country: "IT", + lat: 40.886, + lng: 14.2908, + }, + { + iata: "NAS", + name: "Lynden Pindling", + city: "Nassau", + country: "BS", + lat: 25.039, + lng: -77.4662, + }, + { + iata: "NBO", + name: "Jomo Kenyatta", + city: "Nairobi", + country: "KE", + lat: -1.3192, + lng: 36.9278, + }, + { + iata: "NCE", + name: "Nice Côte d'Azur", + city: "Nice", + country: "FR", + lat: 43.6584, + lng: 7.2159, + }, + { + iata: "NGO", + name: "Chubu Centrair", + city: "Nagoya", + country: "JP", + lat: 34.8584, + lng: 136.8125, + }, + { + iata: "NKG", + name: "Lukou", + city: "Nanjing", + country: "CN", + lat: 31.742, + lng: 118.862, + }, + { + iata: "NNG", + name: "Wuxu", + city: "Nanning", + country: "CN", + lat: 22.6083, + lng: 108.1722, + }, + { + iata: "NOU", + name: "La Tontouta", + city: "Nouméa", + country: "NC", + lat: -22.0146, + lng: 166.2129, + }, + { + iata: "NRT", + name: "Narita", + city: "Tokyo", + country: "JP", + lat: 35.7647, + lng: 140.3864, + }, + { + iata: "NTE", + name: "Nantes Atlantique", + city: "Nantes", + country: "FR", + lat: 47.1532, + lng: -1.6107, + }, + { + iata: "NUE", + name: "Albrecht Dürer", + city: "Nuremberg", + country: "DE", + lat: 49.4987, + lng: 11.0669, + }, + { + iata: "OAK", + name: "Oakland", + city: "Oakland", + country: "US", + lat: 37.7213, + lng: -122.2208, + }, + { + iata: "OKA", + name: "Naha", + city: "Okinawa", + country: "JP", + lat: 26.1958, + lng: 127.646, + }, + { + iata: "OKC", + name: "Will Rogers World", + city: "Oklahoma City", + country: "US", + lat: 35.3931, + lng: -97.6007, + }, + { + iata: "OLB", + name: "Costa Smeralda", + city: "Olbia", + country: "IT", + lat: 40.8987, + lng: 9.5176, + }, + { + iata: "OMA", + name: "Eppley Airfield", + city: "Omaha", + country: "US", + lat: 41.303, + lng: -95.894, + }, + { + iata: "OOL", + name: "Gold Coast", + city: "Gold Coast", + country: "AU", + lat: -28.1644, + lng: 153.5047, + }, + { + iata: "OPO", + name: "Francisco Sá Carneiro", + city: "Porto", + country: "PT", + lat: 41.2481, + lng: -8.6814, + }, + { + iata: "ORD", + name: "O'Hare", + city: "Chicago", + country: "US", + lat: 41.9742, + lng: -87.9073, + }, + { + iata: "ORY", + name: "Orly", + city: "Paris", + country: "FR", + lat: 48.7233, + lng: 2.3794, + }, + { + iata: "OSL", + name: "Gardermoen", + city: "Oslo", + country: "NO", + lat: 60.1976, + lng: 11.1004, + }, + { + iata: "OTP", + name: "Henri Coandă", + city: "Bucharest", + country: "RO", + lat: 44.5711, + lng: 26.085, + }, + { + iata: "PBI", + name: "Palm Beach", + city: "West Palm Beach", + country: "US", + lat: 26.6832, + lng: -80.0956, + }, + { + iata: "PDX", + name: "Portland", + city: "Portland", + country: "US", + lat: 45.5898, + lng: -122.5951, + }, + { + iata: "PEK", + name: "Capital", + city: "Beijing", + country: "CN", + lat: 40.0799, + lng: 116.6031, + }, + { + iata: "PEN", + name: "Penang", + city: "Penang", + country: "MY", + lat: 5.2972, + lng: 100.2768, + }, + { + iata: "PER", + name: "Perth", + city: "Perth", + country: "AU", + lat: -31.9403, + lng: 115.9672, + }, + { + iata: "PFO", + name: "Paphos", + city: "Paphos", + country: "CY", + lat: 34.718, + lng: 32.4857, + }, + { + iata: "PHL", + name: "Philadelphia", + city: "Philadelphia", + country: "US", + lat: 39.8721, + lng: -75.2411, + }, + { + iata: "PHX", + name: "Phoenix Sky Harbor", + city: "Phoenix", + country: "US", + lat: 33.4373, + lng: -112.0078, + }, + { + iata: "PIT", + name: "Pittsburgh", + city: "Pittsburgh", + country: "US", + lat: 40.4915, + lng: -80.2329, + }, + { + iata: "PKX", + name: "Daxing", + city: "Beijing", + country: "CN", + lat: 39.5098, + lng: 116.4105, + }, + { + iata: "PMI", + name: "Palma de Mallorca", + city: "Palma", + country: "ES", + lat: 39.5517, + lng: 2.7388, + }, + { + iata: "PMO", + name: "Falcone-Borsellino", + city: "Palermo", + country: "IT", + lat: 38.176, + lng: 13.091, + }, + { + iata: "PNH", + name: "Phnom Penh", + city: "Phnom Penh", + country: "KH", + lat: 11.5466, + lng: 104.8441, + }, + { + iata: "PNQ", + name: "Pune", + city: "Pune", + country: "IN", + lat: 18.5802, + lng: 73.9197, + }, + { + iata: "POS", + name: "Piarco", + city: "Port of Spain", + country: "TT", + lat: 10.5954, + lng: -61.3372, + }, + { + iata: "PPT", + name: "Faa'a", + city: "Papeete", + country: "PF", + lat: -17.5537, + lng: -149.6069, + }, + { + iata: "PRG", + name: "Václav Havel", + city: "Prague", + country: "CZ", + lat: 50.1008, + lng: 14.26, + }, + { + iata: "PSA", + name: "Galileo Galilei", + city: "Pisa", + country: "IT", + lat: 43.6839, + lng: 10.3927, + }, + { + iata: "PTY", + name: "Tocumen", + city: "Panama City", + country: "PA", + lat: 9.0714, + lng: -79.3835, + }, + { + iata: "PUJ", + name: "Punta Cana", + city: "Punta Cana", + country: "DO", + lat: 18.5674, + lng: -68.3634, + }, + { + iata: "PUS", + name: "Gimhae", + city: "Busan", + country: "KR", + lat: 35.1796, + lng: 128.9382, + }, + { + iata: "PVG", + name: "Pudong", + city: "Shanghai", + country: "CN", + lat: 31.1434, + lng: 121.8052, + }, + { + iata: "PVR", + name: "Gustavo Díaz Ordaz", + city: "Puerto Vallarta", + country: "MX", + lat: 20.6801, + lng: -105.2544, + }, + { + iata: "RAK", + name: "Menara", + city: "Marrakech", + country: "MA", + lat: 31.6069, + lng: -8.0363, + }, + { + iata: "RDU", + name: "Raleigh-Durham", + city: "Raleigh", + country: "US", + lat: 35.8776, + lng: -78.7875, + }, + { + iata: "REP", + name: "Siem Reap", + city: "Siem Reap", + country: "KH", + lat: 13.4107, + lng: 103.8126, + }, + { + iata: "RGN", + name: "Yangon", + city: "Yangon", + country: "MM", + lat: 16.9073, + lng: 96.1332, + }, + { + iata: "RHO", + name: "Diagoras", + city: "Rhodes", + country: "GR", + lat: 36.4054, + lng: 28.0862, + }, + { + iata: "RIC", + name: "Richmond", + city: "Richmond", + country: "US", + lat: 37.5052, + lng: -77.3197, + }, + { + iata: "RIX", + name: "Riga", + city: "Riga", + country: "LV", + lat: 56.9236, + lng: 23.9711, + }, + { + iata: "RNO", + name: "Reno-Tahoe", + city: "Reno", + country: "US", + lat: 39.4991, + lng: -119.768, + }, + { + iata: "RUH", + name: "King Khalid", + city: "Riyadh", + country: "SA", + lat: 24.9576, + lng: 46.6988, + }, + { + iata: "SAN", + name: "San Diego", + city: "San Diego", + country: "US", + lat: 32.7336, + lng: -117.1897, + }, + { + iata: "SAT", + name: "San Antonio", + city: "San Antonio", + country: "US", + lat: 29.5337, + lng: -98.4698, + }, + { + iata: "SAW", + name: "Sabiha Gökçen", + city: "Istanbul", + country: "TR", + lat: 40.8986, + lng: 29.3092, + }, + { + iata: "SCL", + name: "Arturo Merino Benítez", + city: "Santiago", + country: "CL", + lat: -33.393, + lng: -70.7858, + }, + { + iata: "SEA", + name: "Seattle-Tacoma", + city: "Seattle", + country: "US", + lat: 47.4502, + lng: -122.3088, + }, + { + iata: "SEZ", + name: "Seychelles", + city: "Mahé", + country: "SC", + lat: -4.6734, + lng: 55.5218, + }, + { + iata: "SFO", + name: "San Francisco", + city: "San Francisco", + country: "US", + lat: 37.6213, + lng: -122.379, + }, + { + iata: "SGN", + name: "Tan Son Nhat", + city: "Ho Chi Minh City", + country: "VN", + lat: 10.8188, + lng: 106.6519, + }, + { + iata: "SHA", + name: "Hongqiao", + city: "Shanghai", + country: "CN", + lat: 31.1979, + lng: 121.3362, + }, + { + iata: "SIN", + name: "Changi", + city: "Singapore", + country: "SG", + lat: 1.3502, + lng: 103.9944, + }, + { + iata: "SJC", + name: "San José Mineta", + city: "San Jose", + country: "US", + lat: 37.3626, + lng: -121.929, + }, + { + iata: "SJD", + name: "San José del Cabo", + city: "Los Cabos", + country: "MX", + lat: 23.1518, + lng: -109.7215, + }, + { + iata: "SJJ", + name: "Sarajevo", + city: "Sarajevo", + country: "BA", + lat: 43.8246, + lng: 18.3315, + }, + { + iata: "SJO", + name: "Juan Santamaría", + city: "San José", + country: "CR", + lat: 9.9939, + lng: -84.2088, + }, + { + iata: "SJU", + name: "Luis Muñoz Marín", + city: "San Juan", + country: "PR", + lat: 18.4394, + lng: -66.0018, + }, + { + iata: "SKG", + name: "Makedonia", + city: "Thessaloniki", + country: "GR", + lat: 40.5197, + lng: 22.9709, + }, + { + iata: "SKP", + name: "Skopje", + city: "Skopje", + country: "MK", + lat: 41.9616, + lng: 21.6214, + }, + { + iata: "SLC", + name: "Salt Lake City", + city: "Salt Lake City", + country: "US", + lat: 40.7884, + lng: -111.9778, + }, + { + iata: "SMF", + name: "Sacramento", + city: "Sacramento", + country: "US", + lat: 38.6954, + lng: -121.5908, + }, + { + iata: "SNA", + name: "John Wayne", + city: "Orange County", + country: "US", + lat: 33.6757, + lng: -117.8682, + }, + { + iata: "SNN", + name: "Shannon", + city: "Shannon", + country: "IE", + lat: 52.702, + lng: -8.9248, + }, + { + iata: "SOF", + name: "Sofia", + city: "Sofia", + country: "BG", + lat: 42.6952, + lng: 23.4062, + }, + { + iata: "SPU", + name: "Split", + city: "Split", + country: "HR", + lat: 43.5389, + lng: 16.298, + }, + { + iata: "SSA", + name: "Deputado Luís Eduardo Magalhães", + city: "Salvador", + country: "BR", + lat: -12.9086, + lng: -38.3225, + }, + { + iata: "SSH", + name: "Sharm El Sheikh", + city: "Sharm El Sheikh", + country: "EG", + lat: 27.9773, + lng: 34.3947, + }, + { + iata: "STL", + name: "St. Louis Lambert", + city: "St. Louis", + country: "US", + lat: 38.7487, + lng: -90.37, + }, + { + iata: "STN", + name: "Stansted", + city: "London", + country: "GB", + lat: 51.885, + lng: 0.235, + }, + { + iata: "STR", + name: "Stuttgart", + city: "Stuttgart", + country: "DE", + lat: 48.6899, + lng: 9.2219, + }, + { + iata: "SUB", + name: "Juanda", + city: "Surabaya", + country: "ID", + lat: -7.3798, + lng: 112.7868, + }, + { + iata: "SVG", + name: "Sola", + city: "Stavanger", + country: "NO", + lat: 58.8767, + lng: 5.6378, + }, + { + iata: "SVO", + name: "Sheremetyevo", + city: "Moscow", + country: "RU", + lat: 55.9726, + lng: 37.4146, + }, + { + iata: "SVQ", + name: "San Pablo", + city: "Seville", + country: "ES", + lat: 37.418, + lng: -5.8932, + }, + { + iata: "SXM", + name: "Princess Juliana", + city: "Philipsburg", + country: "SX", + lat: 18.0406, + lng: -63.1089, + }, + { + iata: "SYD", + name: "Kingsford Smith", + city: "Sydney", + country: "AU", + lat: -33.9461, + lng: 151.1772, + }, + { + iata: "SZG", + name: "Salzburg", + city: "Salzburg", + country: "AT", + lat: 47.7933, + lng: 13.0043, + }, + { + iata: "SZX", + name: "Bao'an", + city: "Shenzhen", + country: "CN", + lat: 22.6393, + lng: 113.8107, + }, + { + iata: "TAO", + name: "Jiaodong", + city: "Qingdao", + country: "CN", + lat: 36.2461, + lng: 120.3962, + }, + { + iata: "TAS", + name: "Islam Karimov", + city: "Tashkent", + country: "UZ", + lat: 41.2579, + lng: 69.2812, + }, + { + iata: "TBS", + name: "Shota Rustaveli", + city: "Tbilisi", + country: "GE", + lat: 41.6692, + lng: 44.9547, + }, + { + iata: "TFS", + name: "Tenerife South", + city: "Tenerife", + country: "ES", + lat: 28.0445, + lng: -16.5725, + }, + { + iata: "TIA", + name: "Nënë Tereza", + city: "Tirana", + country: "AL", + lat: 41.4147, + lng: 19.7206, + }, + { + iata: "TIP", + name: "Mitiga", + city: "Tripoli", + country: "LY", + lat: 32.8951, + lng: 13.276, + }, + { + iata: "TLL", + name: "Lennart Meri", + city: "Tallinn", + country: "EE", + lat: 59.4133, + lng: 24.8328, + }, + { + iata: "TLS", + name: "Toulouse-Blagnac", + city: "Toulouse", + country: "FR", + lat: 43.629, + lng: 1.3638, + }, + { + iata: "TLV", + name: "Ben Gurion", + city: "Tel Aviv", + country: "IL", + lat: 32.0114, + lng: 34.8867, + }, + { + iata: "TPA", + name: "Tampa", + city: "Tampa", + country: "US", + lat: 27.9755, + lng: -82.5332, + }, + { + iata: "TPE", + name: "Taoyuan", + city: "Taipei", + country: "TW", + lat: 25.0777, + lng: 121.2325, + }, + { + iata: "TRD", + name: "Trondheim", + city: "Trondheim", + country: "NO", + lat: 63.4578, + lng: 10.924, + }, + { + iata: "TRN", + name: "Caselle", + city: "Turin", + country: "IT", + lat: 45.2006, + lng: 7.6497, + }, + { + iata: "TRV", + name: "Trivandrum", + city: "Thiruvananthapuram", + country: "IN", + lat: 8.4821, + lng: 76.9201, + }, + { + iata: "TSA", + name: "Songshan", + city: "Taipei", + country: "TW", + lat: 25.0694, + lng: 121.5523, + }, + { + iata: "TSN", + name: "Binhai", + city: "Tianjin", + country: "CN", + lat: 39.1244, + lng: 117.3462, + }, + { + iata: "TUL", + name: "Tulsa", + city: "Tulsa", + country: "US", + lat: 36.1984, + lng: -95.8881, + }, + { + iata: "TUN", + name: "Tunis-Carthage", + city: "Tunis", + country: "TN", + lat: 36.851, + lng: 10.2272, + }, + { + iata: "TUS", + name: "Tucson", + city: "Tucson", + country: "US", + lat: 32.1161, + lng: -110.9411, + }, + { + iata: "UIO", + name: "Mariscal Sucre", + city: "Quito", + country: "EC", + lat: -0.1292, + lng: -78.3575, + }, + { + iata: "ULN", + name: "Chinggis Khaan", + city: "Ulaanbaatar", + country: "MN", + lat: 47.843, + lng: 106.7667, + }, + { + iata: "USM", + name: "Koh Samui", + city: "Koh Samui", + country: "TH", + lat: 9.5478, + lng: 100.0622, + }, + { + iata: "VCE", + name: "Marco Polo", + city: "Venice", + country: "IT", + lat: 45.5053, + lng: 12.3519, + }, + { + iata: "VIE", + name: "Vienna", + city: "Vienna", + country: "AT", + lat: 48.1103, + lng: 16.5697, + }, + { + iata: "VLC", + name: "Valencia", + city: "Valencia", + country: "ES", + lat: 39.4893, + lng: -0.4816, + }, + { + iata: "VNO", + name: "Vilnius", + city: "Vilnius", + country: "LT", + lat: 54.6341, + lng: 25.2858, + }, + { + iata: "VRN", + name: "Valerio Catullo", + city: "Verona", + country: "IT", + lat: 45.3957, + lng: 10.8885, + }, + { + iata: "VTE", + name: "Wattay", + city: "Vientiane", + country: "LA", + lat: 17.9883, + lng: 102.5633, + }, + { + iata: "WAW", + name: "Chopin", + city: "Warsaw", + country: "PL", + lat: 52.1657, + lng: 20.9671, + }, + { + iata: "WDH", + name: "Hosea Kutako", + city: "Windhoek", + country: "NA", + lat: -22.4799, + lng: 17.4709, + }, + { + iata: "WLG", + name: "Wellington", + city: "Wellington", + country: "NZ", + lat: -41.3272, + lng: 174.8053, + }, + { + iata: "WUH", + name: "Tianhe", + city: "Wuhan", + country: "CN", + lat: 30.7838, + lng: 114.2081, + }, + { + iata: "XIY", + name: "Xianyang", + city: "Xi'an", + country: "CN", + lat: 34.4471, + lng: 108.7516, + }, + { + iata: "XMN", + name: "Gaoqi", + city: "Xiamen", + country: "CN", + lat: 24.544, + lng: 118.1277, + }, + { + iata: "YEG", + name: "Edmonton", + city: "Edmonton", + country: "CA", + lat: 53.3097, + lng: -113.5798, + }, + { + iata: "YHZ", + name: "Halifax Stanfield", + city: "Halifax", + country: "CA", + lat: 44.8808, + lng: -63.5086, + }, + { + iata: "YOW", + name: "Macdonald-Cartier", + city: "Ottawa", + country: "CA", + lat: 45.3225, + lng: -75.6692, + }, + { + iata: "YUL", + name: "Trudeau", + city: "Montreal", + country: "CA", + lat: 45.4706, + lng: -73.7408, + }, + { + iata: "YVR", + name: "Vancouver", + city: "Vancouver", + country: "CA", + lat: 49.1967, + lng: -123.1815, + }, + { + iata: "YWG", + name: "Winnipeg", + city: "Winnipeg", + country: "CA", + lat: 49.91, + lng: -97.2399, + }, + { + iata: "YYC", + name: "Calgary", + city: "Calgary", + country: "CA", + lat: 51.1215, + lng: -114.0076, + }, + { + iata: "YYZ", + name: "Pearson", + city: "Toronto", + country: "CA", + lat: 43.6772, + lng: -79.6306, + }, + { + iata: "ZAD", + name: "Zadar", + city: "Zadar", + country: "HR", + lat: 44.1083, + lng: 15.3467, + }, + { + iata: "ZAG", + name: "Franjo Tuđman", + city: "Zagreb", + country: "HR", + lat: 45.7429, + lng: 16.0688, + }, + { + iata: "ZNZ", + name: "Abeid Amani Karume", + city: "Zanzibar", + country: "TZ", + lat: -6.2242, + lng: 39.2248, + }, + { + iata: "ZRH", + name: "Zürich", + city: "Zürich", + country: "CH", + lat: 47.4647, + lng: 8.5492, + }, +]; + +export function searchAirports(query: string, limit = 20): Airport[] { + const q = query.toLowerCase().trim(); + if (!q) return []; + + const exact: Airport[] = []; + const iataPrefix: Airport[] = []; + const cityStart: Airport[] = []; + const nameStart: Airport[] = []; + const contains: Airport[] = []; + + for (const a of AIRPORTS) { + const iata = a.iata.toLowerCase(); + const city = a.city.toLowerCase(); + const name = a.name.toLowerCase(); + const country = a.country.toLowerCase(); + + if (iata === q) exact.push(a); + else if (iata.startsWith(q)) iataPrefix.push(a); + else if (city.startsWith(q)) cityStart.push(a); + else if (name.startsWith(q)) nameStart.push(a); + else if (city.includes(q) || name.includes(q) || country.startsWith(q)) + contains.push(a); + } + + return [ + ...exact, + ...iataPrefix, + ...cityStart, + ...nameStart, + ...contains, + ].slice(0, limit); +} + +export function findByIata(iata: string): Airport | undefined { + const code = iata.toUpperCase(); + return AIRPORTS.find((a) => a.iata === code); +} + +export function airportToCity(airport: Airport): City { + return { + id: airport.iata.toLowerCase(), + name: airport.city, + country: airport.country, + iata: airport.iata, + coordinates: [airport.lng, airport.lat], + radius: DEFAULT_RADIUS, + }; +} diff --git a/src/lib/cities.ts b/src/lib/cities.ts index d14686d..fa16e4f 100644 --- a/src/lib/cities.ts +++ b/src/lib/cities.ts @@ -14,7 +14,7 @@ export const CITIES: City[] = [ country: "US", iata: "JFK", coordinates: [-73.7781, 40.6413], - radius: 1.5, + radius: 2.5, }, { id: "lax", @@ -22,7 +22,7 @@ export const CITIES: City[] = [ country: "US", iata: "LAX", coordinates: [-118.4085, 33.9416], - radius: 1.5, + radius: 2.5, }, { id: "lhr", @@ -30,7 +30,7 @@ export const CITIES: City[] = [ country: "GB", iata: "LHR", coordinates: [-0.4614, 51.47], - radius: 1.5, + radius: 2.5, }, { id: "dxb", @@ -38,7 +38,7 @@ export const CITIES: City[] = [ country: "AE", iata: "DXB", coordinates: [55.3644, 25.2532], - radius: 1.5, + radius: 2.5, }, { id: "nrt", @@ -46,7 +46,7 @@ export const CITIES: City[] = [ country: "JP", iata: "NRT", coordinates: [140.3929, 35.772], - radius: 1.5, + radius: 2.5, }, { id: "sin", @@ -54,7 +54,7 @@ export const CITIES: City[] = [ country: "SG", iata: "SIN", coordinates: [103.9915, 1.3644], - radius: 1.5, + radius: 2.5, }, { id: "cdg", @@ -62,7 +62,7 @@ export const CITIES: City[] = [ country: "FR", iata: "CDG", coordinates: [2.5479, 49.0097], - radius: 1.5, + radius: 2.5, }, { id: "sfo", @@ -70,7 +70,7 @@ export const CITIES: City[] = [ country: "US", iata: "SFO", coordinates: [-122.379, 37.6213], - radius: 1.5, + radius: 2.5, }, { id: "ord", @@ -78,7 +78,7 @@ export const CITIES: City[] = [ country: "US", iata: "ORD", coordinates: [-87.9073, 41.9742], - radius: 1.5, + radius: 2.5, }, { id: "fra", @@ -86,7 +86,7 @@ export const CITIES: City[] = [ country: "DE", iata: "FRA", coordinates: [8.5622, 50.0379], - radius: 1.5, + radius: 2.5, }, { id: "bom", @@ -94,6 +94,14 @@ export const CITIES: City[] = [ country: "IN", iata: "BOM", coordinates: [72.8679, 19.0896], - radius: 1.5, + radius: 2.5, + }, + { + id: "mia", + name: "Miami", + country: "US", + iata: "MIA", + coordinates: [-80.2906, 25.7959], + radius: 2.5, }, ]; diff --git a/src/lib/map-styles.ts b/src/lib/map-styles.ts index 3f9e3a0..86af833 100644 --- a/src/lib/map-styles.ts +++ b/src/lib/map-styles.ts @@ -88,9 +88,7 @@ const SHADED_RELIEF_STYLE: Record = { "sky-horizon-blend": 0.5, "horizon-fog-blend": 0.1, }, - layers: [ - { id: "satellite-base", type: "raster", source: "esri-satellite" }, - ], + layers: [{ id: "satellite-base", type: "raster", source: "esri-satellite" }], }; export const MAP_STYLES: MapStyle[] = [ diff --git a/src/lib/opensky.ts b/src/lib/opensky.ts index 3f546b0..9b26cb0 100644 --- a/src/lib/opensky.ts +++ b/src/lib/opensky.ts @@ -60,7 +60,8 @@ export type FetchResult = { creditsRemaining: number | null; }; -const clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi); +const clamp = (v: number, lo: number, hi: number) => + Math.min(Math.max(v, lo), hi); export async function fetchFlightsByBbox( lamin: number, @@ -88,24 +89,20 @@ export async function fetchFlightsByBbox( }); if (res.status === 429) { - console.warn("[aeris] OpenSky rate limit hit (429), backing off"); return { flights: [], rateLimited: true, creditsRemaining: null }; } if (!res.ok) { - console.warn(`[aeris] OpenSky returned ${res.status}`); return { flights: [], rateLimited: false, creditsRemaining: null }; } const data: OpenSkyResponse = await res.json(); - const creditsRaw = res.headers.get("x-rate-limit-remaining"); const creditsRemaining = creditsRaw !== null ? parseInt(creditsRaw, 10) : null; - const flights = parseStates(data); return { - flights, + flights: parseStates(data), rateLimited: false, creditsRemaining: Number.isNaN(creditsRemaining) ? null