commit fe89bb2b1c33095ee0f0f236e7acd07c06cf370e Author: grabowski Date: Sat Jul 18 23:15:29 2026 +0700 Initial release: collaborative trip planner Multi-user trip planning web app in a single Docker container. Mullvad-style token accounts, trip sharing via join codes, day-by-day calendar with typed entries (activity, hotel, travel, flight, rental car, immigration, note), multi-leg flight segments with bundled IATA airport dataset, Leaflet/OSM map with per-leg great-circle km (air vs ground), rough km-driven vs rental included-km comparison, cost splitting with settle-up suggestions, flip-clock departure countdown. Node 20 + Express + SQLite (WAL, additive migrations), vanilla JS SPA, 44 API tests. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b15bfb8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,29 @@ +# Dependencies (reinstalled inside the image) +node_modules + +# Persisted data — never bake the SQLite file into the image +data + +# Version control +.git +.gitignore + +# Docs and tests are not needed at runtime +docs +tests + +# Tooling / agent scaffolding +.claude +.claude-flow +.swarm +.mcp.json +ruvector.db + +# Local env files +.env +.env.* + +# The Docker files themselves +Dockerfile +.dockerignore +docker-compose.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d6cf74 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +node_modules/ +/data/ +*.db +*.sqlite +.env +.env.* +npm-debug.log* +.DS_Store +Thumbs.db +.claude-flow/ +.claude/ +.swarm/ +.mcp.json +ruvector.db +.playwright-mcp/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8114e7e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,180 @@ +# Ruflo — Claude Code Configuration + +## Rules + +- Do what has been asked; nothing more, nothing less +- NEVER create files unless absolutely necessary — prefer editing existing files +- NEVER create documentation files unless explicitly requested +- NEVER save working files or tests to root — use `/src`, `/tests`, `/docs`, `/config`, `/scripts` +- ALWAYS read a file before editing it +- NEVER commit secrets, credentials, or .env files +- NEVER add a `Co-Authored-By` trailer to user commits unless this project's `.claude/settings.json` has `attribution.commit` set (#2078). The Claude Code Bash tool may suggest one in its default commit-message template — ignore it. `Co-Authored-By` is semantic authorship attribution under git/GitHub convention; the tool is the facilitator, not a co-author. +- Keep files under 500 lines +- Validate input at system boundaries + +## Agent Comms (SendMessage-First Coordination) + +Named agents coordinate via `SendMessage`, not polling or shared state. + +``` +Lead (you) ←→ architect ←→ developer ←→ tester ←→ reviewer + (named agents message each other directly) +``` + +### Spawning a Coordinated Team + +```javascript +// ALL agents in ONE message, each knows WHO to message next +Agent({ prompt: "Research the codebase. SendMessage findings to 'architect'.", + subagent_type: "researcher", name: "researcher", run_in_background: true }) +Agent({ prompt: "Wait for 'researcher'. Design solution. SendMessage to 'coder'.", + subagent_type: "system-architect", name: "architect", run_in_background: true }) +Agent({ prompt: "Wait for 'architect'. Implement it. SendMessage to 'tester'.", + subagent_type: "coder", name: "coder", run_in_background: true }) +Agent({ prompt: "Wait for 'coder'. Write tests. SendMessage results to 'reviewer'.", + subagent_type: "tester", name: "tester", run_in_background: true }) +Agent({ prompt: "Wait for 'tester'. Review code quality and security.", + subagent_type: "reviewer", name: "reviewer", run_in_background: true }) + +// Kick off the pipeline +SendMessage({ to: "researcher", summary: "Start", message: "[task context]" }) +``` + +### Patterns + +| Pattern | Flow | Use When | +|---------|------|----------| +| **Pipeline** | A → B → C → D | Sequential dependencies (feature dev) | +| **Fan-out** | Lead → A, B, C → Lead | Independent parallel work (research) | +| **Supervisor** | Lead ↔ workers | Ongoing coordination (complex refactor) | + +### Rules + +- ALWAYS name agents — `name: "role"` makes them addressable +- ALWAYS include comms instructions in prompts — who to message, what to send +- Spawn ALL agents in ONE message with `run_in_background: true` +- After spawning: STOP, tell user what's running, wait for results +- NEVER poll status — agents message back or complete automatically + +## Swarm & Routing + +### Config +- **Topology**: hierarchical-mesh (anti-drift) +- **Max Agents**: 15 +- **Memory**: hybrid +- **HNSW**: Enabled +- **Neural**: Enabled + +```bash +npx @claude-flow/cli@latest swarm init --topology hierarchical --max-agents 8 --strategy specialized +``` + +### Agent Routing + +| Task | Agents | Topology | +|------|--------|----------| +| Bug Fix | researcher, coder, tester | hierarchical | +| Feature | architect, coder, tester, reviewer | hierarchical | +| Refactor | architect, coder, reviewer | hierarchical | +| Performance | perf-engineer, coder | hierarchical | +| Security | security-architect, auditor | hierarchical | + +### When to Swarm +- **YES**: 3+ files, new features, cross-module refactoring, API changes, security, performance +- **NO**: single file edits, 1-2 line fixes, docs updates, config changes, questions + +### 3-Tier Model Routing + +| Tier | Handler | Use Cases | +|------|---------|-----------| +| 1 | Agent Booster (WASM) | Simple transforms — skip LLM, use Edit directly | +| 2 | Haiku | Simple tasks, low complexity | +| 3 | Sonnet/Opus | Architecture, security, complex reasoning | + +## Memory & Learning + +### Before Any Task +```bash +npx @claude-flow/cli@latest memory search --query "[task keywords]" --namespace patterns +npx @claude-flow/cli@latest hooks route --task "[task description]" +``` + +### After Success +```bash +npx @claude-flow/cli@latest memory store --namespace patterns --key "[name]" --value "[what worked]" +npx @claude-flow/cli@latest hooks post-task --task-id "[id]" --success true --store-results true +``` + +### MCP Tools (use `ToolSearch("keyword")` to discover) + +| Category | Key Tools | +|----------|-----------| +| **Memory** | `memory_store`, `memory_search`, `memory_search_unified` | +| **Bridge** | `memory_import_claude`, `memory_bridge_status` | +| **Swarm** | `swarm_init`, `swarm_status`, `swarm_health` | +| **Agents** | `agent_spawn`, `agent_list`, `agent_status` | +| **Hooks** | `hooks_route`, `hooks_post-task`, `hooks_worker-dispatch` | +| **Security** | `aidefence_scan`, `aidefence_is_safe`, `aidefence_has_pii` | +| **Hive-Mind** | `hive-mind_init`, `hive-mind_consensus`, `hive-mind_spawn` | + +### Background Workers + +| Worker | When | +|--------|------| +| `audit` | After security changes | +| `optimize` | After performance work | +| `testgaps` | After adding features | +| `map` | Every 5+ file changes | +| `document` | After API changes | + +```bash +npx @claude-flow/cli@latest hooks worker dispatch --trigger audit +``` + +## Agents + +**Core**: `coder`, `reviewer`, `tester`, `planner`, `researcher` +**Architecture**: `system-architect`, `backend-dev`, `mobile-dev` +**Security**: `security-architect`, `security-auditor` +**Performance**: `performance-engineer`, `perf-analyzer` +**Coordination**: `hierarchical-coordinator`, `mesh-coordinator`, `adaptive-coordinator` +**GitHub**: `pr-manager`, `code-review-swarm`, `issue-tracker`, `release-manager` + +Any string works as a custom agent type. + +## Build & Test + +- ALWAYS run tests after code changes +- ALWAYS verify build succeeds before committing + +```bash +npm run build && npm test +``` + +## CLI Quick Reference + +```bash +npx @claude-flow/cli@latest init --wizard # Setup +npx @claude-flow/cli@latest swarm init --v3-mode # Start swarm +npx @claude-flow/cli@latest memory search --query "" # Vector search +npx @claude-flow/cli@latest hooks route --task "" # Route to agent +npx @claude-flow/cli@latest doctor --fix # Diagnostics +npx @claude-flow/cli@latest security scan # Security scan +npx @claude-flow/cli@latest performance benchmark # Benchmarks +``` + +26 commands, 140+ subcommands. Use `--help` on any command for details. + +## Setup + +```bash +claude mcp add claude-flow -- npx -y ruflo@latest mcp start +npx ruflo@latest doctor --fix +``` + +> The background `daemon` is optional. It runs interval workers that each spawn +> a headless `claude` session, so it consumes tokens continuously. Start it only +> if you want those sweeps: `npx ruflo@latest daemon start` (self-stops after 12h +> by default; `--ttl 0` to disable, `daemon status --all` to audit running daemons). + +**Agent tool** handles execution (agents, files, code, git). **MCP tools** handle coordination (swarm, memory, hooks). **CLI** is the same via Bash. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..234b8a2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# Trip Plan — production image +# node:20-slim (Debian/glibc) so better-sqlite3 prebuilt binaries load correctly. +FROM node:20-slim + +ENV NODE_ENV=production \ + PORT=3000 \ + DATA_DIR=/app/data + +WORKDIR /app + +# Install production dependencies first for better layer caching. +COPY package*.json ./ +RUN npm ci --omit=dev && npm cache clean --force + +# Application code. +COPY src/ ./src/ +COPY public/ ./public/ + +# Data directory for the SQLite file (volume-mounted in compose). +# Owned by the unprivileged `node` user that ships with the base image. +RUN mkdir -p /app/data && chown -R node:node /app + +USER node + +EXPOSE 3000 + +# Any HTTP response (including 401) from the API means the server is up. +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD node -e "fetch('http://localhost:3000/api/auth/me').then(()=>process.exit(0)).catch(()=>process.exit(1))" + +CMD ["node", "src/server/index.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..47564af --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# Trip Plan + +A self-hosted, multi-user web tool for collaboratively planning trips. Multiple people log in to the same instance, create trips with a name and date range, and fill in a day-by-day calendar together — flights (with multi-leg connections), hotels, border crossings, drives, rental cars, and activities. Each trip is drawn on an interactive map with the route between stops, per-leg distances, cost splitting between members, and an overall summary. It runs as a single Docker container with a SQLite database. + +## Features + +- **Accounts without passwords** — Mullvad-style: one click generates a random account number (shown once, stored only as a hash). No email, no password, no personal data. +- **Sharing via join codes** — every trip has a short code; anyone with an account enters it to become a co-editor. The owner can regenerate the code to stop further joins. +- **Day-by-day calendar** — a real calendar grid; click a day to add any number of entries (activity, hotel, travel, flight, rental car, immigration, note). +- **Multi-leg flights** — type `CNX-BKK-DXB-FRA` and it expands into segments with per-leg flight number and times; every airport becomes a stop on the map (bundled offline IATA airport database). +- **Rental cars** — brand/model/type, booking ref, pickup/dropoff with dates and times, and included km — compared against the trip's estimated driven km. +- **Locations** — geocoded via OpenStreetMap (Nominatim, English results); lat/lng stored per entry. +- **Map** — Leaflet + OpenStreetMap; stops plotted in order, air legs dashed / ground legs solid, per-leg great-circle km. +- **Costs & splitting** — price per entry with "split equally", "everyone pays their own", or "payer's expense"; per-person share/paid/net table and minimal settle-up transfers, in the trip's currency. +- **Summary** — days/nights, entry counts, total km, **≈ km driven** (rental-car figure) vs km flown, and locations visited. + +## Deployment (Docker + external Caddy for TLS) + +The app serves plain HTTP on port 3000 and is designed to sit behind a reverse +proxy that terminates TLS — e.g. an external [Caddy](https://caddyserver.com/). + +### 1. Run the container + +Using the prebuilt image from the registry: + +```yaml +# docker-compose.yml +services: + tripplan: + image: git.b4l.co.th/grabowski/trip-plan:latest + restart: unless-stopped + environment: + SESSION_SECRET: "" + volumes: + - ./data:/app/data + # No public port mapping needed when Caddy shares a Docker network with + # the app (recommended). For a host-level Caddy, map localhost only: + ports: + - "127.0.0.1:3000:3000" +``` + +```bash +docker compose up -d +``` + +(Or build locally instead of pulling: clone this repo and use `build: .` — the +checked-in `docker-compose.yml` does exactly that.) + +### 2. Point Caddy at it + +If Caddy runs on the same host (host-level Caddy, app bound to `127.0.0.1:3000`): + +```caddyfile +trips.example.com { + reverse_proxy 127.0.0.1:3000 +} +``` + +If Caddy runs as a container, attach both services to a shared Docker network +and proxy by service name instead: + +```caddyfile +trips.example.com { + reverse_proxy tripplan:3000 +} +``` + +That's all — Caddy obtains and renews the certificate automatically; the app +needs no TLS configuration of its own. Session cookies are `SameSite=Lax` and +`httpOnly`, and work as-is behind the proxy. + +### 3. Updating + +```bash +docker compose pull && docker compose up -d +``` + +Schema migrations run automatically at startup; existing data in `./data` is +preserved (back up that directory to back up all trips). + +## Quick start (local, no proxy) + +```bash +docker compose up -d # builds the image from source +# open http://localhost:3000 and create the first account +``` + +> Before exposing the app beyond localhost, set your own random `SESSION_SECRET` +> in `docker-compose.yml`. + +## Local development + +```bash +npm install +npm start # http://localhost:3000 +npm test # API tests (node:test + supertest) +``` + +`npm run dev` starts the server with `--watch` for auto-reload. + +## Environment variables + +| Variable | Default | Description | +|---|---|---| +| `PORT` | `3000` | HTTP port the server listens on. | +| `DATA_DIR` | `./data` (`/app/data` in Docker) | Directory holding the SQLite database file. | +| `SESSION_SECRET` | dev value (warns) | Secret used to sign session cookies. **Set this in production.** | + +## Data + +All state lives in a single SQLite file under `DATA_DIR`. In Docker this is `/app/data`, mounted from `./data` on the host. No external database or services are required (geocoding calls OpenStreetMap's Nominatim, which needs no API key; airport lookups use a bundled offline dataset). + +## Documentation + +- [docs/PROJECT_OVERVIEW.md](docs/PROJECT_OVERVIEW.md) — architecture, data model, and design decisions. +- [docs/API.md](docs/API.md) — the REST API contract. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d2f9bd4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +services: + tripplan: + build: . + ports: + - "3000:3000" + volumes: + # SQLite database persists on the host across container rebuilds. + - ./data:/app/data + environment: + # CHANGE THIS: set a long random string before exposing the app. + # e.g. `openssl rand -hex 32` + SESSION_SECRET: "change-me-to-a-long-random-secret" + restart: unless-stopped diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..fe4aea5 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,215 @@ +# Trip Plan — API Contract + +All endpoints are JSON over REST, prefixed with `/api`. This document is the **binding contract** between backend and frontend. Do not deviate; extend only by agreement. + +## Conventions + +- Dates: `YYYY-MM-DD` strings. Times: `HH:MM` 24h strings (optional fields). +- Auth: cookie session (`cookie-session`). All endpoints except `account` (create) / `login` require auth → otherwise `401 {"error":"unauthorized"}`. +- Identity is Mullvad-style: an account is a random **account token** (the only credential — no username/password). Users additionally have a non-unique, editable `display_name` for humans. +- Trip access: user must be a member of the trip → otherwise `404 {"error":"not found"}` (don't leak existence). +- Errors: `4xx/5xx` with body `{"error": ""}`. +- IDs are integers. + +## Server layout + +- `src/server/app.js` — builds and **exports** the Express app (`export function createApp(dbPath)` and `export default` a ready app is fine, but `createApp` must exist for tests). +- `src/server/index.js` — reads env (`PORT` default 3000, `DATA_DIR` default `./data`, `SESSION_SECRET` default dev value with console warning), ensures DATA_DIR exists, starts listener, serves `public/` statically. +- `src/server/db.js` — `better-sqlite3` connection + schema creation (idempotent `CREATE TABLE IF NOT EXISTS`). +- `src/server/routes/` — `auth.js`, `trips.js`, `entries.js`, `geocode.js`. +- `src/server/util/distance.js` — `haversineKm(lat1, lng1, lat2, lng2)` returns km (number). + +## Data model (SQLite) + +```sql +users (id INTEGER PK, token_hash TEXT UNIQUE NOT NULL, -- sha256 hex of the account token + display_name TEXT NOT NULL, -- auto-generated, editable, NOT unique + created_at TEXT DEFAULT current_timestamp) +trips (id INTEGER PK, name TEXT NOT NULL, start_date TEXT NOT NULL, end_date TEXT NOT NULL, + owner_id INTEGER NOT NULL REFERENCES users(id), + currency TEXT NOT NULL DEFAULT 'USD', -- 3-letter code, display only (no FX) + join_code TEXT UNIQUE NOT NULL, -- share code for joining the trip + created_at TEXT DEFAULT current_timestamp) +trip_members (trip_id INTEGER REFERENCES trips(id), user_id INTEGER REFERENCES users(id), + role TEXT NOT NULL DEFAULT 'editor', -- 'owner' | 'editor' + PRIMARY KEY (trip_id, user_id)) +entries (id INTEGER PK, trip_id INTEGER NOT NULL REFERENCES trips(id), + date TEXT NOT NULL, type TEXT NOT NULL, + title TEXT NOT NULL, details TEXT DEFAULT '', + start_time TEXT, end_time TEXT, + location_name TEXT, lat REAL, lng REAL, + sort_order INTEGER NOT NULL DEFAULT 0, + price REAL, -- null = no cost tracked + paid_by INTEGER REFERENCES users(id), -- who paid (null = unassigned) + split_mode TEXT NOT NULL DEFAULT 'equal', -- 'equal' | 'own' | 'payer' + segments TEXT, -- JSON array, flight entries only (see below) + created_at TEXT DEFAULT current_timestamp) +entry_participants (entry_id INTEGER REFERENCES entries(id), user_id INTEGER REFERENCES users(id), + PRIMARY KEY (entry_id, user_id)) + -- no rows for an entry = "all trip members participate" (dynamic default) +``` + +Entry `type` ∈ `flight | immigration | travel | hotel | activity | rental | note`. + +### Rental car details + +`rental`-type entries can carry structured rental data in `entries.rental` (TEXT, JSON — same storage pattern as flight segments). `null`/absent = plain entry. + +```json +"rental": { + "brand": "Toyota", "model": "Yaris Cross", "car_type": "SUV", + "booking_ref": "RC-889231", "included_km": 1500, + "pickup": { "date": "2026-08-01", "time": "09:00", "location_name": "CNX Airport", + "lat": 18.77, "lng": 98.96 }, + "dropoff": { "date": "2026-08-07", "time": "18:00", "location_name": "Chiang Mai Old Town", + "lat": 18.79, "lng": 98.98 } +} +``` + +Validation (POST/PATCH entries): only allowed when `type === 'rental'` (else 400); all fields optional; `brand`/`model` ≤60, `car_type` ≤40, `booking_ref` ≤60 chars; `included_km` null (= unlimited/unknown) or number ≥ 0; `pickup`/`dropoff` objects with `date` (YYYY-MM-DD, required within the object), optional `time` HH:MM, `location_name` ≤120, lat/lng both-or-neither in range. PATCH `rental: null` clears. Entry JSON always includes `rental` (parsed or null). The entry's own `date` should be the pickup date (frontend sets this); the entry's price/split fields carry the rental cost as usual. + +Summary impact: `summary.rentals` counts rental entries; `summary.includedKm` = sum of non-null `included_km` across rentals, or null when no rental specifies one — the frontend pairs it with `kmDriven` ("≈ 520 km driven / 1,500 km included"). Rental pickup/dropoff locations do NOT feed the route (use the entry's normal location field for a map point if wanted). + +### Flight segments + +Flight entries can carry structured **segments** for multi-leg itineraries (e.g. CNX→BKK→DXB→FRA = 3 segments). Stored as JSON in `entries.segments`, returned parsed. `null`/absent = no segments (plain entry). + +```json +"segments": [ + { "flight_no": "TG103", "dep_time": "10:30", "arr_time": "11:45", + "from": {"code": "CNX", "name": "Chiang Mai Intl", "lat": 18.77, "lng": 98.96}, + "to": {"code": "BKK", "name": "Suvarnabhumi", "lat": 13.68, "lng": 100.75} }, + { "flight_no": "EK385", "dep_time": "13:05", "arr_time": "17:10", + "from": {"code": "BKK", "name": "Suvarnabhumi", "lat": 13.68, "lng": 100.75}, + "to": {"code": "DXB", "name": "Dubai Intl", "lat": 25.25, "lng": 55.36} } +] +``` + +Validation (POST/PATCH entries): only allowed when `type === 'flight'` (else 400); array of 1–8 objects; each needs `from` and `to` with `code` (2–4 uppercase alphanumerics); `name` optional ≤80 chars; `lat`/`lng` both-or-neither, same ranges as entry lat/lng; `flight_no` optional ≤12 chars; `dep_time`/`arr_time` optional `HH:MM`. PATCH with `segments: null` clears them. + +**Route expansion**: an entry whose segments have coordinates contributes airport stops to `/route` instead of its own lat/lng — `from` of the first segment, then `to` of every segment, in order (airports without coords are skipped; consecutive duplicate coords are collapsed). Stops derived this way have `"kind": "airport"` and include `"code"`. So CNX-BKK-DXB-FRA yields 4 stops and 3 measured legs on the map. + +### Airports lookup + +`GET /api/airports?q=` → `200 {results: [{code, name, city, country, lat, lng}]}` (max 8; requires auth). + +Backed by a bundled dataset at `src/server/data/airports.json` generated from the public-domain OurAirports data (filter: has IATA code + scheduled service; keep code/name/municipality/iso_country/lat/lng). Matching: exact/prefix IATA code match first (case-insensitive), then name/city substring. This makes `CNX` → Chiang Mai Intl instant and offline. + +### Cost semantics + +- Only entries with non-null `price` count toward costs. +- **Effective participants** of an entry = its `entry_participants` rows, or **all current trip members** if it has none. +- `split_mode`: + - `equal` — `price` is the TOTAL; split equally among effective participants ("rental car 50/50"). `paid_by` is credited with having paid the total. + - `own` — `price` is PER PERSON; each effective participant owes and pays their own ("everyone pays their own flight"). No debt created. Effective total = price × participant count. + - `payer` — personal expense: `paid_by` owes and pays the whole price alone (requires `paid_by`; validation error otherwise). + +## Endpoints + +### Auth (Mullvad-style account tokens) + +| Method & path | Body | Response | +|---|---|---| +| `POST /api/auth/account` | `{}` (empty) | `201 {user: {id, display_name}, token: "XXXX-XXXX-XXXX-XXXX"}` — creates an account, logs it in. The raw token is returned ONLY here, never again. | +| `POST /api/auth/login` | `{token}` | `200 {user}`; `401` on unknown token | +| `POST /api/auth/logout` | — | `204` | +| `GET /api/auth/me` | — | `200 {user: {id, display_name}}` or `401` | +| `PATCH /api/auth/me` | `{display_name}` | `200 {user}`; validates 1–40 chars after trim | + +Token rules: +- 16 chars from the unambiguous alphabet `ABCDEFGHJKMNPQRSTUVWXYZ23456789` (no I/L/O/0/1), generated with `crypto.randomBytes`, displayed grouped `XXXX-XXXX-XXXX-XXXX`. +- Login normalizes input: strip dashes/spaces, uppercase, then compare `sha256(token)` against `users.token_hash`. Only the hash is stored. +- `display_name` is auto-generated at account creation as `adjective-animal` (e.g. `brave-otter`) from two small built-in word lists; not unique; user-editable via PATCH. + +User JSON shape everywhere: `{id, display_name}`. + +### Trips + +| Method & path | Body | Response | +|---|---|---| +| `GET /api/trips` | — | `200 {trips: [{id, name, start_date, end_date, owner_id, currency, role, member_count, entry_count}]}` (trips where user is member, newest first) | +| `POST /api/trips` | `{name, start_date, end_date, currency?}` | `201 {trip}`; validates: name non-empty ≤120 chars, valid dates, `end_date >= start_date`, range ≤ 365 days, currency (if given) matches `^[A-Z]{3}$` (default `USD`). Creator becomes member with role `owner`. | +| `GET /api/trips/:id` | — | `200 {trip: {id, name, start_date, end_date, owner_id, currency, join_code}, members: [{id, display_name, role}], entries: [entry…]}` entries ordered by `(date, sort_order, id)` | +| `PATCH /api/trips/:id` | any of `{name, start_date, end_date, currency}` | `200 {trip}` (same validation; entries outside new range are kept) | +| `DELETE /api/trips/:id` | — | `204` — owner only, else `403`. Deletes members + entries too. | +| `POST /api/trips/join` | `{code}` | `200 {trip}` — joins the trip with that join_code as `editor` (idempotent: already a member → still `200 {trip}`); `404 {"error":"not found"}` on unknown code. Code normalized like tokens (strip dashes/spaces, uppercase). | +| `POST /api/trips/:id/join-code` | — | `200 {trip}` — regenerates join_code, owner only (`403` otherwise) | +| `DELETE /api/trips/:id/members/:userId` | — | `204` — owner only (owner cannot remove self) | + +`join_code`: 8 chars, same alphabet as account tokens, generated at trip creation, displayed grouped `XXXX-XXXX`. Members see it in trip JSON (it's how they invite others); it is NOT a credential — it only grants membership of that one trip. + +### Entries + +| Method & path | Body | Response | +|---|---|---| +| `POST /api/trips/:id/entries` | `{date, type, title, details?, start_time?, end_time?, location_name?, lat?, lng?, sort_order?, price?, paid_by?, split_mode?, participants?, segments?}` | `201 {entry}`; validates type enum, date format, title non-empty ≤200 chars; lat/lng must both be present or both absent, lat ∈ [-90,90], lng ∈ [-180,180]; price null or number ≥ 0; paid_by null or a trip member's user id; split_mode ∈ `equal\|own\|payer` (`payer` requires paid_by); participants null/[] (= all members) or array of trip-member user ids; segments per "Flight segments" above | +| `PATCH /api/entries/:id` | any subset of the above | `200 {entry}` (member of the entry's trip required; `participants` replaces the whole set) | +| `DELETE /api/entries/:id` | — | `204` (also deletes its entry_participants rows) | + +Entry JSON shape (always full row): `{id, trip_id, date, type, title, details, start_time, end_time, location_name, lat, lng, sort_order, price, paid_by, split_mode, participants, segments}` where `participants` is an array of user ids (`[]` = all members) and `segments` is the parsed array or `null`. + +### Route & summary (computed) + +`GET /api/trips/:id/route` → + +```json +{ + "stops": [ {"entryId": 1, "date": "2026-08-01", "type": "flight", + "title": "BKK → CNX", "location_name": "Chiang Mai", "lat": 18.79, "lng": 98.98} ], + "legs": [ {"fromEntryId": 1, "toEntryId": 4, "km": 587.3, "mode": "ground"} ], + "totalKm": 587.3, + "summary": { + "days": 10, "nights": 9, + "flights": 2, "flightSegments": 4, "hotels": 3, "travelLegs": 1, "activities": 4, + "rentals": 1, "includedKm": 1500, + "kmAir": 0, "kmDriven": 587.3, + "locations": ["Bangkok", "Chiang Mai"] + } +} +``` + +- `stops` = entries having lat/lng, ordered by `(date, sort_order, id)`; flight entries with coordinate-bearing segments are expanded into airport stops instead (see "Flight segments" — `kind: "airport"`, includes `code`). +- `legs` = consecutive stop pairs; skip zero-distance pairs (< 0.05 km) — still include the stop, just no leg. +- Every leg has `mode`: `"air"` when BOTH endpoints are `kind:"airport"` stops expanded from the SAME flight entry (i.e. actual flight segments); `"ground"` for everything else (hotel→airport transfers, city-to-city drives). `summary.kmAir` / `summary.kmDriven` are the per-mode sums (1 decimal). `kmDriven` is the rough rental-car figure: great-circle, so real road km will be somewhat higher. +- `days` = inclusive count from start_date to end_date; `nights = days - 1` (0 for single-day trips). +- `locations` = unique `location_name` values in stop order. +- km rounded to 1 decimal. + +### Costs & splitting (computed) + +`GET /api/trips/:id/costs` → + +```json +{ + "currency": "USD", + "totalCost": 1450.0, + "byType": { "flight": 800.0, "travel": 300.0, "hotel": 350.0 }, + "perUser": [ + { "userId": 1, "displayName": "brave-otter", "share": 725.0, "paid": 950.0, "net": 225.0 }, + { "userId": 2, "displayName": "calm-heron", "share": 725.0, "paid": 500.0, "net": -225.0 } + ], + "settlements": [ { "fromUserId": 2, "toUserId": 1, "amount": 225.0 } ], + "unassigned": 0.0 +} +``` + +Computation (see Cost semantics above): + +- `share` = what the user's part of the trip costs; `paid` = what they actually paid; `net = paid - share` (positive → others owe them). +- `equal` with `paid_by` null: the cost still counts into shares and `byType`, but nobody is credited as payer — accumulate that amount into `unassigned` (frontend shows a hint to assign payers). +- `own`: adds `price` to each participant's `share` AND `paid` (self-paid, no debt). +- `settlements`: minimal-transfer greedy — repeatedly match the largest debtor with the largest creditor until all nets are settled; amounts rounded to 2 decimals, drop transfers < 0.01. +- `totalCost` = sum of effective totals of all priced entries; `byType` groups the same by entry type. +- All money values rounded to 2 decimals in the response. + +### Geocoding proxy + +`GET /api/geocode?q=` → `200 {results: [{name, lat, lng}]}` (max 5). + +Proxies `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=5&accept-language=en&q=…` server-side with header `User-Agent: trip-plan-app/0.1 (self-hosted)` (`accept-language=en` so results come back in Latin script, not the local language). Map Nominatim's `display_name`→`name`, parse lat/lon to numbers. On upstream failure return `502 {"error":"geocoding unavailable"}`. Cache identical queries in-memory for 10 minutes. + +## Frontend contract notes + +- SPA served from `public/`; all non-`/api` GETs fall back to `public/index.html` is NOT required — a single `index.html` with hash-based routing (`#/login`, `#/trips`, `#/trip/:id`) is the expected design, so no server-side fallback is needed. +- Leaflet 1.9.x via unpkg CDN in `index.html`. +- Session cookie is httpOnly; frontend detects auth state via `GET /api/auth/me` on load. diff --git a/docs/PROJECT_OVERVIEW.md b/docs/PROJECT_OVERVIEW.md new file mode 100644 index 0000000..61eccd8 --- /dev/null +++ b/docs/PROJECT_OVERVIEW.md @@ -0,0 +1,133 @@ +# Trip Plan — Project Overview + +A self-hosted, multi-user web tool for collaboratively planning trips. Runs as a single Docker container. + +## Core Idea + +Multiple people log in to the same instance. Anyone can create a trip by picking a name and a date range. The tool generates a day-by-day calendar for that range, and every member of the trip can fill in what happens on each day — flights, hotel stays, border crossings, drives, activities. The trip is visualized on an interactive map with the route drawn between stops, distances per leg, and an overall summary (days, nights, flights, total km). + +## Features + +### 1. Users & Access (privacy-friendly, Mullvad-style) +- **No registration form, no email, no password.** Click "Create account" and you get a random account number (e.g. `7K2M-9QX4-TT8B-3WPF`) — that token IS your login. It's shown once; only its hash is stored server-side. +- Every account gets an auto-generated, editable **display name** (e.g. `brave-otter`) so members and cost splits stay human-readable. +- Trips are shared via a **join code**: every trip has a short code (e.g. `M4TH-8RK2`); anyone with an account can enter it under "Join a trip" to become an editor. The owner can regenerate the code to cut off further joins. + +### 2. Trips +- Create a trip with **name + start date + end date**. +- Editing the date range regenerates the calendar (existing day entries outside the new range are kept but flagged). +- Trip list dashboard showing all trips you're a member of. + +### 3. Day-by-Day Calendar +- Calendar grid generated from the trip's date range (weeks as rows, like a real calendar). +- Click any day to open the day editor and add **entries**: + +| Entry type | Typical fields | +|---|---| +| ✈️ Flight | flight no., from/to airports, departure/arrival time | +| 🛂 Immigration / border | location, notes (visa, documents) | +| 🚗 Travel / transfer | mode (car/train/bus/boat), from → to | +| 🏨 Hotel stay | hotel name, check-in/check-out, booking ref | +| 📍 Activity / sightseeing | place, time, notes | +| 📝 Note | free text | + +- Every entry can have a **location** (searched via OpenStreetMap geocoding — type "Chiang Mai" and pick from suggestions; lat/lng stored automatically). +- Entries show as compact chips inside the day cell; multiple entries per day, ordered. + +### 4. Map Visualization +- Interactive map (Leaflet + OpenStreetMap tiles — free, no API key required, unlike Google Maps). +- All located entries plotted as markers, numbered in chronological order. +- Route polyline connecting the stops in order. +- **Distance per leg** (km, great-circle) shown on the route and in a leg-by-leg list. + +### 5. Costs & Splitting +- Every entry can carry a **price** (flights, hotels, car rental, train tickets, activities…), in the trip's currency (one currency per trip, no FX conversion). +- Each priced entry records **who paid** and how it's **split**: + - `equal` — total split equally among selected participants (e.g. rental car 50/50) + - `own` — price is per person, everyone pays their own (e.g. flights) + - `payer` — personal expense of the payer +- **Cost summary** per trip: total cost, breakdown by type, per-user share vs. paid vs. net balance, and **settle-up suggestions** (who transfers how much to whom). + +### 6. Summary +- Total days and nights. +- Number of flights, hotel stays, travel legs. +- **Total distance in km** across the whole trip. +- Countries/locations visited (from entry locations). + +## Tech Stack + +| Layer | Choice | Why | +|---|---|---| +| Backend | Node.js 20 + Express | Small, ubiquitous, one runtime in the container | +| Database | SQLite (better-sqlite3) | Zero-config single file; persisted via Docker volume | +| Auth | cookie sessions + bcrypt | Simple and adequate for a self-hosted tool | +| Frontend | Vanilla JS SPA (no build step) | Keeps image small and the stack simple | +| Map | Leaflet + OpenStreetMap | Free, no API key, offline-friendly tiles options | +| Geocoding | Nominatim (proxied server-side) | Free location search, no key | +| Packaging | Dockerfile + docker-compose | `docker compose up` and you're running | + +## Architecture + +``` +trip_plan/ +├── src/server/ # Express app +│ ├── index.js # bootstrap, static serving +│ ├── db.js # SQLite schema + connection +│ ├── auth.js # session middleware +│ ├── routes/ # auth, trips, entries, geocode +│ └── util/distance.js # haversine km calc +├── public/ # SPA served statically +│ ├── index.html +│ ├── css/styles.css +│ └── js/ # api client, views (trips, calendar, map, summary) +├── tests/ # API tests (node:test + supertest) +├── data/ # SQLite file (gitignored, volume-mounted) +├── docs/ # this file + API.md +├── Dockerfile +└── docker-compose.yml +``` + +### Data Model + +``` +users (id, username, password_hash, created_at) +trips (id, name, start_date, end_date, owner_id, created_at) +trip_members (trip_id, user_id, role) -- owner | editor +entries (id, trip_id, date, type, title, details, + start_time, end_time, location_name, lat, lng, sort_order) +``` + +Route + km are **derived**: located entries ordered by (date, sort_order) form the route; each consecutive pair is a leg with haversine distance. + +### API (REST, JSON, under /api) + +- `POST /api/auth/account` (create token account) · `POST /api/auth/login` · `POST /api/auth/logout` · `GET/PATCH /api/auth/me` +- `GET/POST /api/trips` · `GET/PATCH/DELETE /api/trips/:id` +- `POST /api/trips/join` (by join code) · `POST /api/trips/:id/join-code` (regenerate) +- `GET/POST /api/trips/:id/entries` · `PATCH/DELETE /api/entries/:id` +- `GET /api/trips/:id/route` (legs + km + summary, computed server-side) +- `GET /api/geocode?q=...` (Nominatim proxy) + +## Running It + +```bash +# development +npm install +npm start # http://localhost:3000 + +# production +docker compose up -d # builds image, mounts ./data for the SQLite file +``` + +## Non-Goals (for now) + +- Real-time collaborative editing (last-write-wins is fine for this scale) +- Driving-route distances via a routing engine (great-circle km first; OSRM could be added later) +- Email invites / password reset (admin resets via CLI) + +## Build Plan + +1. **Scaffold** — repo, package.json, this doc, API contract. ✅ +2. **Swarm build** (parallel agents): backend API + DB · frontend SPA (calendar, map, summary) · Docker/infra + tests. +3. **Integrate & verify** — run tests, boot server, smoke-test in browser, build Docker image. +4. Initial commit. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3ab0866 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1610 @@ +{ + "name": "trip-plan", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "trip-plan", + "version": "0.1.0", + "dependencies": { + "better-sqlite3": "^11.5.0", + "cookie-session": "^2.1.0", + "express": "^4.19.2" + }, + "devDependencies": { + "supertest": "^7.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-session": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.1.1.tgz", + "integrity": "sha512-ji3kym/XZaFVew1+tIZk5ZLp9Z/fLv9rK1aZmpug0FsgE7Cu3ZDrUdRo7FT9vFjMYfNimrrUHJzywDwT7XEFlg==", + "license": "MIT", + "dependencies": { + "cookies": "0.9.1", + "debug": "3.2.7", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "license": "MIT", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..37baeb9 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "trip-plan", + "version": "0.1.0", + "private": true, + "description": "Self-hosted multi-user trip planning web tool", + "type": "module", + "engines": { + "node": ">=20" + }, + "scripts": { + "start": "node src/server/index.js", + "dev": "node --watch src/server/index.js", + "test": "node --test tests/" + }, + "dependencies": { + "better-sqlite3": "^11.5.0", + "cookie-session": "^2.1.0", + "express": "^4.19.2" + }, + "devDependencies": { + "supertest": "^7.0.0" + } +} diff --git a/public/css/flipclock.css b/public/css/flipclock.css new file mode 100644 index 0000000..f86e40c --- /dev/null +++ b/public/css/flipclock.css @@ -0,0 +1,113 @@ +/* Split-flap flip-clock countdown. Cards stay dark in the light theme — that + is the airport/clock aesthetic. */ + +.countdown-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 0 0.2rem; +} +.countdown-caption { + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.18em; + color: var(--text-muted); +} + +.flipclock { + --w: 46px; + --h: 66px; + --fs: 46px; + display: flex; + align-items: flex-start; + gap: 0.5rem; +} +.fc-group { display: flex; flex-direction: column; align-items: center; gap: 0.35rem; } +.fc-digits { display: flex; gap: 3px; } +.fc-label { + font-size: 0.62rem; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--text-faint); +} +.fc-sep { + font-size: calc(var(--fs) * 0.7); + font-weight: 800; + color: var(--text-faint); + line-height: var(--h); + align-self: flex-start; +} + +/* One flip card = two static halves + two animating flip halves. */ +.fc-card { + position: relative; + width: var(--w); + height: var(--h); + perspective: 320px; + filter: drop-shadow(0 3px 5px rgba(15, 23, 42, 0.28)); +} +.fc-face { + position: absolute; + left: 0; + right: 0; + height: calc(var(--h) / 2); + overflow: hidden; + background: #1e2430; +} +.fc-face b { + position: absolute; + left: 0; + right: 0; + height: var(--h); + line-height: var(--h); + text-align: center; + font-size: var(--fs); + font-weight: 700; + color: #f1f5f9; + font-variant-numeric: tabular-nums; +} +.fc-top { top: 0; border-radius: 7px 7px 0 0; border-bottom: 1px solid rgba(0, 0, 0, 0.45); } +.fc-top b { top: 0; } +.fc-bottom { bottom: 0; border-radius: 0 0 7px 7px; } +.fc-bottom b { bottom: 0; } + +.fc-flip { display: none; backface-visibility: hidden; } +.fc-flip-top { top: 0; border-radius: 7px 7px 0 0; border-bottom: 1px solid rgba(0, 0, 0, 0.45); transform-origin: bottom; } +.fc-flip-top b { top: 0; } +.fc-flip-bottom { bottom: 0; border-radius: 0 0 7px 7px; transform-origin: top; transform: rotateX(90deg); } +.fc-flip-bottom b { bottom: 0; } + +.fc-flipping .fc-flip { display: block; } +.fc-flipping .fc-flip-top { animation: fc-top 0.28s ease-in forwards; } +.fc-flipping .fc-flip-bottom { animation: fc-bottom 0.28s ease-in 0.28s forwards; } +@keyframes fc-top { to { transform: rotateX(-90deg); } } +@keyframes fc-bottom { from { transform: rotateX(90deg); } to { transform: rotateX(0deg); } } + +@media (prefers-reduced-motion: reduce) { + .fc-flipping .fc-flip-top, + .fc-flipping .fc-flip-bottom { animation: none; } + .fc-flip { display: none !important; } +} + +/* Ongoing / completed badge (no clock) */ +.countdown-badge { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + border-radius: 999px; + font-weight: 700; + font-size: 1rem; +} +.countdown-badge.ongoing { background: linear-gradient(135deg, var(--brand), var(--accent)); color: #fff; } +.countdown-badge.done { background: var(--surface-2); color: var(--text-muted); border: 1px solid var(--border); } +.cd-badge-icon { font-size: 1.1rem; } + +/* Responsive: shrink digits on narrow screens */ +@media (max-width: 640px) { + .flipclock { --w: 34px; --h: 50px; --fs: 34px; gap: 0.35rem; } +} +@media (max-width: 380px) { + .flipclock { --w: 26px; --h: 40px; --fs: 26px; } +} diff --git a/public/css/styles.css b/public/css/styles.css new file mode 100644 index 0000000..b6fbda5 --- /dev/null +++ b/public/css/styles.css @@ -0,0 +1,433 @@ +/* Trip Plan — SPA styles. Light theme, CSS custom-property palette. */ + +:root { + --brand: #2563eb; + --brand-dark: #1d4ed8; + --brand-soft: #eff4ff; + --accent: #0ea5e9; + + --bg: #f4f6fb; + --surface: #ffffff; + --surface-2: #f8fafc; + --border: #e5e9f2; + --border-strong: #d4dbe8; + + --text: #1e293b; + --text-muted: #64748b; + --text-faint: #94a3b8; + + --danger: #dc2626; + --danger-soft: #fef2f2; + --success: #059669; + --warn: #d97706; + + --radius: 14px; + --radius-sm: 9px; + --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06), 0 1px 3px rgba(15, 23, 42, 0.05); + --shadow-md: 0 4px 14px rgba(15, 23, 42, 0.08); + --shadow-lg: 0 18px 48px rgba(15, 23, 42, 0.22); + + --font: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +* { box-sizing: border-box; } + +html, body { height: 100%; } + +body { + margin: 0; + font-family: var(--font); + color: var(--text); + background: var(--bg); + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +body.no-scroll { overflow: hidden; } + +h1, h2, h3 { margin: 0; font-weight: 700; line-height: 1.25; } +h1 { font-size: 1.6rem; } +h2 { font-size: 1.2rem; } +h3 { font-size: 1rem; } +p { margin: 0; } +a { color: inherit; text-decoration: none; } + +.muted { color: var(--text-muted); font-size: 0.9rem; } +.hint { font-size: 0.82rem; } + +/* ---------- Buttons & inputs ---------- */ +.btn { + display: inline-flex; + align-items: center; + gap: 0.4rem; + justify-content: center; + border: 1px solid var(--border-strong); + background: var(--surface); + color: var(--text); + padding: 0.55rem 1rem; + border-radius: var(--radius-sm); + font-size: 0.92rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, box-shadow 0.15s, border-color 0.15s, transform 0.05s; +} +.btn:hover { background: var(--surface-2); } +.btn:active { transform: translateY(1px); } +.btn:disabled { opacity: 0.6; cursor: default; } +.btn-primary { background: var(--brand); border-color: var(--brand); color: #fff; } +.btn-primary:hover { background: var(--brand-dark); } +.btn-ghost { border-color: transparent; background: transparent; } +.btn-ghost:hover { background: var(--surface-2); } +.btn-danger-ghost { border-color: transparent; background: transparent; color: var(--danger); } +.btn-danger-ghost:hover { background: var(--danger-soft); } +.btn-block { width: 100%; } +.btn-sm { padding: 0.4rem 0.7rem; font-size: 0.85rem; } +.link-btn { + border: none; background: none; color: var(--brand); + font-weight: 600; cursor: pointer; padding: 0; font-size: inherit; +} +.link-btn:hover { text-decoration: underline; } + +.input { + width: 100%; + padding: 0.55rem 0.7rem; + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + font-size: 0.95rem; + font-family: inherit; + color: var(--text); + background: var(--surface); +} +.input:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); } +.input-sm { padding: 0.4rem 0.6rem; font-size: 0.88rem; } +textarea.input { resize: vertical; } + +.field { display: flex; flex-direction: column; gap: 0.3rem; } +.field-grow { flex: 1; } +.field-label { font-size: 0.78rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.03em; } +.form-row { display: flex; gap: 0.8rem; flex-wrap: wrap; } +.form-row > .field { flex: 1; min-width: 140px; } +.form-actions { display: flex; gap: 0.6rem; justify-content: flex-end; margin-top: 0.4rem; } +.form-error { color: var(--danger); font-size: 0.85rem; min-height: 1rem; margin: 0.2rem 0; } + +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + padding: 1.1rem 1.2rem; +} + +/* ---------- Top nav ---------- */ +.topnav { + position: sticky; top: 0; z-index: 20; + display: flex; align-items: center; justify-content: space-between; + padding: 0.7rem 1.4rem; + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(8px); + border-bottom: 1px solid var(--border); +} +.brand { display: flex; align-items: center; gap: 0.5rem; font-weight: 800; font-size: 1.1rem; } +.brand-mark { font-size: 1.3rem; } +.brand-mark-lg { font-size: 2.4rem; } +.nav-right { display: flex; align-items: center; gap: 0.8rem; } +.nav-user { display: flex; align-items: center; gap: 0.45rem; font-weight: 600; font-size: 0.9rem; } +.nav-avatar, .member-avatar { + display: inline-flex; align-items: center; justify-content: center; + width: 1.7rem; height: 1.7rem; border-radius: 50%; + background: var(--brand); color: #fff; font-size: 0.8rem; font-weight: 700; +} + +/* ---------- Layout ---------- */ +.view { display: block; } +.page { max-width: 1180px; margin: 0 auto; padding: 1.4rem; display: flex; flex-direction: column; gap: 1.2rem; } +.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; flex-wrap: wrap; } +.section-head { margin-bottom: 0.8rem; } +.form-slot:empty { display: none; } + +/* ---------- Auth ---------- */ +.auth-wrap { min-height: 100vh; display: grid; place-items: center; padding: 1.5rem; + background: radial-gradient(1200px 600px at 50% -10%, #dbe7ff 0%, var(--bg) 55%); } +.auth-card { width: 100%; max-width: 400px; padding: 1.8rem; } +.auth-head { text-align: center; margin-bottom: 1.3rem; } +.auth-head h1 { margin: 0.4rem 0 0.2rem; } +.auth-tabs { display: flex; gap: 0.3rem; background: var(--surface-2); padding: 0.25rem; border-radius: var(--radius-sm); margin-bottom: 1.1rem; } +.auth-tab { flex: 1; border: none; background: none; padding: 0.5rem; border-radius: var(--radius-sm); cursor: pointer; font-weight: 600; color: var(--text-muted); } +.auth-tab.active { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); } +.auth-form { display: flex; flex-direction: column; gap: 0.9rem; } +.auth-toggle { text-align: center; margin-top: 1rem; font-size: 0.9rem; color: var(--text-muted); } +.auth-panel { display: flex; flex-direction: column; gap: 0.9rem; } +.auth-lead { color: var(--text-muted); font-size: 0.92rem; } +.token-input { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; letter-spacing: 0.08em; text-transform: uppercase; } +.token-reveal { gap: 1rem; } +.token-warning { background: #fffbeb; border: 1px solid #fde68a; color: #92400e; font-size: 0.85rem; padding: 0.7rem 0.85rem; border-radius: var(--radius-sm); line-height: 1.45; } +.token-box { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 1.35rem; font-weight: 700; letter-spacing: 0.1em; text-align: center; padding: 0.9rem; background: var(--surface-2); border: 2px dashed var(--border-strong); border-radius: var(--radius-sm); user-select: all; word-break: break-all; } +.token-actions { display: flex; justify-content: center; } + +/* Nav inline name edit */ +.nav-name { font-weight: 600; } +.nav-edit { border: none; background: none; cursor: pointer; color: var(--text-faint); font-size: 0.85rem; padding: 0 0.15rem; } +.nav-edit:hover { color: var(--brand); } +.nav-name-input { width: 150px; } +.page-head-actions { display: flex; gap: 0.5rem; align-items: center; } +.empty-actions { display: flex; gap: 0.5rem; justify-content: center; } +.trip-card-currency { margin-left: auto; font-weight: 700; font-size: 0.72rem; letter-spacing: 0.03em; color: var(--text-faint); } +.join-form { display: flex; flex-direction: column; gap: 0.8rem; } + +/* Join-code row on the trip header */ +.joincode-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; padding-top: 0.8rem; border-top: 1px solid var(--border); } +.joincode-chip { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-weight: 700; letter-spacing: 0.08em; background: var(--brand-soft); color: var(--brand-dark); padding: 0.25rem 0.6rem; border-radius: var(--radius-sm); user-select: all; } + +/* ---------- Trip list ---------- */ +.trip-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1rem; } +.trip-card { display: flex; flex-direction: column; gap: 0.6rem; cursor: pointer; transition: transform 0.12s, box-shadow 0.12s; } +.trip-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); border-color: var(--border-strong); } +.trip-card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.6rem; } +.trip-card-name { font-size: 1.1rem; } +.trip-card-dates { color: var(--text-muted); font-size: 0.9rem; display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; } +.trip-card-countdown { font-size: 0.72rem; font-weight: 700; color: var(--brand-dark); background: var(--brand-soft); padding: 0.1rem 0.45rem; border-radius: 999px; } +.trip-card-meta { display: flex; gap: 1rem; font-size: 0.85rem; color: var(--text-muted); margin-top: auto; } +.role-badge { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; font-weight: 700; padding: 0.15rem 0.5rem; border-radius: 999px; } +.role-owner { background: #fef3c7; color: #92400e; } +.role-editor { background: var(--brand-soft); color: var(--brand-dark); } +.new-trip-form { display: flex; flex-direction: column; gap: 0.8rem; } + +/* ---------- Empty / loading / error ---------- */ +.loading { display: flex; align-items: center; gap: 0.6rem; justify-content: center; padding: 3rem; color: var(--text-muted); } +.spinner { width: 1.1rem; height: 1.1rem; border: 2px solid var(--border-strong); border-top-color: var(--brand); border-radius: 50%; animation: spin 0.8s linear infinite; } +@keyframes spin { to { transform: rotate(360deg); } } +.empty-state { text-align: center; padding: 3rem 1.5rem; color: var(--text-muted); display: flex; flex-direction: column; gap: 0.6rem; align-items: center; } +.empty-icon { font-size: 2.6rem; } +.empty-state h3 { color: var(--text); } +.error-box { text-align: center; padding: 2rem; color: var(--danger); display: flex; flex-direction: column; gap: 0.8rem; align-items: center; } + +/* ---------- Trip header ---------- */ +.trip-header { display: flex; flex-direction: column; gap: 1rem; } +.trip-header-top { display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap; } +.back-link { font-size: 0.85rem; color: var(--brand); font-weight: 600; } +.back-link:hover { text-decoration: underline; } +.trip-title { margin-top: 0.3rem; } +.trip-subtitle { margin-top: 0.2rem; } +.trip-header-actions { display: flex; gap: 0.5rem; align-items: flex-start; } +.members-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; padding-top: 0.8rem; border-top: 1px solid var(--border); } +.members-label { font-size: 0.82rem; font-weight: 600; color: var(--text-muted); } +.member-chip { display: inline-flex; align-items: center; gap: 0.4rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: 999px; padding: 0.25rem 0.6rem 0.25rem 0.3rem; font-size: 0.85rem; font-weight: 600; } +.member-owner { background: #fffbeb; border-color: #fde68a; } +.member-remove { border: none; background: none; cursor: pointer; color: var(--text-faint); font-size: 1rem; line-height: 1; padding: 0; } +.member-remove:hover { color: var(--danger); } +.invite-form { display: flex; gap: 0.4rem; margin-left: auto; } +.invite-form .input { width: 180px; } +.trip-edit { display: flex; flex-direction: column; gap: 0.8rem; padding-top: 0.9rem; border-top: 1px solid var(--border); } + +/* ---------- Calendar ---------- */ +.legend { display: flex; flex-wrap: wrap; gap: 0.7rem; margin-bottom: 0.9rem; } +.legend-item { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.78rem; color: var(--text-muted); } +.legend-dot { width: 0.7rem; height: 0.7rem; border-radius: 50%; display: inline-block; } +.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; } +.cal-weekday { text-align: center; font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-faint); padding-bottom: 0.2rem; } +.cal-day { min-height: 92px; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.35rem; display: flex; flex-direction: column; gap: 0.25rem; } +.cal-day.clickable { cursor: pointer; transition: border-color 0.12s, box-shadow 0.12s; } +.cal-day.clickable:hover { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-soft); } +.cal-day.cal-out { background: repeating-linear-gradient(45deg, #f1f5f9, #f1f5f9 8px, #eef2f7 8px, #eef2f7 16px); color: var(--text-faint); } +.cal-day-head { display: flex; align-items: center; gap: 0.3rem; } +.cal-daynum { font-weight: 700; font-size: 0.82rem; } +.cal-out .cal-daynum { color: var(--text-faint); font-weight: 600; } +.cal-month { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; color: var(--brand); } +.cal-flag { margin-left: auto; color: var(--warn); font-size: 0.75rem; } +.cal-chips { display: flex; flex-direction: column; gap: 3px; overflow: hidden; } +.cal-chip { display: flex; align-items: center; gap: 0.25rem; font-size: 0.72rem; padding: 0.12rem 0.35rem; border-radius: 6px; background: color-mix(in srgb, var(--chip) 14%, white); border-left: 3px solid var(--chip); color: #334155; } +.chip-icon { flex-shrink: 0; } +.chip-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +/* ---------- Detail grid (map + summary) ---------- */ +.detail-grid { display: grid; grid-template-columns: 1.7fr 1fr; gap: 1.2rem; align-items: start; } +.map-section, .summary-section { display: flex; flex-direction: column; } +.leaflet-map { height: 420px; width: 100%; border-radius: var(--radius-sm); overflow: hidden; border: 1px solid var(--border); z-index: 0; } +.map-empty { text-align: center; padding: 3rem 1rem; color: var(--text-muted); display: flex; flex-direction: column; gap: 0.4rem; align-items: center; } + +.map-pin-wrap { background: none; border: none; } +.map-pin { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 50%; color: #fff; font-weight: 700; font-size: 0.8rem; box-shadow: 0 2px 6px rgba(0,0,0,0.35); border: 2px solid #fff; } +.km-label { background: rgba(37, 99, 235, 0.92); color: #fff; font-size: 0.7rem; font-weight: 700; padding: 0.1rem 0.4rem; border-radius: 999px; white-space: nowrap; box-shadow: 0 1px 3px rgba(0,0,0,0.3); } +.map-popup strong { font-size: 0.95rem; } +.map-popup-sub { color: #64748b; font-size: 0.8rem; margin-top: 0.15rem; } +.map-popup-loc { font-size: 0.82rem; margin-top: 0.15rem; } + +.leg-list, .leg-list-empty { margin-top: 1rem; } +.leg-list h3 { margin-bottom: 0.5rem; } +.leg-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.4rem 0; border-bottom: 1px solid var(--border); font-size: 0.86rem; } +.leg-index { flex-shrink: 0; width: 1.4rem; height: 1.4rem; display: inline-flex; align-items: center; justify-content: center; background: var(--brand-soft); color: var(--brand-dark); border-radius: 50%; font-weight: 700; font-size: 0.75rem; } +.leg-path { flex: 1; overflow: hidden; text-overflow: ellipsis; } +.leg-km { font-weight: 700; color: var(--brand-dark); white-space: nowrap; } + +/* ---------- Summary ---------- */ +.stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.6rem; } +.stat-tile { display: flex; flex-direction: column; align-items: center; gap: 0.1rem; padding: 0.7rem 0.4rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm); } +.stat-icon { font-size: 1.1rem; } +.stat-value { font-size: 1.35rem; font-weight: 800; } +.stat-label { font-size: 0.72rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.03em; } +.km-block { margin: 1rem 0; } +.total-km { padding: 0.9rem; text-align: center; background: linear-gradient(135deg, var(--brand), var(--accent)); color: #fff; border-radius: var(--radius-sm); display: flex; flex-direction: column; } +.total-km-value { font-size: 1.8rem; font-weight: 800; } +.total-km-label { font-size: 0.78rem; opacity: 0.9; } +.km-breakdown { display: flex; flex-direction: column; gap: 0.3rem; margin-top: 0.6rem; } +.km-row { display: flex; align-items: baseline; gap: 0.5rem; font-size: 0.9rem; } +.km-row-icon { flex-shrink: 0; } +.km-row-value { font-weight: 700; font-variant-numeric: tabular-nums; } +.km-row-label { font-size: 0.82rem; } +.leg-mode { flex-shrink: 0; font-size: 0.85rem; } +.loc-block h3 { margin-bottom: 0.5rem; } +.loc-order { margin: 0; padding-left: 1.2rem; display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.9rem; } + +/* ---------- Slide-over day editor ---------- */ +.overlay { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.45); z-index: 50; display: flex; justify-content: flex-end; animation: fade 0.15s ease; } +@keyframes fade { from { opacity: 0; } to { opacity: 1; } } +.slideover { width: min(460px, 100%); height: 100%; background: var(--surface); box-shadow: var(--shadow-lg); padding: 1.3rem; overflow-y: auto; display: flex; flex-direction: column; gap: 1rem; animation: slidein 0.2s ease; } +@keyframes slidein { from { transform: translateX(30px); opacity: 0.4; } to { transform: none; opacity: 1; } } +.slideover-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.6rem; } +.icon-btn { border: none; background: var(--surface-2); border-radius: 8px; width: 2rem; height: 2rem; cursor: pointer; font-size: 1rem; display: inline-flex; align-items: center; justify-content: center; color: var(--text-muted); } +.icon-btn:hover { background: var(--border); color: var(--text); } +.icon-btn.danger:hover { background: var(--danger-soft); color: var(--danger); } + +.entry-list { display: flex; flex-direction: column; gap: 0.5rem; } +.entry-empty { padding: 0.6rem 0; } +.entry-row { display: flex; gap: 0.6rem; align-items: flex-start; padding: 0.6rem; border: 1px solid var(--border); border-left: 4px solid var(--chip); border-radius: var(--radius-sm); background: var(--surface-2); } +.entry-icon { font-size: 1.1rem; } +.entry-body { flex: 1; min-width: 0; } +.entry-title { font-weight: 600; } +.entry-sub { font-size: 0.8rem; } +.entry-details { font-size: 0.84rem; color: var(--text-muted); margin-top: 0.2rem; white-space: pre-wrap; } +.entry-actions { display: flex; gap: 0.25rem; } + +.entry-form { display: flex; flex-direction: column; gap: 0.8rem; padding-top: 1rem; border-top: 1px solid var(--border); } +.loc-field { position: relative; } +.loc-results { display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius-sm); margin-top: 0.3rem; overflow: hidden; } +.loc-results:empty { display: none; } +.loc-result { text-align: left; border: none; background: var(--surface); padding: 0.5rem 0.6rem; cursor: pointer; font-size: 0.85rem; border-bottom: 1px solid var(--border); } +.loc-result:last-child { border-bottom: none; } +.loc-result:hover { background: var(--brand-soft); } +.loc-empty { padding: 0.5rem 0.6rem; font-size: 0.85rem; } +.loc-selected:empty { display: none; } +.loc-chip { display: inline-flex; align-items: center; gap: 0.4rem; margin-top: 0.4rem; background: var(--brand-soft); color: var(--brand-dark); padding: 0.3rem 0.6rem; border-radius: 999px; font-size: 0.85rem; font-weight: 600; } +.loc-clear { border: none; background: none; cursor: pointer; color: var(--brand-dark); font-size: 1rem; line-height: 1; padding: 0; } + +/* ---------- Currency + prices ---------- */ +.field-currency, .field-price { flex: 0 0 110px; min-width: 90px; } +.input-currency { text-transform: uppercase; } +.currency-tag { display: inline-block; font-weight: 700; font-size: 0.7rem; letter-spacing: 0.04em; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-muted); padding: 0.05rem 0.4rem; border-radius: 5px; } +.chip-price { margin-left: auto; font-size: 0.66rem; font-weight: 700; background: rgba(15,23,42,0.06); color: #334155; padding: 0 0.25rem; border-radius: 4px; white-space: nowrap; } +.entry-title-row { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; } +.entry-price { font-size: 0.82rem; font-weight: 700; color: var(--brand-dark); white-space: nowrap; } +.entry-cost { font-size: 0.78rem; margin-top: 0.15rem; } + +/* ---------- Cost subsection in the day editor ---------- */ +.cost-heading { font-size: 0.78rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); margin-top: 0.3rem; } +.cost-section { display: flex; flex-direction: column; gap: 0.7rem; } +.cost-detail { display: flex; flex-direction: column; gap: 0.7rem; transition: opacity 0.15s; } +.cost-detail.disabled { opacity: 0.45; pointer-events: none; } +.participants { display: flex; flex-wrap: wrap; gap: 0.4rem; } +.part-item { display: inline-flex; align-items: center; gap: 0.35rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: 999px; padding: 0.2rem 0.6rem 0.2rem 0.45rem; font-size: 0.85rem; cursor: pointer; } +.part-check { accent-color: var(--brand); cursor: pointer; } + +/* ---------- Costs panel ---------- */ +.detail-side { display: flex; flex-direction: column; gap: 1.2rem; } +.costs-section { display: flex; flex-direction: column; } +.costs-empty { text-align: center; padding: 2.4rem 1rem; color: var(--text-muted); display: flex; flex-direction: column; gap: 0.3rem; align-items: center; } +.costs-total { margin-bottom: 0.9rem; padding: 0.9rem; text-align: center; background: linear-gradient(135deg, #0f766e, #059669); color: #fff; border-radius: var(--radius-sm); display: flex; flex-direction: column; } +.costs-total-value { font-size: 1.7rem; font-weight: 800; } +.costs-total-label { font-size: 0.78rem; opacity: 0.9; } +.costs-warn { background: #fffbeb; border: 1px solid #fde68a; color: #92400e; font-size: 0.82rem; padding: 0.5rem 0.7rem; border-radius: var(--radius-sm); margin-bottom: 0.9rem; } +.cost-block { margin-top: 1rem; } +.cost-block h3 { margin-bottom: 0.5rem; } +.bytype-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0; font-size: 0.88rem; } +.bytype-dot { width: 0.7rem; height: 0.7rem; border-radius: 3px; flex-shrink: 0; } +.bytype-name { flex: 1; } +.bytype-amount { font-weight: 700; } +.cost-table { width: 100%; border-collapse: collapse; font-size: 0.86rem; } +.cost-table th { text-align: left; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-faint); padding: 0.3rem 0.4rem; border-bottom: 1px solid var(--border); } +.cost-table td { padding: 0.4rem 0.4rem; border-bottom: 1px solid var(--border); } +.cost-table .num { text-align: right; font-variant-numeric: tabular-nums; } +.net-pos { color: var(--success); font-weight: 700; } +.net-neg { color: var(--danger); font-weight: 700; } +.net-zero { color: var(--text-muted); } +.settle-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0; border-bottom: 1px solid var(--border); font-size: 0.88rem; } +.settle-from { font-weight: 600; } +.settle-arrow { color: var(--brand); font-weight: 700; } +.settle-to { font-weight: 600; } +.settle-amount { margin-left: auto; font-weight: 700; color: var(--brand-dark); white-space: nowrap; } + +/* ---------- Flight route builder (day editor) ---------- */ +.flight-section { display: flex; flex-direction: column; gap: 0.5rem; } +.seg-quick { display: flex; gap: 0.5rem; align-items: flex-end; } +.seg-quick-input { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; text-transform: uppercase; letter-spacing: 0.06em; } +.seg-list { display: flex; flex-direction: column; gap: 0.6rem; } +.seg-row { border: 1px solid var(--border); border-left: 3px solid var(--brand); border-radius: var(--radius-sm); padding: 0.6rem; background: var(--surface-2); display: flex; flex-direction: column; gap: 0.5rem; } +.seg-row-head { display: flex; align-items: center; gap: 0.5rem; } +.seg-num { flex-shrink: 0; width: 1.4rem; height: 1.4rem; display: inline-flex; align-items: center; justify-content: center; background: var(--brand-soft); color: var(--brand-dark); border-radius: 50%; font-weight: 700; font-size: 0.75rem; } +.seg-flightno { flex: 1; text-transform: uppercase; } +.seg-airports { display: flex; align-items: flex-start; gap: 0.4rem; } +.seg-ap { flex: 1; position: relative; display: flex; flex-direction: column; gap: 0.15rem; } +.seg-code { text-transform: uppercase; letter-spacing: 0.05em; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; } +.seg-ap.ap-unresolved .seg-code { border-color: var(--danger); box-shadow: 0 0 0 2px var(--danger-soft); } +.seg-arrow { color: var(--brand); font-weight: 700; align-self: center; padding-top: 0.3rem; } +.ap-name { font-size: 0.72rem; color: var(--text-muted); min-height: 0.9rem; padding-left: 0.1rem; } +.ap-results { position: absolute; top: 100%; left: 0; right: 0; z-index: 5; background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); box-shadow: var(--shadow-md); overflow: hidden; max-height: 220px; overflow-y: auto; } +.ap-results:empty { display: none; } +.ap-result { display: flex; gap: 0.5rem; align-items: baseline; width: 100%; text-align: left; border: none; background: var(--surface); padding: 0.45rem 0.6rem; cursor: pointer; border-bottom: 1px solid var(--border); font-size: 0.82rem; } +.ap-result:last-child { border-bottom: none; } +.ap-result:hover { background: var(--brand-soft); } +.ap-code { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-weight: 700; color: var(--brand-dark); flex-shrink: 0; } +.ap-desc { color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.ap-empty { padding: 0.5rem 0.6rem; font-size: 0.82rem; } +.seg-times { display: flex; gap: 0.6rem; } +.seg-time { display: flex; align-items: center; gap: 0.35rem; font-size: 0.78rem; color: var(--text-muted); } +.seg-time input { width: auto; } + +/* Flight segments on the day-editor entry rows */ +.entry-segments { display: flex; flex-direction: column; gap: 0.1rem; margin-top: 0.25rem; } +.entry-seg { display: flex; gap: 0.5rem; font-size: 0.8rem; font-variant-numeric: tabular-nums; } +.seg-flight { font-weight: 700; color: var(--brand-dark); } + +.stat-sub { font-size: 0.66rem; color: var(--brand-dark); font-weight: 700; } + +/* ---------- Rental details (day editor) ---------- */ +.rental-section { display: flex; flex-direction: column; gap: 0.5rem; } +.rental-details { display: flex; flex-direction: column; gap: 0.7rem; } +.rental-blocks { display: flex; gap: 0.8rem; flex-wrap: wrap; } +.rental-block { flex: 1; min-width: 200px; border: 1px solid var(--border); border-left: 3px solid #0891b2; border-radius: var(--radius-sm); padding: 0.6rem; background: var(--surface-2); display: flex; flex-direction: column; gap: 0.5rem; } +.rental-block-title { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); } +.entry-rental { font-size: 0.8rem; margin-top: 0.2rem; } + +/* Secondary "dropoff" calendar chip */ +.cal-chip-dropoff { background: transparent; border: 1px dashed var(--chip); border-left: 3px dashed var(--chip); color: var(--text-muted); cursor: pointer; font-style: italic; } +.cal-chip-dropoff:hover { background: color-mix(in srgb, var(--chip) 10%, white); } + +/* Over-budget driven distance */ +.km-row.km-over .km-row-value { color: var(--danger); } +.km-row.km-over .km-row-icon { filter: grayscale(0.2); } + +/* ---------- Toasts ---------- */ +.toast-host { position: fixed; bottom: 1.2rem; right: 1.2rem; z-index: 100; display: flex; flex-direction: column; gap: 0.5rem; } +.toast { padding: 0.7rem 1rem; border-radius: var(--radius-sm); color: #fff; font-size: 0.88rem; font-weight: 600; box-shadow: var(--shadow-md); opacity: 0; transform: translateY(10px); transition: opacity 0.25s, transform 0.25s; max-width: 320px; } +.toast.show { opacity: 1; transform: none; } +.toast-error { background: var(--danger); } +.toast-success { background: var(--success); } + +/* ---------- Responsive ---------- */ +@media (max-width: 1024px) { + .detail-grid { grid-template-columns: 1fr; } +} +@media (max-width: 768px) { + .page { padding: 1rem; } + .cal-day { min-height: 76px; } + .cal-chip .chip-text { display: none; } + .cal-chip { justify-content: center; } + .stat-grid { grid-template-columns: repeat(3, 1fr); } + .invite-form { margin-left: 0; width: 100%; } + .invite-form .input { flex: 1; width: auto; } + .trip-header-actions { width: 100%; } +} +@media (max-width: 480px) { + .calendar-grid { gap: 3px; } + .cal-weekday { font-size: 0.6rem; } + .cal-day { min-height: 60px; padding: 0.2rem; } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..bd7f61f --- /dev/null +++ b/public/index.html @@ -0,0 +1,30 @@ + + + + + + + Trip Plan + + + + + + + + + +
+ + + diff --git a/public/js/api.js b/public/js/api.js new file mode 100644 index 0000000..60eb225 --- /dev/null +++ b/public/js/api.js @@ -0,0 +1,83 @@ +// Thin fetch wrapper for the Trip Plan REST API (see docs/API.md). +// Same-origin, cookies included. Server errors ({error}) become thrown Errors +// whose message is the server's human-readable text and `.status` the code. + +async function request(method, path, body) { + const opts = { + method, + credentials: 'same-origin', + headers: {}, + }; + if (body !== undefined) { + opts.headers['Content-Type'] = 'application/json'; + opts.body = JSON.stringify(body); + } + + let res; + try { + res = await fetch(path, opts); + } catch (networkErr) { + const err = new Error('Network error — is the server running?'); + err.cause = networkErr; + err.status = 0; + throw err; + } + + if (res.status === 204) return null; + + const text = await res.text(); + let data = null; + if (text) { + try { + data = JSON.parse(text); + } catch { + data = null; + } + } + + if (!res.ok) { + const message = (data && data.error) || `Request failed (${res.status})`; + const err = new Error(message); + err.status = res.status; + err.body = data; + throw err; + } + return data; +} + +const get = (p) => request('GET', p); +const post = (p, b) => request('POST', p, b); +const patch = (p, b) => request('PATCH', p, b); +const del = (p) => request('DELETE', p); + +export const api = { + auth: { + // Mullvad-style: create an account (server returns the one-time token). + createAccount: () => post('/api/auth/account', {}), + login: (token) => post('/api/auth/login', { token }), + logout: () => post('/api/auth/logout'), + me: () => get('/api/auth/me'), + updateMe: (patchBody) => patch('/api/auth/me', patchBody), + }, + trips: { + list: () => get('/api/trips'), + create: (payload) => post('/api/trips', payload), + get: (id) => get(`/api/trips/${id}`), + update: (id, patchBody) => patch(`/api/trips/${id}`, patchBody), + remove: (id) => del(`/api/trips/${id}`), + join: (code) => post('/api/trips/join', { code }), + regenerateJoinCode: (id) => post(`/api/trips/${id}/join-code`, {}), + removeMember: (id, userId) => del(`/api/trips/${id}/members/${userId}`), + route: (id) => get(`/api/trips/${id}/route`), + costs: (id) => get(`/api/trips/${id}/costs`), + }, + entries: { + create: (tripId, payload) => post(`/api/trips/${tripId}/entries`, payload), + update: (id, patchBody) => patch(`/api/entries/${id}`, patchBody), + remove: (id) => del(`/api/entries/${id}`), + }, + geocode: (q) => get(`/api/geocode?q=${encodeURIComponent(q)}`), + airports: (q) => get(`/api/airports?q=${encodeURIComponent(q)}`), +}; + +export default api; diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..6529683 --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,171 @@ +// App bootstrap: auth check, top nav, and a tiny hash router. +// Routes: #/login, #/trips, #/trip/:id +import { api } from './api.js'; +import { el, clear, mount, toast, loading } from './dom.js'; +import { renderAuth } from './views/auth.js'; +import { renderTrips } from './views/trips.js'; +import { renderTripDetail } from './views/tripDetail.js'; + +const state = { user: null }; + +function root() { + return document.getElementById('app'); +} + +function navigate(hash) { + if (location.hash === hash) route(); + else location.hash = hash; +} + +// Parse "#/trip/5" -> { name: 'trip', params: { id: '5' } } +function parseHash() { + const raw = (location.hash || '').replace(/^#/, ''); + const parts = raw.split('/').filter(Boolean); + if (parts.length === 0) return { name: 'trips', params: {} }; + if (parts[0] === 'login') return { name: 'login', params: {} }; + if (parts[0] === 'trips') return { name: 'trips', params: {} }; + if (parts[0] === 'trip' && parts[1]) return { name: 'trip', params: { id: parts[1] } }; + return { name: 'trips', params: {} }; +} + +function renderNav() { + const nav = el( + 'header', + { class: 'topnav' }, + el( + 'a', + { class: 'brand', href: '#/trips' }, + el('span', { class: 'brand-mark' }, '🧭'), + el('span', { class: 'brand-name' }, 'Trip Plan'), + ), + el( + 'div', + { class: 'nav-right' }, + state.user ? renderUserArea() : null, + state.user ? el('button', { class: 'btn btn-ghost', onClick: onLogout }, 'Log out') : null, + ), + ); + return nav; +} + +// Current user's display name with an inline pencil-edit (PATCH /api/auth/me). +function renderUserArea() { + const area = el('span', { class: 'nav-user' }); + + function initial() { + return (state.user.display_name || '?').charAt(0).toUpperCase(); + } + + function showDisplay() { + mount( + area, + el('span', { class: 'nav-avatar' }, initial()), + el('span', { class: 'nav-name' }, state.user.display_name || 'me'), + el('button', { class: 'nav-edit', title: 'Edit name', type: 'button', onClick: showEdit }, '✎'), + ); + } + + function showEdit() { + const input = el('input', { + class: 'input input-sm nav-name-input', + type: 'text', + maxlength: '40', + value: state.user.display_name || '', + 'aria-label': 'Display name', + }); + async function save() { + const dn = input.value.trim(); + if (!dn) return showDisplay(); + try { + const data = await api.auth.updateMe({ display_name: dn }); + state.user = data.user; + showDisplay(); + } catch (err) { + toast(err.message); + } + } + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { e.preventDefault(); save(); } + else if (e.key === 'Escape') showDisplay(); + }); + mount( + area, + input, + el('button', { class: 'btn btn-sm btn-primary', type: 'button', onClick: save }, 'Save'), + el('button', { class: 'btn btn-sm btn-ghost', type: 'button', onClick: showDisplay }, 'Cancel'), + ); + input.focus(); + input.select(); + } + + showDisplay(); + return area; +} + +async function onLogout() { + try { + await api.auth.logout(); + } catch (e) { + // Even if the request fails, drop local state. + } + state.user = null; + navigate('#/login'); +} + +function render() { + const view = parseHash(); + + // Defensively remove any body-level overlay (e.g. an open day editor) so it + // can never orphan on top of a freshly rendered view. + document.querySelectorAll('.overlay').forEach((n) => n.remove()); + document.body.classList.remove('no-scroll'); + + // Auth guards. + if (!state.user && view.name !== 'login') { + navigate('#/login'); + return; + } + if (state.user && view.name === 'login') { + navigate('#/trips'); + return; + } + + const container = root(); + clear(container); + if (state.user) container.appendChild(renderNav()); + + const viewEl = el('main', { class: 'view', id: 'view' }); + container.appendChild(viewEl); + + const ctx = { state, navigate, refresh: render }; + + if (view.name === 'login') renderAuth(viewEl, ctx); + else if (view.name === 'trips') renderTrips(viewEl, ctx); + else if (view.name === 'trip') renderTripDetail(viewEl, ctx, view.params.id); +} + +// Exposed so views can update the current user after login/register. +function route() { + render(); +} + +async function bootstrap() { + const container = root(); + clear(container); + container.appendChild(loading('Starting Trip Plan…')); + + try { + const data = await api.auth.me(); + state.user = data && data.user ? data.user : null; + } catch (e) { + state.user = null; // 401 is expected when logged out. + } + + window.addEventListener('hashchange', route); + + // render()'s guards redirect a signed-out visitor to #/login and a + // signed-in one away from it, so a single render() is enough here. + render(); +} + +bootstrap(); diff --git a/public/js/dom.js b/public/js/dom.js new file mode 100644 index 0000000..ce9e573 --- /dev/null +++ b/public/js/dom.js @@ -0,0 +1,91 @@ +// Tiny DOM helpers — no framework, just ergonomic element creation. + +// el('div', { class: 'x', onClick: fn }, child, child, ...) +// Attrs: class, dataset(obj), style(obj), html(innerHTML), on(fn), +// boolean true -> present attribute, anything else -> setAttribute. +export function el(tag, attrs = {}, ...children) { + const node = document.createElement(tag); + for (const [key, val] of Object.entries(attrs || {})) { + if (val == null || val === false) continue; + if (key === 'class') node.className = val; + else if (key === 'dataset') Object.assign(node.dataset, val); + else if (key === 'style' && typeof val === 'object') { + // Custom properties (--x) must go through setProperty; plain assignment + // silently drops them. + for (const [prop, pv] of Object.entries(val)) { + if (pv == null) continue; + if (prop.startsWith('--')) node.style.setProperty(prop, pv); + else node.style[prop] = pv; + } + } + else if (key === 'html') node.innerHTML = val; + else if (key === 'value') node.value = val; + else if (key.startsWith('on') && typeof val === 'function') + node.addEventListener(key.slice(2).toLowerCase(), val); + else if (val === true) node.setAttribute(key, ''); + else node.setAttribute(key, val); + } + appendAll(node, children); + return node; +} + +function appendAll(node, children) { + for (const child of children.flat(Infinity)) { + if (child == null || child === false || child === true) continue; + node.appendChild( + typeof child === 'string' || typeof child === 'number' + ? document.createTextNode(String(child)) + : child, + ); + } +} + +export function clear(node) { + while (node.firstChild) node.removeChild(node.firstChild); + return node; +} + +export function mount(container, ...children) { + clear(container); + appendAll(container, children); + return container; +} + +export function loading(text = 'Loading…') { + return el('div', { class: 'loading' }, el('span', { class: 'spinner' }), text); +} + +export function errorBox(message, onRetry) { + return el( + 'div', + { class: 'error-box' }, + el('p', {}, message || 'Something went wrong.'), + onRetry ? el('button', { class: 'btn', onClick: onRetry }, 'Retry') : null, + ); +} + +export function emptyState(title, subtitle, action) { + return el( + 'div', + { class: 'empty-state' }, + el('div', { class: 'empty-icon' }, '🧭'), + el('h3', {}, title), + subtitle ? el('p', {}, subtitle) : null, + action || null, + ); +} + +let toastHost = null; +export function toast(message, type = 'error', ms = 4200) { + if (!toastHost) { + toastHost = el('div', { class: 'toast-host' }); + document.body.appendChild(toastHost); + } + const node = el('div', { class: `toast toast-${type}` }, message); + toastHost.appendChild(node); + requestAnimationFrame(() => node.classList.add('show')); + setTimeout(() => { + node.classList.remove('show'); + setTimeout(() => node.remove(), 300); + }, ms); +} diff --git a/public/js/format.js b/public/js/format.js new file mode 100644 index 0000000..c72c485 --- /dev/null +++ b/public/js/format.js @@ -0,0 +1,148 @@ +// Shared formatting helpers and the canonical entry-type palette. +// The type config here is the single source of truth for icon + color, +// reused by the calendar chips, map markers/popups, and the day editor. + +// Order here drives the day-editor type picker and the calendar legend, so it +// is roughly most-common-first with Activity as the default for new entries. +export const ENTRY_TYPES = { + activity: { label: 'Activity', icon: '📍', color: '#059669' }, + hotel: { label: 'Hotel', icon: '🏨', color: '#db2777' }, + travel: { label: 'Travel', icon: '🚗', color: '#d97706' }, + flight: { label: 'Flight', icon: '✈️', color: '#2563eb' }, + rental: { label: 'Rental car', icon: '🚙', color: '#0891b2' }, + immigration: { label: 'Immigration', icon: '🛂', color: '#7c3aed' }, + note: { label: 'Note', icon: '📝', color: '#64748b' }, +}; + +export const ENTRY_TYPE_LIST = Object.entries(ENTRY_TYPES).map(([value, meta]) => ({ + value, + ...meta, +})); + +export function typeInfo(type) { + return ENTRY_TYPES[type] || { label: type || 'Entry', icon: '•', color: '#64748b' }; +} + +// Split modes with the human labels the day-editor select shows. +export const SPLIT_MODES = [ + { value: 'equal', label: 'Split equally' }, + { value: 'own', label: 'Everyone pays their own (price per person)' }, + { value: 'payer', label: "Payer's own expense" }, +]; + +export function splitModeLabel(mode) { + const found = SPLIT_MODES.find((m) => m.value === mode); + return found ? found.label : mode; +} + +// Account tokens / join codes: strip separators + uppercase, or regroup for +// display. Works whether the server sends the value raw or already grouped. +export function normalizeCode(str) { + return String(str || '').replace(/[^A-Za-z0-9]/g, '').toUpperCase(); +} + +export function groupCode(str, size = 4) { + const raw = normalizeCode(str); + const groups = raw.match(new RegExp(`.{1,${size}}`, 'g')); + return groups ? groups.join('-') : raw; +} + +// Money as "1,234.56 USD". `compact` drops trailing zeros ("1,200 THB"). +export function formatMoney(amount, currency = 'USD', { compact = false } = {}) { + const n = Number(amount) || 0; + const s = n.toLocaleString(undefined, compact + ? { maximumFractionDigits: 2 } + : { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + return `${s} ${currency}`; +} + +// Parse a YYYY-MM-DD string as a *local* date (avoid UTC off-by-one). +export function parseYMD(str) { + const [y, m, d] = String(str).split('-').map(Number); + return new Date(y, (m || 1) - 1, d || 1); +} + +export function ymd(date) { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; +} + +export function addDays(date, n) { + const d = new Date(date); + d.setDate(d.getDate() + n); + return d; +} + +export function daysBetweenInclusive(startStr, endStr) { + const a = parseYMD(startStr); + const b = parseYMD(endStr); + return Math.round((b - a) / 86400000) + 1; +} + +export function eachDay(startStr, endStr) { + const out = []; + let d = parseYMD(startStr); + const end = parseYMD(endStr); + while (d <= end) { + out.push(ymd(d)); + d = addDays(d, 1); + } + return out; +} + +// Monday-based start of the week containing `date`. +export function startOfWeekMon(date) { + const d = new Date(date); + const offset = (d.getDay() + 6) % 7; // 0 = Monday + return addDays(d, -offset); +} + +export function formatDate(str, opts = { month: 'short', day: 'numeric' }) { + return parseYMD(str).toLocaleDateString(undefined, opts); +} + +export function formatFullDate(str) { + return parseYMD(str).toLocaleDateString(undefined, { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }); +} + +export function formatRange(startStr, endStr) { + const s = parseYMD(startStr); + const e = parseYMD(endStr); + const sameYear = s.getFullYear() === e.getFullYear(); + const sOpts = sameYear + ? { month: 'short', day: 'numeric' } + : { month: 'short', day: 'numeric', year: 'numeric' }; + const eOpts = { month: 'short', day: 'numeric', year: 'numeric' }; + return `${s.toLocaleDateString(undefined, sOpts)} – ${e.toLocaleDateString(undefined, eOpts)}`; +} + +export function formatTimeRange(start, end) { + if (start && end) return `${start}–${end}`; + return start || end || ''; +} + +export function pluralize(n, one, many) { + return `${n} ${n === 1 ? one : many || one + 's'}`; +} + +// "CNX→BKK→DXB→FRA" from a flight entry's segments array. +export function flightChain(segments) { + if (!Array.isArray(segments) || segments.length === 0) return ''; + const codes = [segments[0]?.from?.code, ...segments.map((s) => s?.to?.code)].filter(Boolean); + return codes.join('→'); +} + +export function hasSegments(entry) { + return entry && Array.isArray(entry.segments) && entry.segments.length > 0; +} + +export function hasRental(entry) { + return entry && entry.rental && typeof entry.rental === 'object'; +} diff --git a/public/js/views/auth.js b/public/js/views/auth.js new file mode 100644 index 0000000..193085c --- /dev/null +++ b/public/js/views/auth.js @@ -0,0 +1,162 @@ +// Login view — Mullvad-style account tokens (no username/password). +// Two panels: create a new account (one-time token reveal) or log in with an +// existing account number. +import { api } from '../api.js'; +import { el, mount, toast } from '../dom.js'; +import { groupCode, normalizeCode } from '../format.js'; + +export function renderAuth(container, ctx) { + let mode = 'create'; // 'create' | 'login' + + function draw() { + const card = el( + 'div', + { class: 'auth-card card' }, + el( + 'div', + { class: 'auth-head' }, + el('div', { class: 'brand-mark brand-mark-lg' }, '🧭'), + el('h1', {}, 'Trip Plan'), + el('p', { class: 'muted' }, 'Plan trips together, day by day.'), + ), + el( + 'div', + { class: 'auth-tabs' }, + tab('Create account', mode === 'create', () => setMode('create')), + tab('Log in', mode === 'login', () => setMode('login')), + ), + mode === 'create' ? createPanel() : loginPanel(), + ); + mount(container, el('div', { class: 'auth-wrap' }, card)); + } + + function setMode(next) { + if (mode !== next) { + mode = next; + draw(); + } + } + + function tab(label, active, onClick) { + return el('button', { class: `auth-tab${active ? ' active' : ''}`, type: 'button', onClick }, label); + } + + // ----- Create account ----- + function createPanel() { + const errorEl = el('p', { class: 'form-error' }); + const createBtn = el('button', { class: 'btn btn-primary btn-block', type: 'button' }, 'Create account'); + + async function onCreate() { + errorEl.textContent = ''; + createBtn.disabled = true; + createBtn.textContent = 'Creating…'; + try { + const data = await api.auth.createAccount(); + showToken(data.token, data.user); + } catch (err) { + errorEl.textContent = err.message; + createBtn.disabled = false; + createBtn.textContent = 'Create account'; + } + } + createBtn.addEventListener('click', onCreate); + + return el( + 'div', + { class: 'auth-panel' }, + el('p', { class: 'auth-lead' }, 'No email, no password. We generate a private account number — it is your only key to your trips.'), + errorEl, + createBtn, + ); + } + + function showToken(token, user) { + const grouped = groupCode(token, 4); + const copyBtn = el('button', { class: 'btn', type: 'button' }, '📋 Copy'); + copyBtn.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(grouped); + toast('Account number copied', 'success'); + } catch { + toast('Copy failed — select and copy it manually'); + } + }); + + const panel = el( + 'div', + { class: 'auth-panel token-reveal' }, + el('div', { class: 'token-warning' }, + el('strong', {}, 'This is your only credential.'), + ' Save it now — it will never be shown again. Anyone with it can access your trips.'), + el('div', { class: 'token-box' }, grouped), + el('div', { class: 'token-actions' }, copyBtn), + el( + 'button', + { + class: 'btn btn-primary btn-block', + type: 'button', + onClick: () => { + ctx.state.user = user; + ctx.navigate('#/trips'); + }, + }, + "I've saved it — continue", + ), + ); + + mount(container, el('div', { class: 'auth-wrap' }, el('div', { class: 'auth-card card' }, + el('div', { class: 'auth-head' }, + el('div', { class: 'brand-mark brand-mark-lg' }, '🎉'), + el('h1', {}, 'Account created'), + el('p', { class: 'muted' }, `You're ${user.display_name}. You can rename yourself later.`)), + panel, + ))); + } + + // ----- Log in ----- + function loginPanel() { + const errorEl = el('p', { class: 'form-error' }); + const input = el('input', { + class: 'input token-input', + type: 'text', + autocomplete: 'off', + autocapitalize: 'characters', + spellcheck: 'false', + placeholder: 'XXXX-XXXX-XXXX-XXXX', + 'aria-label': 'Account number', + }); + const submitBtn = el('button', { class: 'btn btn-primary btn-block', type: 'submit' }, 'Log in'); + + async function onSubmit(e) { + e.preventDefault(); + errorEl.textContent = ''; + const token = normalizeCode(input.value); + if (token.length < 8) { + errorEl.textContent = 'Enter your full account number.'; + return; + } + submitBtn.disabled = true; + submitBtn.textContent = 'Signing in…'; + try { + const data = await api.auth.login(token); + ctx.state.user = data.user; + ctx.navigate('#/trips'); + } catch (err) { + errorEl.textContent = err.message; + submitBtn.disabled = false; + submitBtn.textContent = 'Log in'; + } + } + + return el( + 'form', + { class: 'auth-panel auth-form', onSubmit }, + el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'Account number'), input), + el('p', { class: 'hint muted' }, 'Dashes, spaces and letter case do not matter.'), + errorEl, + submitBtn, + ); + } + + draw(); +} diff --git a/public/js/views/calendar.js b/public/js/views/calendar.js new file mode 100644 index 0000000..26412bc --- /dev/null +++ b/public/js/views/calendar.js @@ -0,0 +1,185 @@ +// Calendar grid for the trip's date range. Real weeks as rows (Mon–Sun +// columns); days outside the range are greyed. Each in-range day shows its +// entries as compact, type-coloured chips. Clicking a day opens the editor. +import { el } from '../dom.js'; +import { + ENTRY_TYPES, + typeInfo, + parseYMD, + ymd, + addDays, + startOfWeekMon, + formatMoney, + flightChain, + hasSegments, + hasRental, +} from '../format.js'; + +const WEEKDAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + +export function renderCalendar(tctx) { + const { trip, entries } = tctx.trip; + const currency = trip.currency || 'USD'; + + // Group entries by date for quick per-cell lookup. + const byDate = new Map(); + // Derived "dropoff" chips: a rental whose dropoff day differs from its + // (pickup) entry date gets a secondary chip on the dropoff day. + const dropoffByDate = new Map(); + for (const entry of entries) { + if (!byDate.has(entry.date)) byDate.set(entry.date, []); + byDate.get(entry.date).push(entry); + if (hasRental(entry)) { + const dropDate = entry.rental.dropoff && entry.rental.dropoff.date; + if (dropDate && dropDate !== entry.date) { + if (!dropoffByDate.has(dropDate)) dropoffByDate.set(dropDate, []); + dropoffByDate.get(dropDate).push(entry); + } + } + } + + const rangeStart = parseYMD(trip.start_date); + const rangeEnd = parseYMD(trip.end_date); + const gridStart = startOfWeekMon(rangeStart); + + const section = el( + 'section', + { class: 'card calendar-section' }, + el( + 'div', + { class: 'section-head' }, + el('h2', {}, 'Calendar'), + el('p', { class: 'muted' }, 'Click a day to add or edit entries.'), + ), + legend(), + ); + + const grid = el('div', { class: 'calendar-grid' }); + for (const label of WEEKDAYS) { + grid.appendChild(el('div', { class: 'cal-weekday' }, label)); + } + + // Walk whole weeks from gridStart until we've passed the range end. + let cursor = gridStart; + let guard = 0; + while (cursor <= rangeEnd && guard < 400) { + for (let i = 0; i < 7; i++) { + grid.appendChild(dayCell(cursor, rangeStart, rangeEnd, byDate, dropoffByDate, tctx, currency)); + cursor = addDays(cursor, 1); + } + guard += 7; + } + + section.appendChild(grid); + return section; +} + +function dayCell(date, rangeStart, rangeEnd, byDate, dropoffByDate, tctx, currency) { + const key = ymd(date); + const inRange = date >= rangeStart && date <= rangeEnd; + const dayEntries = byDate.get(key) || []; + const dropoffs = dropoffByDate.get(key) || []; + const isFirstOfMonth = date.getDate() === 1; + + const cell = el('div', { + class: `cal-day${inRange ? '' : ' cal-out'}${dayEntries.length || dropoffs.length ? ' cal-has' : ''}`, + }); + + cell.appendChild( + el( + 'div', + { class: 'cal-day-head' }, + el('span', { class: 'cal-daynum' }, String(date.getDate())), + isFirstOfMonth + ? el('span', { class: 'cal-month' }, date.toLocaleDateString(undefined, { month: 'short' })) + : null, + !inRange && (dayEntries.length || dropoffs.length) + ? el('span', { class: 'cal-flag', title: 'Outside the trip date range' }, '⚠') + : null, + ), + ); + + const chips = el('div', { class: 'cal-chips' }); + for (const entry of dayEntries) chips.appendChild(chip(entry, currency)); + // Secondary dropoff chips: clicking opens the pickup day where the entry lives. + for (const entry of dropoffs) chips.appendChild(dropoffChip(entry, tctx)); + cell.appendChild(chips); + + // In-range days are always clickable; out-of-range days only when they + // hold entries or a derived dropoff chip. + if (inRange || dayEntries.length || dropoffs.length) { + cell.classList.add('clickable'); + cell.tabIndex = 0; + cell.setAttribute('role', 'button'); + cell.addEventListener('click', () => tctx.openDay(key)); + cell.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + tctx.openDay(key); + } + }); + } + + return cell; +} + +function chip(entry, currency) { + const info = typeInfo(entry.type); + const hasPrice = entry.price != null; + const chain = hasSegments(entry) ? flightChain(entry.segments) : ''; + const car = hasRental(entry) ? [entry.rental.brand, entry.rental.model].filter(Boolean).join(' ') : ''; + const label = chain || car || entry.title; + return el( + 'div', + { + class: 'cal-chip', + style: { '--chip': info.color }, + title: `${info.label}: ${chain ? `${entry.title} (${chain})` : entry.title}${hasPrice ? ` · ${formatMoney(entry.price, currency, { compact: true })}` : ''}`, + }, + el('span', { class: 'chip-icon' }, info.icon), + el('span', { class: 'chip-text' }, label), + hasPrice + ? el('span', { class: 'chip-price' }, formatMoney(entry.price, currency, { compact: true })) + : null, + ); +} + +// Secondary, outlined chip shown on a rental's dropoff day. Clicking opens the +// PICKUP day's editor (the day the entry actually lives on). +function dropoffChip(entry, tctx) { + const info = typeInfo(entry.type); + const car = [entry.rental.brand, entry.rental.model].filter(Boolean).join(' '); + const node = el( + 'div', + { + class: 'cal-chip cal-chip-dropoff', + style: { '--chip': info.color }, + role: 'button', + tabindex: '0', + title: `Rental dropoff${car ? `: ${car}` : ''} — opens the pickup day`, + }, + el('span', { class: 'chip-icon' }, info.icon), + el('span', { class: 'chip-text' }, 'dropoff'), + ); + const open = (e) => { e.stopPropagation(); tctx.openDay(entry.date); }; + node.addEventListener('click', open); + node.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(e); } + }); + return node; +} + +function legend() { + const wrap = el('div', { class: 'legend' }); + for (const [type, info] of Object.entries(ENTRY_TYPES)) { + wrap.appendChild( + el( + 'span', + { class: 'legend-item', 'data-type': type }, + el('span', { class: 'legend-dot', style: { background: info.color } }), + el('span', {}, `${info.icon} ${info.label}`), + ), + ); + } + return wrap; +} diff --git a/public/js/views/costForm.js b/public/js/views/costForm.js new file mode 100644 index 0000000..4fa8a30 --- /dev/null +++ b/public/js/views/costForm.js @@ -0,0 +1,86 @@ +// Cost & splitting subsection for the day editor. Self-contained so dayEditor +// stays small. read() returns the cost fields for the entry payload: +// { price: null } — no cost +// { price, paid_by, split_mode, participants } — cost set +// { error } — validation message +import { el } from '../dom.js'; +import { SPLIT_MODES } from '../format.js'; + +export function createCostForm({ members, currency }) { + const priceInput = el('input', { class: 'input', type: 'number', min: '0', step: '0.01', placeholder: '0.00' }); + const payerSelect = el( + 'select', + { class: 'input' }, + el('option', { value: '' }, '— unassigned —'), + ...members.map((m) => el('option', { value: String(m.id) }, m.display_name)), + ); + const modeSelect = el( + 'select', + { class: 'input' }, + ...SPLIT_MODES.map((m) => el('option', { value: m.value }, m.label)), + ); + const participantChecks = members.map((m) => + el('input', { type: 'checkbox', class: 'part-check', value: String(m.id), checked: true }), + ); + const participantsBox = el( + 'div', + { class: 'participants' }, + ...members.map((m, i) => + el('label', { class: 'part-item' }, participantChecks[i], el('span', {}, m.display_name)), + ), + ); + const costDetail = el( + 'div', + { class: 'cost-detail' }, + el('div', { class: 'form-row' }, + el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'Paid by'), payerSelect), + el('label', { class: 'field field-grow' }, el('span', { class: 'field-label' }, 'Split'), modeSelect)), + el('div', { class: 'field' }, el('span', { class: 'field-label' }, 'Participants'), participantsBox), + ); + const node = el( + 'div', + { class: 'cost-section' }, + el('div', { class: 'form-row' }, + el('label', { class: 'field field-price' }, el('span', { class: 'field-label' }, `Price (${currency})`), priceInput)), + costDetail, + ); + + // Dim the payer/split/participants controls until a price is entered. + function syncVisibility() { + costDetail.classList.toggle('disabled', priceInput.value.trim() === ''); + } + priceInput.addEventListener('input', syncVisibility); + + function read() { + const priceRaw = priceInput.value.trim(); + if (priceRaw === '') return { price: null }; + const price = Number(priceRaw); + if (!Number.isFinite(price) || price < 0) return { error: 'Price must be a number ≥ 0.' }; + const split_mode = modeSelect.value; + const paid_by = payerSelect.value ? Number(payerSelect.value) : null; + if (split_mode === 'payer' && paid_by == null) { + return { error: "Choose who paid for a payer's own expense." }; + } + const checked = participantChecks.filter((c) => c.checked).map((c) => Number(c.value)); + if (checked.length === 0) { + return { error: 'Select at least one participant (or clear the price to drop the cost).' }; + } + // [] means "all trip members"; only send an explicit list for a subset. + const participants = checked.length === members.length ? [] : checked; + return { price, paid_by, split_mode, participants }; + } + + function prefill(entry) { + priceInput.value = entry.price != null ? String(entry.price) : ''; + payerSelect.value = entry.paid_by != null ? String(entry.paid_by) : ''; + modeSelect.value = entry.split_mode || 'equal'; + const parts = Array.isArray(entry.participants) ? entry.participants : []; + for (const cb of participantChecks) { + cb.checked = parts.length === 0 || parts.includes(Number(cb.value)); + } + syncVisibility(); + } + + syncVisibility(); + return { node, read, prefill }; +} diff --git a/public/js/views/costs.js b/public/js/views/costs.js new file mode 100644 index 0000000..7ffd51e --- /dev/null +++ b/public/js/views/costs.js @@ -0,0 +1,148 @@ +// Costs & splitting panel from GET /api/trips/:id/costs. +// Total + currency, breakdown by entry type, a per-user share/paid/net table +// (net colored green when owed to them, red when they owe), a settle-up list, +// and a hint when some priced entries have no payer assigned. +import { el } from '../dom.js'; +import { typeInfo, formatMoney } from '../format.js'; + +export function renderCosts(tctx) { + const costs = tctx.costs || {}; + const currency = costs.currency || (tctx.trip.trip && tctx.trip.trip.currency) || 'USD'; + const total = costs.totalCost || 0; + const perUser = costs.perUser || []; + const byType = costs.byType || {}; + const settlements = costs.settlements || []; + const unassigned = costs.unassigned || 0; + + const money = (n) => formatMoney(n, currency); + + const nameById = new Map(perUser.map((u) => [u.userId, u.displayName])); + const memberName = (id) => { + if (nameById.has(id)) return nameById.get(id); + const m = (tctx.trip.members || []).find((x) => x.id === id); + return m ? m.display_name : `user ${id}`; + }; + + const section = el('section', { class: 'card costs-section' }); + section.appendChild( + el( + 'div', + { class: 'section-head' }, + el('h2', {}, 'Costs'), + el('p', { class: 'muted' }, 'Who owes whom, split across the trip.'), + ), + ); + + const hasCosts = total > 0 || perUser.some((u) => u.share || u.paid); + if (!hasCosts) { + section.appendChild( + el( + 'div', + { class: 'costs-empty' }, + el('div', { class: 'empty-icon' }, '💰'), + el('p', {}, 'No costs tracked yet.'), + el('p', { class: 'muted' }, 'Add a price to an entry to start splitting expenses.'), + ), + ); + return section; + } + + // Total. + section.appendChild( + el( + 'div', + { class: 'costs-total' }, + el('span', { class: 'costs-total-value' }, money(total)), + el('span', { class: 'costs-total-label' }, 'total trip cost'), + ), + ); + + if (unassigned > 0) { + section.appendChild( + el( + 'div', + { class: 'costs-warn' }, + `⚠ ${money(unassigned)} of priced entries have no payer assigned — assign payers to settle them.`, + ), + ); + } + + // Breakdown by type. + const typeKeys = Object.keys(byType); + if (typeKeys.length) { + const bt = el('div', { class: 'cost-block' }, el('h3', {}, 'By type')); + for (const type of typeKeys) { + const info = typeInfo(type); + bt.appendChild( + el( + 'div', + { class: 'bytype-row' }, + el('span', { class: 'bytype-dot', style: { background: info.color } }), + el('span', { class: 'bytype-name' }, `${info.icon} ${info.label}`), + el('span', { class: 'bytype-amount' }, money(byType[type])), + ), + ); + } + section.appendChild(bt); + } + + // Per-user table. + if (perUser.length) { + const table = el( + 'table', + { class: 'cost-table' }, + el( + 'thead', + {}, + el( + 'tr', + {}, + el('th', {}, 'Member'), + el('th', { class: 'num' }, 'Share'), + el('th', { class: 'num' }, 'Paid'), + el('th', { class: 'num' }, 'Net'), + ), + ), + el( + 'tbody', + {}, + ...perUser.map((u) => { + const net = u.net || 0; + const netClass = net > 0.004 ? 'net-pos' : net < -0.004 ? 'net-neg' : 'net-zero'; + const netText = `${net > 0 ? '+' : ''}${money(net)}`; + return el( + 'tr', + {}, + el('td', {}, u.displayName), + el('td', { class: 'num' }, money(u.share || 0)), + el('td', { class: 'num' }, money(u.paid || 0)), + el('td', { class: `num ${netClass}` }, netText), + ); + }), + ), + ); + section.appendChild(el('div', { class: 'cost-block' }, el('h3', {}, 'Per person'), table)); + } + + // Settle-up list. + const settleBlock = el('div', { class: 'cost-block' }, el('h3', {}, 'Settle up')); + if (!settlements.length) { + settleBlock.appendChild(el('p', { class: 'muted' }, 'All square — no transfers needed.')); + } else { + for (const s of settlements) { + settleBlock.appendChild( + el( + 'div', + { class: 'settle-row' }, + el('span', { class: 'settle-from' }, memberName(s.fromUserId)), + el('span', { class: 'settle-arrow' }, '→'), + el('span', { class: 'settle-to' }, memberName(s.toUserId)), + el('span', { class: 'settle-amount' }, money(s.amount)), + ), + ); + } + } + section.appendChild(settleBlock); + + return section; +} diff --git a/public/js/views/dayEditor.js b/public/js/views/dayEditor.js new file mode 100644 index 0000000..f7bcdb3 --- /dev/null +++ b/public/js/views/dayEditor.js @@ -0,0 +1,443 @@ +// Slide-over panel for a single day: lists existing entries (edit/delete) and +// a form to add/update one, including a debounced geocode-backed location +// autocomplete. On any change it calls tctx.refreshTrip() so the calendar, +// map and summary update; while open it registers tctx._onModalRefresh so +// this panel re-renders itself from the freshly fetched trip data too. +import { api } from '../api.js'; +import { el, clear, mount, toast } from '../dom.js'; +import { + ENTRY_TYPE_LIST, + splitModeLabel, + typeInfo, + formatFullDate, + formatTimeRange, + formatMoney, + flightChain, + hasSegments, + hasRental, +} from '../format.js'; +import { createFlightRoute, renderSegmentLines } from './segments.js'; +import { createRentalDetails, renderRentalLine } from './rental.js'; +import { createCostForm } from './costForm.js'; + +export function openDayEditor(tctx, date) { + // A form-state object for the entry currently being added/edited. + let editing = null; // entry id being edited, or null for a new entry + let loc = null; // { name, lat, lng } | null + + const members = () => tctx.trip.members || []; + const currency = () => (tctx.trip.trip && tctx.trip.trip.currency) || 'USD'; + const memberName = (id) => { + const m = members().find((x) => x.id === id); + return m ? m.display_name : `user ${id}`; + }; + + const overlay = el('div', { class: 'overlay' }); + const panel = el('aside', { class: 'slideover', role: 'dialog', 'aria-modal': 'true' }); + overlay.appendChild(panel); + document.body.appendChild(overlay); + document.body.classList.add('no-scroll'); + + function close() { + tctx._onModalRefresh = null; + document.body.classList.remove('no-scroll'); + overlay.remove(); + document.removeEventListener('keydown', onKey); + window.removeEventListener('hashchange', close); + } + + function onKey(e) { + if (e.key === 'Escape') close(); + } + document.addEventListener('keydown', onKey); + // Self-close on any navigation so the overlay never orphans over another view. + window.addEventListener('hashchange', close); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) close(); + }); + + // Re-render this panel whenever the underlying trip data changes. + tctx._onModalRefresh = () => draw(); + + function entriesForDate() { + return (tctx.trip.entries || []).filter((e) => e.date === date); + } + + function draw() { + const dayEntries = entriesForDate(); + + const header = el( + 'div', + { class: 'slideover-head' }, + el( + 'div', + {}, + el('h2', {}, formatFullDate(date)), + el('p', { class: 'muted' }, dayEntries.length + ? `${dayEntries.length} ${dayEntries.length === 1 ? 'entry' : 'entries'}` + : 'No entries yet'), + ), + el('button', { class: 'icon-btn', title: 'Close', onClick: close }, '×'), + ); + + const list = el('div', { class: 'entry-list' }); + if (!dayEntries.length) { + list.appendChild(el('p', { class: 'muted entry-empty' }, 'Nothing planned for this day yet.')); + } else { + for (const entry of dayEntries) list.appendChild(entryRow(entry)); + } + + mount(panel, header, list, formSection(dayEntries)); + panel.scrollTop = 0; + } + + function entryRow(entry) { + const info = typeInfo(entry.type); + const time = formatTimeRange(entry.start_time, entry.end_time); + const hasPrice = entry.price != null; + const flight = hasSegments(entry); + return el( + 'div', + { class: 'entry-row', style: { '--chip': info.color } }, + el('span', { class: 'entry-icon' }, info.icon), + el( + 'div', + { class: 'entry-body' }, + el( + 'div', + { class: 'entry-title-row' }, + el('span', { class: 'entry-title' }, entry.title), + hasPrice + ? el('span', { class: 'entry-price' }, formatMoney(entry.price, currency(), { compact: true })) + : null, + ), + el( + 'div', + { class: 'entry-sub muted' }, + info.label, + time ? ` · ${time}` : '', + flight ? ` · ✈️ ${flightChain(entry.segments)}` : '', + !flight && entry.location_name ? ` · 📍 ${entry.location_name}` : '', + ), + flight ? renderSegmentLines(entry.segments) : null, + hasRental(entry) ? renderRentalLine(entry.rental) : null, + hasPrice + ? el( + 'div', + { class: 'entry-cost muted' }, + `💰 ${splitModeLabel(entry.split_mode)}`, + entry.paid_by != null ? ` · paid by ${memberName(entry.paid_by)}` : ' · no payer set', + ) + : null, + entry.details ? el('div', { class: 'entry-details' }, entry.details) : null, + ), + el( + 'div', + { class: 'entry-actions' }, + el('button', { class: 'icon-btn', title: 'Edit', onClick: () => startEdit(entry) }, '✎'), + el('button', { class: 'icon-btn danger', title: 'Delete', onClick: () => onDelete(entry) }, '🗑'), + ), + ); + } + + function startEdit(entry) { + editing = entry.id; + loc = entry.lat != null && entry.lng != null + ? { name: entry.location_name || '', lat: entry.lat, lng: entry.lng } + : null; + draw(); + // Populate fields from the entry after (re)draw. + fields.type.value = entry.type; + fields.title.value = entry.title; + fields.details.value = entry.details || ''; + fields.start.value = entry.start_time || ''; + fields.end.value = entry.end_time || ''; + fields.cost.prefill(entry); + // Flight segments (load() triggers the flight-route onChange -> UI sync). + fields.flightRoute.load(Array.isArray(entry.segments) ? entry.segments : []); + fields.rentalDetails.load(entry.rental || null); + renderLoc(); + panel.querySelector('.entry-form').scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + fields.title.focus(); + } + + // Field references for the current form render, so edit can populate them. + let fields = {}; + + function formSection(dayEntries) { + const typeSelect = el( + 'select', + { class: 'input' }, + ...ENTRY_TYPE_LIST.map((t) => el('option', { value: t.value }, `${t.icon} ${t.label}`)), + ); + const titleInput = el('input', { class: 'input', type: 'text', maxlength: '200', placeholder: 'Title (e.g. Flight BKK → CNX)' }); + const detailsInput = el('textarea', { class: 'input', rows: '2', placeholder: 'Details (optional)' }); + const startInput = el('input', { class: 'input', type: 'time' }); + const endInput = el('input', { class: 'input', type: 'time' }); + + const locWrap = el('div', { class: 'loc-field' }); + const locInput = el('input', { + class: 'input', + type: 'text', + placeholder: 'Search a place (OpenStreetMap)…', + autocomplete: 'off', + }); + const locResults = el('div', { class: 'loc-results' }); + const locSelected = el('div', { class: 'loc-selected' }); + locWrap.append(locInput, locResults, locSelected); + const locationField = el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'Location'), locWrap); + + // ----- Flight route subsection (shown only for flight entries) ----- + // The sub-modules fire onChange during construction (load of initial + // state), before the section elements below exist — gate until wired. + let typeUIReady = false; + const flightRoute = createFlightRoute({ onChange: () => { if (typeUIReady) syncTypeUI(); } }); + const flightSection = el( + 'div', + { class: 'flight-section' }, + el('div', { class: 'cost-heading' }, 'Flight route (optional)'), + el('p', { class: 'hint muted' }, 'Add legs for a multi-stop flight; airports plot the route on the map.'), + flightRoute.node, + ); + + // ----- Rental details subsection (shown only for rental entries) ----- + const rentalDetails = createRentalDetails({ entryDate: date, onChange: () => { if (typeUIReady) syncTypeUI(); } }); + const rentalSection = el( + 'div', + { class: 'rental-section' }, + el('div', { class: 'cost-heading' }, 'Rental details'), + rentalDetails.node, + ); + + // Show the flight route for flights and the rental block for rentals; hide + // the generic location field only once flight segments exist (route then + // comes from the airport coords). Rentals keep the location field — their + // pickup/dropoff are plain text and don't feed the route. + function syncTypeUI() { + const type = typeSelect.value; + flightSection.style.display = type === 'flight' ? '' : 'none'; + rentalSection.style.display = type === 'rental' ? '' : 'none'; + locationField.style.display = type === 'flight' && flightRoute.hasSegments() ? 'none' : ''; + } + typeSelect.addEventListener('change', syncTypeUI); + typeUIReady = true; + syncTypeUI(); + + // ----- Cost subsection (self-contained module) ----- + const costForm = createCostForm({ members: members(), currency: currency() }); + + fields = { + type: typeSelect, title: titleInput, details: detailsInput, start: startInput, end: endInput, + locInput, locResults, locSelected, + cost: costForm, flightRoute, rentalDetails, + }; + + wireGeocode(locInput, locResults); + + const errorEl = el('p', { class: 'form-error' }); + const submitBtn = el('button', { class: 'btn btn-primary', type: 'submit' }, editing ? 'Save entry' : 'Add entry'); + + async function onSubmit(e) { + e.preventDefault(); + errorEl.textContent = ''; + const title = titleInput.value.trim(); + if (!title) return (errorEl.textContent = 'Title is required.'); + + const payload = { + date, + type: typeSelect.value, + title, + details: detailsInput.value.trim(), + start_time: startInput.value || null, + end_time: endInput.value || null, + location_name: loc ? loc.name : null, + lat: loc ? loc.lat : null, + lng: loc ? loc.lng : null, + }; + if (!editing) { + payload.sort_order = dayEntries.length; + } + + // Flight segments (flight entries only). Clear them otherwise so changing + // an entry's type away from flight drops any prior segments. + if (typeSelect.value === 'flight') { + const res = flightRoute.read(); + if (res.error) return (errorEl.textContent = res.error); + payload.segments = res.segments; // array or null + // When segments exist, the route comes from the airports — clear the + // generic location so it doesn't add a stray stop. + if (res.segments) { + payload.location_name = null; + payload.lat = null; + payload.lng = null; + } + } else { + payload.segments = null; + } + + // Rental details (rental entries only). The entry's own date follows the + // pickup date so it lives on the pickup day. + if (typeSelect.value === 'rental') { + const res = rentalDetails.read(); + if (res.error) return (errorEl.textContent = res.error); + payload.rental = res.rental; // object or null + if (res.pickupDate) payload.date = res.pickupDate; + } else { + payload.rental = null; + } + + // Cost fields: price is the toggle. When set, send the full cost set; + // when blank, send price:null (clears any prior cost) and omit the rest. + const costRes = costForm.read(); + if (costRes.error) return (errorEl.textContent = costRes.error); + payload.price = costRes.price != null ? costRes.price : null; + if (costRes.price != null) { + payload.paid_by = costRes.paid_by; + payload.split_mode = costRes.split_mode; + payload.participants = costRes.participants; + } + + submitBtn.disabled = true; + submitBtn.textContent = 'Saving…'; + try { + if (editing) await api.entries.update(editing, payload); + else await api.entries.create(tctx.tripId, payload); + toast(editing ? 'Entry updated' : 'Entry added', 'success'); + editing = null; + loc = null; + await tctx.refreshTrip(); // triggers _onModalRefresh -> draw() + } catch (err) { + errorEl.textContent = err.message; + submitBtn.disabled = false; + submitBtn.textContent = editing ? 'Save entry' : 'Add entry'; + } + } + + const cancelEdit = editing + ? el('button', { + class: 'btn btn-ghost', + type: 'button', + onClick: () => { editing = null; loc = null; draw(); }, + }, 'Cancel edit') + : null; + + const form = el( + 'form', + { class: 'entry-form', onSubmit }, + el('h3', {}, editing ? 'Edit entry' : 'Add entry'), + el( + 'div', + { class: 'form-row' }, + el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'Type'), typeSelect), + el('label', { class: 'field field-grow' }, el('span', { class: 'field-label' }, 'Title'), titleInput), + ), + el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'Details'), detailsInput), + el( + 'div', + { class: 'form-row' }, + el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'Start time'), startInput), + el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'End time'), endInput), + ), + flightSection, + rentalSection, + locationField, + el('div', { class: 'cost-heading' }, 'Cost (optional)'), + costForm.node, + errorEl, + el('div', { class: 'form-actions' }, cancelEdit, submitBtn), + ); + + renderLoc(); + syncTypeUI(); + return form; + } + + function renderLoc() { + const box = fields.locSelected; + if (!box) return; + clear(box); + if (loc) { + box.appendChild( + el( + 'div', + { class: 'loc-chip' }, + el('span', {}, `📍 ${loc.name}`), + el('button', { + class: 'loc-clear', + type: 'button', + title: 'Clear location', + onClick: () => { loc = null; fields.locInput.value = ''; renderLoc(); }, + }, '×'), + ), + ); + } + } + + function wireGeocode(input, resultsBox) { + let timer = null; + let seq = 0; + input.addEventListener('input', () => { + const q = input.value.trim(); + clearTimeout(timer); + if (q.length < 2) { + clear(resultsBox); + return; + } + timer = setTimeout(async () => { + const mySeq = ++seq; + resultsBox.classList.add('loading'); + try { + const data = await api.geocode(q); + if (mySeq !== seq) return; // a newer query superseded this one + showResults(data.results || [], resultsBox); + } catch (err) { + if (mySeq !== seq) return; + clear(resultsBox); + toast(err.message || 'Location search failed'); + } finally { + resultsBox.classList.remove('loading'); + } + }, 400); + }); + } + + function showResults(results, resultsBox) { + clear(resultsBox); + if (!results.length) { + resultsBox.appendChild(el('div', { class: 'loc-empty muted' }, 'No matches')); + return; + } + for (const r of results) { + resultsBox.appendChild( + el( + 'button', + { + class: 'loc-result', + type: 'button', + onClick: () => { + loc = { name: r.name, lat: r.lat, lng: r.lng }; + fields.locInput.value = ''; + clear(resultsBox); + renderLoc(); + }, + }, + el('span', { class: 'loc-result-name' }, r.name), + ), + ); + } + } + + async function onDelete(entry) { + if (!window.confirm(`Delete "${entry.title}"?`)) return; + try { + await api.entries.remove(entry.id); + toast('Entry deleted', 'success'); + if (editing === entry.id) { editing = null; loc = null; } + await tctx.refreshTrip(); + } catch (err) { + toast(err.message); + } + } + + draw(); +} diff --git a/public/js/views/flipclock.js b/public/js/views/flipclock.js new file mode 100644 index 0000000..91c4627 --- /dev/null +++ b/public/js/views/flipclock.js @@ -0,0 +1,189 @@ +// Split-flap flip-clock countdown to a trip's start. Vanilla JS + CSS, no +// library. The static top/bottom halves ALWAYS show the current digit (so the +// number is correct even if the flip animation is interrupted); the flip +// layers are a cosmetic overlay. +// +// Lifecycle: only one countdown is ever mounted, so the 1s interval id lives in +// module state. renderCountdown clears any previous timer before starting a new +// one (so refreshTrip's re-render never leaks or double-ticks), and a hashchange +// handler stops it on navigation away. +import { el, clear } from '../dom.js'; +import { parseYMD, ymd, daysBetweenInclusive } from '../format.js'; + +const UNITS = [ + { key: 'days', label: 'Days' }, + { key: 'hours', label: 'Hours' }, + { key: 'mins', label: 'Min' }, + { key: 'secs', label: 'Sec' }, +]; + +let activeTimer = null; +let activeHashHandler = null; + +export function stopCountdown() { + if (activeTimer) { + clearInterval(activeTimer); + activeTimer = null; + } + if (activeHashHandler) { + window.removeEventListener('hashchange', activeHashHandler); + activeHashHandler = null; + } +} + +export function renderCountdown(tctx) { + stopCountdown(); + const trip = tctx.trip.trip; + const section = el('section', { class: 'countdown-section' }); + + function paint() { + const state = computeState(trip); + if (state.kind === 'future') { + buildClock(section, trip, tctx); + } else if (state.kind === 'ongoing') { + clear(section); + section.appendChild(el('div', { class: 'countdown-badge ongoing' }, + el('span', { class: 'cd-badge-icon' }, '✈'), + el('span', {}, `Day ${state.dayN} of ${state.total}`))); + } else { + clear(section); + section.appendChild(el('div', { class: 'countdown-badge done' }, + el('span', { class: 'cd-badge-icon' }, '🏁'), + el('span', {}, 'Trip completed'))); + } + } + + paint(); + return section; +} + +function computeState(trip) { + const today = ymd(new Date()); + if (today < trip.start_date) return { kind: 'future' }; + if (today <= trip.end_date) { + return { + kind: 'ongoing', + dayN: daysBetweenInclusive(trip.start_date, today), + total: daysBetweenInclusive(trip.start_date, trip.end_date), + }; + } + return { kind: 'past' }; +} + +function buildClock(section, trip, tctx) { + const target = parseYMD(trip.start_date); // local midnight of the start day + clear(section); + + const groups = {}; + const row = el('div', { class: 'flipclock' }); + UNITS.forEach((u, i) => { + if (i > 0) row.appendChild(el('div', { class: 'fc-sep' }, ':')); + const digitsWrap = el('div', { class: 'fc-digits' }); + // days can be 3 wide; the rest are 2. Digit cards are created lazily to + // match the current width so a 3-digit day count still renders. + groups[u.key] = { wrap: digitsWrap, cards: [] }; + row.appendChild( + el('div', { class: 'fc-group' }, digitsWrap, el('div', { class: 'fc-label' }, u.label)), + ); + }); + + section.appendChild(el('p', { class: 'countdown-caption' }, 'until departure')); + section.appendChild(row); + + function values() { + const totalSec = Math.max(0, Math.floor((target.getTime() - Date.now()) / 1000)); + return { + days: Math.floor(totalSec / 86400), + hours: Math.floor((totalSec % 86400) / 3600), + mins: Math.floor((totalSec % 3600) / 60), + secs: totalSec % 60, + totalSec, + }; + } + + function update() { + const v = values(); + setGroup(groups.days, v.days, Math.max(2, String(v.days).length)); + setGroup(groups.hours, v.hours, 2); + setGroup(groups.mins, v.mins, 2); + setGroup(groups.secs, v.secs, 2); + if (v.totalSec <= 0) { + // Departure reached — swap to the "ongoing" badge (no network needed). + stopCountdown(); + renderInto(section, tctx); + } + } + + update(); + activeTimer = setInterval(update, 1000); + activeHashHandler = stopCountdown; + window.addEventListener('hashchange', activeHashHandler); +} + +// Re-run the whole countdown paint (used at the zero-crossing). +function renderInto(section, tctx) { + const trip = tctx.trip.trip; + const state = computeState(trip); + clear(section); + if (state.kind === 'ongoing') { + section.appendChild(el('div', { class: 'countdown-badge ongoing' }, + el('span', { class: 'cd-badge-icon' }, '✈'), + el('span', {}, `Day ${state.dayN} of ${state.total}`))); + } else { + section.appendChild(el('div', { class: 'countdown-badge done' }, + el('span', { class: 'cd-badge-icon' }, '🏁'), + el('span', {}, 'Trip completed'))); + } +} + +function setGroup(group, value, width) { + const str = String(value).padStart(width, '0'); + // Rebuild the card set if the digit count changed (e.g. 100 -> 99 days). + if (group.cards.length !== str.length) { + clear(group.wrap); + group.cards = []; + for (const ch of str) { + const card = makeCard(ch); + group.cards.push(card); + group.wrap.appendChild(card.node); + } + return; + } + str.split('').forEach((ch, i) => setDigit(group.cards[i], ch)); +} + +function makeCard(digit) { + const top = face('fc-top', digit); + const bottom = face('fc-bottom', digit); + const flipTop = face('fc-flip fc-flip-top', digit); + const flipBottom = face('fc-flip fc-flip-bottom', digit); + const node = el('div', { class: 'fc-card' }, top, bottom, flipTop, flipBottom); + const card = { node, value: digit, top, bottom, flipTop, flipBottom }; + card.node.addEventListener('animationend', (e) => { + if (e.animationName === 'fc-bottom') card.node.classList.remove('fc-flipping'); + }); + return card; +} + +function face(cls, digit) { + return el('div', { class: `fc-face ${cls}` }, el('b', {}, digit)); +} + +function setDigit(card, next) { + if (card.value === next) return; + const prev = card.value; + card.value = next; + // Static halves show the new digit immediately (correctness guaranteed). + digitText(card.top, next); + digitText(card.bottom, next); + // Cosmetic flip: old top falls, new bottom rises. + digitText(card.flipTop, prev); + digitText(card.flipBottom, next); + card.node.classList.remove('fc-flipping'); + void card.node.offsetWidth; // restart the animation + card.node.classList.add('fc-flipping'); +} + +function digitText(face, ch) { + face.firstChild.textContent = ch; +} diff --git a/public/js/views/map.js b/public/js/views/map.js new file mode 100644 index 0000000..71aa031 --- /dev/null +++ b/public/js/views/map.js @@ -0,0 +1,190 @@ +// Leaflet map: numbered markers for located stops, a polyline through them, +// per-leg km labels, and a leg-by-leg list. Graceful empty state when the +// trip has no located entries yet. Uses the /route response from tctx.route. +import { el } from '../dom.js'; +import { typeInfo } from '../format.js'; + +export function renderMap(tctx) { + const route = tctx.route || { stops: [], legs: [], totalKm: 0 }; + const stops = route.stops || []; + + const section = el('section', { class: 'card map-section' }); + section.appendChild( + el( + 'div', + { class: 'section-head' }, + el('h2', {}, 'Map'), + el('p', { class: 'muted' }, stops.length + ? `${stops.length} located ${stops.length === 1 ? 'stop' : 'stops'} · ${fmtKm(route.totalKm)} km total` + : 'Add a location to an entry to see it here.'), + ), + ); + + if (!stops.length) { + section.appendChild( + el( + 'div', + { class: 'map-empty' }, + el('div', { class: 'empty-icon' }, '🗺️'), + el('p', {}, 'No located entries yet.'), + el('p', { class: 'muted' }, 'Open a day, add an entry, and give it a location to plot it on the map.'), + ), + ); + return section; + } + + const mapDiv = el('div', { class: 'leaflet-map', id: `map-${tctx.tripId}` }); + section.appendChild(mapDiv); + section.appendChild(legList(route, stops)); + + // Leaflet needs the container attached with a real size, so init on the + // next tick after this section is mounted into the page. + setTimeout(() => initMap(mapDiv, route, stops), 0); + + return section; +} + +function initMap(mapDiv, route, stops) { + const L = window.L; + if (!L) { + mapDiv.appendChild(el('p', { class: 'muted' }, 'Map library failed to load.')); + return; + } + + const map = L.map(mapDiv, { scrollWheelZoom: true }); + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + maxZoom: 19, + }).addTo(map); + + // Geometry is driven by stop ORDER, not entryId: a multi-leg flight expands + // into several airport stops that all share the same entryId, so keying by + // entryId would collapse them. + const points = stops.map((s) => [s.lat, s.lng]); + stops.forEach((stop, i) => { + const info = typeInfo(stop.type); + L.marker(points[i], { icon: numberedIcon(L, i + 1, info.color) }) + .addTo(map) + .bindPopup(popupHtml(stop, info)); + }); + + // One polyline per measured leg, styled by mode (air = dashed blue, ground = + // solid teal), each with a km label at its midpoint. Legs are matched to + // consecutive non-coincident stop pairs (skip <0.05km, the server's rule). + const legs = route.legs || []; + let li = 0; + for (let i = 0; i < stops.length - 1 && li < legs.length; i++) { + const a = points[i]; + const b = points[i + 1]; + if (haversineKm(a[0], a[1], b[0], b[1]) < 0.05) continue; + const leg = legs[li]; + li += 1; + const air = leg.mode === 'air'; + L.polyline([a, b], { + color: air ? '#2563eb' : '#0f766e', + weight: 3, + opacity: 0.75, + dashArray: air ? '6 6' : null, + }).addTo(map); + const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]; + L.marker(mid, { icon: kmLabel(L, leg.km), interactive: false }).addTo(map); + } + + if (points.length === 1) { + map.setView(points[0], 10); + } else { + map.fitBounds(L.latLngBounds(points).pad(0.2)); + } + map.invalidateSize(); +} + +// Great-circle km — mirrors the server rule for skipping zero-distance legs. +function haversineKm(lat1, lng1, lat2, lng2) { + const R = 6371; + const toRad = (d) => (d * Math.PI) / 180; + const dLat = toRad(lat2 - lat1); + const dLng = toRad(lng2 - lng1); + const a = Math.sin(dLat / 2) ** 2 + + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; + return 2 * R * Math.asin(Math.min(1, Math.sqrt(a))); +} + +function numberedIcon(L, n, color) { + return L.divIcon({ + className: 'map-pin-wrap', + html: `${n}`, + iconSize: [26, 26], + iconAnchor: [13, 26], + popupAnchor: [0, -24], + }); +} + +function kmLabel(L, km) { + return L.divIcon({ + className: 'km-label-wrap', + html: `${fmtKm(km)} km`, + iconSize: [0, 0], + }); +} + +// Built from server-provided fields; escape to keep the popup injection-safe. +function popupHtml(stop, info) { + const isAirport = stop.kind === 'airport' && stop.code; + const heading = isAirport ? `${info.icon} ${esc(stop.code)}` : `${info.icon} ${esc(stop.title)}`; + const sub = isAirport + ? `${esc(info.label)} · ${esc(stop.date)}${stop.title ? ` · ${esc(stop.title)}` : ''}` + : `${esc(info.label)} · ${esc(stop.date)}`; + const locIcon = isAirport ? '✈️' : '📍'; + return ( + `
` + + `${heading}` + + `
${sub}
` + + (stop.location_name ? `
${locIcon} ${esc(stop.location_name)}
` : '') + + `
` + ); +} + +function stopLabel(stop) { + if (stop.kind === 'airport' && stop.code) return stop.code; + return stop.location_name || stop.title || '?'; +} + +function legList(route, stops) { + const legs = route.legs || []; + if (!legs.length) return el('div', { class: 'leg-list-empty muted' }, 'A single stop — no legs to measure yet.'); + + const wrap = el('div', { class: 'leg-list' }, el('h3', {}, 'Legs')); + let li = 0; + let shown = 0; + for (let i = 0; i < stops.length - 1 && li < legs.length; i++) { + const a = stops[i]; + const b = stops[i + 1]; + if (haversineKm(a.lat, a.lng, b.lat, b.lng) < 0.05) continue; + const leg = legs[li++]; + shown += 1; + const air = leg.mode === 'air'; + wrap.appendChild( + el( + 'div', + { class: 'leg-row' }, + el('span', { class: 'leg-index' }, String(shown)), + el('span', { class: 'leg-mode', title: air ? 'Flight' : 'Ground' }, air ? '✈️' : '🚗'), + el('span', { class: 'leg-path' }, stopLabel(a), ' → ', stopLabel(b)), + el('span', { class: 'leg-km' }, `${fmtKm(leg.km)} km`), + ), + ); + } + return wrap; +} + +function fmtKm(n) { + return (Math.round((Number(n) || 0) * 10) / 10).toLocaleString(); +} + +function esc(str) { + return String(str == null ? '' : str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} diff --git a/public/js/views/rental.js b/public/js/views/rental.js new file mode 100644 index 0000000..7bbacd4 --- /dev/null +++ b/public/js/views/rental.js @@ -0,0 +1,131 @@ +// Rental-car details subsection used inside the day editor for rental entries, +// plus a compact one-line renderer for the entry list. Pickup/dropoff use +// plain-text location names only (they do not feed the route), so there is no +// geocode/lat-lng UI here. Kept separate so dayEditor.js stays small. +import { el } from '../dom.js'; +import { formatDate } from '../format.js'; + +// createRentalDetails({ initial, entryDate, onChange }) -> +// { node, read(), load(rental) } +// read() -> { rental: {...} | null, pickupDate } or { error }. +export function createRentalDetails({ initial = null, entryDate, onChange = () => {} } = {}) { + const brandI = textInput('Brand (e.g. Toyota)', 60); + const modelI = textInput('Model (e.g. Yaris Cross)', 60); + const carTypeI = textInput('Type (e.g. SUV)', 40); + const bookingI = textInput('Booking ref', 60); + const includedI = el('input', { class: 'input', type: 'number', min: '0', step: '1', placeholder: 'e.g. 1500' }); + + const pDate = el('input', { class: 'input', type: 'date' }); + const pTime = el('input', { class: 'input', type: 'time' }); + const pLoc = textInput('Location (free text)', 120); + const dDate = el('input', { class: 'input', type: 'date' }); + const dTime = el('input', { class: 'input', type: 'time' }); + const dLoc = textInput('Location (free text)', 120); + + for (const inp of [brandI, modelI, carTypeI, bookingI, includedI, pDate, pTime, pLoc, dDate, dTime, dLoc]) { + inp.addEventListener('input', onChange); + } + + const node = el( + 'div', + { class: 'rental-details' }, + el('div', { class: 'form-row' }, + field('Brand', brandI), field('Model', modelI)), + el('div', { class: 'form-row' }, + field('Car type', carTypeI), field('Booking ref', bookingI), + field('Included km', includedI)), + el('div', { class: 'rental-blocks' }, + rentalBlock('Pickup', pDate, pTime, pLoc), + rentalBlock('Dropoff', dDate, dTime, dLoc)), + ); + + function read() { + const brand = brandI.value.trim(); + const model = modelI.value.trim(); + const car_type = carTypeI.value.trim(); + const booking_ref = bookingI.value.trim(); + const includedRaw = includedI.value.trim(); + const pickupDate = pDate.value || entryDate; + const dropoffDate = dDate.value || pickupDate; + + const anyContent = brand || model || car_type || booking_ref || includedRaw || + pTime.value || pLoc.value.trim() || dTime.value || dLoc.value.trim(); + if (!anyContent) return { rental: null, pickupDate }; + + let included_km = null; + if (includedRaw !== '') { + const n = Number(includedRaw); + if (!Number.isFinite(n) || n < 0) return { error: 'Included km must be a number ≥ 0.' }; + included_km = n; + } + + const rental = {}; + if (brand) rental.brand = brand; + if (model) rental.model = model; + if (car_type) rental.car_type = car_type; + if (booking_ref) rental.booking_ref = booking_ref; + if (included_km != null) rental.included_km = included_km; + rental.pickup = { date: pickupDate }; + if (pTime.value) rental.pickup.time = pTime.value; + if (pLoc.value.trim()) rental.pickup.location_name = pLoc.value.trim(); + rental.dropoff = { date: dropoffDate }; + if (dTime.value) rental.dropoff.time = dTime.value; + if (dLoc.value.trim()) rental.dropoff.location_name = dLoc.value.trim(); + return { rental, pickupDate }; + } + + function load(rental) { + const r = rental || {}; + brandI.value = r.brand || ''; + modelI.value = r.model || ''; + carTypeI.value = r.car_type || ''; + bookingI.value = r.booking_ref || ''; + includedI.value = r.included_km != null ? String(r.included_km) : ''; + const p = r.pickup || {}; + pDate.value = p.date || entryDate || ''; + pTime.value = p.time || ''; + pLoc.value = p.location_name || ''; + const d = r.dropoff || {}; + dDate.value = d.date || p.date || entryDate || ''; + dTime.value = d.time || ''; + dLoc.value = d.location_name || ''; + onChange(); + } + + // Initial values (defaults: both dates to the entry's day). + load(initial || { pickup: { date: entryDate }, dropoff: { date: entryDate } }); + + return { node, read, load }; +} + +// "🚙 Toyota Yaris Cross · pickup 09:00 CNX Airport → dropoff 7 Aug 18:00 · 1,500 km included · RC-889231" +export function renderRentalLine(rental) { + const car = [rental.brand, rental.model].filter(Boolean).join(' ') || 'Rental car'; + const p = rental.pickup || {}; + const d = rental.dropoff || {}; + const pickup = ['pickup', p.time, p.location_name].filter(Boolean).join(' '); + const dropoff = ['dropoff', d.date ? formatDate(d.date) : null, d.time, d.location_name].filter(Boolean).join(' '); + const bits = [`🚙 ${car}`, `${pickup} → ${dropoff}`]; + if (rental.included_km != null) bits.push(`${Number(rental.included_km).toLocaleString()} km included`); + if (rental.booking_ref) bits.push(rental.booking_ref); + return el('div', { class: 'entry-rental muted' }, bits.join(' · ')); +} + +function textInput(placeholder, maxlength) { + return el('input', { class: 'input', type: 'text', maxlength: String(maxlength), placeholder }); +} + +function field(label, input) { + return el('label', { class: 'field field-grow' }, el('span', { class: 'field-label' }, label), input); +} + +function rentalBlock(title, dateInput, timeInput, locInput) { + return el( + 'div', + { class: 'rental-block' }, + el('h4', { class: 'rental-block-title' }, title), + el('div', { class: 'form-row' }, + field('Date', dateInput), field('Time', timeInput)), + field('Location', locInput), + ); +} diff --git a/public/js/views/segments.js b/public/js/views/segments.js new file mode 100644 index 0000000..d63e856 --- /dev/null +++ b/public/js/views/segments.js @@ -0,0 +1,332 @@ +// Flight-route builder used inside the day editor for flight entries. +// Provides a quick "CNX-BKK-DXB-FRA" expander plus editable per-leg rows with +// airport-code autocomplete backed by GET /api/airports. Kept as its own +// module so dayEditor.js stays small. +import { el, clear, toast } from '../dom.js'; +import { api } from '../api.js'; +import { formatTimeRange } from '../format.js'; + +const CODE_RE = /^[A-Z0-9]{2,4}$/; + +// Per-segment display lines for an entry row: "TG103 CNX→BKK 10:30–11:45". +export function renderSegmentLines(segments) { + return el( + 'div', + { class: 'entry-segments' }, + ...segments.map((s) => { + const legTime = formatTimeRange(s.dep_time, s.arr_time); + const codes = `${s.from?.code || '?'}→${s.to?.code || '?'}`; + return el( + 'div', + { class: 'entry-seg muted' }, + s.flight_no ? el('span', { class: 'seg-flight' }, s.flight_no) : null, + el('span', {}, codes), + legTime ? el('span', {}, legTime) : null, + ); + }), + ); +} + +// createFlightRoute({ initialSegments, onChange }) -> +// { node, read(), hasSegments() } +// read() returns { segments: [...] | null } or { error: 'message' }. +export function createFlightRoute({ initialSegments = null, onChange = () => {} } = {}) { + const rows = []; // { state:{flight_no,dep_time,arr_time,from,to}, node, refs } + const listEl = el('div', { class: 'seg-list' }); + + const quickInput = el('input', { + class: 'input seg-quick-input', + type: 'text', + placeholder: 'CNX-BKK-DXB-FRA', + autocomplete: 'off', + spellcheck: 'false', + 'aria-label': 'Airport code chain', + }); + const buildBtn = el('button', { class: 'btn btn-sm', type: 'button' }, 'Build route'); + buildBtn.addEventListener('click', () => buildFromChain(quickInput.value)); + + const addBtn = el('button', { class: 'btn btn-sm btn-ghost', type: 'button' }, '+ Add leg'); + addBtn.addEventListener('click', () => { + const prev = rows[rows.length - 1]; + addRow(prev ? { from: { ...prev.state.to } } : {}); + renumber(); + onChange(); + }); + + const node = el( + 'div', + { class: 'flight-route' }, + el('div', { class: 'seg-quick' }, + el('label', { class: 'field field-grow' }, + el('span', { class: 'field-label' }, 'Quick route (airport codes)'), + quickInput), + buildBtn), + listEl, + addBtn, + ); + + function emptyAirport() { + return { code: '', name: '', lat: null, lng: null }; + } + + function addRow(seg = {}) { + const state = { + flight_no: seg.flight_no || '', + dep_time: seg.dep_time || '', + arr_time: seg.arr_time || '', + from: { ...emptyAirport(), ...(seg.from || {}) }, + to: { ...emptyAirport(), ...(seg.to || {}) }, + }; + const { rowNode, refs } = buildRowDom(state); + const row = { state, node: rowNode, refs }; + rows.push(row); + listEl.appendChild(rowNode); + refreshRowLabels(row); + return row; + } + + function removeRow(row) { + const i = rows.indexOf(row); + if (i === -1) return; + rows.splice(i, 1); + row.node.remove(); + renumber(); + onChange(); + } + + function renumber() { + rows.forEach((r, i) => { r.refs.num.textContent = String(i + 1); }); + } + + function buildRowDom(state) { + const num = el('span', { class: 'seg-num' }, '1'); + const flightNo = el('input', { class: 'input input-sm seg-flightno', type: 'text', maxlength: '12', placeholder: 'Flight no.', value: state.flight_no }); + flightNo.addEventListener('input', () => { state.flight_no = flightNo.value; }); + + const from = buildAirportField(state.from, 'From'); + const to = buildAirportField(state.to, 'To'); + + const dep = el('input', { class: 'input input-sm', type: 'time', 'aria-label': 'Departure time' }); + dep.value = state.dep_time || ''; + dep.addEventListener('input', () => { state.dep_time = dep.value; }); + const arr = el('input', { class: 'input input-sm', type: 'time', 'aria-label': 'Arrival time' }); + arr.value = state.arr_time || ''; + arr.addEventListener('input', () => { state.arr_time = arr.value; }); + + const removeBtn = el('button', { class: 'icon-btn danger', type: 'button', title: 'Remove leg' }, '×'); + + const rowNode = el( + 'div', + { class: 'seg-row' }, + el('div', { class: 'seg-row-head' }, num, flightNo, removeBtn), + el('div', { class: 'seg-airports' }, from.field, el('span', { class: 'seg-arrow' }, '→'), to.field), + el('div', { class: 'seg-times' }, + el('label', { class: 'seg-time' }, el('span', {}, 'Dep'), dep), + el('label', { class: 'seg-time' }, el('span', {}, 'Arr'), arr)), + ); + + const refs = { num, from, to }; + removeBtn.addEventListener('click', () => { + const row = rows.find((r) => r.node === rowNode); + if (row) removeRow(row); + }); + return { rowNode, refs }; + } + + // One airport code input + autocomplete dropdown + resolved-name line. + function buildAirportField(airportState, label) { + const input = el('input', { + class: 'input input-sm seg-code', + type: 'text', + maxlength: '4', + placeholder: label === 'From' ? 'From (e.g. CNX)' : 'To (e.g. BKK)', + autocomplete: 'off', + spellcheck: 'false', + 'aria-label': `${label} airport code`, + }); + input.value = airportState.code || ''; + const results = el('div', { class: 'ap-results' }); + const nameLine = el('span', { class: 'ap-name' }); + const field = el('div', { class: 'seg-ap' }, input, results, nameLine); + + let timer = null; + let seq = 0; + input.addEventListener('input', () => { + const val = input.value.toUpperCase(); + // Manual edit clears any previously resolved coordinates. + airportState.code = val; + airportState.name = ''; + airportState.lat = null; + airportState.lng = null; + setNameLine(nameLine, airportState); + clearTimeout(timer); + const q = input.value.trim(); + if (q.length < 2) { clear(results); return; } + timer = setTimeout(async () => { + const mine = ++seq; + try { + const data = await api.airports(q); + if (mine !== seq) return; + showAirportResults(results, data.results || [], (ap) => { + applyAirport(airportState, ap); + input.value = ap.code; + clear(results); + setNameLine(nameLine, airportState); + onChange(); + }); + } catch (err) { + if (mine !== seq) return; + clear(results); + } + }, 300); + }); + + return { field, input, nameLine, state: airportState }; + } + + function refreshRowLabels(row) { + setNameLine(row.refs.from.nameLine, row.state.from); + setNameLine(row.refs.to.nameLine, row.state.to); + } + + // Quick-build: split codes, make N-1 legs, resolve each code to coords. + async function buildFromChain(raw) { + const codes = String(raw || '') + .split(/[\s,>\-]+/) + .map((s) => s.trim().toUpperCase()) + .filter(Boolean); + if (codes.length < 2) { + toast('Enter at least two airport codes, e.g. CNX-BKK'); + return; + } + if (codes.length - 1 > 8) { + toast('A flight can have at most 8 legs.'); + return; + } + // Replace existing rows. + rows.slice().forEach((r) => { r.node.remove(); }); + rows.length = 0; + for (let i = 0; i < codes.length - 1; i++) { + addRow({ from: { code: codes[i] }, to: { code: codes[i + 1] } }); + } + renumber(); + onChange(); + + // Resolve unique codes once, then apply to every matching field. + const unique = [...new Set(codes)]; + const resolved = {}; + await Promise.all(unique.map(async (code) => { + resolved[code] = await resolveCode(code); + })); + for (const row of rows) { + applyResolved(row.state.from, resolved[row.state.from.code], row.refs.from); + applyResolved(row.state.to, resolved[row.state.to.code], row.refs.to); + } + onChange(); + } + + function applyResolved(airportState, ap, ref) { + if (ap) { + applyAirport(airportState, ap); + ref.input.value = ap.code; + ref.field.classList.remove('ap-unresolved'); + } else { + // Keep the typed code but flag that it has no coordinates. + ref.field.classList.add('ap-unresolved'); + } + setNameLine(ref.nameLine, airportState); + } + + async function resolveCode(code) { + try { + const data = await api.airports(code); + const list = data.results || []; + return list.find((r) => (r.code || '').toUpperCase() === code) || null; + } catch { + return null; + } + } + + function read() { + if (rows.length === 0) return { segments: null }; + if (rows.length > 8) return { error: 'A flight can have at most 8 legs.' }; + const segments = []; + for (const r of rows) { + const fromCode = (r.state.from.code || '').trim().toUpperCase(); + const toCode = (r.state.to.code || '').trim().toUpperCase(); + if (!CODE_RE.test(fromCode) || !CODE_RE.test(toCode)) { + return { error: 'Every leg needs a valid from and to airport code (2–4 characters).' }; + } + const seg = { from: airportOut(r.state.from, fromCode), to: airportOut(r.state.to, toCode) }; + if (r.state.flight_no.trim()) seg.flight_no = r.state.flight_no.trim(); + if (r.state.dep_time) seg.dep_time = r.state.dep_time; + if (r.state.arr_time) seg.arr_time = r.state.arr_time; + segments.push(seg); + } + return { segments }; + } + + // Replace all rows from a segments array (used when editing an entry). + function load(segments) { + rows.slice().forEach((r) => { r.node.remove(); }); + rows.length = 0; + if (Array.isArray(segments)) { + for (const seg of segments) addRow(seg); + } + renumber(); + onChange(); + } + + // Prefill from an existing entry's segments (edit). + if (Array.isArray(initialSegments)) { + for (const seg of initialSegments) addRow(seg); + renumber(); + } + + return { node, read, load, hasSegments: () => rows.length > 0 }; +} + +function applyAirport(airportState, ap) { + airportState.code = (ap.code || '').toUpperCase(); + airportState.name = ap.name || ''; + airportState.lat = ap.lat != null ? ap.lat : null; + airportState.lng = ap.lng != null ? ap.lng : null; +} + +function airportOut(airportState, code) { + const out = { code }; + if (airportState.name) out.name = airportState.name; + if (airportState.lat != null && airportState.lng != null) { + out.lat = airportState.lat; + out.lng = airportState.lng; + } + return out; +} + +function setNameLine(nameLine, airportState) { + if (airportState.name) { + nameLine.textContent = airportState.name; + nameLine.classList.remove('muted'); + } else if (airportState.code) { + nameLine.textContent = 'no coordinates — pick from the list'; + nameLine.classList.add('muted'); + } else { + nameLine.textContent = ''; + } +} + +function showAirportResults(box, results, onPick) { + clear(box); + if (!results.length) { + box.appendChild(el('div', { class: 'ap-empty muted' }, 'No airports found')); + return; + } + for (const r of results) { + const meta = [r.city, r.country].filter(Boolean).join(', '); + box.appendChild( + el('button', { class: 'ap-result', type: 'button', onClick: () => onPick(r) }, + el('span', { class: 'ap-code' }, r.code), + el('span', { class: 'ap-desc' }, `${r.name}${meta ? ` — ${meta}` : ''}`)), + ); + } +} diff --git a/public/js/views/summary.js b/public/js/views/summary.js new file mode 100644 index 0000000..8c568b3 --- /dev/null +++ b/public/js/views/summary.js @@ -0,0 +1,111 @@ +// Summary panel built from the /route response's `summary` block: +// days, nights, flights, hotels, travel legs, activities, total km, and the +// list of locations in visit order. +import { el } from '../dom.js'; + +export function renderSummary(tctx) { + const route = tctx.route || {}; + const s = route.summary || { + days: 0, nights: 0, flights: 0, flightSegments: 0, hotels: 0, travelLegs: 0, activities: 0, locations: [], + }; + const totalKm = route.totalKm || 0; + // Show the leg count under the Flights tile only when a flight has segments. + const flightSub = s.flightSegments > 0 + ? `${s.flightSegments} ${s.flightSegments === 1 ? 'leg' : 'legs'}` + : ''; + + const section = el('section', { class: 'card summary-section' }); + section.appendChild( + el( + 'div', + { class: 'section-head' }, + el('h2', {}, 'Summary'), + el('p', { class: 'muted' }, 'Trip at a glance.'), + ), + ); + + const tiles = el( + 'div', + { class: 'stat-grid' }, + tile('🗓️', s.days, s.days === 1 ? 'Day' : 'Days'), + tile('🌙', s.nights, s.nights === 1 ? 'Night' : 'Nights'), + tile('✈️', s.flights, 'Flights', flightSub), + tile('🏨', s.hotels, 'Hotels'), + tile('🚗', s.travelLegs, 'Travel legs'), + tile('📍', s.activities, 'Activities'), + s.rentals > 0 ? tile('🚙', s.rentals, s.rentals === 1 ? 'Rental' : 'Rentals') : null, + ); + section.appendChild(tiles); + + const kmDriven = s.kmDriven || 0; + const kmAir = s.kmAir || 0; + const includedKm = s.includedKm != null ? s.includedKm : null; + const kmBlock = el( + 'div', + { class: 'km-block' }, + el( + 'div', + { class: 'total-km' }, + el('span', { class: 'total-km-value' }, fmtKm(totalKm)), + el('span', { class: 'total-km-label' }, 'km total (great-circle)'), + ), + ); + const breakdown = el('div', { class: 'km-breakdown' }); + if (kmDriven > 0) { + // Rental allowance (includedKm) is paired with the rough driven figure; + // flag red when the rough estimate already exceeds the included allowance. + const over = includedKm != null && kmDriven > includedKm; + const drivenText = includedKm != null + ? `≈ ${fmtKm(kmDriven)} km / ${fmtKm(includedKm)} incl.` + : `≈ ${fmtKm(kmDriven)} km`; + breakdown.appendChild(kmRow('🚗', drivenText, 'driven', + over + ? 'Rough great-circle estimate already exceeds the included allowance — real road km will be higher' + : 'Rough great-circle estimate — not routed driving distance', + over ? 'km-over' : '')); + } + if (kmAir > 0) { + breakdown.appendChild(kmRow('✈️', `${fmtKm(kmAir)} km`, 'flown', + 'Great-circle distance between airports')); + } + if (breakdown.childElementCount) kmBlock.appendChild(breakdown); + section.appendChild(kmBlock); + + const locations = s.locations || []; + const locBlock = el('div', { class: 'loc-block' }, el('h3', {}, 'Locations in order')); + if (!locations.length) { + locBlock.appendChild(el('p', { class: 'muted' }, 'No located stops yet.')); + } else { + const ol = el('ol', { class: 'loc-order' }); + for (const name of locations) ol.appendChild(el('li', {}, name)); + locBlock.appendChild(ol); + } + section.appendChild(locBlock); + + return section; +} + +function kmRow(icon, valueText, label, title, cls = '') { + return el( + 'div', + { class: `km-row${cls ? ` ${cls}` : ''}`, title }, + el('span', { class: 'km-row-icon' }, icon), + el('span', { class: 'km-row-value' }, valueText), + el('span', { class: 'km-row-label muted' }, label), + ); +} + +function tile(icon, value, label, sub) { + return el( + 'div', + { class: 'stat-tile' }, + el('span', { class: 'stat-icon' }, icon), + el('span', { class: 'stat-value' }, String(value ?? 0)), + el('span', { class: 'stat-label' }, label), + sub ? el('span', { class: 'stat-sub' }, sub) : null, + ); +} + +function fmtKm(n) { + return (Math.round((Number(n) || 0) * 10) / 10).toLocaleString(); +} diff --git a/public/js/views/tripDetail.js b/public/js/views/tripDetail.js new file mode 100644 index 0000000..6e7a9bb --- /dev/null +++ b/public/js/views/tripDetail.js @@ -0,0 +1,264 @@ +// Orchestrates the trip page: header + calendar, then map + summary. +// Owns the shared trip context passed to the calendar, map, summary and +// day-editor sub-views: { state, navigate, tripId, trip, route, refreshTrip, +// openDay }. After any mutation, refreshTrip() re-fetches the trip and its +// derived /route data so all three panels stay in sync. +import { api } from '../api.js'; +import { el, clear, mount, loading, errorBox, toast } from '../dom.js'; +import { formatRange, daysBetweenInclusive, pluralize, groupCode } from '../format.js'; +import { renderCalendar } from './calendar.js'; +import { renderMap } from './map.js'; +import { renderSummary } from './summary.js'; +import { renderCosts } from './costs.js'; +import { renderCountdown } from './flipclock.js'; +import { openDayEditor } from './dayEditor.js'; + +export function renderTripDetail(container, ctx, id) { + const tctx = { + ...ctx, + tripId: id, + trip: null, + route: null, + costs: null, + _onModalRefresh: null, + }; + + mount(container, loading('Loading trip…')); + init(); + + async function load() { + const [trip, route, costs] = await Promise.all([ + api.trips.get(id), + api.trips.route(id), + api.trips.costs(id), + ]); + tctx.trip = trip; + tctx.route = route; + tctx.costs = costs; + } + + tctx.refreshTrip = async () => { + try { + await load(); + draw(); + if (typeof tctx._onModalRefresh === 'function') tctx._onModalRefresh(); + } catch (err) { + toast(err.message); + } + }; + + tctx.openDay = (date) => openDayEditor(tctx, date); + + async function init() { + try { + await load(); + draw(); + } catch (err) { + clear(container); + if (err.status === 404) { + toast('Trip not found (or you are not a member).'); + ctx.navigate('#/trips'); + return; + } + mount(container, errorBox(err.message, init)); + } + } + + function draw() { + const page = el('div', { class: 'page' }); + page.appendChild(renderHeader()); + page.appendChild(renderCountdown(tctx)); + page.appendChild(renderCalendar(tctx)); + page.appendChild( + el( + 'div', + { class: 'detail-grid' }, + renderMap(tctx), + el('div', { class: 'detail-side' }, renderSummary(tctx), renderCosts(tctx)), + ), + ); + mount(container, page); + } + + function renderHeader() { + const { trip, members } = tctx.trip; + const isOwner = trip.owner_id === tctx.state.user.id; + const dayCount = daysBetweenInclusive(trip.start_date, trip.end_date); + + const header = el('div', { class: 'trip-header card' }); + + const titleRow = el( + 'div', + { class: 'trip-header-top' }, + el( + 'div', + {}, + el( + 'a', + { class: 'back-link', href: '#/trips' }, + '← All trips', + ), + el('h1', { class: 'trip-title' }, trip.name), + el( + 'p', + { class: 'trip-subtitle muted' }, + '📅 ', formatRange(trip.start_date, trip.end_date), + ' · ', pluralize(dayCount, 'day', 'days'), + ' · ', el('span', { class: 'currency-tag' }, trip.currency || 'USD'), + ), + ), + el( + 'div', + { class: 'trip-header-actions' }, + el('button', { class: 'btn btn-ghost', onClick: () => toggleEdit(header) }, '✎ Edit'), + isOwner + ? el('button', { class: 'btn btn-danger-ghost', onClick: onDelete }, 'Delete') + : null, + ), + ); + + const membersRow = el( + 'div', + { class: 'members-row' }, + el('span', { class: 'members-label' }, 'Members:'), + ...members.map((m) => + el( + 'span', + { class: `member-chip${m.role === 'owner' ? ' member-owner' : ''}` }, + el('span', { class: 'member-avatar' }, (m.display_name || '?').charAt(0).toUpperCase()), + m.display_name, + isOwner && m.role !== 'owner' + ? el('button', { + class: 'member-remove', + title: `Remove ${m.display_name}`, + onClick: () => onRemoveMember(m), + }, '×') + : null, + ), + ), + ); + + header.appendChild(titleRow); + header.appendChild(joinCodeRow(trip, isOwner)); + header.appendChild(membersRow); + return header; + } + + // Share code for inviting others — display only, not a credential. + function joinCodeRow(trip, isOwner) { + const grouped = groupCode(trip.join_code, 4); + const copyBtn = el('button', { class: 'btn btn-sm', type: 'button', title: 'Copy join code' }, '📋 Copy'); + copyBtn.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(grouped); + toast('Join code copied', 'success'); + } catch { + toast('Copy failed — select and copy it manually'); + } + }); + + async function onRegenerate() { + if (!window.confirm('Regenerate the join code? The old code will stop working immediately.')) return; + try { + await api.trips.regenerateJoinCode(tctx.tripId); + toast('Join code regenerated', 'success'); + await tctx.refreshTrip(); + } catch (err) { + toast(err.message); + } + } + + return el( + 'div', + { class: 'joincode-row' }, + el('span', { class: 'members-label' }, 'Join code:'), + el('span', { class: 'joincode-chip' }, grouped), + copyBtn, + isOwner + ? el('button', { class: 'btn btn-sm btn-ghost', type: 'button', onClick: onRegenerate }, '↻ Regenerate') + : null, + ); + } + + function toggleEdit(header) { + const existing = header.querySelector('.trip-edit'); + if (existing) { + existing.remove(); + return; + } + const { trip } = tctx.trip; + const nameInput = el('input', { class: 'input', type: 'text', maxlength: '120', value: trip.name }); + const startInput = el('input', { class: 'input', type: 'date', value: trip.start_date }); + const endInput = el('input', { class: 'input', type: 'date', value: trip.end_date }); + const currencyInput = el('input', { class: 'input input-currency', type: 'text', maxlength: '3', value: trip.currency || 'USD', 'aria-label': 'Currency code' }); + const errorEl = el('p', { class: 'form-error' }); + const saveBtn = el('button', { class: 'btn btn-primary', type: 'submit' }, 'Save changes'); + + async function onSubmit(e) { + e.preventDefault(); + errorEl.textContent = ''; + const name = nameInput.value.trim(); + const start_date = startInput.value; + const end_date = endInput.value; + const currency = currencyInput.value.trim().toUpperCase() || 'USD'; + if (!name) return (errorEl.textContent = 'Name cannot be empty.'); + if (end_date < start_date) return (errorEl.textContent = 'End date must be on or after the start date.'); + if (!/^[A-Z]{3}$/.test(currency)) return (errorEl.textContent = 'Currency must be a 3-letter code, e.g. USD.'); + saveBtn.disabled = true; + saveBtn.textContent = 'Saving…'; + try { + await api.trips.update(tctx.tripId, { name, start_date, end_date, currency }); + toast('Trip updated', 'success'); + await tctx.refreshTrip(); + } catch (err) { + errorEl.textContent = err.message; + saveBtn.disabled = false; + saveBtn.textContent = 'Save changes'; + } + } + + const editBox = el( + 'form', + { class: 'trip-edit', onSubmit }, + el( + 'div', + { class: 'form-row' }, + el('label', { class: 'field field-grow' }, el('span', { class: 'field-label' }, 'Name'), nameInput), + ), + el( + 'div', + { class: 'form-row' }, + el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'Start date'), startInput), + el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'End date'), endInput), + el('label', { class: 'field field-currency' }, el('span', { class: 'field-label' }, 'Currency'), currencyInput), + ), + el('p', { class: 'hint muted' }, 'Changing the range regenerates the calendar; entries outside the new range are kept and flagged.'), + errorEl, + el('div', { class: 'form-actions' }, saveBtn), + ); + header.appendChild(editBox); + } + + async function onRemoveMember(member) { + if (!window.confirm(`Remove ${member.display_name} from this trip?`)) return; + try { + await api.trips.removeMember(tctx.tripId, member.id); + toast(`${member.display_name} removed`, 'success'); + await tctx.refreshTrip(); + } catch (err) { + toast(err.message); + } + } + + async function onDelete() { + const { trip } = tctx.trip; + if (!window.confirm(`Delete "${trip.name}"? This removes all its entries and cannot be undone.`)) return; + try { + await api.trips.remove(tctx.tripId); + toast('Trip deleted', 'success'); + ctx.navigate('#/trips'); + } catch (err) { + toast(err.message); + } + } +} diff --git a/public/js/views/trips.js b/public/js/views/trips.js new file mode 100644 index 0000000..84033a3 --- /dev/null +++ b/public/js/views/trips.js @@ -0,0 +1,221 @@ +// Trip list dashboard: cards, a "new trip" form, and a "join a trip" form. +import { api } from '../api.js'; +import { el, mount, clear, loading, errorBox, emptyState, toast } from '../dom.js'; +import { formatRange, ymd, parseYMD, pluralize, normalizeCode } from '../format.js'; + +export function renderTrips(container, ctx) { + mount(container, loading('Loading your trips…')); + load(); + + async function load() { + try { + const data = await api.trips.list(); + draw(data.trips || []); + } catch (err) { + mount(container, errorBox(err.message, load)); + } + } + + function draw(trips) { + const page = el('div', { class: 'page' }); + + // The form slot is created up front so every button below can capture a + // fully-initialized reference (no reliance on declaration ordering). + const formSlot = el('div', { class: 'form-slot' }); + + const newTripBtn = el('button', { class: 'btn btn-primary', onClick: () => openNewTrip(formSlot) }, '+ New trip'); + const joinBtn = el('button', { class: 'btn', onClick: () => openJoin(formSlot) }, 'Join a trip'); + + page.appendChild( + el( + 'div', + { class: 'page-head' }, + el( + 'div', + {}, + el('h1', {}, 'Your Trips'), + el('p', { class: 'muted' }, trips.length + ? pluralize(trips.length, 'trip', 'trips') + : 'Start planning your next adventure.'), + ), + el('div', { class: 'page-head-actions' }, joinBtn, newTripBtn), + ), + ); + + page.appendChild(formSlot); + + if (!trips.length) { + page.appendChild( + emptyState( + 'No trips yet', + 'Create your first trip, or join one with a code a friend shared.', + el('div', { class: 'empty-actions' }, + el('button', { class: 'btn btn-primary', onClick: () => openNewTrip(formSlot) }, '+ New trip'), + el('button', { class: 'btn', onClick: () => openJoin(formSlot) }, 'Join a trip'), + ), + ), + ); + } else { + const grid = el('div', { class: 'trip-grid' }); + for (const trip of trips) grid.appendChild(tripCard(trip)); + page.appendChild(grid); + } + + mount(container, page); + } + + function tripCard(trip) { + const today = ymd(new Date()); + const future = trip.start_date > today; + const daysUntil = future + ? Math.round((parseYMD(trip.start_date) - parseYMD(today)) / 86400000) + : 0; + return el( + 'a', + { class: 'trip-card card', href: `#/trip/${trip.id}` }, + el( + 'div', + { class: 'trip-card-top' }, + el('h3', { class: 'trip-card-name' }, trip.name), + el('span', { class: `role-badge role-${trip.role}` }, trip.role), + ), + el( + 'div', + { class: 'trip-card-dates' }, + '📅 ', formatRange(trip.start_date, trip.end_date), + future ? el('span', { class: 'trip-card-countdown' }, `in ${daysUntil} ${daysUntil === 1 ? 'day' : 'days'}`) : null, + ), + el( + 'div', + { class: 'trip-card-meta' }, + el('span', {}, '👥 ', pluralize(trip.member_count ?? 1, 'member', 'members')), + el('span', {}, '📝 ', pluralize(trip.entry_count ?? 0, 'entry', 'entries')), + el('span', { class: 'trip-card-currency' }, trip.currency || 'USD'), + ), + ); + } + + // Render `build()` into the slot, or close it if the same panel is open. + function togglePanel(slot, kind, build) { + if (slot.dataset.open === kind) { + clear(slot); + slot.dataset.open = ''; + return; + } + mount(slot, build()); + slot.dataset.open = kind; + } + + function openNewTrip(slot) { + togglePanel(slot, 'new', () => newTripForm(slot)); + } + + function openJoin(slot) { + togglePanel(slot, 'join', () => joinForm(slot)); + } + + function closeSlot(slot) { + clear(slot); + slot.dataset.open = ''; + } + + function newTripForm(slot) { + const today = ymd(new Date()); + const nameInput = el('input', { class: 'input', type: 'text', maxlength: '120', placeholder: 'e.g. Northern Thailand' }); + const startInput = el('input', { class: 'input', type: 'date', value: today }); + const endInput = el('input', { class: 'input', type: 'date', value: today }); + const currencyInput = el('input', { class: 'input input-currency', type: 'text', maxlength: '3', value: 'USD', placeholder: 'USD', 'aria-label': 'Currency code' }); + const errorEl = el('p', { class: 'form-error' }); + const submitBtn = el('button', { class: 'btn btn-primary', type: 'submit' }, 'Create trip'); + + async function onSubmit(e) { + e.preventDefault(); + errorEl.textContent = ''; + const name = nameInput.value.trim(); + const start_date = startInput.value; + const end_date = endInput.value; + const currency = currencyInput.value.trim().toUpperCase() || 'USD'; + if (!name) return (errorEl.textContent = 'Please give the trip a name.'); + if (!start_date || !end_date) return (errorEl.textContent = 'Pick a start and end date.'); + if (end_date < start_date) return (errorEl.textContent = 'End date must be on or after the start date.'); + if (!/^[A-Z]{3}$/.test(currency)) return (errorEl.textContent = 'Currency must be a 3-letter code, e.g. USD.'); + + submitBtn.disabled = true; + submitBtn.textContent = 'Creating…'; + try { + const data = await api.trips.create({ name, start_date, end_date, currency }); + toast('Trip created', 'success'); + ctx.navigate(`#/trip/${data.trip.id}`); + } catch (err) { + errorEl.textContent = err.message; + submitBtn.disabled = false; + submitBtn.textContent = 'Create trip'; + } + } + + const form = el( + 'form', + { class: 'card new-trip-form', onSubmit }, + el('h3', {}, 'New trip'), + el('div', { class: 'form-row' }, + el('label', { class: 'field field-grow' }, el('span', { class: 'field-label' }, 'Name'), nameInput)), + el('div', { class: 'form-row' }, + el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'Start date'), startInput), + el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'End date'), endInput), + el('label', { class: 'field field-currency' }, el('span', { class: 'field-label' }, 'Currency'), currencyInput)), + errorEl, + el('div', { class: 'form-actions' }, + el('button', { class: 'btn btn-ghost', type: 'button', onClick: () => closeSlot(slot) }, 'Cancel'), + submitBtn), + ); + setTimeout(() => nameInput.focus(), 0); + return form; + } + + function joinForm(slot) { + const codeInput = el('input', { + class: 'input token-input', + type: 'text', + autocomplete: 'off', + autocapitalize: 'characters', + spellcheck: 'false', + placeholder: 'XXXX-XXXX', + 'aria-label': 'Join code', + }); + const errorEl = el('p', { class: 'form-error' }); + const submitBtn = el('button', { class: 'btn btn-primary', type: 'submit' }, 'Join trip'); + + async function onSubmit(e) { + e.preventDefault(); + errorEl.textContent = ''; + const code = normalizeCode(codeInput.value); + if (code.length < 4) return (errorEl.textContent = 'Enter the join code your friend shared.'); + submitBtn.disabled = true; + submitBtn.textContent = 'Joining…'; + try { + const data = await api.trips.join(code); + toast('Joined trip', 'success'); + ctx.navigate(`#/trip/${data.trip.id}`); + } catch (err) { + errorEl.textContent = err.status === 404 ? 'No trip found for that code.' : err.message; + submitBtn.disabled = false; + submitBtn.textContent = 'Join trip'; + } + } + + const form = el( + 'form', + { class: 'card join-form', onSubmit }, + el('h3', {}, 'Join a trip'), + el('p', { class: 'muted' }, 'Paste the join code from a trip you were invited to.'), + el('div', { class: 'form-row' }, + el('label', { class: 'field field-grow' }, el('span', { class: 'field-label' }, 'Join code'), codeInput)), + errorEl, + el('div', { class: 'form-actions' }, + el('button', { class: 'btn btn-ghost', type: 'button', onClick: () => closeSlot(slot) }, 'Cancel'), + submitBtn), + ); + setTimeout(() => codeInput.focus(), 0); + return form; + } +} diff --git a/scripts/generate-airports.mjs b/scripts/generate-airports.mjs new file mode 100644 index 0000000..4f5939a --- /dev/null +++ b/scripts/generate-airports.mjs @@ -0,0 +1,83 @@ +// Regenerate src/server/data/airports.json from the public-domain OurAirports +// dataset. Usage: node scripts/generate-airports.mjs +// +// Keeps airports that have an IATA code AND scheduled service, projecting each +// to { code, name, city, country, lat, lng }. + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const SOURCE_URL = 'https://davidmegginson.github.io/ourairports-data/airports.csv'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const OUT_PATH = path.join(__dirname, '..', 'src', 'server', 'data', 'airports.json'); + +// Minimal RFC-4180 CSV parser (handles quotes, escaped quotes, embedded commas/newlines). +function parseCsv(str) { + const rows = []; + let row = []; + let field = ''; + let inQuotes = false; + for (let i = 0; i < str.length; i++) { + const c = str[i]; + if (inQuotes) { + if (c === '"') { + if (str[i + 1] === '"') { field += '"'; i++; } + else inQuotes = false; + } else field += c; + } else if (c === '"') inQuotes = true; + else if (c === ',') { row.push(field); field = ''; } + else if (c === '\n') { row.push(field); rows.push(row); row = []; field = ''; } + else if (c === '\r') { /* ignore */ } + else field += c; + } + if (field.length || row.length) { row.push(field); rows.push(row); } + return rows; +} + +async function main() { + console.log(`Downloading ${SOURCE_URL} ...`); + const res = await fetch(SOURCE_URL, { headers: { 'User-Agent': 'trip-plan-app/0.1 (self-hosted)' } }); + if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`); + const text = await res.text(); + + const rows = parseCsv(text); + const header = rows[0]; + const col = (name) => header.indexOf(name); + const iIata = col('iata_code'); + const iSched = col('scheduled_service'); + const iName = col('name'); + const iCity = col('municipality'); + const iCountry = col('iso_country'); + const iLat = col('latitude_deg'); + const iLng = col('longitude_deg'); + + const airports = []; + for (let r = 1; r < rows.length; r++) { + const row = rows[r]; + if (!row || row.length < header.length) continue; + const code = (row[iIata] || '').trim(); + if (!code) continue; + if ((row[iSched] || '').trim() !== 'yes') continue; + const lat = Number(row[iLat]); + const lng = Number(row[iLng]); + airports.push({ + code, + name: (row[iName] || '').trim(), + city: (row[iCity] || '').trim(), + country: (row[iCountry] || '').trim(), + lat: Number.isFinite(lat) ? lat : null, + lng: Number.isFinite(lng) ? lng : null, + }); + } + + fs.mkdirSync(path.dirname(OUT_PATH), { recursive: true }); + fs.writeFileSync(OUT_PATH, JSON.stringify(airports)); + const bytes = fs.statSync(OUT_PATH).size; + console.log(`Wrote ${airports.length} airports to ${OUT_PATH} (${bytes} bytes)`); +} + +main().catch((err) => { + console.error(err.message); + process.exit(1); +}); diff --git a/src/server/app.js b/src/server/app.js new file mode 100644 index 0000000..887b441 --- /dev/null +++ b/src/server/app.js @@ -0,0 +1,74 @@ +import express from 'express'; +import cookieSession from 'cookie-session'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { openDb } from './db.js'; +import { requireAuth } from './auth.js'; +import authRoutes from './routes/auth.js'; +import tripsRoutes from './routes/trips.js'; +import entriesRoutes from './routes/entries.js'; +import geocodeRoutes from './routes/geocode.js'; +import airportsRoutes from './routes/airports.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public'); + +// Build the Express app. Accepts either an options object +// { dbPath, sessionSecret } or a bare dbPath string (per API.md). +export function createApp(options = {}) { + const opts = typeof options === 'string' ? { dbPath: options } : options; + const dbPath = opts.dbPath || ':memory:'; + const sessionSecret = opts.sessionSecret || 'dev-secret'; + + const db = openDb(dbPath); + const app = express(); + app.disable('x-powered-by'); + + app.use(express.json()); + app.use( + cookieSession({ + name: 'trip_session', + secret: sessionSecret, + httpOnly: true, + sameSite: 'lax', + maxAge: 30 * 24 * 60 * 60 * 1000, + }) + ); + + // no-cache (not no-store): browsers must revalidate, ETags turn unchanged + // files into 304s. Without this, upgraded deployments keep serving a stale + // cached frontend against the new API. + app.use( + express.static(PUBLIC_DIR, { + etag: true, + lastModified: true, + setHeaders: (res) => res.setHeader('Cache-Control', 'no-cache'), + }) + ); + + app.use('/api/auth', authRoutes(db)); + app.use('/api/trips', requireAuth, tripsRoutes(db)); + app.use('/api', requireAuth, entriesRoutes(db)); // /trips/:id/entries + /entries/:id + app.use('/api/geocode', requireAuth, geocodeRoutes(db)); + app.use('/api/airports', requireAuth, airportsRoutes()); + + // JSON 404 for any unmatched /api route. + app.use('/api', (req, res) => { + res.status(404).json({ error: 'not found' }); + }); + + // JSON error handler (malformed body, unexpected failures). + // eslint-disable-next-line no-unused-vars + app.use((err, req, res, next) => { + if (err && err.type === 'entity.parse.failed') { + return res.status(400).json({ error: 'invalid JSON body' }); + } + console.error(err); + res.status(500).json({ error: 'internal server error' }); + }); + + app.locals.db = db; + return app; +} + +export default createApp; diff --git a/src/server/auth.js b/src/server/auth.js new file mode 100644 index 0000000..28b5a2c --- /dev/null +++ b/src/server/auth.js @@ -0,0 +1,8 @@ +// Session guard: rejects unauthenticated requests with 401 JSON. +// Identity is Mullvad-style (account tokens); token hashing lives in util/token.js. +export function requireAuth(req, res, next) { + if (!req.session || !req.session.userId) { + return res.status(401).json({ error: 'unauthorized' }); + } + next(); +} diff --git a/src/server/data/airports.json b/src/server/data/airports.json new file mode 100644 index 0000000..6986b17 --- /dev/null +++ b/src/server/data/airports.json @@ -0,0 +1 @@ +[{"code":"UTK","name":"Utirik Airport","city":"Utirik Island","country":"MH","lat":11.222219,"lng":169.851429},{"code":"NUP","name":"Nunapitchuk Airport","city":"Nunapitchuk","country":"US","lat":60.905591,"lng":-162.440454},{"code":"PPV","name":"Port Protection Seaplane Base","city":"Port Protection","country":"US","lat":56.3288,"lng":-133.610001},{"code":"BYW","name":"Blakely Island Airport","city":"Blakely Island","country":"US","lat":48.579162,"lng":-122.823844},{"code":"ATT","name":"Atmautluak Airport","city":"Atmautluak","country":"US","lat":60.866699,"lng":-162.272996},{"code":"PDB","name":"Pedro Bay Airport","city":"Pedro Bay","country":"US","lat":59.796925,"lng":-154.129994},{"code":"KOZ","name":"Ouzinkie Airport","city":"Ouzinkie","country":"US","lat":57.942094,"lng":-152.464314},{"code":"TNK","name":"Tununak Airport","city":"Tununak","country":"US","lat":60.569638,"lng":-165.246649},{"code":"WHD","name":"Hyder Seaplane Base","city":"Hyder","country":"US","lat":55.903324,"lng":-130.009975},{"code":"MNT","name":"Minto Al Wright Airport","city":"Minto","country":"US","lat":65.147991,"lng":-149.368658},{"code":"WKK","name":"Aleknagik / New Airport","city":"Aleknagik","country":"US","lat":59.2826004028,"lng":-158.617996216},{"code":"WFB","name":"Ketchikan Harbor Seaplane Base","city":"Ketchikan","country":"US","lat":55.349899,"lng":-131.677002},{"code":"BKF","name":"Lake Brooks Seaplane Base","city":"Katmai National Park","country":"US","lat":58.554798,"lng":-155.776993},{"code":"TSS","name":"East 34th Street Heliport","city":"New York","country":"US","lat":40.742372,"lng":-73.972074},{"code":"NYS","name":"New York Skyports Inc Seaplane Base","city":"New York","country":"US","lat":40.735061,"lng":-73.972814},{"code":"CWS","name":"Center Island Airport","city":"Center Island","country":"US","lat":48.490046,"lng":-122.831612},{"code":"TEK","name":"Tatitlek Airport","city":"Tatitlek","country":"US","lat":60.871449,"lng":-146.690297},{"code":"SSW","name":"Stuart Island Airpark","city":"Friday Harbor","country":"US","lat":48.673379,"lng":-123.175439},{"code":"WDN","name":"Waldron Airstrip","city":"Eastsound","country":"US","lat":48.71141,"lng":-123.018208},{"code":"WWP","name":"Whale Pass Seaplane Float Harbor Facility","city":"Whale Pass","country":"US","lat":56.116299,"lng":-133.121994},{"code":"CHU","name":"Chuathbaluk Airport","city":"Chuathbaluk","country":"US","lat":61.579102,"lng":-159.216003},{"code":"WTL","name":"Tuntutuliak Airport","city":"Tuntutuliak","country":"US","lat":60.351243,"lng":-162.654476},{"code":"TWA","name":"Twin Hills Airport","city":"Twin Hills","country":"US","lat":59.074713,"lng":-160.274992},{"code":"KCQ","name":"Chignik Lake Airport","city":"Chignik Lake","country":"US","lat":56.255001,"lng":-158.774994},{"code":"BBQ","name":"Burton-Nibbs International Airport","city":"Codrington","country":"AG","lat":17.621194,"lng":-61.798347},{"code":"AKS","name":"Gwaunaru'u Airport","city":"Auki","country":"SB","lat":-8.70257,"lng":160.682007},{"code":"BNY","name":"Bellona/Anua Airport","city":"Anua","country":"SB","lat":-11.302222222200001,"lng":159.798333333},{"code":"CHY","name":"Choiseul Bay Airport","city":"Choiseul Bay","country":"SB","lat":-6.711944,"lng":156.396111},{"code":"BAS","name":"Ballalae Airport","city":"Ballalae","country":"SB","lat":-6.990745,"lng":155.886656},{"code":"FRE","name":"Fera/Maringe Airport","city":"Fera Island","country":"SB","lat":-8.1075,"lng":159.576996},{"code":"HIR","name":"Honiara International Airport","city":"Honiara","country":"SB","lat":-9.428,"lng":160.054993},{"code":"IRA","name":"Ngorangora Airport","city":"Kirakira","country":"SB","lat":-10.449700355500001,"lng":161.897994995},{"code":"SCZ","name":"Santa Cruz/Graciosa Bay/Luova Airport","city":"Santa Cruz/Graciosa Bay/Luova","country":"SB","lat":-10.7203,"lng":165.794998},{"code":"MUA","name":"Munda Airport","city":"Munda","country":"SB","lat":-8.32797,"lng":157.263},{"code":"GZO","name":"Nusatupe Airport","city":"Gizo","country":"SB","lat":-8.09778022766,"lng":156.863998413},{"code":"MNY","name":"Mono Airport","city":"Stirling Island","country":"SB","lat":-7.41694,"lng":155.565002},{"code":"RNL","name":"Rennell/Tingoa Airport","city":"Rennell Island","country":"SB","lat":-11.55,"lng":160.062778},{"code":"NNB","name":"Santa Ana Airport","city":"Santa Ana Island","country":"SB","lat":-10.847994,"lng":162.454108},{"code":"RUS","name":"Marau Airport","city":"Marau","country":"SB","lat":-9.86167,"lng":160.824997},{"code":"VAO","name":"Suavanao Airport","city":"Suavanao","country":"SB","lat":-7.58556,"lng":158.731003},{"code":"XYA","name":"Yandina Airport","city":"Yandina","country":"SB","lat":-9.092816,"lng":159.21841},{"code":"KGE","name":"Kaghau Airport","city":"Kagau Island","country":"SB","lat":-7.3305,"lng":157.585},{"code":"MHM","name":"Manaoba Airport","city":"Manaoba","country":"SB","lat":-8.325008,"lng":160.80051},{"code":"GTA","name":"Gatokae Aerodrome","city":"Gatokae","country":"SB","lat":-8.738358,"lng":158.202836},{"code":"RBV","name":"Ramata Airport","city":"Ramata","country":"SB","lat":-8.168060302734375,"lng":157.64300537109375},{"code":"NKI","name":"Naukati Bay Seaplane Base","city":"Tuxekan Island","country":"US","lat":55.849602,"lng":-133.227994},{"code":"AOS","name":"Amook Bay Seaplane Base","city":"Amook Bay","country":"US","lat":57.4715003967,"lng":-153.815002441},{"code":"ALZ","name":"Alitak Seaplane Base","city":"Lazy Bay","country":"US","lat":56.897294,"lng":-154.249692},{"code":"INU","name":"Nauru International Airport","city":"Yaren","country":"NR","lat":-0.547893,"lng":166.919525},{"code":"GYZ","name":"Gruyere Airport","city":"Cosmo Newbery","country":"AU","lat":-28.034548,"lng":123.81506},{"code":"HIS","name":"Hayman Island Resort Seaplane Base","city":"Hayman Island","country":"AU","lat":-20.059999465942383,"lng":148.88099670410156},{"code":"AXF","name":"Alxa Left Banner Bayanhot Airport","city":"Bayanhot","country":"CN","lat":38.748317,"lng":105.58416},{"code":"VMU","name":"Baimuru Airport","city":"Baimuru","country":"PG","lat":-7.496955,"lng":144.821756},{"code":"BUA","name":"Buka Airport","city":"Buka Island","country":"PG","lat":-5.422299,"lng":154.672698},{"code":"OPU","name":"Balimo Airport","city":"Balimo","country":"PG","lat":-8.05000019073,"lng":142.932998657},{"code":"CMU","name":"Chimbu Airport","city":"Kundiawa","country":"PG","lat":-6.024290084838867,"lng":144.9709930419922},{"code":"DAU","name":"Daru Airport","city":"Daru","country":"PG","lat":-9.08676,"lng":143.207993},{"code":"BOT","name":"Bosset Airport","city":"Bosset","country":"PG","lat":-7.237257,"lng":141.1063222},{"code":"GKA","name":"Goroka Airport","city":"Goronka","country":"PG","lat":-6.081689834590001,"lng":145.391998291},{"code":"GUR","name":"Gurney Airport","city":"Gurney","country":"PG","lat":-10.3114995956,"lng":150.333999634},{"code":"PNP","name":"Girua Airport","city":"Popondetta","country":"PG","lat":-8.80453968048,"lng":148.309005737},{"code":"GMI","name":"Gasmata Island Airport","city":"Gasmata Island","country":"PG","lat":-6.27111005783,"lng":150.330993652},{"code":"HKN","name":"Hoskins Airport","city":"Kimbe","country":"PG","lat":-5.463846,"lng":150.407327},{"code":"KIE","name":"Aropa Airport","city":"Kieta","country":"PG","lat":-6.305727,"lng":155.728249},{"code":"LSA","name":"Losuia Airport","city":"Losuia","country":"PG","lat":-8.505820274353027,"lng":151.08099365234375},{"code":"UNG","name":"Kiunga Airport","city":"Kiunga","country":"PG","lat":-6.1257100105285645,"lng":141.28199768066406},{"code":"KRI","name":"Kikori Airport","city":"Kikori","country":"PG","lat":-7.42438,"lng":144.250076},{"code":"KMA","name":"Kerema Airport","city":"Kerema","country":"PG","lat":-7.96361017227,"lng":145.770996094},{"code":"KVG","name":"Kavieng Airport","city":"Kavieng","country":"PG","lat":-2.57940006256,"lng":150.807998657},{"code":"LNV","name":"Londolovit Airport","city":"Londolovit","country":"PG","lat":-3.04361009598,"lng":152.628997803},{"code":"MAG","name":"Madang Airport","city":"Madang","country":"PG","lat":-5.20707988739,"lng":145.789001465},{"code":"HGU","name":"Mount Hagen Kagamuga Airport","city":"Mount Hagen","country":"PG","lat":-5.828212,"lng":144.299412},{"code":"MDU","name":"Mendi Airport","city":"Mendi","country":"PG","lat":-6.14774,"lng":143.656998},{"code":"MAS","name":"Momote Airport","city":"Manus Island","country":"PG","lat":-2.06189,"lng":147.423996},{"code":"MXH","name":"Moro Airport","city":"Moro","country":"PG","lat":-6.36332988739,"lng":143.238006592},{"code":"MIS","name":"Misima Island Airport","city":"Misima Island","country":"PG","lat":-10.689200401299999,"lng":152.837997437},{"code":"LAE","name":"Nadzab Tomodachi International Airport","city":"Lae","country":"PG","lat":-6.567995,"lng":146.726544},{"code":"OBX","name":"Obo Airport","city":"Obo","country":"PG","lat":-7.590622421369999,"lng":141.324262619},{"code":"POM","name":"Port Moresby Jacksons International Airport","city":"Port Moresby","country":"PG","lat":-9.443380355834961,"lng":147.22000122070312},{"code":"TDS","name":"Sasereme Airport","city":"Sasereme","country":"PG","lat":-7.6217,"lng":142.868},{"code":"TIZ","name":"Tari Airport","city":"Tari","country":"PG","lat":-5.845,"lng":142.947998},{"code":"TBG","name":"Tabubil Airport","city":"Tabubil","country":"PG","lat":-5.280471,"lng":141.227576},{"code":"TPI","name":"Tapini Airport","city":"Tapini","country":"PG","lat":-8.35666666667,"lng":146.989166667},{"code":"RAB","name":"Tokua Airport","city":"Kokopo","country":"PG","lat":-4.34046,"lng":152.380005},{"code":"TFI","name":"Tufi Airport","city":"Tufi","country":"PG","lat":-9.07595443726,"lng":149.319839478},{"code":"VAI","name":"Vanimo Airport","city":"Vanimo","country":"PG","lat":-2.6926,"lng":141.3028},{"code":"WBM","name":"Wapenamanda Airport","city":"Wapenamanda","country":"PG","lat":-5.635293,"lng":143.892231},{"code":"WWK","name":"Wewak International Airport","city":"Wewak","country":"PG","lat":-3.58383011818,"lng":143.669006348},{"code":"JEG","name":"Aasiaat Airport","city":"Aasiaat","country":"GL","lat":68.721802,"lng":-52.784698},{"code":"AOQ","name":"Aappilattoq Heliport","city":"Aappilattoq","country":"GL","lat":72.88703,"lng":-55.596287},{"code":"AGM","name":"Tasiilaq Heliport","city":"Tasiilaq","country":"GL","lat":65.612296,"lng":-37.618335},{"code":"LLU","name":"Alluitsup Paa Heliport","city":"Alluitsup Paa","country":"GL","lat":60.46445,"lng":-45.56917},{"code":"QUV","name":"Aappilattoq Heliport","city":"Aappilattoq","country":"GL","lat":60.148357,"lng":-44.286916},{"code":"QUW","name":"Ammassivik Heliport","city":"Ammassivik","country":"GL","lat":60.597376,"lng":-45.382445},{"code":"QGQ","name":"Attu Heliport","city":"Attu","country":"GL","lat":67.940663,"lng":-53.62187},{"code":"UAK","name":"Narsarsuaq Airport","city":"Narsarsuaq","country":"GL","lat":61.15958,"lng":-45.42469},{"code":"JCH","name":"Qasigiannguit Heliport","city":"Qasigiannguit","country":"GL","lat":68.822816,"lng":-51.173447},{"code":"CNP","name":"Neerlerit Inaat Airport","city":"Neerlerit Inaat","country":"GL","lat":70.743103,"lng":-22.650499},{"code":"QFG","name":"Eqalugaarsuit Heliport","city":"Eqalugaarsuit","country":"GL","lat":60.61972,"lng":-45.914062},{"code":"QFN","name":"Narsarmijit Heliport","city":"Narsarmijit","country":"GL","lat":60.004694,"lng":-44.656935},{"code":"JGR","name":"Kangilinnguit Heliport","city":"Kangilinnguit","country":"GL","lat":61.22896,"lng":-48.09698},{"code":"GOH","name":"Nuuk International Airport","city":"Nuuk","country":"GL","lat":64.191066,"lng":-51.67914},{"code":"JGO","name":"Qeqertarsuaq Heliport","city":"Qeqertarsuaq","country":"GL","lat":69.251182,"lng":-53.514876},{"code":"IKE","name":"Ikerasak Heliport","city":"Ikerasak","country":"GL","lat":70.498145,"lng":-51.303084},{"code":"QFI","name":"Iginniarfik Heliport","city":"Iginniarfik","country":"GL","lat":68.14583,"lng":-53.16916},{"code":"QRY","name":"Ikerassaarsuk Heliport","city":"Ikerassaarsuk","country":"GL","lat":68.140881,"lng":-53.441459},{"code":"XIQ","name":"Ilimanaq Heliport","city":"Ilimanaq","country":"GL","lat":69.080915,"lng":-51.114315},{"code":"IUI","name":"Innarsuit Heliport","city":"Innarsuit","country":"GL","lat":73.199895,"lng":-56.010817},{"code":"IOQ","name":"Isortoq Heliport","city":"Isortoq","country":"GL","lat":65.547792,"lng":-38.976552},{"code":"QJI","name":"Ikamiut Heliport","city":"Ikamiut","country":"GL","lat":68.634253,"lng":-51.832262},{"code":"JAV","name":"Ilulissat Airport","city":"Ilulissat","country":"GL","lat":69.243202,"lng":-51.057098},{"code":"QPW","name":"Kangaatsiaq Heliport","city":"Kangaatsiaq","country":"GL","lat":68.312657,"lng":-53.460208},{"code":"KUS","name":"Kulusuk Airport","city":"Kulusuk","country":"GL","lat":65.573601,"lng":-37.1236},{"code":"KUZ","name":"Kuummiut Heliport","city":"Kuummiut","country":"GL","lat":65.863935,"lng":-36.997919},{"code":"KGQ","name":"Kangersuatsiaq Heliport","city":"Kangersuatsiaq","country":"GL","lat":72.381092,"lng":-55.536586},{"code":"QJE","name":"Kitsissuarsuit Heliport","city":"Kitsissuarsuit","country":"GL","lat":68.857926,"lng":-53.123295},{"code":"IOT","name":"Illorsuit Heliport","city":"Illorsuit","country":"GL","lat":71.241944,"lng":-53.562877},{"code":"JSU","name":"Maniitsoq Airport","city":"Maniitsoq","country":"GL","lat":65.412498,"lng":-52.9394},{"code":"JNN","name":"Nanortalik Heliport","city":"Nanortalik","country":"GL","lat":60.141884,"lng":-45.232977},{"code":"JUU","name":"Nuugaatsiaq Heliport","city":"Nuugaatsiaq","country":"GL","lat":71.538769,"lng":-53.205038},{"code":"JNS","name":"Narsaq Heliport","city":"Narsaq","country":"GL","lat":60.917283,"lng":-46.059923},{"code":"NIQ","name":"Niaqornat Heliport","city":"Niaqornat","country":"GL","lat":70.789385,"lng":-53.662945},{"code":"JFR","name":"Paamiut Airport","city":"Paamiut","country":"GL","lat":62.014736,"lng":-49.670937},{"code":"PQT","name":"Qeqertaq Heliport","city":"Qeqertaq","country":"GL","lat":69.995861,"lng":-51.300917},{"code":"NAQ","name":"Qaanaaq Airport","city":"Qaanaaq","country":"GL","lat":77.488602,"lng":-69.388702},{"code":"QJH","name":"Qassimiut Heliport","city":"Qassimiut","country":"GL","lat":60.780959,"lng":-47.156249},{"code":"OBY","name":"Ittoqqortoormiit Heliport","city":"Ittoqqortoormiit","country":"GL","lat":70.488229,"lng":-21.97168},{"code":"SFJ","name":"Kangerlussuaq International Airport","city":"Kangerlussuaq","country":"GL","lat":67.010446,"lng":-50.715294},{"code":"SGG","name":"Sermiligaaq Heliport","city":"Sermiligaaq","country":"GL","lat":65.90592,"lng":-36.378254},{"code":"SRK","name":"Siorapaluk Heliport","city":"Siorapaluk","country":"GL","lat":77.786517,"lng":-70.638657},{"code":"QOQ","name":"Saarloq Heliport","city":"Saarloq","country":"GL","lat":60.537778,"lng":-46.024722},{"code":"JHS","name":"Sisimiut Airport","city":"Sisimiut","country":"GL","lat":66.951302,"lng":-53.729301},{"code":"SAE","name":"Saattut Heliport","city":"Saattut","country":"GL","lat":70.811172,"lng":-51.63129},{"code":"SVR","name":"Savissivik Heliport","city":"Savissivik","country":"GL","lat":76.018613,"lng":-65.117683},{"code":"TQA","name":"Tasiusaq Heliport","city":"Tasiusaq","country":"GL","lat":73.373055,"lng":-56.06028},{"code":"THU","name":"Pituffik Space Base","city":"Pituffik","country":"GL","lat":76.53063,"lng":-68.700541},{"code":"TQI","name":"Tiniteqilaaq Heliport","city":"Tiniteqilaaq","country":"GL","lat":65.892027,"lng":-37.783409},{"code":"XEQ","name":"Tasiusaq Heliport","city":"Tasiusak","country":"GL","lat":60.194305,"lng":-44.813447},{"code":"JUV","name":"Upernavik Airport","city":"Upernavik","country":"GL","lat":72.790199,"lng":-56.1306},{"code":"UMD","name":"Uummannaq Heliport","city":"Uummannaq","country":"GL","lat":70.680428,"lng":-52.11163},{"code":"JQA","name":"Qaarsut Airport","city":"Uummannaq","country":"GL","lat":70.7342,"lng":-52.696201},{"code":"JUK","name":"Ukkusissat Heliport","city":"Ukkusissat","country":"GL","lat":71.049438,"lng":-51.890016},{"code":"AEY","name":"Akureyri International Airport","city":"Akureyri","country":"IS","lat":65.656573,"lng":-18.072018},{"code":"EGS","name":"Egilsstaðir Airport","city":"Egilsstaðir","country":"IS","lat":65.2833023071289,"lng":-14.401399612426758},{"code":"GRY","name":"Grímsey Airport","city":"Grímsey/Sandvík","country":"IS","lat":66.5458,"lng":-18.0173},{"code":"HFN","name":"Hornafjörður Airport","city":"Höfn","country":"IS","lat":64.295601,"lng":-15.2272},{"code":"IFJ","name":"Ísafjörður Airport","city":"Ísafjörður","country":"IS","lat":66.058098,"lng":-23.1353},{"code":"KEF","name":"Keflavik International Airport","city":"Reykjavík","country":"IS","lat":63.985001,"lng":-22.6056},{"code":"RKV","name":"Reykjavík Domestic Airport","city":"Reykjavík","country":"IS","lat":64.128732,"lng":-21.93759},{"code":"THO","name":"Þórshöfn Airport","city":"Þórshöfn","country":"IS","lat":66.218459,"lng":-15.334739},{"code":"VPN","name":"Vopnafjörður Airport","city":"Vopnafjörður","country":"IS","lat":65.720596,"lng":-14.8506},{"code":"BJT","name":"Bentota River Waterdrome","city":"Bentota","country":"LK","lat":6.431,"lng":79.996},{"code":"PRN","name":"Priština Adem Jashari International Airport","city":"Prishtina","country":"XK","lat":42.5728,"lng":21.035801},{"code":"CJZ","name":"Pedro Vieira Moreira Airport","city":"Cajazeiras","country":"BR","lat":-6.8827,"lng":-38.61583},{"code":"VDC","name":"Glauber de Andrade Rocha Airport","city":"Vitória da Conquista","country":"BR","lat":-14.907885,"lng":-40.914804},{"code":"NSB","name":"Bimini North Seaplane Base","city":"Bimini","country":"BS","lat":25.767,"lng":-79.25},{"code":"BGK","name":"Big Creek Airport","city":"Big Creek","country":"BZ","lat":16.519369,"lng":-88.407913},{"code":"CUK","name":"Caye Caulker Airport","city":"Caye Caulker","country":"BZ","lat":17.735015,"lng":-88.032862},{"code":"CYC","name":"Caye Chapel Airport","city":"Caye Chapel","country":"BZ","lat":17.683792,"lng":-88.044985},{"code":"CZH","name":"Corozal Airport","city":"Corozal","country":"BZ","lat":18.3822,"lng":-88.411903},{"code":"DGA","name":"Dangriga Airport","city":"Dangriga","country":"BZ","lat":16.98251,"lng":-88.230988},{"code":"INB","name":"Independence Airport","city":"Independence","country":"BZ","lat":16.53456,"lng":-88.441615},{"code":"ORZ","name":"H.E Alfredo Martinez (Tower Hill) Airstrip","city":"Orange Walk","country":"BZ","lat":18.046767,"lng":-88.583858},{"code":"PND","name":"Punta Gorda Airport","city":"Punta Gorda","country":"BZ","lat":16.1024,"lng":-88.808296},{"code":"SPR","name":"John Greif II Airport","city":"San Pedro","country":"BZ","lat":17.9139,"lng":-87.9711},{"code":"TZA","name":"Sir Barry Bowen Municipal Airport","city":"Belize City","country":"BZ","lat":17.517239,"lng":-88.195775},{"code":"YMF","name":"Montague Harbour Seaplane Base","city":"Galiano Island","country":"CA","lat":48.89225,"lng":-123.39215},{"code":"YBW","name":"Bedwell Harbour Seaplane Base","city":"Bedwell Harbour","country":"CA","lat":48.75,"lng":-123.233002},{"code":"YTP","name":"Tofino Harbour Seaplane Base","city":"Tofino","country":"CA","lat":49.155,"lng":-125.91},{"code":"ZNA","name":"Nanaimo Harbour Water Aerodrome","city":"Nanaimo","country":"CA","lat":49.169813,"lng":-123.933845},{"code":"YZZ","name":"Trail Regional Airport","city":"Trail","country":"CA","lat":49.055599,"lng":-117.609001},{"code":"YHH","name":"Campbell River Seaplane Base","city":"Campbell River","country":"CA","lat":50.049999,"lng":-125.25},{"code":"YWS","name":"Whistler/Green Lake Water Aerodrome","city":"Whistler","country":"CA","lat":50.1436004639,"lng":-122.948997498},{"code":"YBF","name":"Bamfield Seaplane Base","city":"Bamfield","country":"CA","lat":48.835967,"lng":-125.137453},{"code":"YIG","name":"Big Bay Seaplane Base","city":"Stuart Island","country":"CA","lat":50.3923,"lng":-125.1372},{"code":"YAA","name":"Anahim Lake Airport","city":"Anahim Lake","country":"CA","lat":52.451501,"lng":-125.303776},{"code":"WPL","name":"Powell Lake Seaplane Base","city":"Powell River","country":"CA","lat":49.887335,"lng":-124.53372},{"code":"XQU","name":"Qualicum Beach Airport","city":"Qualicum Beach","country":"CA","lat":49.337456,"lng":-124.393086},{"code":"YMP","name":"Port McNeill Airport","city":"Port McNeill","country":"CA","lat":50.57348,"lng":-127.027702},{"code":"YTG","name":"Sullivan Bay Seaplane Base","city":"Sullivan Bay","country":"CA","lat":50.885359,"lng":-126.831069},{"code":"YAV","name":"Mayne Island Seaplane Base","city":"Miners Bay","country":"CA","lat":48.852241,"lng":-123.301982},{"code":"YGG","name":"Ganges Seaplane Base","city":"Salt Spring Island","country":"CA","lat":48.8545,"lng":-123.4969},{"code":"ZEL","name":"Bella Bella (Campbell Island) Airport","city":"Bella Bella","country":"CA","lat":52.185001,"lng":-128.156994},{"code":"YWM","name":"Williams Harbour Airport","city":"Williams Harbour","country":"CA","lat":52.567377,"lng":-55.784883},{"code":"YSO","name":"Postville Airport","city":"Postville","country":"CA","lat":54.9105,"lng":-59.78507},{"code":"YBI","name":"Black Tickle Airport","city":"Black Tickle","country":"CA","lat":53.469836,"lng":-55.787501},{"code":"YHG","name":"Charlottetown Airport","city":"Charlottetown","country":"CA","lat":52.765774,"lng":-56.11237},{"code":"YFX","name":"St. Lewis (Fox Harbour) Airport","city":"St. Lewis","country":"CA","lat":52.372799,"lng":-55.673901},{"code":"YHA","name":"Port Hope Simpson Airport","city":"Port Hope Simpson","country":"CA","lat":52.528099060058594,"lng":-56.28609848022461},{"code":"YRG","name":"Rigolet Airport","city":"Rigolet","country":"CA","lat":54.1796989440918,"lng":-58.45750045776367},{"code":"YLE","name":"Whatì Airport","city":"Whatì","country":"CA","lat":63.131699,"lng":-117.246002},{"code":"CGA","name":"Craig Seaplane Base","city":"Craig","country":"US","lat":55.478802,"lng":-133.147995},{"code":"SUR","name":"Summer Beaver Airport","city":"Summer Beaver","country":"CA","lat":52.70859909057617,"lng":-88.54190063476562},{"code":"CKD","name":"Crooked Creek Airport","city":"Crooked Creek","country":"US","lat":61.870315,"lng":-158.137636},{"code":"YAX","name":"Wapekeka Airport","city":"Angling Lake","country":"CA","lat":53.84920120239258,"lng":-89.57939910888672},{"code":"WNN","name":"Wunnumin Lake Airport","city":"Wunnumin Lake","country":"CA","lat":52.893902,"lng":-89.2892},{"code":"YNO","name":"North Spirit Lake Airport","city":"North Spirit Lake","country":"CA","lat":52.4900016784668,"lng":-92.97109985351562},{"code":"CKX","name":"Chicken Airport","city":"Chicken","country":"US","lat":64.066495,"lng":-141.951342},{"code":"BZX","name":"Bazhong Enyang Airport","city":"Bazhong","country":"CN","lat":31.73842,"lng":106.644872},{"code":"HSC","name":"Shaoguan Danxia Airport","city":"Shaoguan","country":"CN","lat":24.9786,"lng":113.420998},{"code":"JGS","name":"Jinggangshan Airport","city":"Ji'an","country":"CN","lat":26.856899,"lng":114.737},{"code":"JNH","name":"Jiaxing Nanhu Airport","city":"Xiuzhou, Hangzhou","country":"CN","lat":30.698056,"lng":120.663056},{"code":"DDR","name":"Shigatse Tingri Airport","city":"Xigazê (Dingri)","country":"CN","lat":28.604567,"lng":86.798},{"code":"HQL","name":"Tashikuergan Hongqilafu Airport","city":"Tashikuergan","country":"CN","lat":37.661333,"lng":75.288877},{"code":"LSG","name":"Leshan Airport","city":"Leshan (Wutongqiao)","country":"CN","lat":29.438627,"lng":103.749218},{"code":"ZFL","name":"Zhaosu Tianma Airport","city":"Zhaosu","country":"CN","lat":43.088477,"lng":81.223013},{"code":"HJB","name":"Hejing Bayinbuluke Airport","city":"Hejing","country":"CN","lat":42.976395,"lng":83.995543},{"code":"JBK","name":"Qitai Jiangbulake Airport","city":"Qitai","country":"CN","lat":44.166086,"lng":89.55014},{"code":"BFY","name":"Bengbu Tenghu Airport","city":"Bengbu","country":"CN","lat":33.166292,"lng":117.058344},{"code":"XBE","name":"Bearskin Lake Airport","city":"Bearskin Lake","country":"CA","lat":53.965599060058594,"lng":-91.0271987915039},{"code":"YNP","name":"Natuashish Airport","city":"Natuashish","country":"CA","lat":55.913898,"lng":-61.184399},{"code":"KIF","name":"Kingfisher Lake Airport","city":"Kingfisher Lake","country":"CA","lat":53.01250076293945,"lng":-89.85530090332031},{"code":"LCR","name":"Virgilio Barco Vargas (La Chorrera) Airport","city":"La Chorrera","country":"CO","lat":-1.456361,"lng":-72.801086},{"code":"KEW","name":"Keewaywin Airport","city":"Keewaywin","country":"CA","lat":52.9911,"lng":-92.836403},{"code":"YKU","name":"Chisasibi Airport","city":"Chisasibi","country":"CA","lat":53.805599212646484,"lng":-78.91690063476562},{"code":"ZTB","name":"Tête-à-la-Baleine Airport","city":"Tête-à-la-Baleine","country":"CA","lat":50.674400329589844,"lng":-59.38359832763672},{"code":"ZGS","name":"La Romaine Airport","city":"Le Golfe-du-Saint-Laurent","country":"CA","lat":50.259553,"lng":-60.674357},{"code":"ZLT","name":"La Tabatière Airport","city":"La Tabatière","country":"CA","lat":50.8307991027832,"lng":-58.97560119628906},{"code":"YAB","name":"Arctic Bay Airport","city":"Arctic Bay","country":"CA","lat":73.006101,"lng":-85.04616},{"code":"YAC","name":"Cat Lake Airport","city":"Cat Lake","country":"CA","lat":51.7272,"lng":-91.824402},{"code":"YAG","name":"Fort Frances Municipal Airport","city":"Fort Frances","country":"CA","lat":48.655749,"lng":-93.44349},{"code":"YAL","name":"Alert Bay Airport","city":"Alert Bay","country":"CA","lat":50.582199,"lng":-126.916},{"code":"YAM","name":"Sault Ste Marie Airport","city":"Sault Ste Marie","country":"CA","lat":46.483216,"lng":-84.508467},{"code":"XKS","name":"Kasabonika Airport","city":"Kasabonika","country":"CA","lat":53.52470016479492,"lng":-88.6427993774414},{"code":"YKG","name":"Kangirsuk Airport","city":"Kangirsuk","country":"CA","lat":60.027199,"lng":-69.999199},{"code":"YAT","name":"Attawapiskat Airport","city":"Attawapiskat","country":"CA","lat":52.927502,"lng":-82.4319},{"code":"YAY","name":"St. Anthony Airport","city":"St. Anthony","country":"CA","lat":51.391909,"lng":-56.08321},{"code":"YAZ","name":"Tofino / Long Beach Airport","city":"Tofino","country":"CA","lat":49.079833,"lng":-125.775583},{"code":"YBB","name":"Kugaaruk Airport","city":"Kugaaruk","country":"CA","lat":68.535711,"lng":-89.805508},{"code":"YBC","name":"Baie-Comeau Airport","city":"Baie-Comeau","country":"CA","lat":49.1325,"lng":-68.204399},{"code":"QBC","name":"Bella Coola Airport","city":"Bella Coola","country":"CA","lat":52.387501,"lng":-126.596001},{"code":"YBE","name":"Uranium City Airport","city":"Uranium City","country":"CA","lat":59.561401,"lng":-108.481003},{"code":"YBY","name":"Bonnyville Airport","city":"Bonnyville","country":"CA","lat":54.304199,"lng":-110.744003},{"code":"YBG","name":"Saguenay-Bagotville Airport","city":"Saguenay","country":"CA","lat":48.330123,"lng":-70.992012},{"code":"YBK","name":"Baker Lake Airport","city":"Baker Lake","country":"CA","lat":64.29889678960001,"lng":-96.077796936},{"code":"YBL","name":"Campbell River Airport","city":"Campbell River","country":"CA","lat":49.950802,"lng":-125.271004},{"code":"XTL","name":"Tadoule Lake Airport","city":"Tadoule Lake","country":"CA","lat":58.706308,"lng":-98.511057},{"code":"YBR","name":"Brandon Municipal Airport","city":"Brandon","country":"CA","lat":49.91,"lng":-99.951897},{"code":"YBT","name":"Brochet Airport","city":"Brochet","country":"CA","lat":57.8894,"lng":-101.679001},{"code":"YBV","name":"Berens River Airport","city":"Berens River","country":"CA","lat":52.358898,"lng":-97.018303},{"code":"YBX","name":"Lourdes-de-Blanc-Sablon Airport","city":"Blanc-Sablon","country":"CA","lat":51.4436,"lng":-57.185299},{"code":"YRF","name":"Cartwright Airport","city":"Cartwright","country":"CA","lat":53.682501,"lng":-57.042303},{"code":"YCB","name":"Cambridge Bay Airport","city":"Cambridge Bay","country":"CA","lat":69.1081008911,"lng":-105.138000488},{"code":"YCD","name":"Nanaimo Airport","city":"Nanaimo","country":"CA","lat":49.05497,"lng":-123.869863},{"code":"YCG","name":"Castlegar/West Kootenay Regional Airport","city":"Castlegar","country":"CA","lat":49.296398,"lng":-117.632004},{"code":"YCL","name":"Charlo Airport","city":"Charlo","country":"CA","lat":47.990799,"lng":-66.330299},{"code":"YCO","name":"Kugluktuk Airport","city":"Kugluktuk","country":"CA","lat":67.816408,"lng":-115.14307},{"code":"YCR","name":"Cross Lake (Charlie Sinclair Memorial) Airport","city":"Cross Lake","country":"CA","lat":54.609758,"lng":-97.762442},{"code":"YCS","name":"Chesterfield Inlet Airport","city":"Chesterfield Inlet","country":"CA","lat":63.346901,"lng":-90.731102},{"code":"YCY","name":"Clyde River Airport","city":"Clyde River","country":"CA","lat":70.486099,"lng":-68.516701},{"code":"YDA","name":"Dawson City Airport","city":"Dawson City","country":"CA","lat":64.04309844970703,"lng":-139.1280059814453},{"code":"YDF","name":"Deer Lake Airport","city":"Deer Lake","country":"CA","lat":49.208159,"lng":-57.396147},{"code":"YDL","name":"Dease Lake Airport","city":"Dease Lake","country":"CA","lat":58.422199,"lng":-130.031998},{"code":"YDN","name":"Dauphin Barker Airport","city":"Dauphin","country":"CA","lat":51.1008,"lng":-100.052002},{"code":"YDP","name":"Nain Airport","city":"Nain","country":"CA","lat":56.550778,"lng":-61.682224},{"code":"YEG","name":"Edmonton International Airport","city":"Edmonton","country":"CA","lat":53.3097,"lng":-113.580002},{"code":"YEK","name":"Arviat Airport","city":"Arviat","country":"CA","lat":61.0942,"lng":-94.070801},{"code":"YER","name":"Fort Severn Airport","city":"Fort Severn","country":"CA","lat":56.018902,"lng":-87.676102},{"code":"YEV","name":"Inuvik Mike Zubko Airport","city":"Inuvik","country":"CA","lat":68.30419921880001,"lng":-133.483001709},{"code":"YFA","name":"Fort Albany Airport","city":"Fort Albany","country":"CA","lat":52.203451,"lng":-81.695194},{"code":"YFB","name":"Iqaluit Airport","city":"Iqaluit","country":"CA","lat":63.756402,"lng":-68.555801},{"code":"YFC","name":"Fredericton International Airport","city":"Fredericton","country":"CA","lat":45.868697,"lng":-66.529891},{"code":"YFH","name":"Fort Hope Airport","city":"Fort Hope","country":"CA","lat":51.561901,"lng":-87.907799},{"code":"YFO","name":"Flin Flon Airport","city":"Flin Flon","country":"CA","lat":54.678101,"lng":-101.681999},{"code":"YFS","name":"Fort Simpson Airport","city":"Fort Simpson","country":"CA","lat":61.76020050048828,"lng":-121.23699951171875},{"code":"YMN","name":"Makkovik Airport","city":"Makkovik","country":"CA","lat":55.077335,"lng":-59.187942},{"code":"YGH","name":"Fort Good Hope Airport","city":"Fort Good Hope","country":"CA","lat":66.240658,"lng":-128.647757},{"code":"YGL","name":"La Grande Rivière Airport","city":"La Grande Rivière","country":"CA","lat":53.625301361083984,"lng":-77.7042007446289},{"code":"YGO","name":"Gods Lake Narrows Airport","city":"Gods Lake Narrows","country":"CA","lat":54.55768,"lng":-94.490061},{"code":"YGP","name":"Michel-Pouliot Gaspé Airport","city":"Gaspé","country":"CA","lat":48.774915,"lng":-64.481893},{"code":"YGR","name":"Îles-de-la-Madeleine Airport","city":"Les Îles-de-la-Madeleine","country":"CA","lat":47.425242,"lng":-61.778612},{"code":"YGT","name":"Igloolik Airport","city":"Igloolik","country":"CA","lat":69.3647,"lng":-81.816101},{"code":"YGV","name":"Havre-Saint-Pierre Airport","city":"Havre-Saint-Pierre","country":"CA","lat":50.281898,"lng":-63.611401},{"code":"YGW","name":"Kuujjuarapik Airport","city":"Kuujjuarapik","country":"CA","lat":55.281898498535156,"lng":-77.76529693603516},{"code":"YGX","name":"Gillam Airport","city":"Gillam","country":"CA","lat":56.35712,"lng":-94.711504},{"code":"YGZ","name":"Grise Fiord Airport","city":"Grise Fiord","country":"CA","lat":76.425823,"lng":-82.908583},{"code":"YQC","name":"Quaqtaq Airport","city":"Quaqtaq","country":"CA","lat":61.046398,"lng":-69.617798},{"code":"CXH","name":"Vancouver Harbour Water Aerodrome","city":"Vancouver","country":"CA","lat":49.290739,"lng":-123.118541},{"code":"YNS","name":"Nemiscau Airport","city":"Nemiscau","country":"CA","lat":51.691101,"lng":-76.135597},{"code":"YHI","name":"Ulukhaktok Holman Airport","city":"Ulukhaktok","country":"CA","lat":70.762802,"lng":-117.806},{"code":"YHK","name":"Gjoa Haven Airport","city":"Gjoa Haven","country":"CA","lat":68.635597,"lng":-95.849701},{"code":"YHM","name":"John C. Munro Hamilton International Airport","city":"Hamilton","country":"CA","lat":43.17348,"lng":-79.931185},{"code":"YHO","name":"Hopedale Airport","city":"Hopedale","country":"CA","lat":55.448757,"lng":-60.228124},{"code":"YHP","name":"Poplar Hill Airport","city":"Poplar Hill","country":"CA","lat":52.1133,"lng":-94.2556},{"code":"YHR","name":"Chevery Airport","city":"Chevery","country":"CA","lat":50.468322,"lng":-59.637823},{"code":"YHU","name":"Montréal / Saint-Hubert Metropolitan Airport","city":"Montréal","country":"CA","lat":45.517502,"lng":-73.416901},{"code":"YHY","name":"Hay River / Merlyn Carter Airport","city":"Hay River","country":"CA","lat":60.8396987915,"lng":-115.782997131},{"code":"YHZ","name":"Halifax / Stanfield International Airport","city":"Halifax","country":"CA","lat":44.8807983398,"lng":-63.5085983276},{"code":"YIF","name":"St Augustin Airport","city":"St-Augustin","country":"CA","lat":51.2117,"lng":-58.658298},{"code":"YIK","name":"Ivujivik Airport","city":"Ivujivik","country":"CA","lat":62.417301,"lng":-77.925301},{"code":"YIO","name":"Pond Inlet Airport","city":"Pond Inlet","country":"CA","lat":72.689494,"lng":-77.968941},{"code":"YIV","name":"Island Lake Airport","city":"Island Lake","country":"CA","lat":53.857200622558594,"lng":-94.65360260009766},{"code":"YJT","name":"Stephenville Dymond International Airport","city":"Stephenville","country":"CA","lat":48.54339,"lng":-58.552892},{"code":"YKA","name":"Kamloops John Moose Fulton Field Regional Airport","city":"Kamloops","country":"CA","lat":50.703038,"lng":-120.448641},{"code":"LAK","name":"Aklavik/Freddie Carmichael Airport","city":"Aklavik","country":"CA","lat":68.223297,"lng":-135.00599},{"code":"YKF","name":"Region of Waterloo International Airport","city":"Breslau","country":"CA","lat":43.4608,"lng":-80.378601},{"code":"YWB","name":"Kangiqsujuaq (Wakeham Bay) Airport","city":"Kangiqsujuaq","country":"CA","lat":61.5886,"lng":-71.929398},{"code":"YKL","name":"Schefferville Airport","city":"Schefferville","country":"CA","lat":54.805301666259766,"lng":-66.8052978515625},{"code":"AKV","name":"Akulivik Airport","city":"Akulivik","country":"CA","lat":60.8186,"lng":-78.148598},{"code":"YOG","name":"Ogoki Post Airport","city":"Ogoki Post","country":"CA","lat":51.6585998535,"lng":-85.9017028809},{"code":"YKQ","name":"Waskaganish Airport","city":"Waskaganish","country":"CA","lat":51.473301,"lng":-78.758301},{"code":"YPJ","name":"Aupaluk Airport","city":"Aupaluk","country":"CA","lat":59.2967,"lng":-69.599701},{"code":"YLC","name":"Kimmirut Airport","city":"Kimmirut","country":"CA","lat":62.848253,"lng":-69.877853},{"code":"YLH","name":"Lansdowne House Airport","city":"Lansdowne House","country":"CA","lat":52.195599,"lng":-87.934196},{"code":"YSG","name":"Lutselk'e Airport","city":"Lutselk'e","country":"CA","lat":62.417823,"lng":-110.682793},{"code":"YLL","name":"Lloydminster Airport","city":"Lloydminster","country":"CA","lat":53.3092,"lng":-110.072998},{"code":"YLK","name":"Barrie-Lake Simcoe Regional Airport","city":"Barrie","country":"CA","lat":44.485056,"lng":-79.554663},{"code":"XGR","name":"Kangiqsualujjuaq (Georges River) Airport","city":"Kangiqsualujjuaq","country":"CA","lat":58.711399,"lng":-65.992798},{"code":"YLW","name":"Kelowna International Airport","city":"Kelowna","country":"CA","lat":49.9561,"lng":-119.377998},{"code":"CYM","name":"Chatham Seaplane Base","city":"Chatham","country":"US","lat":57.5149,"lng":-134.945999},{"code":"YMH","name":"Mary's Harbour Airport","city":"Mary's Harbour","country":"CA","lat":52.302837,"lng":-55.847433},{"code":"YMM","name":"Fort McMurray International Airport","city":"Fort McMurray","country":"CA","lat":56.653301,"lng":-111.222},{"code":"YMO","name":"Moosonee Airport","city":"Moosonee","country":"CA","lat":51.291099548339844,"lng":-80.60780334472656},{"code":"YMT","name":"Chapais Airport","city":"Chibougamau","country":"CA","lat":49.77190017700195,"lng":-74.5280990600586},{"code":"YUD","name":"Umiujaq Airport","city":"Umiujaq","country":"CA","lat":56.536098,"lng":-76.518303},{"code":"YMX","name":"Montreal Mirabel International Airport","city":"Montréal","country":"CA","lat":45.679501,"lng":-74.038696},{"code":"YNA","name":"Natashquan Airport","city":"Natashquan","country":"CA","lat":50.190114,"lng":-61.78898},{"code":"YNC","name":"Wemindji Airport","city":"Wemindji","country":"CA","lat":53.010601,"lng":-78.8311},{"code":"YND","name":"Ottawa / Gatineau Airport","city":"Gatineau","country":"CA","lat":45.521702,"lng":-75.563599},{"code":"YNE","name":"Norway House Airport","city":"Norway House","country":"CA","lat":53.958302,"lng":-97.8442},{"code":"YNL","name":"Points North Landing Airport","city":"Points North Landing","country":"CA","lat":58.27669906616211,"lng":-104.08200073242188},{"code":"YOC","name":"Old Crow Airport","city":"Old Crow","country":"CA","lat":67.570602,"lng":-139.839005},{"code":"YOH","name":"Oxford House Airport","city":"Oxford House","country":"CA","lat":54.9333,"lng":-95.2789},{"code":"YOJ","name":"High Level Airport","city":"High Level","country":"CA","lat":58.62139892578125,"lng":-117.16500091552734},{"code":"YOW","name":"Ottawa Macdonald-Cartier International Airport","city":"Ottawa","country":"CA","lat":45.322498,"lng":-75.669197},{"code":"YPA","name":"Prince Albert Glass Field","city":"Prince Albert","country":"CA","lat":53.214199066199996,"lng":-105.672996521},{"code":"YPC","name":"Paulatuk (Nora Aliqatchialuk Ruben) Airport","city":"Paulatuk","country":"CA","lat":69.360838,"lng":-124.07547},{"code":"YPE","name":"Peace River Airport","city":"Peace River","country":"CA","lat":56.226898,"lng":-117.446999},{"code":"YPH","name":"Inukjuak Airport","city":"Inukjuak","country":"CA","lat":58.471901,"lng":-78.076897},{"code":"YPL","name":"Pickle Lake Airport","city":"Pickle Lake","country":"CA","lat":51.4463996887207,"lng":-90.21420288085938},{"code":"YPM","name":"Pikangikum Airport","city":"Pikangikum","country":"CA","lat":51.819698,"lng":-93.973297},{"code":"YPN","name":"Port-Menier Airport","city":"Port-Menier","country":"CA","lat":49.836399,"lng":-64.288597},{"code":"YPO","name":"Peawanuck Airport","city":"Peawanuck","country":"CA","lat":54.987932,"lng":-85.442648},{"code":"YPQ","name":"Peterborough Regional Airport","city":"Peterborough","country":"CA","lat":44.232251,"lng":-78.362053},{"code":"YPR","name":"Prince Rupert Airport","city":"Prince Rupert","country":"CA","lat":54.286098480199996,"lng":-130.445007324},{"code":"YPW","name":"Powell River Airport","city":"Powell River","country":"CA","lat":49.83420181274414,"lng":-124.5},{"code":"YPX","name":"Puvirnituq Airport","city":"Puvirnituq","country":"CA","lat":60.050598,"lng":-77.286903},{"code":"YPY","name":"Fort Chipewyan Airport","city":"Fort Chipewyan","country":"CA","lat":58.7672004699707,"lng":-111.11699676513672},{"code":"YPZ","name":"Burns Lake Airport","city":"Burns Lake","country":"CA","lat":54.3764,"lng":-125.950996},{"code":"YQA","name":"Muskoka Airport","city":"Gravenhurst","country":"CA","lat":44.975376,"lng":-79.306546},{"code":"YQB","name":"Quebec Jean Lesage International Airport","city":"Quebec","country":"CA","lat":46.7911,"lng":-71.393303},{"code":"YQD","name":"The Pas Airport","city":"The Pas","country":"CA","lat":53.97140121459961,"lng":-101.09100341796875},{"code":"YQG","name":"Windsor International Airport","city":"Windsor","country":"CA","lat":42.2756,"lng":-82.955597},{"code":"YQH","name":"Watson Lake Airport","city":"Watson Lake","country":"CA","lat":60.116839,"lng":-128.821993},{"code":"YQK","name":"Kenora Airport","city":"Kenora","country":"CA","lat":49.788299560546875,"lng":-94.36309814453125},{"code":"YQL","name":"Lethbridge County Airport","city":"Lethbridge","country":"CA","lat":49.6302986145,"lng":-112.800003052},{"code":"YQM","name":"Greater Moncton Roméo LeBlanc International Airport","city":"Moncton","country":"CA","lat":46.113189,"lng":-64.67719},{"code":"YQN","name":"Nakina Airport","city":"Nakina","country":"CA","lat":50.18280029296875,"lng":-86.69640350341797},{"code":"YQQ","name":"Comox Valley International Airport / CFB Comox","city":"Comox","country":"CA","lat":49.7108,"lng":-124.887001},{"code":"YQR","name":"Regina International Airport","city":"Regina","country":"CA","lat":50.431938,"lng":-104.660906},{"code":"YQT","name":"Thunder Bay International Airport","city":"Thunder Bay","country":"CA","lat":48.371899,"lng":-89.323898},{"code":"YQU","name":"Grande Prairie Airport","city":"Grande Prairie","country":"CA","lat":55.1796989441,"lng":-118.885002136},{"code":"YQX","name":"Gander International Airport","city":"Gander","country":"CA","lat":48.936258,"lng":-54.567719},{"code":"YQY","name":"Sydney / J.A. Douglas McCurdy Airport","city":"Sydney","country":"CA","lat":46.161072,"lng":-60.049759},{"code":"YQZ","name":"Quesnel Airport","city":"Quesnel","country":"CA","lat":53.026100158691406,"lng":-122.51000213623047},{"code":"YRA","name":"Rae Lakes Airport","city":"Gamètì","country":"CA","lat":64.116096,"lng":-117.309998},{"code":"YRB","name":"Resolute Bay Airport","city":"Resolute Bay","country":"CA","lat":74.7169036865,"lng":-94.9693984985},{"code":"YRJ","name":"Roberval Airport","city":"Roberval","country":"CA","lat":48.519665,"lng":-72.265735},{"code":"YRL","name":"Red Lake Airport","city":"Red Lake","country":"CA","lat":51.066898345947266,"lng":-93.79309844970703},{"code":"YRO","name":"Ottawa / Rockcliffe Airport","city":"Ottawa","country":"CA","lat":45.460496,"lng":-75.644001},{"code":"YRS","name":"Red Sucker Lake Airport","city":"Red Sucker Lake","country":"CA","lat":54.167198,"lng":-93.557198},{"code":"YRT","name":"Rankin Inlet Airport","city":"Rankin Inlet","country":"CA","lat":62.8114013672,"lng":-92.1157989502},{"code":"YSB","name":"Sudbury Airport","city":"Sudbury","country":"CA","lat":46.625,"lng":-80.79889678955078},{"code":"YSF","name":"Stony Rapids Airport","city":"Stony Rapids","country":"CA","lat":59.250301361083984,"lng":-105.84100341796875},{"code":"YSJ","name":"Saint John Airport","city":"Saint John","country":"CA","lat":45.31610107421875,"lng":-65.89029693603516},{"code":"YSK","name":"Sanikiluaq Airport","city":"Sanikiluaq","country":"CA","lat":56.536891,"lng":-79.250221},{"code":"YSL","name":"Saint-Léonard Airport","city":"Saint-Léonard","country":"CA","lat":47.157097,"lng":-67.836242},{"code":"YSM","name":"Fort Smith Airport","city":"Fort Smith","country":"CA","lat":60.020302,"lng":-111.961998},{"code":"YCM","name":"Niagara District Airport","city":"Niagara-on-the-Lake","country":"CA","lat":43.191598,"lng":-79.171686},{"code":"YST","name":"St. Theresa Point Airport","city":"St. Theresa Point","country":"CA","lat":53.845261,"lng":-94.851966},{"code":"YSY","name":"Sachs Harbour (David Nasogaluak Jr. Saaryuaq) Airport","city":"Sachs Harbour","country":"CA","lat":71.993896,"lng":-125.242996},{"code":"YTE","name":"Cape Dorset Airport","city":"Kinngait","country":"CA","lat":64.230003,"lng":-76.526703},{"code":"YTH","name":"Thompson Airport","city":"Thompson","country":"CA","lat":55.80110168457031,"lng":-97.86419677734375},{"code":"YTL","name":"Big Trout Lake Airport","city":"Big Trout Lake","country":"CA","lat":53.817799,"lng":-89.896896},{"code":"YTQ","name":"Tasiujaq Airport","city":"Tasiujaq","country":"CA","lat":58.667801,"lng":-69.955803},{"code":"YTS","name":"Timmins/Victor M. Power","city":"Timmins","country":"CA","lat":48.569698333699996,"lng":-81.376701355},{"code":"YTZ","name":"Billy Bishop Toronto City Airport","city":"Toronto","country":"CA","lat":43.627852,"lng":-79.39553},{"code":"YUL","name":"Montreal / Pierre Elliott Trudeau International Airport","city":"Montréal","country":"CA","lat":45.467837,"lng":-73.742294},{"code":"YUT","name":"Naujaat Airport","city":"Repulse Bay","country":"CA","lat":66.521024,"lng":-86.225166},{"code":"YUX","name":"Hall Beach Airport","city":"Sanirajak","country":"CA","lat":68.7761,"lng":-81.2425},{"code":"YUY","name":"Rouyn Noranda Airport","city":"Rouyn-Noranda","country":"CA","lat":48.20610046386719,"lng":-78.83560180664062},{"code":"YVB","name":"Bonaventure Airport","city":"Bonaventure","country":"CA","lat":48.071098,"lng":-65.460297},{"code":"YVC","name":"La Ronge Airport","city":"La Ronge","country":"CA","lat":55.151401519800004,"lng":-105.262001038},{"code":"YCK","name":"Tommy Kochon Airport","city":"Colville Lake","country":"CA","lat":67.02015,"lng":-126.128364},{"code":"YVM","name":"Qikiqtarjuaq Airport","city":"Qikiqtarjuaq","country":"CA","lat":67.54656,"lng":-64.03141},{"code":"YVO","name":"Val-d'Or Airport","city":"Val-d'Or","country":"CA","lat":48.0532989502,"lng":-77.7827987671},{"code":"YVP","name":"Kuujjuaq Airport","city":"Kuujjuaq","country":"CA","lat":58.096099853515625,"lng":-68.4269027709961},{"code":"YVQ","name":"Norman Wells Airport","city":"Norman Wells","country":"CA","lat":65.28160095214844,"lng":-126.7979965209961},{"code":"YVR","name":"Vancouver International Airport","city":"Vancouver","country":"CA","lat":49.193901,"lng":-123.183998},{"code":"YVV","name":"Wiarton Airport","city":"Wiarton","country":"CA","lat":44.7458,"lng":-81.107201},{"code":"YVZ","name":"Deer Lake Airport","city":"Deer Lake","country":"CA","lat":52.655613,"lng":-94.061208},{"code":"YFJ","name":"Wekweètì Airport","city":"Wekweètì","country":"CA","lat":64.190804,"lng":-114.077002},{"code":"YWG","name":"Winnipeg / James Armstrong Richardson International Airport","city":"Winnipeg","country":"CA","lat":49.909999847399995,"lng":-97.2398986816},{"code":"YWH","name":"Victoria Harbour Seaplane Base","city":"Victoria","country":"CA","lat":48.423782,"lng":-123.37198},{"code":"YWJ","name":"Déline Airport","city":"Déline","country":"CA","lat":65.211098,"lng":-123.435997},{"code":"YWK","name":"Wabush Airport","city":"Wabush","country":"CA","lat":52.92190170288086,"lng":-66.8644027709961},{"code":"YWL","name":"Williams Lake Airport","city":"Williams Lake","country":"CA","lat":52.1831016541,"lng":-122.054000854},{"code":"YWP","name":"Webequie Airport","city":"Webequie","country":"CA","lat":52.959393,"lng":-87.374868},{"code":"YXC","name":"Cranbrook/Canadian Rockies International Airport","city":"Cranbrook","country":"CA","lat":49.610802,"lng":-115.781998},{"code":"YXE","name":"Saskatoon John G. Diefenbaker International Airport","city":"Saskatoon","country":"CA","lat":52.170723,"lng":-106.700793},{"code":"YXH","name":"Medicine Hat Regional Airport","city":"Medicine Hat","country":"CA","lat":50.018902,"lng":-110.721001},{"code":"YXJ","name":"Fort St John / North Peace Regional Airport","city":"Fort Saint John","country":"CA","lat":56.238098,"lng":-120.739998},{"code":"YXK","name":"Rimouski Airport","city":"Rimouski","country":"CA","lat":48.477644,"lng":-68.496323},{"code":"YXL","name":"Sioux Lookout Airport","city":"Sioux Lookout","country":"CA","lat":50.11389923095703,"lng":-91.9052963256836},{"code":"YXN","name":"Whale Cove Airport","city":"Whale Cove","country":"CA","lat":62.240002,"lng":-92.598099},{"code":"YXP","name":"Pangnirtung Airport","city":"Pangnirtung","country":"CA","lat":66.144878,"lng":-65.713606},{"code":"YXS","name":"Prince George (International) Airport","city":"Prince George","country":"CA","lat":53.884311,"lng":-122.666554},{"code":"YXT","name":"Northwest Regional Airport Terrace-Kitimat","city":"Terrace","country":"CA","lat":54.468498,"lng":-128.576009},{"code":"YXU","name":"London International Airport","city":"London","country":"CA","lat":43.032845,"lng":-81.149003},{"code":"YXX","name":"Abbotsford International Airport","city":"Abbotsford","country":"CA","lat":49.025299,"lng":-122.361},{"code":"YXY","name":"Whitehorse / Erik Nielsen International Airport","city":"Whitehorse","country":"CA","lat":60.708533,"lng":-135.065705},{"code":"YYB","name":"North Bay Jack Garland Airport","city":"North Bay","country":"CA","lat":46.363602,"lng":-79.422798},{"code":"YYC","name":"Calgary International Airport","city":"Calgary","country":"CA","lat":51.118822,"lng":-114.009933},{"code":"YYD","name":"Smithers Airport","city":"Smithers","country":"CA","lat":54.82469940185547,"lng":-127.18299865722656},{"code":"YYE","name":"Fort Nelson Airport","city":"Fort Nelson","country":"CA","lat":58.8363990784,"lng":-122.597000122},{"code":"YYF","name":"Penticton Airport","city":"Penticton","country":"CA","lat":49.46310043334961,"lng":-119.60199737548828},{"code":"YYG","name":"Charlottetown Airport","city":"Charlottetown","country":"CA","lat":46.288911,"lng":-63.125174},{"code":"YYH","name":"Taloyoak Airport","city":"Taloyoak","country":"CA","lat":69.5467,"lng":-93.576698},{"code":"YYJ","name":"Victoria International Airport","city":"Victoria","country":"CA","lat":48.647201,"lng":-123.427792},{"code":"YYL","name":"Lynn Lake Airport","city":"Lynn Lake","country":"CA","lat":56.86389923095703,"lng":-101.07599639892578},{"code":"YYQ","name":"Churchill Airport","city":"Churchill","country":"CA","lat":58.739200592041016,"lng":-94.06500244140625},{"code":"YYR","name":"Goose Bay Airport","city":"Goose Bay","country":"CA","lat":53.3191986084,"lng":-60.4258003235},{"code":"YYT","name":"St. John's International Airport","city":"St. John's","country":"CA","lat":47.618599,"lng":-52.7519},{"code":"YYY","name":"Mont Joli Airport","city":"Mont-Joli","country":"CA","lat":48.60860061645508,"lng":-68.20809936523438},{"code":"YYZ","name":"Toronto Pearson International Airport","city":"Toronto","country":"CA","lat":43.675935,"lng":-79.629421},{"code":"YZF","name":"Yellowknife International Airport","city":"Yellowknife","country":"CA","lat":62.462799,"lng":-114.440002},{"code":"YZG","name":"Salluit Airport","city":"Salluit","country":"CA","lat":62.179401,"lng":-75.667198},{"code":"YZP","name":"Sandspit Airport","city":"Sandspit","country":"CA","lat":53.25429916379999,"lng":-131.813995361},{"code":"YZS","name":"Coral Harbour Airport","city":"Coral Harbour","country":"CA","lat":64.1932983398,"lng":-83.3593978882},{"code":"YZT","name":"Port Hardy Airport","city":"Port Hardy","country":"CA","lat":50.680599212646484,"lng":-127.36699676513672},{"code":"YZU","name":"Whitecourt Airport","city":"Whitecourt","country":"CA","lat":54.143902,"lng":-115.787003},{"code":"YZV","name":"Sept-Îles Airport","city":"Sept-Îles","country":"CA","lat":50.22330093383789,"lng":-66.2656021118164},{"code":"ILF","name":"Ilford Airport","city":"Ilford","country":"CA","lat":56.051622,"lng":-95.61882},{"code":"ZBF","name":"Bathurst Airport","city":"South Tetagouche","country":"CA","lat":47.6297,"lng":-65.738899},{"code":"ZEM","name":"Eastmain River Airport","city":"Eastmain River","country":"CA","lat":52.226398,"lng":-78.522499},{"code":"ZFD","name":"Fond-du-Lac Airport","city":"Fond-du-Lac","country":"CA","lat":59.3344,"lng":-107.181999},{"code":"XPK","name":"Pukatawagan Airport","city":"Pukatawagan","country":"CA","lat":55.749199,"lng":-101.265999},{"code":"ZFM","name":"Fort Mcpherson Airport","city":"Fort Mcpherson","country":"CA","lat":67.406993,"lng":-134.860182},{"code":"ZFN","name":"Tulita Airport","city":"Tulita","country":"CA","lat":64.909496,"lng":-125.570297},{"code":"ZGI","name":"Gods River Airport","city":"Gods River","country":"CA","lat":54.839699,"lng":-94.078598},{"code":"ZKE","name":"Kashechewan Airport","city":"Kashechewan","country":"CA","lat":52.282501,"lng":-81.677803},{"code":"MSA","name":"Muskrat Dam Airport","city":"Muskrat Dam","country":"CA","lat":53.441399,"lng":-91.762802},{"code":"ZMT","name":"Masset Airport","city":"Masset","country":"CA","lat":54.0275,"lng":-132.125},{"code":"ZPB","name":"Sachigo Lake Airport","city":"Sachigo Lake","country":"CA","lat":53.890885,"lng":-92.195935},{"code":"ZRJ","name":"Round Lake (Weagamow Lake) Airport","city":"Round Lake","country":"CA","lat":52.9436,"lng":-91.312798},{"code":"ZSJ","name":"Sandy Lake Airport","city":"Sandy Lake","country":"CA","lat":53.06420135498047,"lng":-93.34439849853516},{"code":"XSI","name":"South Indian Lake Airport","city":"South Indian Lake","country":"CA","lat":56.792801,"lng":-98.907204},{"code":"YDV","name":"Bloodvein River Airport","city":"Bloodvein River","country":"CA","lat":51.784568,"lng":-96.692305},{"code":"ZTM","name":"Shamattawa Airport","city":"Shamattawa","country":"CA","lat":55.86356,"lng":-92.081137},{"code":"ZUM","name":"Churchill Falls Airport","city":"Churchill Falls","country":"CA","lat":53.5619010925293,"lng":-64.10639953613281},{"code":"XLB","name":"Lac Brochet Airport","city":"Lac Brochet","country":"CA","lat":58.614279,"lng":-101.468954},{"code":"ZWL","name":"Wollaston Lake Airport","city":"Wollaston Lake","country":"CA","lat":58.106899,"lng":-103.171997},{"code":"BJA","name":"Soummam–Abane Ramdane Airport","city":"Béjaïa","country":"DZ","lat":36.712547,"lng":5.069909},{"code":"ALG","name":"Houari Boumediene Airport","city":"Algiers","country":"DZ","lat":36.693886,"lng":3.214531},{"code":"DJG","name":"Tiska Djanet Airport","city":"Djanet","country":"DZ","lat":24.285448,"lng":9.463652},{"code":"VVZ","name":"Illizi Takhamalt Airport","city":"Illizi","country":"DZ","lat":26.723499,"lng":8.62265},{"code":"QSF","name":"Ain Arnat Airport","city":"Sétif","country":"DZ","lat":36.178488,"lng":5.329909},{"code":"TMR","name":"Aguenar – Hadj Bey Akhamok Airport","city":"Tamanrasset","country":"DZ","lat":22.810992,"lng":5.450832},{"code":"GJL","name":"Jijel Ferhat Abbas Airport","city":"Tahir","country":"DZ","lat":36.794125,"lng":5.873652},{"code":"AAE","name":"Annaba Rabah Bitat Airport","city":"Annaba","country":"DZ","lat":36.826781,"lng":7.81334},{"code":"CZL","name":"Mohamed Boudiaf International Airport","city":"Constantine","country":"DZ","lat":36.276001,"lng":6.62039},{"code":"TEE","name":"Cheikh Larbi Tébessi Airport","city":"Tébessi","country":"DZ","lat":35.4315986633,"lng":8.12071990967},{"code":"BLJ","name":"Batna Mostefa Ben Boulaid Airport","city":"Batna","country":"DZ","lat":35.752102,"lng":6.30859},{"code":"TIN","name":"Tindouf Airport","city":"Tindouf","country":"DZ","lat":27.7003993988,"lng":-8.167099952700001},{"code":"CFK","name":"Chlef Aboubakr Belkaid International Airport","city":"Chlef","country":"DZ","lat":36.216614,"lng":1.341114},{"code":"TLM","name":"Zenata – Messali El Hadj Airport","city":"Zenata","country":"DZ","lat":35.012732,"lng":-1.457117},{"code":"ORN","name":"Oran Es-Sénia (Ahmed Ben Bella) International Airport","city":"Es-Sénia","country":"DZ","lat":35.620648,"lng":-0.622486},{"code":"CBH","name":"Béchar Boudghene Ben Ali Lotfi Airport","city":"Béchar","country":"DZ","lat":31.645700454711914,"lng":-2.269860029220581},{"code":"BMW","name":"Bordj Badji Mokhtar Airport","city":"Bordj Badji Mokhtar","country":"DZ","lat":21.3778,"lng":0.92698},{"code":"AZR","name":"Touat-Cheikh Sidi Mohamed Belkebir Airport","city":"Adrar","country":"DZ","lat":27.837601,"lng":-0.186414},{"code":"BSK","name":"Biskra - Mohamed Khider Airport","city":"Biskra","country":"DZ","lat":34.79318,"lng":5.738899},{"code":"ELG","name":"El Golea Airport","city":"El Menia","country":"DZ","lat":30.580732,"lng":2.861595},{"code":"GHA","name":"Noumérat - Moufdi Zakaria Airport","city":"El Atteuf","country":"DZ","lat":32.384102,"lng":3.79411},{"code":"HME","name":"Hassi Messaoud-Oued Irara Krim Belkacem Airport","city":"Hassi Messaoud","country":"DZ","lat":31.673,"lng":6.14044},{"code":"INZ","name":"In Salah Airport","city":"In Salah","country":"DZ","lat":27.250999,"lng":2.51202},{"code":"TGR","name":"Touggourt Sidi Madhi Airport","city":"Touggourt","country":"DZ","lat":33.067799,"lng":6.08867},{"code":"ELU","name":"Guemar Airport - مطار قمار بالوادي","city":"Guemar","country":"DZ","lat":33.511398,"lng":6.77679},{"code":"TMX","name":"Timimoun Airport","city":"Timimoun","country":"DZ","lat":29.237101,"lng":0.276033},{"code":"OGX","name":"Ain Beida Airport","city":"Ouargla","country":"DZ","lat":31.9172,"lng":5.41278},{"code":"IAM","name":"Zarzaitine - In Aménas Airport","city":"In Aménas","country":"DZ","lat":28.0515,"lng":9.64291},{"code":"COO","name":"Cotonou Cadjehoun International Airport","city":"Cotonou","country":"BJ","lat":6.35723,"lng":2.38435},{"code":"DEX","name":"Nop Goliat Dekai Airport","city":"Dekai","country":"ID","lat":-4.8557,"lng":139.482006},{"code":"OUA","name":"Ouagadougou Thomas Sankara International Airport","city":"Ouagadougou","country":"BF","lat":12.3532,"lng":-1.51242},{"code":"BOY","name":"Bobo Dioulasso Airport","city":"Bobo Dioulasso","country":"BF","lat":11.1601,"lng":-4.33097},{"code":"ACC","name":"Kotoka International Airport","city":"Accra","country":"GH","lat":5.605189800262451,"lng":-0.16678600013256073},{"code":"TML","name":"Yakubu Tali International Airport","city":"Tamale","country":"GH","lat":9.553909,"lng":-0.86606},{"code":"KMS","name":"Prempeh I International Airport","city":"Kumasi","country":"GH","lat":6.71456,"lng":-1.59082},{"code":"NYI","name":"Sunyani Airport","city":"Sunyani","country":"GH","lat":7.36183,"lng":-2.32876},{"code":"TKD","name":"Takoradi Airport","city":"Sekondi-Takoradi","country":"GH","lat":4.896059989929199,"lng":-1.7747600078582764},{"code":"DHB","name":"Deer Harbor SPB","city":"Deer Harbor","country":"US","lat":48.616667,"lng":-123.002778},{"code":"ABJ","name":"Félix-Houphouët-Boigny International Airport","city":"Abidjan","country":"CI","lat":5.26139,"lng":-3.92629},{"code":"BYK","name":"Bouaké Airport","city":"Bouaké","country":"CI","lat":7.7388,"lng":-5.07367},{"code":"HGO","name":"Korhogo Airport","city":"Korhogo","country":"CI","lat":9.38718,"lng":-5.55666},{"code":"SPY","name":"San Pedro Airport","city":"","country":"CI","lat":4.74672,"lng":-6.66082},{"code":"DLR","name":"Dalnerechensk Airport","city":"Dalnerechensk","country":"RU","lat":45.879857,"lng":133.737629},{"code":"DIO","name":"Diomede Heliport","city":"Diomede","country":"US","lat":65.758367,"lng":-168.953676},{"code":"DKA","name":"Umaru Musa Yar'adua Airport","city":"Katsina","country":"NG","lat":13.0078,"lng":7.66045},{"code":"ABV","name":"Nnamdi Azikiwe International Airport","city":"Abuja","country":"NG","lat":9.00679,"lng":7.26317},{"code":"QUO","name":"Akwa Ibom International Airport","city":"Uyo","country":"NG","lat":4.8725,"lng":8.093},{"code":"AKR","name":"Akure Airport","city":"Akure","country":"NG","lat":7.246739864349365,"lng":5.3010101318359375},{"code":"ABB","name":"Asaba International Airport","city":"Asaba","country":"NG","lat":6.204167,"lng":6.665278},{"code":"BCU","name":"Sir Abubakar Tafawa Balewa Bauchi State International Airport","city":"Bauchi","country":"NG","lat":10.482833,"lng":9.744},{"code":"BNI","name":"Benin Airport","city":"Benin","country":"NG","lat":6.316979885101318,"lng":5.5995001792907715},{"code":"CBQ","name":"Margaret Ekpo International Airport","city":"Calabar","country":"NG","lat":4.976019859313965,"lng":8.347200393676758},{"code":"ENU","name":"Akanu Ibiam International Airport","city":"Enegu","country":"NG","lat":6.473718,"lng":7.560463},{"code":"GMO","name":"Gombe Lawanti International Airport","city":"Gombe","country":"NG","lat":10.298889,"lng":10.9},{"code":"IBA","name":"Ibadan Airport","city":"Ibadan","country":"NG","lat":7.362460136413574,"lng":3.97832989692688},{"code":"ILR","name":"General Tunde Idiagbon International Airport","city":"Ilorin/Ogbomosho","country":"NG","lat":8.44021,"lng":4.49392},{"code":"QOW","name":"Sam Mbakwe International Cargo Airport","city":"Owerri","country":"NG","lat":5.42706,"lng":7.20603},{"code":"JOS","name":"Yakubu Gowon Airport","city":"Jos","country":"NG","lat":9.639829635620117,"lng":8.869050025939941},{"code":"KAD","name":"Kaduna International Airport","city":"Kaduna","country":"NG","lat":10.696,"lng":7.32011},{"code":"KAN","name":"Mallam Aminu Kano International Airport","city":"Kano","country":"NG","lat":12.045613,"lng":8.523566},{"code":"MIU","name":"Maiduguri International Airport","city":"Maiduguri","country":"NG","lat":11.854163,"lng":13.080702},{"code":"MDI","name":"Makurdi Airport","city":"Makurdi","country":"NG","lat":7.70388,"lng":8.61394},{"code":"LOS","name":"Murtala Muhammed International Airport","city":"Lagos","country":"NG","lat":6.57737,"lng":3.32116},{"code":"PHC","name":"Port Harcourt International Airport","city":"Port Harcourt","country":"NG","lat":5.01549,"lng":6.94959},{"code":"SKO","name":"Sadiq Abubakar III International Airport","city":"Sokoto","country":"NG","lat":12.915729,"lng":5.207547},{"code":"QRW","name":"Warri Airport","city":"Okpe","country":"NG","lat":5.59821,"lng":5.8187},{"code":"YOL","name":"Yola Airport","city":"Yola","country":"NG","lat":9.257550239562988,"lng":12.430399894714355},{"code":"DPT","name":"Deputatskiy Airport","city":"Deputatskiy","country":"RU","lat":69.392503,"lng":139.890012},{"code":"NIM","name":"Diori Hamani International Airport","city":"Niamey","country":"NE","lat":13.4815,"lng":2.18361},{"code":"ZND","name":"Zinder Airport","city":"Zinder","country":"NE","lat":13.779000282287598,"lng":8.983759880065918},{"code":"TBJ","name":"Tabarka-Aïn Draham International Airport","city":"Tabarka","country":"TN","lat":36.98,"lng":8.87694},{"code":"MIR","name":"Monastir Habib Bourguiba International Airport","city":"Monastir","country":"TN","lat":35.75809860229492,"lng":10.75469970703125},{"code":"NBE","name":"Enfidha - Hammamet International Airport","city":"Enfidha","country":"TN","lat":36.075833,"lng":10.438611},{"code":"TUN","name":"Tunis Carthage International Airport","city":"Tunis","country":"TN","lat":36.851002,"lng":10.2272},{"code":"GAF","name":"Gafsa Ksar International Airport","city":"Gafsa","country":"TN","lat":34.422000885009766,"lng":8.822500228881836},{"code":"GAE","name":"Gabès Matmata International Airport","city":"Gabès","country":"TN","lat":33.733691,"lng":9.91941},{"code":"DJE","name":"Djerba Zarzis International Airport","city":"Mellita","country":"TN","lat":33.873719,"lng":10.7773},{"code":"SFA","name":"Sfax Thyna International Airport","city":"Sfax","country":"TN","lat":34.717999,"lng":10.691},{"code":"TOE","name":"Tozeur Nefta International Airport","city":"Tozeur","country":"TN","lat":33.939701080322266,"lng":8.110560417175293},{"code":"DWO","name":"Diyawanna Oya Seaplane Base","city":"Sri Jayawardenepura Kotte","country":"LK","lat":6.906585,"lng":79.909},{"code":"LFW","name":"Lomé–Tokoin International Airport","city":"Lomé","country":"TG","lat":6.16561,"lng":1.25451},{"code":"ANR","name":"Antwerp International Airport (Deurne)","city":"Antwerp","country":"BE","lat":51.190667,"lng":4.463153},{"code":"BRU","name":"Brussels Airport","city":"Zaventem","country":"BE","lat":50.901402,"lng":4.48444},{"code":"CRL","name":"Brussels South Charleroi Airport","city":"Charleroi","country":"BE","lat":50.461963,"lng":4.459562},{"code":"LGG","name":"Liège Airport","city":"Grâce-Hollogne","country":"BE","lat":50.638574,"lng":5.443897},{"code":"OST","name":"Ostend-Bruges International Airport","city":"Oostende","country":"BE","lat":51.1998,"lng":2.874673},{"code":"EDA","name":"Edna Bay Seaplane Base","city":"Edna Bay","country":"US","lat":55.949653,"lng":-133.661012},{"code":"HDF","name":"Heringsdorf Airport","city":"Zirchow","country":"DE","lat":53.8787,"lng":14.1523},{"code":"BER","name":"Berlin Brandenburg Airport","city":"Berlin","country":"DE","lat":52.361738,"lng":13.502341},{"code":"DRS","name":"Dresden Airport","city":"Dresden","country":"DE","lat":51.134123,"lng":13.767831},{"code":"ERF","name":"Erfurt-Weimar Airport","city":"Erfurt","country":"DE","lat":50.978281,"lng":10.960713},{"code":"FRA","name":"Frankfurt Main Airport","city":"Frankfurt am Main","country":"DE","lat":50.026706,"lng":8.55835},{"code":"FMO","name":"Münster Osnabrück Airport","city":"Greven","country":"DE","lat":52.133816,"lng":7.688482},{"code":"HAM","name":"Hamburg Helmut Schmidt Airport","city":"Hamburg","country":"DE","lat":53.630402,"lng":9.98823},{"code":"CGN","name":"Cologne Bonn Airport","city":"Köln (Cologne)","country":"DE","lat":50.865898,"lng":7.14274},{"code":"DUS","name":"Düsseldorf Airport","city":"Düsseldorf","country":"DE","lat":51.289501,"lng":6.76678},{"code":"MUC","name":"Munich Airport","city":"Munich","country":"DE","lat":48.353802,"lng":11.7861},{"code":"NUE","name":"Nuremberg Airport","city":"Nuremberg","country":"DE","lat":49.498699,"lng":11.078056},{"code":"LEJ","name":"Leipzig/Halle Airport","city":"Schkeuditz","country":"DE","lat":51.420657,"lng":12.232705},{"code":"SCN","name":"Saarbrücken Airport","city":"Saarbrücken","country":"DE","lat":49.214545,"lng":7.109735},{"code":"STR","name":"Stuttgart Airport","city":"Stuttgart","country":"DE","lat":48.689899,"lng":9.22196},{"code":"HAJ","name":"Hannover Airport","city":"Hannover","country":"DE","lat":52.461102,"lng":9.68508},{"code":"BRE","name":"Bremen Airport","city":"Bremen","country":"DE","lat":53.046786,"lng":8.78932},{"code":"HHN","name":"Frankfurt-Hahn Airport","city":"Frankfurt am Main (Lautzenhausen)","country":"DE","lat":49.946353,"lng":7.261734},{"code":"MHG","name":"Mannheim-City Airport","city":"Mannheim","country":"DE","lat":49.473057,"lng":8.514167},{"code":"LBC","name":"Lübeck Blankensee Airport","city":"Lübeck","country":"DE","lat":53.805401,"lng":10.7192},{"code":"FMM","name":"Memmingen Allgau Airport","city":"Memmingen","country":"DE","lat":47.988092,"lng":10.238248},{"code":"PAD","name":"Paderborn Lippstadt Airport","city":"Büren","country":"DE","lat":51.612527,"lng":8.617459},{"code":"NRN","name":"Weeze (Niederrhein) Airport","city":"Weeze","country":"DE","lat":51.601362,"lng":6.141228},{"code":"DTM","name":"Dortmund Airport","city":"Dortmund","country":"DE","lat":51.518299,"lng":7.61224},{"code":"FDH","name":"Bodensee Airport Friedrichshafen","city":"Friedrichshafen","country":"DE","lat":47.671299,"lng":9.51149},{"code":"FKB","name":"Karlsruhe Baden-Baden Airport","city":"Rheinmünster","country":"DE","lat":48.7794,"lng":8.0805},{"code":"KSF","name":"Kassel Airport","city":"Calden","country":"DE","lat":51.418395,"lng":9.391638},{"code":"EME","name":"Emden Airport","city":"Emden","country":"DE","lat":53.391109,"lng":7.2275},{"code":"AGE","name":"Wangerooge Airport","city":"Wangerooge","country":"DE","lat":53.782544,"lng":7.919584},{"code":"JUI","name":"Juist Airport","city":"Juist","country":"DE","lat":53.681421,"lng":7.056398},{"code":"BMK","name":"Borkum Airport","city":"Borkum","country":"DE","lat":53.59639,"lng":6.709167},{"code":"NOD","name":"Norden-Norddeich Airport","city":"Norddeich","country":"DE","lat":53.633057,"lng":7.190278},{"code":"NRD","name":"Norderney Airport","city":"Norderney","country":"DE","lat":53.706944,"lng":7.23},{"code":"BMR","name":"Baltrum Airport","city":"Baltrum","country":"DE","lat":53.72485,"lng":7.373257},{"code":"HEI","name":"Heide-Büsum Airport","city":"Oesterdeichstrich","country":"DE","lat":54.153332,"lng":8.901667},{"code":"HGL","name":"Helgoland-Düne Airport","city":"Helgoland","country":"DE","lat":54.18528,"lng":7.915833},{"code":"GWT","name":"Westerland Sylt Airport","city":"Sylt","country":"DE","lat":54.9132,"lng":8.34047},{"code":"CHE","name":"Tallinn Linnahall Heliport","city":"Tallinn","country":"EE","lat":59.448015,"lng":24.753313},{"code":"KDL","name":"Kärdla Airport","city":"Kärdla","country":"EE","lat":58.99079895019531,"lng":22.830699920654297},{"code":"URE","name":"Kuressaare Airport","city":"Kuressaare","country":"EE","lat":58.22990036010742,"lng":22.50950050354004},{"code":"EPU","name":"Pärnu Airport","city":"Pärnu","country":"EE","lat":58.41899871826172,"lng":24.47279930114746},{"code":"TLL","name":"Lennart Meri Tallinn Airport","city":"Tallinn","country":"EE","lat":59.413246,"lng":24.83264},{"code":"TAY","name":"Tartu Airport","city":"Tartu","country":"EE","lat":58.307438,"lng":26.686473},{"code":"ENF","name":"Enontekio Airport","city":"Enontekio","country":"FI","lat":68.362602233887,"lng":23.424299240112},{"code":"HEL","name":"Helsinki Vantaa Airport","city":"Helsinki (Vantaa)","country":"FI","lat":60.318363,"lng":24.963341},{"code":"IVL","name":"Ivalo Airport","city":"Ivalo","country":"FI","lat":68.6073,"lng":27.4053},{"code":"JOE","name":"Joensuu Airport","city":"Joensuu","country":"FI","lat":62.658815,"lng":29.619398},{"code":"JYV","name":"Jyväskylä Airport","city":"Jyväskylän Maalaiskunta","country":"FI","lat":62.399502,"lng":25.678301},{"code":"KEM","name":"Kemi-Tornio Airport","city":"Kemi / Tornio","country":"FI","lat":65.778701782227,"lng":24.582099914551},{"code":"KAJ","name":"Kajaani Airport","city":"Kajaani","country":"FI","lat":64.2855,"lng":27.6924},{"code":"KOK","name":"Kokkola-Pietarsaari Airport","city":"Kokkola / Kruunupyy","country":"FI","lat":63.721199,"lng":23.143101},{"code":"KAO","name":"Kuusamo Airport","city":"Kuusamo","country":"FI","lat":65.987602,"lng":29.239401},{"code":"KTT","name":"Kittilä International Airport","city":"Kittilä","country":"FI","lat":67.700996,"lng":24.8468},{"code":"KUO","name":"Kuopio Airport","city":"Kuopio / Siilinjärvi","country":"FI","lat":63.007099,"lng":27.7978},{"code":"LPP","name":"Lappeenranta Airport","city":"Lappeenranta","country":"FI","lat":61.044601,"lng":28.144743},{"code":"MHQ","name":"Mariehamn Airport","city":"Mariehamn","country":"FI","lat":60.1222,"lng":19.898199},{"code":"OUL","name":"Oulu Airport","city":"Oulu / Oulunsalo","country":"FI","lat":64.930099,"lng":25.354601},{"code":"POR","name":"Pori Airport","city":"Pori","country":"FI","lat":61.4617,"lng":21.799999},{"code":"RVN","name":"Rovaniemi Airport","city":"Rovaniemi","country":"FI","lat":66.563327,"lng":25.829751},{"code":"SVL","name":"Savonlinna Airport","city":"Savonlinna","country":"FI","lat":61.9431,"lng":28.945101},{"code":"TMP","name":"Tampere-Pirkkala Airport","city":"Tampere / Pirkkala","country":"FI","lat":61.414101,"lng":23.604401},{"code":"TKU","name":"Turku Airport","city":"Turku","country":"FI","lat":60.514099,"lng":22.2628},{"code":"VAA","name":"Vaasa Airport","city":"Vaasa","country":"FI","lat":63.05023,"lng":21.762543},{"code":"BFS","name":"Belfast International Airport","city":"Belfast","country":"GB","lat":54.6575012207,"lng":-6.2158298492399995},{"code":"BHD","name":"George Best Belfast City Airport","city":"Belfast","country":"GB","lat":54.618099,"lng":-5.8725},{"code":"LDY","name":"City of Derry Airport","city":"Derry, Derry and Strabane","country":"GB","lat":55.042801,"lng":-7.16111},{"code":"BHX","name":"Birmingham Airport","city":"Birmingham, West Midlands","country":"GB","lat":52.453899,"lng":-1.74803},{"code":"MAN","name":"Manchester Airport","city":"Manchester, Greater Manchester","country":"GB","lat":53.349375,"lng":-2.279521},{"code":"CAL","name":"Campbeltown Airport","city":"Campbeltown","country":"GB","lat":55.437199,"lng":-5.68639},{"code":"EOI","name":"Eday Airport","city":"Eday","country":"GB","lat":59.19060134887695,"lng":-2.7722198963165283},{"code":"FIE","name":"Fair Isle Airport","city":"Fair Isle","country":"GB","lat":59.534721,"lng":-1.628509},{"code":"COL","name":"Coll Airport","city":"Coll Island","country":"GB","lat":56.601898,"lng":-6.61778},{"code":"NRL","name":"North Ronaldsay Airport","city":"North Ronaldsay","country":"GB","lat":59.3675003052,"lng":-2.43443989754},{"code":"OBN","name":"Oban Airport","city":"North Connel","country":"GB","lat":56.463501,"lng":-5.39967},{"code":"PPW","name":"Papa Westray Airport","city":"Papa Westray, Orkney Islands","country":"GB","lat":59.351012,"lng":-2.900353},{"code":"SOY","name":"Stronsay Airport","city":"Stronsay","country":"GB","lat":59.1553,"lng":-2.64139},{"code":"NDY","name":"Sanday Airport","city":"Sanday","country":"GB","lat":59.250301,"lng":-2.57667},{"code":"LWK","name":"Lerwick / Tingwall Airport","city":"Lerwick, Shetland Islands","country":"GB","lat":60.1922,"lng":-1.24361},{"code":"WRY","name":"Westray Airport","city":"Westray, Orkney Islands","country":"GB","lat":59.350475,"lng":-2.950062},{"code":"CSA","name":"Colonsay Airstrip","city":"Colonsay","country":"GB","lat":56.057499,"lng":-6.24306},{"code":"CWL","name":"Cardiff International Airport","city":"Cardiff","country":"GB","lat":51.396702,"lng":-3.34333},{"code":"BRS","name":"Bristol Airport","city":"Bristol","country":"GB","lat":51.382326,"lng":-2.716453},{"code":"LPL","name":"Liverpool John Lennon Airport","city":"Liverpool","country":"GB","lat":53.334863,"lng":-2.849637},{"code":"LTN","name":"London Luton Airport","city":"Luton, Luton","country":"GB","lat":51.874699,"lng":-0.368333},{"code":"LEQ","name":"Land's End Airport","city":"Land's End, Cornwall","country":"GB","lat":50.102798,"lng":-5.67056},{"code":"ISC","name":"St. Mary's Airport","city":"St. Mary's, Isles of Scilly","country":"GB","lat":49.913375,"lng":-6.291894},{"code":"BOH","name":"Bournemouth Airport","city":"Bournemouth","country":"GB","lat":50.780483,"lng":-1.839576},{"code":"SOU","name":"Southampton Airport","city":"Southampton","country":"GB","lat":50.950298,"lng":-1.3568},{"code":"NQY","name":"Cornwall Airport Newquay","city":"Newquay","country":"GB","lat":50.440601,"lng":-4.99541},{"code":"ACI","name":"Alderney Airport","city":"Saint Anne","country":"GG","lat":49.706104,"lng":-2.21472},{"code":"GCI","name":"Guernsey Airport","city":"Saint Peter Port","country":"GG","lat":49.435001,"lng":-2.60197},{"code":"JER","name":"Jersey Airport","city":"St. Peter","country":"JE","lat":49.207901,"lng":-2.19551},{"code":"LGW","name":"London Gatwick Airport","city":"London","country":"GB","lat":51.148744,"lng":-0.185739},{"code":"LCY","name":"London City Airport","city":"London","country":"GB","lat":51.505299,"lng":0.055278},{"code":"LHR","name":"London Heathrow Airport","city":"London","country":"GB","lat":51.470748,"lng":-0.459909},{"code":"SEN","name":"London Southend Airport","city":"Southend-on-Sea, Essex","country":"GB","lat":51.570562,"lng":0.693627},{"code":"HUY","name":"Humberside Airport","city":"Grimsby, Lincolnshire","country":"GB","lat":53.576152,"lng":-0.34954},{"code":"LBA","name":"Leeds Bradford Airport","city":"Leeds, West Yorkshire","country":"GB","lat":53.865898,"lng":-1.66057},{"code":"IOM","name":"Isle of Man Airport","city":"Castletown","country":"IM","lat":54.083081,"lng":-4.623871},{"code":"NCL","name":"Newcastle International Airport","city":"Newcastle upon Tyne, Tyne and Wear","country":"GB","lat":55.037958,"lng":-1.689577},{"code":"MME","name":"Teesside International Airport","city":"Darlington, Durham","country":"GB","lat":54.509201,"lng":-1.42941},{"code":"EMA","name":"East Midlands Airport","city":"Nottingham, Leicestershire","country":"GB","lat":52.8311,"lng":-1.32806},{"code":"KOI","name":"Kirkwall Airport","city":"Kirkwall, Orkney Islands","country":"GB","lat":58.957903,"lng":-2.905077},{"code":"LSI","name":"Sumburgh Airport","city":"Lerwick, Shetland","country":"GB","lat":59.878899,"lng":-1.29556},{"code":"WIC","name":"Wick John O'Groats Airport","city":"Wick","country":"GB","lat":58.4589,"lng":-3.09306},{"code":"ABZ","name":"Aberdeen International Airport","city":"Aberdeen","country":"GB","lat":57.2019,"lng":-2.19778},{"code":"INV","name":"Inverness Airport","city":"Inverness","country":"GB","lat":57.5425,"lng":-4.0475},{"code":"GLA","name":"Glasgow Airport","city":"Glasgow","country":"GB","lat":55.871899,"lng":-4.43306},{"code":"EDI","name":"Edinburgh Airport","city":"Edinburgh","country":"GB","lat":55.950145,"lng":-3.372288},{"code":"ILY","name":"Islay Airport","city":"Isle of Islay, Argyll and Bute","country":"GB","lat":55.682708,"lng":-6.257525},{"code":"PIK","name":"Glasgow Prestwick Airport","city":"Prestwick, South Ayrshire","country":"GB","lat":55.501499,"lng":-4.577182},{"code":"BEB","name":"Benbecula Airport","city":"Balivanich","country":"GB","lat":57.481098,"lng":-7.36278},{"code":"DND","name":"Dundee Airport","city":"Dundee","country":"GB","lat":56.452499,"lng":-3.02583},{"code":"SYY","name":"Stornoway Airport","city":"Stornoway, Western Isles","country":"GB","lat":58.215599,"lng":-6.33111},{"code":"BRR","name":"Barra Airport","city":"Eoligarry","country":"GB","lat":57.0228,"lng":-7.44306},{"code":"TRE","name":"Tiree Airport","city":"Balemartine, Argyll and Bute","country":"GB","lat":56.499199,"lng":-6.86917},{"code":"NWI","name":"Norwich Airport","city":"Norwich, Norfolk","country":"GB","lat":52.6758,"lng":1.28278},{"code":"STN","name":"London Stansted Airport","city":"London, Essex","country":"GB","lat":51.884998,"lng":0.235},{"code":"EXT","name":"Exeter International Airport","city":"Exeter, Devon","country":"GB","lat":50.734261,"lng":-3.413984},{"code":"MPN","name":"Mount Pleasant Airport / RAF Mount Pleasant","city":"Mount Pleasant","country":"FK","lat":-51.822642,"lng":-58.445777},{"code":"AMS","name":"Amsterdam Airport Schiphol","city":"Amsterdam","country":"NL","lat":52.308601,"lng":4.76389},{"code":"MST","name":"Maastricht Aachen Airport","city":"Maastricht","country":"NL","lat":50.911087,"lng":5.769401},{"code":"EIN","name":"Eindhoven Airport","city":"Eindhoven","country":"NL","lat":51.4500999451,"lng":5.37452983856},{"code":"GRQ","name":"Groningen Airport Eelde","city":"Groningen","country":"NL","lat":53.119107,"lng":6.577652},{"code":"RTM","name":"Rotterdam The Hague Airport","city":"Rotterdam","country":"NL","lat":51.956902,"lng":4.43722},{"code":"NNR","name":"Connemara Regional Airport","city":"Inverin","country":"IE","lat":53.23030090332031,"lng":-9.467780113220215},{"code":"ORK","name":"Cork International Airport","city":"Cork","country":"IE","lat":51.841301,"lng":-8.49111},{"code":"CFN","name":"Donegal Airport","city":"Donegal","country":"IE","lat":55.0442008972168,"lng":-8.340999603271484},{"code":"DUB","name":"Dublin Airport","city":"Dublin","country":"IE","lat":53.428713,"lng":-6.262121},{"code":"IOR","name":"Inishmore Aerodrome","city":"Inis Mór","country":"IE","lat":53.1067008972168,"lng":-9.653610229492188},{"code":"INQ","name":"Inisheer Aerodrome","city":"Inis Oírr","country":"IE","lat":53.064701,"lng":-9.5109},{"code":"NOC","name":"Ireland West Airport Knock","city":"Charlestown","country":"IE","lat":53.910439,"lng":-8.817002},{"code":"KIR","name":"Kerry Airport","city":"Farranfore","country":"IE","lat":52.180901,"lng":-9.52378},{"code":"IIA","name":"Inishmaan Aerodrome","city":"Inis Meáin","country":"IE","lat":53.092999,"lng":-9.56806},{"code":"SNN","name":"Shannon Airport","city":"Shannon","country":"IE","lat":52.702,"lng":-8.92482},{"code":"EJT","name":"Enejit Airport","city":"Enejit Island","country":"MH","lat":6.0404,"lng":171.9846},{"code":"AAR","name":"Aarhus Airport","city":"Aarhus","country":"DK","lat":56.303331,"lng":10.618286},{"code":"BLL","name":"Billund Airport","city":"Billund","country":"DK","lat":55.740335,"lng":9.157019},{"code":"CPH","name":"Copenhagen Kastrup Airport","city":"Copenhagen","country":"DK","lat":55.617900848389,"lng":12.656000137329},{"code":"EBJ","name":"Esbjerg Airport","city":"Esbjerg","country":"DK","lat":55.525902,"lng":8.5534},{"code":"KRP","name":"Midtjyllands Airport / Air Base Karup","city":"Karup","country":"DK","lat":56.297139,"lng":9.104311},{"code":"BYR","name":"Læsø Airport","city":"Læsø","country":"DK","lat":57.277199,"lng":11.0001},{"code":"ODE","name":"Odense Hans Christian Andersen Airport","city":"Odense","country":"DK","lat":55.475307,"lng":10.327206},{"code":"RKE","name":"Copenhagen Roskilde Airport","city":"Roskilde","country":"DK","lat":55.585602,"lng":12.1314},{"code":"RNN","name":"Bornholm Airport","city":"Rønne","country":"DK","lat":55.063301,"lng":14.7596},{"code":"SGD","name":"Sønderborg Airport","city":"Sønderborg","country":"DK","lat":54.96440124511719,"lng":9.791729927062988},{"code":"FAE","name":"Vágar Airport","city":"Vágar","country":"FO","lat":62.063256,"lng":-7.275782},{"code":"AAL","name":"Aalborg Airport","city":"Aalborg","country":"DK","lat":57.094763,"lng":9.84993},{"code":"LUX","name":"Luxembourg-Findel International Airport","city":"Luxembourg","country":"LU","lat":49.626845,"lng":6.212134},{"code":"AES","name":"Ålesund Airport","city":"Ålesund","country":"NO","lat":62.560443,"lng":6.110845},{"code":"ANX","name":"Andøya Airport, Andenes","city":"Andenes","country":"NO","lat":69.295193,"lng":16.139379},{"code":"ALF","name":"Alta Airport","city":"Alta","country":"NO","lat":69.976097,"lng":23.3717},{"code":"FDE","name":"Førde Airport, Bringeland","city":"Førde","country":"NO","lat":61.391102,"lng":5.75694},{"code":"BNN","name":"Brønnøysund Airport, Brønnøy","city":"Brønnøy","country":"NO","lat":65.461098,"lng":12.2175},{"code":"BOO","name":"Bodø Airport","city":"Bodø","country":"NO","lat":67.269203,"lng":14.3653},{"code":"BGO","name":"Bergen Airport, Flesland","city":"Bergen","country":"NO","lat":60.2934,"lng":5.21814},{"code":"BJF","name":"Båtsfjord Airport","city":"Båtsfjord","country":"NO","lat":70.60025,"lng":29.692612},{"code":"BVG","name":"Berlevåg Airport","city":"Berlevåg","country":"NO","lat":70.871517,"lng":29.034097},{"code":"KRS","name":"Kristiansand Airport","city":"Kristiansand(Kjevik)","country":"NO","lat":58.204201,"lng":8.08537},{"code":"BDU","name":"Bardufoss Airport","city":"Målselv","country":"NO","lat":69.055801,"lng":18.5404},{"code":"EVE","name":"Harstad/Narvik Airport","city":"Evenes","country":"NO","lat":68.491302,"lng":16.678101},{"code":"FRO","name":"Florø Airport","city":"Florø","country":"NO","lat":61.583599090576,"lng":5.0247201919556},{"code":"OSL","name":"Oslo-Gardermoen International Airport","city":"Oslo (Gardermoen)","country":"NO","lat":60.193901,"lng":11.1004},{"code":"HAU","name":"Haugesund Airport, Karmøy","city":"Karmøy","country":"NO","lat":59.345299,"lng":5.20836},{"code":"HFT","name":"Hammerfest Airport","city":"Hammerfest","country":"NO","lat":70.679703,"lng":23.6686},{"code":"HAA","name":"Hasvik Airport","city":"Hasvik","country":"NO","lat":70.486702,"lng":22.1397},{"code":"HVG","name":"Honningsvåg Airport, Valan","city":"Honningsvåg","country":"NO","lat":71.009697,"lng":25.983601},{"code":"KSU","name":"Kristiansund Airport, Kvernberget","city":"Kvernberget","country":"NO","lat":63.111801,"lng":7.82452},{"code":"KKN","name":"Kirkenes Airport, Høybuktmoen","city":"Kirkenes","country":"NO","lat":69.7258,"lng":29.8913},{"code":"LKN","name":"Leknes Airport","city":"Leknes","country":"NO","lat":68.152496337891,"lng":13.609399795532},{"code":"MEH","name":"Mehamn Airport","city":"Mehamn","country":"NO","lat":71.029701,"lng":27.8267},{"code":"MOL","name":"Molde Airport, Årø","city":"Årø","country":"NO","lat":62.744701,"lng":7.2625},{"code":"MJF","name":"Mosjøen Airport, Kjærstad","city":"Mosjøen","country":"NO","lat":65.783997,"lng":13.2149},{"code":"LKL","name":"Lakselv Airport, Banak","city":"Lakselv","country":"NO","lat":70.068802,"lng":24.973499},{"code":"OSY","name":"Namsos Airport","city":"Namsos","country":"NO","lat":64.472198,"lng":11.5786},{"code":"OLA","name":"Ørland Airport","city":"Ørland","country":"NO","lat":63.69889831542969,"lng":9.604000091552734},{"code":"HOV","name":"Ørsta-Volda Airport, Hovden","city":"Ørsta","country":"NO","lat":62.180000305176,"lng":6.0741000175476},{"code":"MQN","name":"Mo i Rana Airport, Røssvoll","city":"Mo i Rana","country":"NO","lat":66.363899230957,"lng":14.301400184631},{"code":"RVK","name":"Rørvik Airport, Ryum","city":"Rørvik","country":"NO","lat":64.838302612305,"lng":11.14610004425},{"code":"RRS","name":"Røros Airport","city":"Røros","country":"NO","lat":62.578399658203,"lng":11.342300415039},{"code":"RET","name":"Røst Airport","city":"Røst","country":"NO","lat":67.526482,"lng":12.101575},{"code":"LYR","name":"Svalbard Airport, Longyear","city":"Longyearbyen","country":"NO","lat":78.246101,"lng":15.4656},{"code":"SDN","name":"Sandane Airport, Anda","city":"Sandane","country":"NO","lat":61.830002,"lng":6.10583},{"code":"SOG","name":"Sogndal Airport, Haukåsen","city":"Sogndal","country":"NO","lat":61.156101,"lng":7.13778},{"code":"SVJ","name":"Svolvær Airport, Helle","city":"Svolvær","country":"NO","lat":68.243301,"lng":14.6692},{"code":"SKN","name":"Stokmarknes Airport, Skagen","city":"Hadsel","country":"NO","lat":68.578827,"lng":15.033417},{"code":"SRP","name":"Stord Airport, Sørstokken","city":"Leirvik","country":"NO","lat":59.791901,"lng":5.34085},{"code":"SOJ","name":"Sørkjosen Airport","city":"Sørkjosen","country":"NO","lat":69.786797,"lng":20.9594},{"code":"VAW","name":"Vardø Airport, Svartnes","city":"Vardø","country":"NO","lat":70.3554,"lng":31.044901},{"code":"SSJ","name":"Sandnessjøen Airport, Stokka","city":"Alstahaug","country":"NO","lat":65.956802,"lng":12.4689},{"code":"TOS","name":"Tromsø Airport","city":"Tromsø","country":"NO","lat":69.683296,"lng":18.9189},{"code":"TRF","name":"Sandefjord Airport, Torp","city":"Sandefjord(Torp)","country":"NO","lat":59.186699,"lng":10.2586},{"code":"TRD","name":"Trondheim Airport, Værnes","city":"Trondheim","country":"NO","lat":63.457802,"lng":10.924},{"code":"VDS","name":"Vadsø Airport","city":"Vadsø","country":"NO","lat":70.0653,"lng":29.8447},{"code":"VRY","name":"Værøy Heliport","city":"Værøy","country":"NO","lat":67.654555,"lng":12.727257},{"code":"SVG","name":"Stavanger Airport, Sola","city":"Stavanger","country":"NO","lat":58.876701,"lng":5.63778},{"code":"BZG","name":"Ignacy Jan Paderewski Bydgoszcz Airport","city":"Bydgoszcz","country":"PL","lat":53.096802,"lng":17.977699},{"code":"GDN","name":"Gdańsk Lech Wałęsa Airport","city":"Gdańsk","country":"PL","lat":54.377602,"lng":18.4662},{"code":"KRK","name":"Kraków John Paul II International Airport","city":"Balice","country":"PL","lat":50.077702,"lng":19.7848},{"code":"KTW","name":"Katowice Wojciech Korfanty International Airport","city":"Katowice","country":"PL","lat":50.476015,"lng":19.080705},{"code":"LUZ","name":"Lublin Airport","city":"Lublin","country":"PL","lat":51.240157,"lng":22.713461},{"code":"LCJ","name":"Łódź Władysław Reymont Airport","city":"Łódź","country":"PL","lat":51.721901,"lng":19.3981},{"code":"WMI","name":"Warsaw Modlin Airport","city":"Nowy Dwór Mazowiecki","country":"PL","lat":52.451099,"lng":20.6518},{"code":"POZ","name":"Poznań-Ławica Airport","city":"Poznań","country":"PL","lat":52.421598,"lng":16.823359},{"code":"RDO","name":"Warsaw Radom Airport","city":"Radom","country":"PL","lat":51.38937,"lng":21.214742},{"code":"RZE","name":"Rzeszów-Jasionka Airport","city":"Jasionka","country":"PL","lat":50.109791,"lng":22.024155},{"code":"SZZ","name":"Solidarity Szczecin–Goleniów Airport","city":"Szczecin(Glewice)","country":"PL","lat":53.584702,"lng":14.9022},{"code":"SZY","name":"Olsztyn-Mazury Airport","city":"Szymany","country":"PL","lat":53.481899,"lng":20.9377},{"code":"WAW","name":"Warsaw Chopin Airport","city":"Warsaw","country":"PL","lat":52.165699,"lng":20.9671},{"code":"WRO","name":"Copernicus Wrocław Airport","city":"Wrocław","country":"PL","lat":51.103719,"lng":16.882096},{"code":"IEG","name":"Zielona Góra-Babimost Airport","city":"Nowe Kramsko","country":"PL","lat":52.1385,"lng":15.7986},{"code":"RNB","name":"Ronneby Airport","city":"Ronneby","country":"SE","lat":56.266701,"lng":15.265},{"code":"GOT","name":"Göteborg Landvetter Airport","city":"Göteborg","country":"SE","lat":57.6628,"lng":12.2798},{"code":"JKG","name":"Jönköping Airport","city":"Jönköping","country":"SE","lat":57.757598876953125,"lng":14.068699836730957},{"code":"THN","name":"Trollhättan-Vänersborg Airport","city":"Trollhättan","country":"SE","lat":58.31809997558594,"lng":12.345000267028809},{"code":"MXX","name":"Mora Airport","city":"Mora","country":"SE","lat":60.957901,"lng":14.5114},{"code":"NYO","name":"Stockholm Skavsta Airport","city":"Nyköping","country":"SE","lat":58.789656,"lng":16.911479},{"code":"SCR","name":"Scandinavian Mountains Airport","city":"Malung-Sälen","country":"SE","lat":61.16508,"lng":12.83352},{"code":"KLR","name":"Kalmar Airport","city":"Kalmar","country":"SE","lat":56.685501,"lng":16.2876},{"code":"MMX","name":"Malmö Sturup Airport","city":"Malmö","country":"SE","lat":55.535564,"lng":13.376327},{"code":"HAD","name":"Halmstad Airport","city":"Halmstad","country":"SE","lat":56.69110107421875,"lng":12.820199966430664},{"code":"VXO","name":"Växjö Kronoberg Airport","city":"Växjö","country":"SE","lat":56.929100036621094,"lng":14.727999687194824},{"code":"EVG","name":"Sveg Airport","city":"Sveg","country":"SE","lat":62.047798,"lng":14.4229},{"code":"GEV","name":"Gällivare Airport","city":"Gällivare","country":"SE","lat":67.13240051269531,"lng":20.814599990844727},{"code":"KRF","name":"Kramfors-Sollefteå Höga Kusten Airport","city":"Nyland","country":"SE","lat":63.048599,"lng":17.7689},{"code":"LYC","name":"Lycksele Airport","city":"Lycksele","country":"SE","lat":64.548302,"lng":18.7162},{"code":"SDL","name":"Sundsvall-Härnösand Airport","city":"Sundsvall/ Härnösand","country":"SE","lat":62.528099060058594,"lng":17.443899154663086},{"code":"OER","name":"Örnsköldsvik Airport","city":"Örnsköldsvik","country":"SE","lat":63.40829849243164,"lng":18.989999771118164},{"code":"KRN","name":"Kiruna Airport","city":"Kiruna","country":"SE","lat":67.821999,"lng":20.3368},{"code":"SFT","name":"Skellefteå Airport","city":"Skellefteå","country":"SE","lat":64.62480163574219,"lng":21.076900482177734},{"code":"UME","name":"Umeå Airport","city":"Umeå","country":"SE","lat":63.791801,"lng":20.282801},{"code":"VHM","name":"Vilhelmina South Lapland Airport","city":"Vilhelmina","country":"SE","lat":64.579102,"lng":16.833599},{"code":"AJR","name":"Arvidsjaur Airport","city":"Arvidsjaur","country":"SE","lat":65.59030151367188,"lng":19.28190040588379},{"code":"OSD","name":"Åre Östersund Airport","city":"Östersund","country":"SE","lat":63.193546,"lng":14.504168},{"code":"ORB","name":"Örebro Airport","city":"Örebro","country":"SE","lat":59.22370147705078,"lng":15.038000106811523},{"code":"HFS","name":"Hagfors Airport","city":"Råda","country":"SE","lat":60.0201,"lng":13.5789},{"code":"KSD","name":"Karlstad Airport","city":"Karlstad","country":"SE","lat":59.444698,"lng":13.3374},{"code":"VST","name":"Stockholm Västerås Airport","city":"Stockholm / Västerås","country":"SE","lat":59.589401,"lng":16.6336},{"code":"LLA","name":"Luleå Airport","city":"Luleå","country":"SE","lat":65.5438,"lng":22.122},{"code":"ARN","name":"Stockholm-Arlanda Airport","city":"Stockholm","country":"SE","lat":59.64849,"lng":17.928829},{"code":"BMA","name":"Stockholm-Bromma Airport","city":"Stockholm","country":"SE","lat":59.354401,"lng":17.9417},{"code":"BLE","name":"Dala Airport","city":"Borlange","country":"SE","lat":60.422001,"lng":15.5152},{"code":"LPI","name":"Linköping City Airport","city":"Linköping","country":"SE","lat":58.404909,"lng":15.684499},{"code":"NRK","name":"Norrköping Airport","city":"Norrköping","country":"SE","lat":58.586299896240234,"lng":16.250600814819336},{"code":"TYF","name":"Torsby Airport","city":"Torsby","country":"SE","lat":60.1576,"lng":12.9913},{"code":"VBY","name":"Visby Airport","city":"Visby","country":"SE","lat":57.6628,"lng":18.346201},{"code":"AGH","name":"Ängelholm-Helsingborg Airport","city":"Ängelholm","country":"SE","lat":56.29610061645508,"lng":12.847100257873535},{"code":"PJA","name":"Pajala Airport","city":"Pajala","country":"SE","lat":67.244569,"lng":23.074894},{"code":"HMV","name":"Hemavan Airport","city":"Hemavan","country":"SE","lat":65.806099,"lng":15.0828},{"code":"SHC","name":"Shire Inda Selassie Airport","city":"Shire Inda Selassie","country":"ET","lat":14.075699,"lng":38.273592},{"code":"FCN","name":"Sea-Airport Cuxhaven/Nordholz / Nordholz Naval Airbase","city":"Wurster Nordseeküste","country":"DE","lat":53.767689,"lng":8.659201},{"code":"RLG","name":"Rostock-Laage Airport","city":"Laage","country":"DE","lat":53.918201,"lng":12.2783},{"code":"RIX","name":"Riga International Airport","city":"Riga","country":"LV","lat":56.920752,"lng":23.970711},{"code":"EXI","name":"Excursion Inlet Seaplane Base","city":"Excursion Inlet","country":"US","lat":58.420502,"lng":-135.449005},{"code":"KUN","name":"Kaunas International Airport","city":"Kaunas","country":"LT","lat":54.963965,"lng":24.08582},{"code":"PLQ","name":"Palanga International Airport","city":"Palanga","country":"LT","lat":55.973202,"lng":21.093901},{"code":"VNO","name":"Vilnius International Airport","city":"Vilnius","country":"LT","lat":54.634102,"lng":25.285801},{"code":"BFN","name":"Bram Fischer International Airport","city":"Bloemfontein","country":"ZA","lat":-29.092699,"lng":26.302401},{"code":"CPT","name":"Cape Town International Airport","city":"Cape Town","country":"ZA","lat":-33.97403,"lng":18.604333},{"code":"ELS","name":"King Phalo Airport","city":"East London","country":"ZA","lat":-33.035599,"lng":27.825899},{"code":"GRJ","name":"George Airport","city":"George","country":"ZA","lat":-34.0056,"lng":22.378902},{"code":"HDS","name":"Eastgate Airport / Air Force Base Hoedspruit","city":"Hoedspruit","country":"ZA","lat":-24.361864,"lng":31.052921},{"code":"KIM","name":"Kimberley Airport","city":"Kimberley","country":"ZA","lat":-28.805401,"lng":24.764872},{"code":"MQP","name":"Kruger Mpumalanga International Airport","city":"Mbombela","country":"ZA","lat":-25.383327,"lng":31.105325},{"code":"HLA","name":"Lanseria International Airport","city":"Johannesburg","country":"ZA","lat":-25.938964,"lng":27.926642},{"code":"DUR","name":"King Shaka International Airport","city":"Durban","country":"ZA","lat":-29.6144444444,"lng":31.1197222222},{"code":"MGH","name":"Margate Airport","city":"Margate","country":"ZA","lat":-30.8574008942,"lng":30.343000412},{"code":"MBD","name":"Mmabatho International Airport","city":"Mafeking","country":"ZA","lat":-25.798400878900004,"lng":25.548000335699996},{"code":"MZQ","name":"Mkuze Airport","city":"Mkuze","country":"ZA","lat":-27.626101,"lng":32.0443},{"code":"JNB","name":"O.R. Tambo International Airport","city":"Johannesburg","country":"ZA","lat":-26.140081,"lng":28.246801},{"code":"PLZ","name":"Chief Dawid Stuurman International Airport","city":"Gqeberha (Port Elizabeth)","country":"ZA","lat":-33.989715,"lng":25.617353},{"code":"PHW","name":"Hendrik Van Eck Airport","city":"Phalaborwa","country":"ZA","lat":-23.937200546299998,"lng":31.1553993225},{"code":"PZB","name":"Pietermaritzburg Airport","city":"Pietermaritzburg","country":"ZA","lat":-29.649000167799997,"lng":30.3987007141},{"code":"PTG","name":"Polokwane International Airport","city":"Polokwane","country":"ZA","lat":-23.845269,"lng":29.458615},{"code":"RCB","name":"Richards Bay Airport","city":"Richards Bay","country":"ZA","lat":-28.740999221800003,"lng":32.0920982361},{"code":"SIS","name":"Sishen Airport","city":"Sishen","country":"ZA","lat":-27.6486,"lng":22.9993},{"code":"SZK","name":"Skukuza Airport","city":"Skukuza","country":"ZA","lat":-24.96085,"lng":31.588587},{"code":"UTN","name":"Upington Airport","city":"Upington","country":"ZA","lat":-28.400246,"lng":21.263592},{"code":"UTT","name":"K. D. Matanzima Airport","city":"Mthatha","country":"ZA","lat":-31.546363184900002,"lng":28.6733551025},{"code":"FRW","name":"Phillip Gaonwe Matante International Airport","city":"Francistown","country":"BW","lat":-21.159183,"lng":27.468826},{"code":"BBK","name":"Kasane International Airport","city":"Kasane","country":"BW","lat":-17.831653,"lng":25.166195},{"code":"MUB","name":"Maun International Airport","city":"Maun","country":"BW","lat":-19.970491,"lng":23.431409},{"code":"GBE","name":"Sir Seretse Khama International Airport","city":"Gaborone","country":"BW","lat":-24.555201,"lng":25.9182},{"code":"SWX","name":"Shakawe Airport","city":"Shakawe","country":"BW","lat":-18.373899459838867,"lng":21.832599639892578},{"code":"BZV","name":"Maya-Maya International Airport","city":"Brazzaville","country":"CG","lat":-4.2517,"lng":15.253},{"code":"PNR","name":"Antonio Agostinho-Neto International Airport","city":"Pointe Noire","country":"CG","lat":-4.81603,"lng":11.8866},{"code":"SHO","name":"King Mswati III International Airport","city":"Mpaka","country":"SZ","lat":-26.358611,"lng":31.716944},{"code":"BGF","name":"Bangui M'Poko International Airport","city":"Bangui","country":"CF","lat":4.39848,"lng":18.518801},{"code":"NBN","name":"Annobón Airport","city":"San Antonio de Palé","country":"GQ","lat":-1.408615,"lng":5.624163},{"code":"BSG","name":"Bata International Airport","city":"Bata","country":"GQ","lat":1.90547,"lng":9.80568},{"code":"GEM","name":"President Obiang Nguema International Airport","city":"Mengomeyén","country":"GQ","lat":1.676437,"lng":11.024904},{"code":"SSG","name":"Malabo International Airport","city":"Malabo","country":"GQ","lat":3.75527,"lng":8.70872},{"code":"ASI","name":"RAF Ascension Island","city":"Cat Hill","country":"SH","lat":-7.970179,"lng":-14.392744},{"code":"HLE","name":"Saint Helena International Airport","city":"Jamestown","country":"SH","lat":-15.958898,"lng":-5.646066},{"code":"MRU","name":"Sir Seewoosagur Ramgoolam International Airport","city":"Plaine Magnien","country":"MU","lat":-20.430201,"lng":57.683601},{"code":"RRG","name":"Sir Charles Gaetan Duval Airport","city":"Port Mathurin","country":"MU","lat":-19.756696,"lng":63.359326},{"code":"DLA","name":"Douala International Airport","city":"Douala","country":"CM","lat":4.00608,"lng":9.71948},{"code":"MVR","name":"Salak Airport","city":"Maroua","country":"CM","lat":10.451399803161621,"lng":14.257399559020996},{"code":"NGE","name":"N'Gaoundéré Airport","city":"N'Gaoundéré","country":"CM","lat":7.3570098876953125,"lng":13.559200286865234},{"code":"GOU","name":"Garoua International Airport","city":"Garoua","country":"CM","lat":9.334785,"lng":13.372129},{"code":"NSI","name":"Yaoundé Nsimalen International Airport","city":"Yaoundé","country":"CM","lat":3.72256,"lng":11.5533},{"code":"KAA","name":"Kasama Airport","city":"Kasama","country":"ZM","lat":-10.210169,"lng":31.123045},{"code":"LVI","name":"Harry Mwanga Nkumbula International Airport","city":"Livingstone","country":"ZM","lat":-17.821524,"lng":25.819642},{"code":"LUN","name":"Kenneth Kaunda International Airport","city":"Lusaka","country":"ZM","lat":-15.330833,"lng":28.452722},{"code":"MFU","name":"Mfuwe International Airport","city":"Mfuwe","country":"ZM","lat":-13.2589,"lng":31.9366},{"code":"NLA","name":"Simon Mwansa Kapwepwe International Airport","city":"Ndola","country":"ZM","lat":-12.96513,"lng":28.515624},{"code":"SLI","name":"Solwesi Airport","city":"Solwesi","country":"ZM","lat":-12.173700332641602,"lng":26.365100860595703},{"code":"HAH","name":"Prince Said Ibrahim International Airport","city":"Moroni","country":"KM","lat":-11.5337,"lng":43.2719},{"code":"AJN","name":"Ouani Airport","city":"Ouani","country":"KM","lat":-12.131047,"lng":44.430044},{"code":"DZA","name":"Dzaoudzi Pamandzi International Airport","city":"Dzaoudzi","country":"YT","lat":-12.809319,"lng":45.281815},{"code":"RUN","name":"Roland Garros Airport","city":"Sainte-Marie","country":"RE","lat":-20.890087,"lng":55.518894},{"code":"ZSE","name":"Saint-Pierre Pierrefonds Airport","city":"Saint-Pierre","country":"RE","lat":-21.319362,"lng":55.422506},{"code":"TNR","name":"Ivato International Airport","city":"Antananarivo","country":"MG","lat":-18.7969,"lng":47.478802},{"code":"SMS","name":"Sainte Marie Airport","city":"Vohilava","country":"MG","lat":-17.093901,"lng":49.8158},{"code":"TMM","name":"Toamasina Ambalamanasy Airport","city":"Toamasina","country":"MG","lat":-18.113537,"lng":49.392262},{"code":"WTA","name":"Tambohorano Airport","city":"Tambohorano","country":"MG","lat":-17.476101,"lng":43.972801},{"code":"MOQ","name":"Morondava Airport","city":"Morondava","country":"MG","lat":-20.2847,"lng":44.3176},{"code":"DIE","name":"Arrachart Airport","city":"Antisiranana","country":"MG","lat":-12.3494,"lng":49.291698},{"code":"MJN","name":"Amborovy Airport","city":"Mahajanga","country":"MG","lat":-15.666842,"lng":46.351233},{"code":"NOS","name":"Nosy Be International Airport","city":"Nosy Be","country":"MG","lat":-13.3121,"lng":48.3148},{"code":"DWB","name":"Soalala Airport","city":"Soalala","country":"MG","lat":-16.10169,"lng":45.358837},{"code":"BPY","name":"Besalampy Airport","city":"Besalampy","country":"MG","lat":-16.74453,"lng":44.482484},{"code":"WMN","name":"Maroantsetra Airport","city":"Maroantsetra","country":"MG","lat":-15.437742,"lng":49.689081},{"code":"SVB","name":"Sambava Airport","city":"Sambava","country":"MG","lat":-14.2786,"lng":50.174702},{"code":"TTS","name":"Tsaratanana Airport","city":"Tsaratanana","country":"MG","lat":-16.751064,"lng":47.619016},{"code":"FTU","name":"Tôlanaro Airport","city":"Tôlanaro","country":"MG","lat":-25.038099,"lng":46.9561},{"code":"MNJ","name":"Mananjary Airport","city":"Mananjary","country":"MG","lat":-21.201799,"lng":48.358299},{"code":"TLE","name":"Toliara Airport","city":"Toliara","country":"MG","lat":-23.3834,"lng":43.7285},{"code":"SSY","name":"Mbanza Congo Airport","city":"Mbanza Congo","country":"AO","lat":-6.269899845123291,"lng":14.246999740600586},{"code":"NBJ","name":"Dr. Antonio Agostinho Neto International Airport","city":"Luanda (Ícolo e Bengo)","country":"AO","lat":-9.050734,"lng":13.499078},{"code":"CAB","name":"Cabinda Airport","city":"Cabinda","country":"AO","lat":-5.59839,"lng":12.188145},{"code":"CBT","name":"Catumbela Airport","city":"Catumbela","country":"AO","lat":-12.4792,"lng":13.4869},{"code":"FND","name":"Funadhoo Airport","city":"Funadhoo","country":"MV","lat":6.162443,"lng":73.28752},{"code":"DUE","name":"Dundo Airport","city":"Chitato","country":"AO","lat":-7.400889873504639,"lng":20.818500518798828},{"code":"VPE","name":"Ngjiva Pereira Airport","city":"Ngiva","country":"AO","lat":-17.0435009003,"lng":15.683799743700002},{"code":"NOV","name":"Albano Machado Airport","city":"Huambo","country":"AO","lat":-12.8089,"lng":15.7605},{"code":"LAD","name":"Quatro de Fevereiro International Airport","city":"Luanda","country":"AO","lat":-8.85837,"lng":13.2312},{"code":"MEG","name":"Malanje Airport","city":"Malanje","country":"AO","lat":-9.525090217590332,"lng":16.312400817871094},{"code":"SPP","name":"Menongue Airport","city":"Menongue","country":"AO","lat":-14.657600402832031,"lng":17.71980094909668},{"code":"MSZ","name":"Welwitschia Mirabilis International Airport","city":"Moçâmedes","country":"AO","lat":-15.2612,"lng":12.1468},{"code":"GXG","name":"Negage Airport","city":"Negage","country":"AO","lat":-7.754509925842285,"lng":15.287699699401855},{"code":"SZA","name":"Soyo Airport","city":"Soyo","country":"AO","lat":-6.141089916229248,"lng":12.371800422668457},{"code":"SDD","name":"Lubango Mukanka International Airport","city":"Lubango","country":"AO","lat":-14.924839,"lng":13.576653},{"code":"OYE","name":"Oyem Airport","city":"Oyem","country":"GA","lat":1.5431100130081177,"lng":11.581399917602539},{"code":"POG","name":"Port Gentil International Airport","city":"Port Gentil","country":"GA","lat":-0.711739,"lng":8.75438},{"code":"MKU","name":"Makokou Airport","city":"Makokou","country":"GA","lat":0.5792109966278076,"lng":12.890899658203125},{"code":"LBV","name":"Libreville Leon M'ba International Airport","city":"Libreville","country":"GA","lat":0.458951,"lng":9.41214},{"code":"MVB","name":"M'Vengue El Hadj Omar Bongo Ondimba International Airport","city":"Franceville","country":"GA","lat":-1.65616,"lng":13.438},{"code":"PCP","name":"Principe Airport","city":"São Tomé & Príncipe","country":"ST","lat":1.661155,"lng":7.411081},{"code":"TMS","name":"São Tomé International Airport","city":"São Tomé","country":"ST","lat":0.378175,"lng":6.71215},{"code":"BEW","name":"Beira International Airport","city":"Beira","country":"MZ","lat":-19.7964,"lng":34.9076},{"code":"VPY","name":"Chimoio Airport","city":"Chimoio","country":"MZ","lat":-19.15130043029785,"lng":33.42900085449219},{"code":"INH","name":"Inhambane Airport","city":"Inhambane","country":"MZ","lat":-23.8764,"lng":35.408501},{"code":"VXC","name":"Lichinga Airport","city":"Lichinga","country":"MZ","lat":-13.274,"lng":35.2663},{"code":"MPM","name":"Maputo Airport","city":"Maputo","country":"MZ","lat":-25.920799,"lng":32.572601},{"code":"MNC","name":"Nacala International Airport","city":"Nacala","country":"MZ","lat":-14.4882,"lng":40.7122},{"code":"APL","name":"Nampula Airport","city":"Nampula","country":"MZ","lat":-15.1056,"lng":39.281799},{"code":"POL","name":"Pemba Airport","city":"Pemba","country":"MZ","lat":-12.993302,"lng":40.524867},{"code":"UEL","name":"Quelimane Airport","city":"Quelimane","country":"MZ","lat":-17.855499267578125,"lng":36.86909866333008},{"code":"TET","name":"Tete Airport","city":"Tete","country":"MZ","lat":-16.104799,"lng":33.640202},{"code":"VNX","name":"Vilankulo Airport","city":"Vilanculo","country":"MZ","lat":-22.018400192260742,"lng":35.31330108642578},{"code":"VJB","name":"Xai-Xai Chongoene Airport","city":"Xai-Xai","country":"MZ","lat":-24.892909,"lng":33.753024},{"code":"SEZ","name":"Seychelles International Airport","city":"Victoria","country":"SC","lat":-4.67434,"lng":55.521801},{"code":"PRI","name":"Praslin Island Airport","city":"Praslin Island","country":"SC","lat":-4.319256,"lng":55.691561},{"code":"AEH","name":"Abeche Airport","city":"Abeche","country":"TD","lat":13.847,"lng":20.844299},{"code":"NDJ","name":"N'Djamena International Airport","city":"N'Djamena","country":"TD","lat":12.1337,"lng":15.034},{"code":"BUQ","name":"Joshua Mqabuko Nkomo International Airport","city":"Bulawayo","country":"ZW","lat":-20.016284,"lng":28.622897},{"code":"VFA","name":"Victoria Falls International Airport","city":"Victoria Falls","country":"ZW","lat":-18.097437,"lng":25.836867},{"code":"HRE","name":"Robert Gabriel Mugabe International Airport","city":"Harare","country":"ZW","lat":-17.931801,"lng":31.0928},{"code":"KAB","name":"Kariba Airport","city":"Kariba","country":"ZW","lat":-16.5198,"lng":28.885},{"code":"BLZ","name":"Chileka International Airport","city":"Blantyre","country":"MW","lat":-15.677189,"lng":34.972319},{"code":"LLW","name":"Kamuzu International Airport","city":"Lumbadzi","country":"MW","lat":-13.7894,"lng":33.780998},{"code":"MSU","name":"Moshoeshoe I International Airport","city":"Maseru(Mazenod)","country":"LS","lat":-29.456277,"lng":27.554456},{"code":"HAL","name":"Halali Airport","city":"Halali","country":"NA","lat":-19.0285,"lng":16.4585},{"code":"MPA","name":"Katima Mulilo Airport","city":"Mpacha","country":"NA","lat":-17.634258,"lng":24.176688},{"code":"LUD","name":"Luderitz Airport","city":"Luderitz","country":"NA","lat":-26.687400817871094,"lng":15.242899894714355},{"code":"OND","name":"Ondangwa Airport","city":"Ondangwa","country":"NA","lat":-17.878201,"lng":15.9526},{"code":"OMD","name":"Oranjemund Airport","city":"Oranjemund","country":"NA","lat":-28.585318,"lng":16.44638},{"code":"NDU","name":"Rundu Airport","city":"Rundu","country":"NA","lat":-17.956499,"lng":19.7194},{"code":"WVB","name":"Walvis Bay International Airport","city":"Walvis Bay(Rooikop)","country":"NA","lat":-22.979332,"lng":14.647102},{"code":"ERS","name":"Eros Airport","city":"Windhoek","country":"NA","lat":-22.60492,"lng":17.079002},{"code":"WDH","name":"Hosea Kutako International Airport","city":"Windhoek","country":"NA","lat":-22.4799,"lng":17.4709},{"code":"FIH","name":"Ndjili International Airport","city":"Kinshasa","country":"CD","lat":-4.38575,"lng":15.4446},{"code":"INO","name":"Inongo Airport","city":"Inongo","country":"CD","lat":-1.94722,"lng":18.285801},{"code":"FDU","name":"Bandundu Airport","city":"Bandundu","country":"CD","lat":-3.31132,"lng":17.381701},{"code":"KKW","name":"Kikwit Airport","city":"Kikwit","country":"CD","lat":-5.03577,"lng":18.785601},{"code":"MDK","name":"Mbandaka Airport","city":"Mbandaka","country":"CD","lat":0.0226000007242,"lng":18.2887001038},{"code":"BDT","name":"Gbadolite Airport","city":"Gbadolite","country":"CD","lat":4.252745,"lng":20.975271},{"code":"GMA","name":"Gemena Airport","city":"Gemena","country":"CD","lat":3.23537,"lng":19.771299},{"code":"BNB","name":"Boende Airport","city":"Boende","country":"CD","lat":-0.286652,"lng":20.883551},{"code":"FKI","name":"Bangoka International Airport","city":"Kisangani","country":"CD","lat":0.481639,"lng":25.337999},{"code":"IRP","name":"Matari Airport","city":"Isiro","country":"CD","lat":2.82761,"lng":27.588301},{"code":"BUX","name":"Bunia Airport","city":"Bunia","country":"CD","lat":1.565743,"lng":30.220685},{"code":"GOM","name":"Goma International Airport","city":"Goma","country":"CD","lat":-1.666758,"lng":29.238037},{"code":"KND","name":"Kindu Airport","city":"Kindu","country":"CD","lat":-2.91917991638,"lng":25.915399551399997},{"code":"FBM","name":"Lubumbashi International Airport","city":"Lubumbashi","country":"CD","lat":-11.591494,"lng":27.530753},{"code":"KWZ","name":"Kolwezi Airport","city":"Kolwezi","country":"CD","lat":-10.7659,"lng":25.505699},{"code":"FMI","name":"Kalemie Airport","city":"Kalemie","country":"CD","lat":-5.87556,"lng":29.25},{"code":"KMN","name":"Kamina City Airport","city":"Kamina","country":"CD","lat":-8.728878,"lng":24.991407},{"code":"KGA","name":"Kananga Airport","city":"Kananga","country":"CD","lat":-5.90005,"lng":22.4692},{"code":"PFR","name":"Ilebo Airport","city":"Ilebo","country":"CD","lat":-4.329919,"lng":20.590124},{"code":"MJM","name":"Mbuji Mayi Airport","city":"Mbuji Mayi","country":"CD","lat":-6.121240139010001,"lng":23.569000244099996},{"code":"GAX","name":"Gamba Airport","city":"Gamba","country":"GA","lat":-2.785278,"lng":10.047222},{"code":"BKO","name":"Modibo Keita International Airport","city":"Bamako","country":"ML","lat":12.5335,"lng":-7.94994},{"code":"GAQ","name":"Gao International Airport","city":"Gao","country":"ML","lat":16.248466,"lng":-0.005389},{"code":"KYS","name":"Kayes Dag Dag Airport","city":"Kayes","country":"ML","lat":14.482521,"lng":-11.399335},{"code":"MZI","name":"Mopti Airport","city":"Sévaré","country":"ML","lat":14.5128,"lng":-4.07956},{"code":"TOM","name":"Tombouktou Airport","city":"Timbuktu","country":"ML","lat":16.730499,"lng":-3.00758},{"code":"PZE","name":"Penzance Heliport","city":"Penzance, Cornwall","country":"GB","lat":50.130376,"lng":-5.513946},{"code":"FOA","name":"Foula Airfield","city":"Foula","country":"GB","lat":60.121916,"lng":-2.053439},{"code":"BJL","name":"Banjul International Airport","city":"Banjul (Yundum)","country":"GM","lat":13.338,"lng":-16.652201},{"code":"FUE","name":"Fuerteventura Airport","city":"El Matorral","country":"ES","lat":28.4527,"lng":-13.8638},{"code":"GMZ","name":"La Gomera Airport","city":"Alajero, La Gomera Island","country":"ES","lat":28.0296,"lng":-17.2146},{"code":"VDE","name":"El Hierro Airport","city":"El Hierro Island","country":"ES","lat":27.8148,"lng":-17.8871},{"code":"SPC","name":"La Palma Airport","city":"Sta Cruz de la Palma, La Palma Island","country":"ES","lat":28.626499,"lng":-17.7556},{"code":"LPA","name":"Gran Canaria Airport","city":"Gran Canaria Island","country":"ES","lat":27.9319,"lng":-15.3866},{"code":"ACE","name":"César Manrique-Lanzarote Airport","city":"San Bartolomé","country":"ES","lat":28.945499,"lng":-13.6052},{"code":"TFS","name":"Tenerife Sur Airport","city":"Tenerife","country":"ES","lat":28.0445,"lng":-16.5725},{"code":"TFN","name":"Tenerife Norte-Ciudad de La Laguna Airport","city":"Tenerife","country":"ES","lat":28.482752,"lng":-16.341707},{"code":"JCU","name":"Ceuta Heliport","city":"Ceuta","country":"ES","lat":35.892657,"lng":-5.306321},{"code":"MLN","name":"Melilla Airport","city":"Melilla","country":"ES","lat":35.2798,"lng":-2.95626},{"code":"FNA","name":"Lungi International Airport","city":"Freetown (Lungi-Town)","country":"SL","lat":8.61644,"lng":-13.1955},{"code":"OXB","name":"Osvaldo Vieira International Airport","city":"Bissau","country":"GW","lat":11.894262,"lng":-15.653596},{"code":"JJU","name":"Qaqortoq Airport","city":"Qaqortoq","country":"GL","lat":60.76383,"lng":-46.065001},{"code":"QCU","name":"Akunnaaq Heliport","city":"Akunnaaq","country":"GL","lat":68.744273,"lng":-52.340369},{"code":"MLW","name":"Spriggs Payne Airport","city":"Monrovia","country":"LR","lat":6.28906,"lng":-10.7587},{"code":"ROB","name":"Roberts International Airport","city":"Monrovia","country":"LR","lat":6.23379,"lng":-10.3623},{"code":"AGA","name":"Al Massira Airport","city":"Agadir (Temsia)","country":"MA","lat":30.322478,"lng":-9.412003},{"code":"TTA","name":"Tan Tan Airport","city":"Tan Tan","country":"MA","lat":28.447563,"lng":-11.161749},{"code":"OZG","name":"Zagora Airport","city":"Zagora","country":"MA","lat":30.265788,"lng":-5.860808},{"code":"FEZ","name":"Fes Saïss International Airport","city":"Saïss","country":"MA","lat":33.927299,"lng":-4.97796},{"code":"ERH","name":"Moulay Ali Cherif Airport","city":"Errachidia","country":"MA","lat":31.9475002289,"lng":-4.39833021164},{"code":"OUD","name":"Oujda Angads Airport","city":"Ahl Angad","country":"MA","lat":34.789558,"lng":-1.926041},{"code":"SMW","name":"Smara Airport","city":"Smara","country":"EH","lat":26.731987,"lng":-11.683655},{"code":"BEM","name":"Beni Mellal Airport","city":"Oulad Yaich","country":"MA","lat":32.401895,"lng":-6.315905},{"code":"RBA","name":"Rabat-Salé Airport","city":"Rabat","country":"MA","lat":34.051498,"lng":-6.75152},{"code":"VIL","name":"Dakhla Airport","city":"Dakhla","country":"EH","lat":23.7183,"lng":-15.932},{"code":"ESU","name":"Essaouira-Mogador Airport","city":"Essaouira","country":"MA","lat":31.397499,"lng":-9.68167},{"code":"EUN","name":"Laayoune Hassan I International Airport","city":"El Aaiún","country":"EH","lat":27.142467,"lng":-13.224947},{"code":"CMN","name":"Mohammed V International Airport","city":"Casablanca","country":"MA","lat":33.3675,"lng":-7.58997},{"code":"NDR","name":"Nador Al Aaroui International Airport","city":"Al Aaroui","country":"MA","lat":34.9888,"lng":-3.02821},{"code":"RAK","name":"Marrakesh Menara Airport","city":"Marrakesh","country":"MA","lat":31.604807,"lng":-8.035788},{"code":"OZZ","name":"Ouarzazate International Airport","city":"Ouarzazate","country":"MA","lat":30.9391,"lng":-6.90943},{"code":"TTU","name":"Sania Ramel Airport","city":"Tétouan","country":"MA","lat":35.594299,"lng":-5.32002},{"code":"TNG","name":"Tangier Ibn Battuta Airport","city":"Tangier","country":"MA","lat":35.731741,"lng":-5.921459},{"code":"GNU","name":"Goodnews Airport","city":"Goodnews","country":"US","lat":59.117561,"lng":-161.575241},{"code":"DSS","name":"Blaise Diagne International Airport","city":"Dakar","country":"SN","lat":14.670878,"lng":-17.072829},{"code":"ZIG","name":"Ziguinchor Airport","city":"Ziguinchor","country":"SN","lat":12.55559,"lng":-16.283298},{"code":"CSK","name":"Cap Skirring Airport","city":"Cap Skirring","country":"SN","lat":12.39533,"lng":-16.748},{"code":"DKR","name":"Léopold Sédar Senghor International Airport","city":"Dakar","country":"SN","lat":14.742337,"lng":-17.479222},{"code":"NKC","name":"Nouakchott–Oumtounsy International Airport","city":"Nouakchott","country":"MR","lat":18.31,"lng":-15.969722},{"code":"NDB","name":"Nouadhibou International Airport","city":"Nouadhibou","country":"MR","lat":20.932404,"lng":-17.030199},{"code":"OUZ","name":"Tazadit Airport","city":"Zouérate","country":"MR","lat":22.757345,"lng":-12.482228},{"code":"CKY","name":"Ahmed Sékou Touré International Airport","city":"Conakry","country":"GN","lat":9.57689,"lng":-13.612},{"code":"SID","name":"Amílcar Cabral International Airport","city":"Espargos","country":"CV","lat":16.7414,"lng":-22.9494},{"code":"BVC","name":"Aristides Pereira International Airport","city":"Rabil","country":"CV","lat":16.136499,"lng":-22.888901},{"code":"MMO","name":"Maio Airport","city":"Vila do Maio","country":"CV","lat":15.155900001525879,"lng":-23.213699340820312},{"code":"RAI","name":"Nelson Mandela International Airport","city":"Praia","country":"CV","lat":14.941126,"lng":-23.484728},{"code":"SFL","name":"São Filipe Airport","city":"São Filipe","country":"CV","lat":14.8850002289,"lng":-24.4799995422},{"code":"SNE","name":"Preguiça Airport","city":"Preguiça","country":"CV","lat":16.588852,"lng":-24.284132},{"code":"VXE","name":"Cesaria Evora International Airport","city":"São Pedro","country":"CV","lat":16.833363,"lng":-25.055308},{"code":"ADD","name":"Addis Ababa Bole International Airport","city":"Addis Ababa","country":"ET","lat":8.97789,"lng":38.799301},{"code":"AMH","name":"Arba Minch Airport","city":"Arba Minch","country":"ET","lat":6.03939,"lng":37.5905},{"code":"AXU","name":"Axum Airport","city":"Axum","country":"ET","lat":14.1468,"lng":38.7728},{"code":"BJR","name":"Bahir Dar Airport","city":"Bahir Dar","country":"ET","lat":11.6081,"lng":37.321602},{"code":"DSE","name":"Kombolcha Airport","city":"Dessie","country":"ET","lat":11.10975,"lng":39.725911},{"code":"DEM","name":"Dembidollo Airport","city":"Dembidollo","country":"ET","lat":8.554,"lng":34.858002},{"code":"DIR","name":"Aba Tenna Dejazmach Yilma International Airport","city":"Dire Dawa","country":"ET","lat":9.623549,"lng":41.855027},{"code":"GMB","name":"Gambela Airport","city":"Gambela","country":"ET","lat":8.12876,"lng":34.563099},{"code":"GDQ","name":"Gondar Airport","city":"Azezo","country":"ET","lat":12.5199,"lng":37.433998},{"code":"GDE","name":"Gode Airport","city":"Gode","country":"ET","lat":5.93513011932,"lng":43.5786018372},{"code":"JIJ","name":"Gerad Wilwal International Airport","city":"Jijiga","country":"ET","lat":9.331907,"lng":42.911811},{"code":"BCO","name":"Jinka Airport","city":"Jinka","country":"ET","lat":5.749655,"lng":36.560165},{"code":"JIM","name":"Jimma Airport","city":"Jimma","country":"ET","lat":7.66609001159668,"lng":36.81660079956055},{"code":"ABK","name":"Kebri Dahar Airport","city":"Kebri Dahar","country":"ET","lat":6.732577,"lng":44.241339},{"code":"AWA","name":"Hawassa International Airport","city":"Hawassa","country":"ET","lat":7.100611,"lng":38.396455},{"code":"MQX","name":"Mekele Alula Aba Nega Airport","city":"Mekele","country":"ET","lat":13.4674,"lng":39.533501},{"code":"MTF","name":"Mizan Teferi Airport","city":"Mizan Teferi","country":"ET","lat":6.9571,"lng":35.5547},{"code":"HIL","name":"Shilavo Airport","city":"Shilavo","country":"ET","lat":6.077545,"lng":44.763736},{"code":"SZE","name":"Semera Airport","city":"Semera","country":"ET","lat":11.7875,"lng":40.9915},{"code":"ASO","name":"Asosa Airport","city":"Asosa","country":"ET","lat":10.018500328063965,"lng":34.586299896240234},{"code":"TIE","name":"Tippi Airport","city":"Tippi","country":"ET","lat":7.2024,"lng":35.415},{"code":"BJM","name":"Bujumbura Melchior Ndadaye International Airport","city":"Bujumbura","country":"BI","lat":-3.32402,"lng":29.318501},{"code":"GID","name":"Gitega Airport","city":"Gitega","country":"BI","lat":-3.417209,"lng":29.911308},{"code":"KRE","name":"Kirundo Airport","city":"Kirundo","country":"BI","lat":-2.544772,"lng":30.094575},{"code":"HBT","name":"Hambantota Seaplane Base","city":"Hambantota","country":"LK","lat":6.124043,"lng":81.103},{"code":"BSA","name":"Bender Qassim International Airport","city":"Bosaso","country":"SO","lat":11.275235,"lng":49.139231},{"code":"HGA","name":"Egal International Airport","city":"Hargeisa","country":"SO","lat":9.514082,"lng":44.083469},{"code":"BBO","name":"Berbera Airport","city":"Berbera","country":"SO","lat":10.385035,"lng":44.936723},{"code":"MGQ","name":"Aden Adde International Airport","city":"Mogadishu","country":"SO","lat":2.01444,"lng":45.304699},{"code":"BLW","name":"Beledweyne Airport","city":"Beledweyne","country":"SO","lat":4.766976,"lng":45.2388},{"code":"GLK","name":"Galcaio Airport","city":"Galcaio","country":"SO","lat":6.78082990646,"lng":47.45470047},{"code":"GGR","name":"Garowe Airport","city":"Garowe","country":"SO","lat":8.458474,"lng":48.568136},{"code":"JIB","name":"Djibouti-Ambouli Airport","city":"Djibouti City","country":"DJ","lat":11.5473,"lng":43.1595},{"code":"DBB","name":"El Alamein International Airport","city":"El Alamein","country":"EG","lat":30.924324,"lng":28.46161},{"code":"AAC","name":"El Arish International Airport","city":"El Arish","country":"EG","lat":31.055324,"lng":33.827964},{"code":"ATZ","name":"Asyut International Airport","city":"Asyut","country":"EG","lat":27.045962,"lng":31.01276},{"code":"HBE","name":"Alexandria International Airport","city":"Alexandria","country":"EG","lat":30.93249,"lng":29.696437},{"code":"ABS","name":"Abu Simbel Airport","city":"Abu Simbel","country":"EG","lat":22.375924,"lng":31.611734},{"code":"CAI","name":"Cairo International Airport","city":"Cairo","country":"EG","lat":30.111534,"lng":31.396694},{"code":"CCE","name":"Capital International Airport","city":"New Cairo","country":"EG","lat":30.06465,"lng":31.839974},{"code":"HRG","name":"Hurghada International Airport","city":"Hurghada","country":"EG","lat":27.176776,"lng":33.796692},{"code":"LXR","name":"Luxor International Airport","city":"Luxor","country":"EG","lat":25.671018,"lng":32.706446},{"code":"RMF","name":"Marsa Alam International Airport","city":"Marsa Alam","country":"EG","lat":25.555548,"lng":34.59245},{"code":"MUH","name":"Mersa Matruh International Airport","city":"Marsa Matruh","country":"EG","lat":31.324313,"lng":27.22227},{"code":"PSD","name":"Port Said International Airport","city":"Port Said","country":"EG","lat":31.279285,"lng":32.240582},{"code":"HMB","name":"Suhaj International Airport","city":"Suhaj","country":"EG","lat":26.342515,"lng":31.743017},{"code":"SSH","name":"Sharm El Sheikh International Airport","city":"Sharm El Sheikh","country":"EG","lat":27.977272,"lng":34.394717},{"code":"ASW","name":"Aswan International Airport","city":"Aswan","country":"EG","lat":23.961075,"lng":32.820382},{"code":"SPX","name":"Sphinx International Airport","city":"Al Jiza","country":"EG","lat":30.108179,"lng":30.895728},{"code":"TCP","name":"Taba International Airport","city":"Taba","country":"EG","lat":29.594496,"lng":34.775752},{"code":"ASM","name":"Asmara International Airport","city":"Asmara","country":"ER","lat":15.2919,"lng":38.910702},{"code":"JUB","name":"Juba International Airport","city":"Juba","country":"SS","lat":4.87201,"lng":31.601101},{"code":"ASV","name":"Amboseli Airport","city":"Ol Tukai","country":"KE","lat":-2.644786,"lng":37.252922},{"code":"EDL","name":"Eldoret International Airport","city":"Eldoret","country":"KE","lat":0.404458,"lng":35.238899},{"code":"NBO","name":"Jomo Kenyatta International Airport","city":"Nairobi","country":"KE","lat":-1.318886,"lng":36.928233},{"code":"KIS","name":"Kisumu International Airport","city":"Kisumu","country":"KE","lat":-0.086139,"lng":34.728901},{"code":"LAU","name":"Manda Airport","city":"Lamu","country":"KE","lat":-2.252431,"lng":40.912892},{"code":"JJM","name":"Mulika Lodge Airport","city":"Meru-Kinna","country":"KE","lat":0.165083,"lng":38.195141},{"code":"MYD","name":"Malindi International Airport","city":"Malindi","country":"KE","lat":-3.22931,"lng":40.1017},{"code":"MBA","name":"Moi International Airport","city":"Mombasa","country":"KE","lat":-4.03483,"lng":39.5942},{"code":"MRE","name":"Mara Serena Lodge Airstrip","city":"Serena","country":"KE","lat":-1.404588,"lng":35.008349},{"code":"NYK","name":"Nanyuki Civil Airport","city":"Gathiuru","country":"KE","lat":-0.062352,"lng":37.041296},{"code":"WIL","name":"Nairobi Wilson Airport","city":"Nairobi","country":"KE","lat":-1.32172,"lng":36.8148},{"code":"WJR","name":"Wajir Airport","city":"Wajir","country":"KE","lat":1.73324,"lng":40.091599},{"code":"GHT","name":"Ghat Airport","city":"Ghat","country":"LY","lat":25.1455993652,"lng":10.142600059500001},{"code":"AKF","name":"Kufra Airport","city":"Kufra","country":"LY","lat":24.178699493408203,"lng":23.31399917602539},{"code":"BEN","name":"Benina International Airport","city":"Benina","country":"LY","lat":32.096802,"lng":20.269501},{"code":"MJI","name":"Mitiga International Airport","city":"Tripoli","country":"LY","lat":32.89177,"lng":13.287878},{"code":"LAQ","name":"Al Abraq International Airport","city":"Al Albraq","country":"LY","lat":32.789036,"lng":21.954864},{"code":"SEB","name":"Sabha Airport","city":"Sabha","country":"LY","lat":26.992452,"lng":14.466162},{"code":"MRA","name":"Misrata International Airport","city":"Misrata","country":"LY","lat":32.325001,"lng":15.061},{"code":"LTD","name":"Ghadames Airport","city":"Ghadames","country":"LY","lat":30.145484,"lng":9.702095},{"code":"KGL","name":"Kigali International Airport","city":"Kigali","country":"RW","lat":-1.96863,"lng":30.1395},{"code":"KME","name":"Kamembe Airport","city":"Kamembe","country":"RW","lat":-2.462239980697632,"lng":28.907899856567383},{"code":"DOG","name":"Dongola Airport","city":"Dongola","country":"SD","lat":19.153666,"lng":30.430069},{"code":"ELF","name":"El Fasher Airport","city":"El Fasher","country":"SD","lat":13.614899635314941,"lng":25.324600219726562},{"code":"KSL","name":"Kassala Airport","city":"Kassala","country":"SD","lat":15.387499809265137,"lng":36.328800201416016},{"code":"UYL","name":"Nyala Airport","city":"Nyala","country":"SD","lat":12.0535,"lng":24.9562},{"code":"EBD","name":"El-Obeid Airport","city":"El-Obeid","country":"SD","lat":13.1532,"lng":30.2327},{"code":"PZU","name":"Port Sudan New International Airport","city":"Port Sudan","country":"SD","lat":19.434558,"lng":37.234125},{"code":"KRT","name":"Khartoum International Airport","city":"Khartoum","country":"SD","lat":15.5895,"lng":32.5532},{"code":"MAK","name":"Malakal International Airport","city":"Malakal","country":"SS","lat":9.558739,"lng":31.651937},{"code":"WUU","name":"Wau Airport","city":"Wau","country":"SS","lat":7.72583,"lng":27.975},{"code":"ARK","name":"Arusha Airport","city":"Arusha","country":"TZ","lat":-3.3677899837493896,"lng":36.63330078125},{"code":"BKZ","name":"Bukoba Airport","city":"Bukoba","country":"TZ","lat":-1.332,"lng":31.8212},{"code":"DAR","name":"Julius Nyerere International Airport","city":"Dar es Salaam","country":"TZ","lat":-6.873499,"lng":39.207288},{"code":"DOD","name":"Dodoma Airport","city":"Dodoma","country":"TZ","lat":-6.170559,"lng":35.756036},{"code":"MBI","name":"Songwe Airport","city":"Mbeya","country":"TZ","lat":-8.919942,"lng":33.273981},{"code":"TKQ","name":"Kigoma Airport","city":"Kigoma","country":"TZ","lat":-4.8862,"lng":29.6709},{"code":"JRO","name":"Kilimanjaro International Airport","city":"Arusha","country":"TZ","lat":-3.427049,"lng":37.07353},{"code":"MFA","name":"Mafia Airport","city":"Kilindoni","country":"TZ","lat":-7.917478,"lng":39.668502},{"code":"MYW","name":"Mtwara Airport","city":"Mtwara","country":"TZ","lat":-10.336204,"lng":40.181997},{"code":"MUZ","name":"Musoma Airport","city":"Musoma","country":"TZ","lat":-1.503,"lng":33.8021},{"code":"MWZ","name":"Mwanza International Airport","city":"Mwanza","country":"TZ","lat":-2.446563,"lng":32.936049},{"code":"SEU","name":"Seronera Airport","city":"Seronera","country":"TZ","lat":-2.45806,"lng":34.822498},{"code":"SHY","name":"Shinyanga Airport","city":"Shinyanga","country":"TZ","lat":-3.6093,"lng":33.5035},{"code":"TBO","name":"Tabora Airport","city":"Tabora","country":"TZ","lat":-5.07639,"lng":32.833302},{"code":"TGT","name":"Tanga Airport","city":"Tanga","country":"TZ","lat":-5.09236,"lng":39.071201},{"code":"ZNZ","name":"Abeid Amani Karume International Airport","city":"Zanzibar","country":"TZ","lat":-6.22202,"lng":39.224899},{"code":"RUA","name":"Arua Airport","city":"Arua","country":"UG","lat":3.049152,"lng":30.911714},{"code":"EBB","name":"Entebbe International Airport","city":"Entebbe","country":"UG","lat":0.042386,"lng":32.443501},{"code":"ULU","name":"Gulu Airport","city":"Gulu","country":"UG","lat":2.8055601119995117,"lng":32.27180099487305},{"code":"SRT","name":"Soroti Airport","city":"Soroti","country":"UG","lat":1.7276899814605713,"lng":33.622798919677734},{"code":"HYL","name":"Hollis Clark Bay Seaplane Base","city":"Hollis","country":"US","lat":55.481529,"lng":-132.646008},{"code":"FSH","name":"Syekh Hamzah Fansyuri Airport","city":"Singkil","country":"ID","lat":2.269614,"lng":97.967863},{"code":"DHX","name":"Dhoho International Airport","city":"Kediri","country":"ID","lat":-7.749527,"lng":111.946831},{"code":"KRC","name":"Departi Parbo Airport","city":"Sungai Penuh","country":"ID","lat":-2.094042,"lng":101.47025},{"code":"RKI","name":"Mentawai Airport","city":"Sipura Island","country":"ID","lat":-2.1001,"lng":99.7044},{"code":"UGU","name":"Bilorai Airport","city":"Bilogai","country":"ID","lat":-3.739503,"lng":137.031183},{"code":"AYJ","name":"Maharshi Valmiki International Airport","city":"Faizabad","country":"IN","lat":26.747736,"lng":82.163664},{"code":"SDW","name":"Sindhudurg Airport","city":"Chipi","country":"IN","lat":16.002552,"lng":73.529846},{"code":"NMI","name":"Navi Mumbai International Airport","city":"Navi Mumbai","country":"IN","lat":18.984597,"lng":73.065253},{"code":"HGI","name":"Itanagar Donyi Polo Hollongi Airport","city":"Hollongi","country":"IN","lat":26.96683,"lng":93.638792},{"code":"AHA","name":"Maa Mahamaya Airport","city":"Ambikapur","country":"IN","lat":22.98753,"lng":83.19612},{"code":"JGB","name":"Jagdalpur Airport","city":"Jagdalpur","country":"IN","lat":19.074301,"lng":82.036797},{"code":"TQR","name":"San Domino Island Heliport","city":"Tremiti Islands","country":"IT","lat":42.117775,"lng":15.490655},{"code":"JIO","name":"Jos Orno Imsula Airport","city":"Tiakur","country":"ID","lat":-8.140683,"lng":127.909669},{"code":"JRA","name":"West 30th Street Heliport","city":"New York","country":"US","lat":40.755107,"lng":-74.006896},{"code":"JUH","name":"Chizhou Jiuhuashan Airport","city":"Chizhou","country":"CN","lat":30.7403,"lng":117.6856},{"code":"BLD","name":"Boulder City Municipal Airport","city":"Boulder City","country":"US","lat":35.947161,"lng":-114.858799},{"code":"OTS","name":"Anacortes Airport","city":"Anacortes","country":"US","lat":48.498513,"lng":-122.662513},{"code":"ABE","name":"Lehigh Valley International Airport","city":"Allentown/Bethlehem","country":"US","lat":40.651773,"lng":-75.442797},{"code":"ABI","name":"Abilene Regional Airport","city":"Abilene","country":"US","lat":32.4113006592,"lng":-99.68190002440001},{"code":"ABQ","name":"Albuquerque International Sunport","city":"Albuquerque","country":"US","lat":35.039976,"lng":-106.608925},{"code":"ABR","name":"Aberdeen Regional Airport","city":"Aberdeen","country":"US","lat":45.449100494384766,"lng":-98.42179870605469},{"code":"ABY","name":"Southwest Georgia Regional Airport","city":"Albany","country":"US","lat":31.532946,"lng":-84.196215},{"code":"ACK","name":"Nantucket Memorial Airport","city":"Nantucket","country":"US","lat":41.25310135,"lng":-70.06020355},{"code":"ACT","name":"Waco Regional Airport","city":"Waco","country":"US","lat":31.611299514770508,"lng":-97.23049926757812},{"code":"ACV","name":"California Redwood Coast-Humboldt County Airport","city":"Arcata/Eureka","country":"US","lat":40.978101,"lng":-124.109},{"code":"ACY","name":"Atlantic City International Airport","city":"Atlantic City","country":"US","lat":39.456201,"lng":-74.577511},{"code":"KAE","name":"Kake Seaplane Base","city":"Kake","country":"US","lat":56.973,"lng":-133.945999},{"code":"AEX","name":"Alexandria International Airport","city":"Alexandria","country":"US","lat":31.325828,"lng":-92.546702},{"code":"AGS","name":"Augusta Regional At Bush Field","city":"Augusta","country":"US","lat":33.3699,"lng":-81.9645},{"code":"AIA","name":"Alliance Municipal Airport","city":"Alliance","country":"US","lat":42.052543,"lng":-102.803965},{"code":"ALB","name":"Albany International Airport","city":"Albany","country":"US","lat":42.748299,"lng":-73.801697},{"code":"ALO","name":"Waterloo Regional Airport","city":"Waterloo","country":"US","lat":42.557098388671875,"lng":-92.40029907226562},{"code":"ALS","name":"San Luis Valley Regional Airport/Bergman Field","city":"Alamosa","country":"US","lat":37.434898,"lng":-105.866997},{"code":"ALW","name":"Walla Walla Regional Airport","city":"Walla Walla","country":"US","lat":46.09489822,"lng":-118.288002},{"code":"AMA","name":"Rick Husband Amarillo International Airport","city":"Amarillo","country":"US","lat":35.217857,"lng":-101.706444},{"code":"AOO","name":"Altoona Blair County Airport","city":"Altoona","country":"US","lat":40.296398,"lng":-78.32},{"code":"APN","name":"Alpena County Regional Airport","city":"Alpena","country":"US","lat":45.0780983,"lng":-83.56030273},{"code":"ART","name":"Watertown International Airport","city":"Watertown","country":"US","lat":43.991901,"lng":-76.021698},{"code":"ASE","name":"Aspen-Pitkin County Airport (Sardy Field)","city":"Aspen","country":"US","lat":39.223202,"lng":-106.869003},{"code":"ATL","name":"Hartsfield Jackson Atlanta International Airport","city":"Atlanta","country":"US","lat":33.6367,"lng":-84.428101},{"code":"ATW","name":"Appleton International Airport","city":"Appleton","country":"US","lat":44.258515,"lng":-88.519},{"code":"ATY","name":"Watertown Regional Airport","city":"Watertown","country":"US","lat":44.91400146,"lng":-97.15470123},{"code":"AUG","name":"Augusta State Airport","city":"Augusta","country":"US","lat":44.320598602299995,"lng":-69.7973022461},{"code":"AUS","name":"Austin Bergstrom International Airport","city":"Austin","country":"US","lat":30.197535,"lng":-97.662015},{"code":"AVL","name":"Asheville Regional Airport","city":"Asheville","country":"US","lat":35.435488,"lng":-82.541853},{"code":"AVP","name":"Wilkes-Barre/Scranton International Airport","city":"Wilkes-Barre/Scranton","country":"US","lat":41.337064,"lng":-75.724232},{"code":"AZO","name":"Kalamazoo/Battle Creek International Airport","city":"Kalamazoo","country":"US","lat":42.232105,"lng":-85.549637},{"code":"BKG","name":"Branson Airport","city":"Branson","country":"US","lat":36.532082,"lng":-93.200544},{"code":"BDL","name":"Bradley International Airport","city":"Hartford","country":"US","lat":41.938555,"lng":-72.688016},{"code":"BED","name":"Laurence G Hanscom Field","city":"Bedford","country":"US","lat":42.470001,"lng":-71.289001},{"code":"BFD","name":"Bradford Regional Airport","city":"Bradford","country":"US","lat":41.8031005859375,"lng":-78.64009857177734},{"code":"BFF","name":"Western Neb. Rgnl/William B. Heilig Airport","city":"Scottsbluff","country":"US","lat":41.87400055,"lng":-103.5960007},{"code":"BFI","name":"King County International Airport - Boeing Field","city":"Seattle","country":"US","lat":47.527042,"lng":-122.29995},{"code":"BFL","name":"Meadows Field","city":"Bakersfield","country":"US","lat":35.433601,"lng":-119.056999},{"code":"BGM","name":"Greater Binghamton/Edwin A Link field","city":"Binghamton","country":"US","lat":42.20869827,"lng":-75.97979736},{"code":"BGR","name":"Bangor International Airport","city":"Bangor","country":"US","lat":44.806364,"lng":-68.826668},{"code":"BHB","name":"Hancock County-Bar Harbor Airport","city":"Bar Harbor","country":"US","lat":44.45000076,"lng":-68.3615036},{"code":"BHM","name":"Birmingham-Shuttlesworth International Airport","city":"Birmingham","country":"US","lat":33.562877,"lng":-86.750712},{"code":"BID","name":"Block Island State Airport","city":"Block Island","country":"US","lat":41.168446,"lng":-71.578631},{"code":"BIH","name":"Eastern Sierra Regional Airport","city":"Bishop","country":"US","lat":37.3731,"lng":-118.363998},{"code":"BIL","name":"Billings Logan International Airport","city":"Billings","country":"US","lat":45.808932,"lng":-108.541242},{"code":"BIS","name":"Bismarck Municipal Airport","city":"Bismarck","country":"US","lat":46.772734,"lng":-100.746874},{"code":"BJC","name":"Rocky Mountain Metropolitan Airport","city":"Denver","country":"US","lat":39.908798,"lng":-105.116997},{"code":"BKW","name":"Raleigh County Memorial Airport","city":"Beaver","country":"US","lat":37.7873,"lng":-81.124199},{"code":"BLI","name":"Bellingham International Airport","city":"Bellingham","country":"US","lat":48.79280090332031,"lng":-122.53800201416016},{"code":"BLV","name":"Scott AFB/Midamerica Airport","city":"Belleville","country":"US","lat":38.5452,"lng":-89.835197},{"code":"BMI","name":"Central Illinois Regional Airport at Bloomington-Normal","city":"Bloomington/Normal","country":"US","lat":40.4771,"lng":-88.915901},{"code":"BNA","name":"Nashville International Airport","city":"Nashville","country":"US","lat":36.1245,"lng":-86.6782},{"code":"BOI","name":"Boise Air Terminal/Gowen Field","city":"Boise","country":"US","lat":43.5644,"lng":-116.223},{"code":"BOS","name":"Boston Logan International Airport","city":"Boston","country":"US","lat":42.36197,"lng":-71.0079},{"code":"BPT","name":"Jack Brooks Regional Airport","city":"Beaumont/Port Arthur","country":"US","lat":29.9508,"lng":-94.020699},{"code":"BQK","name":"Brunswick Golden Isles Airport","city":"Brunswick","country":"US","lat":31.258801,"lng":-81.466499},{"code":"BRD","name":"Brainerd Lakes Regional Airport","city":"Brainerd","country":"US","lat":46.402861,"lng":-94.129727},{"code":"BRL","name":"Southeast Iowa Regional Airport","city":"Burlington","country":"US","lat":40.783199310302734,"lng":-91.12550354003906},{"code":"BRO","name":"Brownsville South Padre Island International Airport","city":"Brownsville","country":"US","lat":25.907238,"lng":-97.425235},{"code":"BTM","name":"Bert Mooney Airport","city":"Butte","country":"US","lat":45.95479965209961,"lng":-112.49700164794922},{"code":"BTR","name":"Baton Rouge Metropolitan Airport","city":"Baton Rouge","country":"US","lat":30.533199,"lng":-91.149597},{"code":"BTV","name":"Patrick Leahy Burlington International Airport","city":"Burlington","country":"US","lat":44.471901,"lng":-73.153297},{"code":"BUF","name":"Buffalo Niagara International Airport","city":"Buffalo","country":"US","lat":42.940498,"lng":-78.732201},{"code":"BUR","name":"Hollywood Burbank/Bob Hope Airport","city":"Burbank","country":"US","lat":34.202834,"lng":-118.35805},{"code":"BWI","name":"Baltimore/Washington International Thurgood Marshall Airport","city":"Baltimore","country":"US","lat":39.1754,"lng":-76.668297},{"code":"BZN","name":"Bozeman Yellowstone International Airport","city":"Bozeman","country":"US","lat":45.778853,"lng":-111.153711},{"code":"CAE","name":"Columbia Metropolitan Airport","city":"Columbia","country":"US","lat":33.938172,"lng":-81.123022},{"code":"CAK","name":"Akron Canton Regional Airport","city":"Akron","country":"US","lat":40.916099548339844,"lng":-81.44219970703125},{"code":"KCC","name":"Coffman Cove Seaplane Base","city":"Coffman Cove","country":"US","lat":56.012586,"lng":-132.835093},{"code":"CCR","name":"Buchanan Field","city":"Concord","country":"US","lat":37.9897,"lng":-122.056999},{"code":"CDC","name":"Cedar City Regional Airport","city":"Cedar City","country":"US","lat":37.70100021362305,"lng":-113.0989990234375},{"code":"CDR","name":"Chadron Municipal Airport","city":"Chadron","country":"US","lat":42.837601,"lng":-103.095001},{"code":"CEC","name":"Jack Mc Namara Field Airport","city":"Crescent City","country":"US","lat":41.778929,"lng":-124.236403},{"code":"CEZ","name":"Cortez Municipal Airport","city":"Cortez","country":"US","lat":37.303001,"lng":-108.627998},{"code":"CGI","name":"Cape Girardeau Regional Airport","city":"Cape Girardeau","country":"US","lat":37.2253,"lng":-89.570801},{"code":"CHA","name":"Chattanooga Metropolitan Airport (Lovell Field)","city":"Chattanooga","country":"US","lat":35.035301,"lng":-85.203796},{"code":"CHO","name":"Charlottesville Albemarle Airport","city":"Charlottesville","country":"US","lat":38.13859939575195,"lng":-78.4529037475586},{"code":"CHS","name":"Charleston International Airport","city":"Charleston","country":"US","lat":32.896159,"lng":-80.038151},{"code":"CID","name":"The Eastern Iowa Airport","city":"Cedar Rapids","country":"US","lat":41.884701,"lng":-91.7108},{"code":"CIU","name":"Chippewa County International Airport","city":"Kincheloe","country":"US","lat":46.242038,"lng":-84.462082},{"code":"CKB","name":"North Central West Virginia Airport","city":"Bridgeport","country":"US","lat":39.2966,"lng":-80.228104},{"code":"CLE","name":"Cleveland Hopkins International Airport","city":"Cleveland","country":"US","lat":41.411701,"lng":-81.8498},{"code":"CLL","name":"Easterwood Field","city":"College Station","country":"US","lat":30.5886,"lng":-96.3638},{"code":"CLT","name":"Charlotte Douglas International Airport","city":"Charlotte","country":"US","lat":35.2140007019043,"lng":-80.94309997558594},{"code":"CMH","name":"John Glenn Columbus International Airport","city":"Columbus","country":"US","lat":39.998001,"lng":-82.891899},{"code":"CMI","name":"University of Illinois Willard Airport","city":"Savoy","country":"US","lat":40.039819,"lng":-88.276249},{"code":"CMX","name":"Houghton County Memorial Airport","city":"Hancock","country":"US","lat":47.168399810791016,"lng":-88.48909759521484},{"code":"CNM","name":"Cavern City Air Terminal","city":"Carlsbad","country":"US","lat":32.337501525878906,"lng":-104.26300048828125},{"code":"CNY","name":"Canyonlands Regional Airport","city":"Moab","country":"US","lat":38.755001,"lng":-109.754997},{"code":"COD","name":"Yellowstone Regional Airport","city":"Cody","country":"US","lat":44.520199,"lng":-109.024002},{"code":"COS","name":"City of Colorado Springs Municipal Airport","city":"Colorado Springs","country":"US","lat":38.805801,"lng":-104.700996},{"code":"COU","name":"Columbia Regional Airport","city":"Columbia","country":"US","lat":38.8181,"lng":-92.219597},{"code":"CPR","name":"Casper-Natrona County International Airport","city":"Casper","country":"US","lat":42.907381,"lng":-106.461609},{"code":"CRP","name":"Corpus Christi International Airport","city":"Corpus Christi","country":"US","lat":27.770399,"lng":-97.501198},{"code":"CLD","name":"McClellan-Palomar Airport","city":"Carlsbad","country":"US","lat":33.1283,"lng":-117.279999},{"code":"CRW","name":"Yeager Airport","city":"Charleston","country":"US","lat":38.3731,"lng":-81.593201},{"code":"CSG","name":"Columbus Airport","city":"Columbus","country":"US","lat":32.516415,"lng":-84.939602},{"code":"CVG","name":"Cincinnati Northern Kentucky International Airport","city":"Cincinnati / Covington","country":"US","lat":39.048801,"lng":-84.667801},{"code":"CVN","name":"Clovis Municipal Airport","city":"Clovis","country":"US","lat":34.426594,"lng":-103.078794},{"code":"CWA","name":"Central Wisconsin Airport","city":"Mosinee","country":"US","lat":44.777448,"lng":-89.670238},{"code":"CYS","name":"Cheyenne Regional Jerry Olson Field","city":"Cheyenne","country":"US","lat":41.155701,"lng":-104.811997},{"code":"DAB","name":"Daytona Beach International Airport","city":"Daytona Beach","country":"US","lat":29.18255,"lng":-81.059464},{"code":"DAL","name":"Dallas Love Field","city":"Dallas","country":"US","lat":32.844776,"lng":-96.847653},{"code":"DAY","name":"James M. Cox Dayton International Airport","city":"Dayton","country":"US","lat":39.902401,"lng":-84.219398},{"code":"DBQ","name":"Dubuque Regional Airport","city":"Dubuque","country":"US","lat":42.402,"lng":-90.709503},{"code":"DCA","name":"Ronald Reagan Washington National Airport","city":"Washington","country":"US","lat":38.8521,"lng":-77.037697},{"code":"DDC","name":"Dodge City Regional Airport","city":"Dodge City","country":"US","lat":37.76340103149414,"lng":-99.9655990600586},{"code":"DEC","name":"Decatur Airport","city":"Decatur","country":"US","lat":39.834598541259766,"lng":-88.8656997680664},{"code":"DEN","name":"Denver International Airport","city":"Denver","country":"US","lat":39.860027,"lng":-104.673792},{"code":"DFW","name":"Dallas Fort Worth International Airport","city":"Dallas-Fort Worth","country":"US","lat":32.896801,"lng":-97.038002},{"code":"DHN","name":"Dothan Regional Airport","city":"Dothan","country":"US","lat":31.321300506591797,"lng":-85.44960021972656},{"code":"DIK","name":"Dickinson Theodore Roosevelt Regional Airport","city":"Dickinson","country":"US","lat":46.797533,"lng":-102.801905},{"code":"DLH","name":"Duluth International Airport","city":"Duluth","country":"US","lat":46.841873,"lng":-92.198746},{"code":"DOV","name":"Dover Civil Air Terminal/Dover Air Force Base","city":"Dover","country":"US","lat":39.129501,"lng":-75.466003},{"code":"DRO","name":"Durango La Plata County Airport","city":"Durango","country":"US","lat":37.151501,"lng":-107.753998},{"code":"DSM","name":"Des Moines International Airport","city":"Des Moines","country":"US","lat":41.534027,"lng":-93.656719},{"code":"DSI","name":"Destin Executive Airport","city":"Destin","country":"US","lat":30.400101,"lng":-86.471497},{"code":"DTW","name":"Detroit Metropolitan Wayne County Airport","city":"Detroit","country":"US","lat":42.21377,"lng":-83.353786},{"code":"DUJ","name":"DuBois Regional Airport","city":"Dubois","country":"US","lat":41.17829895,"lng":-78.8986969},{"code":"DVL","name":"Devils Lake Regional Airport","city":"Devils Lake","country":"US","lat":48.115454,"lng":-98.908796},{"code":"KDZ","name":"Polgolla Reservoir Seaplane Base","city":"Kandy","country":"LK","lat":7.3251,"lng":80.6422},{"code":"EAR","name":"Kearney Regional Airport","city":"Kearney","country":"US","lat":40.727001,"lng":-99.006798},{"code":"EAT","name":"Pangborn Memorial Airport","city":"Wenatchee","country":"US","lat":47.398899,"lng":-120.207001},{"code":"EAU","name":"Chippewa Valley Regional Airport","city":"Eau Claire","country":"US","lat":44.86579895019531,"lng":-91.48429870605469},{"code":"KEB","name":"Nanwalek Airport","city":"Nanwalek","country":"US","lat":59.3521,"lng":-151.925003},{"code":"ECP","name":"Northwest Florida Beaches International Airport","city":"Panama City Beach","country":"US","lat":30.357106,"lng":-85.795414},{"code":"EGE","name":"Eagle County Regional Airport","city":"Eagle","country":"US","lat":39.64260101,"lng":-106.9179993},{"code":"EKO","name":"Elko Regional Airport","city":"Elko","country":"US","lat":40.82490158081055,"lng":-115.79199981689453},{"code":"ELD","name":"South Arkansas Regional Airport at Goodwin Field","city":"El Dorado","country":"US","lat":33.221001,"lng":-92.813301},{"code":"ELM","name":"Elmira Corning Regional Airport","city":"Elmira/Corning","country":"US","lat":42.1599006652832,"lng":-76.8916015625},{"code":"ELP","name":"El Paso International Airport","city":"El Paso","country":"US","lat":31.809908,"lng":-106.375607},{"code":"ERI","name":"Erie International Tom Ridge Field","city":"Erie","country":"US","lat":42.083127,"lng":-80.173867},{"code":"ESC","name":"Delta County Airport","city":"Escanaba","country":"US","lat":45.723199,"lng":-87.088623},{"code":"EUG","name":"Eugene Airport","city":"Eugene","country":"US","lat":44.124599,"lng":-123.211998},{"code":"EVV","name":"Evansville Regional Airport","city":"Evansville","country":"US","lat":38.0369987488,"lng":-87.5324020386},{"code":"EWB","name":"New Bedford Regional Airport","city":"New Bedford","country":"US","lat":41.67610168457031,"lng":-70.95690155029297},{"code":"EWN","name":"Coastal Carolina Regional Airport","city":"New Bern","country":"US","lat":35.0730018616,"lng":-77.04290008539999},{"code":"EWR","name":"Newark Liberty International Airport","city":"Newark","country":"US","lat":40.6894,"lng":-74.170545},{"code":"EYW","name":"Key West International Airport","city":"Key West","country":"US","lat":24.556119,"lng":-81.759579},{"code":"FAR","name":"Hector International Airport","city":"Fargo","country":"US","lat":46.9207,"lng":-96.815804},{"code":"FAT","name":"Fresno Yosemite International Airport","city":"Fresno","country":"US","lat":36.775767,"lng":-119.718018},{"code":"FAY","name":"Fayetteville Regional Airport - Grannis Field","city":"Fayetteville","country":"US","lat":34.991199,"lng":-78.880302},{"code":"FRD","name":"Friday Harbor Airport","city":"Friday Harbor","country":"US","lat":48.523654,"lng":-123.024645},{"code":"FLG","name":"Flagstaff Pulliam Airport","city":"Flagstaff","country":"US","lat":35.139757,"lng":-111.669826},{"code":"FLL","name":"Fort Lauderdale Hollywood International Airport","city":"Fort Lauderdale","country":"US","lat":26.072599,"lng":-80.152702},{"code":"FLO","name":"Florence Regional Airport","city":"Florence","country":"US","lat":34.18539810180664,"lng":-79.7238998413086},{"code":"FNT","name":"Bishop International Airport","city":"Flint","country":"US","lat":42.969294,"lng":-83.743355},{"code":"FOD","name":"Fort Dodge Regional Airport","city":"Fort Dodge","country":"US","lat":42.552624,"lng":-94.191237},{"code":"FSD","name":"Sioux Falls Regional Airport","city":"Sioux Falls","country":"US","lat":43.585463,"lng":-96.741152},{"code":"FSM","name":"Fort Smith Regional Airport","city":"Fort Smith","country":"US","lat":35.336601,"lng":-94.367401},{"code":"FTW","name":"Fort Worth Meacham International Airport","city":"Fort Worth","country":"US","lat":32.819919,"lng":-97.360818},{"code":"FWA","name":"Fort Wayne International Airport","city":"Fort Wayne","country":"US","lat":40.978896,"lng":-85.194465},{"code":"GCC","name":"Northeast Wyoming Regional Airport","city":"Gillette","country":"US","lat":44.3489,"lng":-105.539001},{"code":"GCK","name":"Garden City Regional Airport","city":"Garden City","country":"US","lat":37.9275016785,"lng":-100.723999023},{"code":"GCN","name":"Grand Canyon National Park Airport","city":"Grand Canyon - Tusayan","country":"US","lat":35.9524,"lng":-112.147003},{"code":"GDV","name":"Dawson Community Airport","city":"Glendive","country":"US","lat":47.137658,"lng":-104.80691},{"code":"GEG","name":"Spokane International Airport","city":"Spokane","country":"US","lat":47.6199,"lng":-117.533997},{"code":"GFK","name":"Grand Forks International Airport","city":"Grand Forks","country":"US","lat":47.949299,"lng":-97.176102},{"code":"GGG","name":"East Texas Regional Airport","city":"Longview","country":"US","lat":32.38399887084961,"lng":-94.71150207519531},{"code":"GGW","name":"Glasgow Valley County Airport Wokal Field","city":"Glasgow","country":"US","lat":48.212502,"lng":-106.614998},{"code":"GJT","name":"Grand Junction Regional Airport","city":"Grand Junction","country":"US","lat":39.126663,"lng":-108.529387},{"code":"GLH","name":"Mid Delta Regional Airport","city":"Greenville","country":"US","lat":33.482899,"lng":-90.985603},{"code":"GNV","name":"Gainesville Regional Airport","city":"Gainesville","country":"US","lat":29.6900997162,"lng":-82.2717971802},{"code":"FCA","name":"Glacier Park International Airport","city":"Kalispell","country":"US","lat":48.310501,"lng":-114.255997},{"code":"GPT","name":"Gulfport Biloxi International Airport","city":"Gulfport","country":"US","lat":30.405582,"lng":-89.069836},{"code":"GRB","name":"Austin Straubel International Airport","city":"Green Bay","country":"US","lat":44.483459,"lng":-88.130805},{"code":"GRI","name":"Central Nebraska Regional Airport","city":"Grand Island","country":"US","lat":40.967498779296875,"lng":-98.30960083007812},{"code":"GRK","name":"Killeen Regional Airport / Robert Gray Army Airfield","city":"Fort Cavazos","country":"US","lat":31.0672,"lng":-97.828903},{"code":"GRR","name":"Gerald R. Ford International Airport","city":"Grand Rapids","country":"US","lat":42.880798,"lng":-85.522797},{"code":"GSO","name":"Piedmont Triad International Airport","city":"Greensboro","country":"US","lat":36.09937,"lng":-79.937262},{"code":"GSP","name":"Greenville-Spartanburg International Airport","city":"Greenville/Greer/Spartanburg","country":"US","lat":34.895699,"lng":-82.218903},{"code":"GTF","name":"Great Falls International Airport","city":"Great Falls","country":"US","lat":47.481998,"lng":-111.371002},{"code":"GTR","name":"Golden Triangle Regional Airport","city":"Columbus/W Point/Starkville","country":"US","lat":33.450298,"lng":-88.5914},{"code":"GUC","name":"Gunnison Crested Butte Regional Airport","city":"Gunnison","country":"US","lat":38.534672,"lng":-106.934566},{"code":"GUP","name":"Gallup Municipal Airport","city":"Gallup","country":"US","lat":35.511653,"lng":-108.788166},{"code":"KGX","name":"Grayling Airport","city":"Grayling","country":"US","lat":62.895187,"lng":-160.066289},{"code":"GYY","name":"Gary/Chicago International Airport","city":"Gary","country":"US","lat":41.617087,"lng":-87.413206},{"code":"DSY","name":"Dara Sakor International Airport","city":"Ta Noun","country":"KH","lat":10.914244,"lng":103.226652},{"code":"KTI","name":"Techo International Airport","city":"Phnom Penh (Boeng Khyang)","country":"KH","lat":11.359987,"lng":104.921272},{"code":"HDN","name":"Yampa Valley Airport","city":"Hayden","country":"US","lat":40.481201,"lng":-107.218002},{"code":"HGR","name":"Hagerstown Regional Richard A Henson Field","city":"Hagerstown","country":"US","lat":39.708771,"lng":-77.728014},{"code":"HHR","name":"Jack Northrop Field Hawthorne Municipal Airport","city":"Hawthorne","country":"US","lat":33.922798,"lng":-118.334999},{"code":"HIB","name":"Range Regional Airport","city":"Hibbing","country":"US","lat":47.384831,"lng":-92.836905},{"code":"HII","name":"Lake Havasu City Airport","city":"Lake Havasu City","country":"US","lat":34.570506,"lng":-114.357745},{"code":"HLN","name":"Helena Regional Airport","city":"Helena","country":"US","lat":46.6068,"lng":-111.983002},{"code":"HOB","name":"Lea County Regional Airport","city":"Hobbs","country":"US","lat":32.6875,"lng":-103.217003},{"code":"HOT","name":"Memorial Field Airport","city":"Hot Springs","country":"US","lat":34.4788,"lng":-93.096262},{"code":"HOU","name":"William P. Hobby Airport","city":"Houston","country":"US","lat":29.645336,"lng":-95.276812},{"code":"HPN","name":"Westchester County Airport","city":"White Plains","country":"US","lat":41.06700134277344,"lng":-73.70760345458984},{"code":"HRL","name":"Valley International Airport","city":"Harlingen","country":"US","lat":26.228500366210938,"lng":-97.65440368652344},{"code":"HRO","name":"Boone County Airport","city":"Harrison","country":"US","lat":36.26150131225586,"lng":-93.15470123291016},{"code":"HSV","name":"Huntsville International Airport","city":"Huntsville","country":"US","lat":34.636244,"lng":-86.774378},{"code":"HTS","name":"Tri-State Airport / Milton J. Ferguson Field","city":"Huntington","country":"US","lat":38.366699,"lng":-82.557999},{"code":"HVN","name":"Tweed New Haven Airport","city":"New Haven","country":"US","lat":41.262869,"lng":-72.887694},{"code":"HVR","name":"Havre City County Airport","city":"Havre","country":"US","lat":48.541387,"lng":-109.762859},{"code":"HHH","name":"Hilton Head Airport","city":"Hilton Head Island","country":"US","lat":32.2244,"lng":-80.697502},{"code":"HYA","name":"Cape Cod Gateway Airport","city":"Hyannis","country":"US","lat":41.6693,"lng":-70.280403},{"code":"HYS","name":"Hays Regional Airport","city":"Hays","country":"US","lat":38.844521,"lng":-99.273148},{"code":"IAD","name":"Washington Dulles International Airport","city":"Dulles","country":"US","lat":38.9445,"lng":-77.455803},{"code":"IAG","name":"Niagara Falls International Airport","city":"Niagara Falls","country":"US","lat":43.1073,"lng":-78.946198},{"code":"IAH","name":"George Bush Intercontinental Airport","city":"Houston","country":"US","lat":29.9844,"lng":-95.3414},{"code":"ICT","name":"Wichita Dwight D. Eisenhower National Airport","city":"Wichita","country":"US","lat":37.650314,"lng":-97.428583},{"code":"IDA","name":"Idaho Falls Regional Airport","city":"Idaho Falls","country":"US","lat":43.514599,"lng":-112.070999},{"code":"ILG","name":"Wilmington Airport","city":"Wilmington","country":"US","lat":39.678699,"lng":-75.606499},{"code":"ILM","name":"Wilmington International Airport","city":"Wilmington","country":"US","lat":34.272345,"lng":-77.905117},{"code":"IMT","name":"Ford Airport","city":"Kingsford","country":"US","lat":45.81911,"lng":-88.114557},{"code":"IND","name":"Indianapolis International Airport","city":"Indianapolis","country":"US","lat":39.7173,"lng":-86.294403},{"code":"INL","name":"Falls International Airport","city":"International Falls","country":"US","lat":48.566200256347656,"lng":-93.4030990600586},{"code":"IPL","name":"Imperial County Airport","city":"Imperial","country":"US","lat":32.83539,"lng":-115.574026},{"code":"IPT","name":"Williamsport Regional Airport","city":"Williamsport","country":"US","lat":41.242126,"lng":-76.92245},{"code":"IRK","name":"Kirksville Regional Airport","city":"Kirksville","country":"US","lat":40.09349822998047,"lng":-92.5448989868164},{"code":"ISP","name":"Long Island MacArthur Airport","city":"Islip","country":"US","lat":40.796324,"lng":-73.101703},{"code":"ITH","name":"Ithaca Tompkins Regional Airport","city":"Ithaca","country":"US","lat":42.49100112915039,"lng":-76.4583969116211},{"code":"AZA","name":"Mesa Gateway Airport","city":"Mesa","country":"US","lat":33.3078,"lng":-111.654999},{"code":"IWD","name":"Gogebic Iron County Airport","city":"Ironwood","country":"US","lat":46.525298,"lng":-90.131578},{"code":"JAC","name":"Jackson Hole Airport","city":"Jackson","country":"US","lat":43.6073,"lng":-110.737999},{"code":"JAN","name":"Jackson-Medgar Wiley Evers International Airport","city":"Jackson","country":"US","lat":32.311199,"lng":-90.075897},{"code":"JAX","name":"Jacksonville International Airport","city":"Jacksonville","country":"US","lat":30.492469,"lng":-81.687813},{"code":"JBR","name":"Jonesboro Municipal Airport","city":"Jonesboro","country":"US","lat":35.83169937133789,"lng":-90.64640045166016},{"code":"JFK","name":"John F. Kennedy International Airport","city":"New York","country":"US","lat":40.639447,"lng":-73.779317},{"code":"JLN","name":"Joplin Regional Airport","city":"Joplin","country":"US","lat":37.151798,"lng":-94.498299},{"code":"JMS","name":"Jamestown Regional Airport","city":"Jamestown","country":"US","lat":46.92969894,"lng":-98.67819977},{"code":"USA","name":"Concord-Padgett Regional Airport","city":"Concord","country":"US","lat":35.387798,"lng":-80.709099},{"code":"JST","name":"John Murtha Johnstown Cambria County Airport","city":"Johnstown","country":"US","lat":40.31610107421875,"lng":-78.83390045166016},{"code":"KKB","name":"Kitoi Bay Seaplane Base","city":"Kitoi Bay","country":"US","lat":58.190899,"lng":-152.369995},{"code":"LAF","name":"Purdue University Airport","city":"West Lafayette","country":"US","lat":40.412875,"lng":-86.939363},{"code":"LAL","name":"Lakeland Linder International Airport","city":"Lakeland","country":"US","lat":27.989267,"lng":-82.020715},{"code":"LAN","name":"Capital Region International Airport","city":"Lansing","country":"US","lat":42.777582,"lng":-84.585721},{"code":"LAR","name":"Laramie Regional Airport","city":"Laramie","country":"US","lat":41.31209945678711,"lng":-105.67500305175781},{"code":"LAS","name":"Harry Reid International Airport","city":"Las Vegas","country":"US","lat":36.083361,"lng":-115.151817},{"code":"LAW","name":"Lawton Fort Sill Regional Airport","city":"Lawton","country":"US","lat":34.567699,"lng":-98.416603},{"code":"LAX","name":"Los Angeles International Airport","city":"Los Angeles","country":"US","lat":33.942501,"lng":-118.407997},{"code":"LBB","name":"Lubbock Preston Smith International Airport","city":"Lubbock","country":"US","lat":33.663601,"lng":-101.822998},{"code":"LBE","name":"Arnold Palmer Regional Airport","city":"Latrobe","country":"US","lat":40.275902,"lng":-79.4048},{"code":"LBF","name":"North Platte Regional Airport Lee Bird Field","city":"North Platte","country":"US","lat":41.12620163,"lng":-100.6839981},{"code":"LBL","name":"Liberal Mid-America Regional Airport","city":"Liberal","country":"US","lat":37.0442009,"lng":-100.9599991},{"code":"LCH","name":"Lake Charles Regional Airport","city":"Lake Charles","country":"US","lat":30.126100540161133,"lng":-93.22329711914062},{"code":"LCK","name":"Rickenbacker International Airport","city":"Columbus","country":"US","lat":39.813801,"lng":-82.927803},{"code":"LEB","name":"Lebanon Municipal Airport","city":"Lebanon","country":"US","lat":43.626098632799994,"lng":-72.30419921880001},{"code":"LEX","name":"Blue Grass Airport","city":"Lexington","country":"US","lat":38.035066,"lng":-84.606738},{"code":"LFT","name":"Lafayette Regional Airport","city":"Lafayette","country":"US","lat":30.205299,"lng":-91.987602},{"code":"LGA","name":"LaGuardia Airport","city":"New York","country":"US","lat":40.777199,"lng":-73.872597},{"code":"LGB","name":"Long Beach International Airport","city":"Long Beach","country":"US","lat":33.816523,"lng":-118.149891},{"code":"LIT","name":"Bill & Hillary Clinton National Airport/Adams Field","city":"Little Rock","country":"US","lat":34.729222,"lng":-92.223591},{"code":"LNK","name":"Lincoln Airport","city":"Lincoln","country":"US","lat":40.844882,"lng":-96.76178},{"code":"LNS","name":"Lancaster Airport","city":"Lancaster","country":"US","lat":40.121700286865234,"lng":-76.29609680175781},{"code":"LRD","name":"Laredo International Airport","city":"Laredo","country":"US","lat":27.5438,"lng":-99.461601},{"code":"LRU","name":"Las Cruces International Airport","city":"Las Cruces","country":"US","lat":32.289398,"lng":-106.921997},{"code":"LSE","name":"La Crosse Regional Airport","city":"La Crosse","country":"US","lat":43.879002,"lng":-91.256699},{"code":"LUK","name":"Cincinnati Municipal Airport Lunken Field","city":"Cincinnati","country":"US","lat":39.102357,"lng":-84.418945},{"code":"LWB","name":"Greenbrier Valley Airport","city":"Lewisburg","country":"US","lat":37.857914,"lng":-80.400438},{"code":"LWS","name":"Lewiston Nez Perce County Airport","city":"Lewiston","country":"US","lat":46.3745002746582,"lng":-117.01499938964844},{"code":"LYH","name":"Lynchburg Regional Airport - Preston Glenn Field","city":"Lynchburg","country":"US","lat":37.326698,"lng":-79.200401},{"code":"MAF","name":"Midland International Air and Space Port","city":"Midland","country":"US","lat":31.942499,"lng":-102.202003},{"code":"MBL","name":"Manistee County Blacker Airport","city":"Manistee","country":"US","lat":44.272707,"lng":-86.246495},{"code":"MBS","name":"MBS International Airport","city":"Freeland","country":"US","lat":43.533181,"lng":-84.083104},{"code":"MCE","name":"Merced Regional Macready Field","city":"Merced","country":"US","lat":37.284698,"lng":-120.514},{"code":"MCI","name":"Kansas City International Airport","city":"Kansas City","country":"US","lat":39.301699,"lng":-94.713893},{"code":"MCK","name":"McCook Ben Nelson Regional Airport","city":"McCook","country":"US","lat":40.207787,"lng":-100.592794},{"code":"MCN","name":"Middle Georgia Regional Airport","city":"Macon","country":"US","lat":32.69279861450195,"lng":-83.64920043945312},{"code":"MCO","name":"Orlando International Airport","city":"Orlando","country":"US","lat":28.429399490356445,"lng":-81.30899810791016},{"code":"MCW","name":"Mason City Municipal Airport","city":"Mason City","country":"US","lat":43.159801,"lng":-93.329716},{"code":"MDT","name":"Harrisburg International Airport","city":"Harrisburg","country":"US","lat":40.192838,"lng":-76.762333},{"code":"MDW","name":"Chicago Midway International Airport","city":"Chicago","country":"US","lat":41.785999,"lng":-87.752403},{"code":"MEI","name":"Key Field / Meridian Regional Airport","city":"Meridian","country":"US","lat":32.3326,"lng":-88.7519},{"code":"MEM","name":"Memphis International Airport","city":"Memphis","country":"US","lat":35.043845,"lng":-89.97634},{"code":"MFE","name":"McAllen Miller International Airport","city":"McAllen","country":"US","lat":26.176141,"lng":-98.237965},{"code":"MFR","name":"Rogue Valley International-Medford Airport","city":"Medford","country":"US","lat":42.374199,"lng":-122.873001},{"code":"MGC","name":"Michigan City Municipal Airport","city":"Michigan City","country":"US","lat":41.7033,"lng":-86.821198},{"code":"MGM","name":"Montgomery Regional (Dannelly Field) Airport","city":"Montgomery","country":"US","lat":32.300598,"lng":-86.393997},{"code":"MGW","name":"Morgantown Municipal Airport Walter L. (Bill) Hart Field","city":"Morgantown","country":"US","lat":39.643305,"lng":-79.917598},{"code":"MHK","name":"Manhattan Regional Airport","city":"Manhattan","country":"US","lat":39.14099884033203,"lng":-96.6707992553711},{"code":"MHT","name":"Manchester-Boston Regional Airport","city":"Manchester","country":"US","lat":42.932598,"lng":-71.435699},{"code":"MIA","name":"Miami International Airport","city":"Miami","country":"US","lat":25.796011,"lng":-80.289751},{"code":"MKE","name":"General Mitchell International Airport","city":"Milwaukee","country":"US","lat":42.947201,"lng":-87.896599},{"code":"MKG","name":"Muskegon County Airport","city":"Muskegon","country":"US","lat":43.169498,"lng":-86.238197},{"code":"MKL","name":"McKellar-Sipes Regional Airport","city":"Jackson","country":"US","lat":35.599899,"lng":-88.915604},{"code":"MLB","name":"Melbourne Orlando International Airport","city":"Melbourne","country":"US","lat":28.102009,"lng":-80.641088},{"code":"MLI","name":"Quad City International Airport","city":"Moline","country":"US","lat":41.448502,"lng":-90.5075},{"code":"MLU","name":"Monroe Regional Airport","city":"Monroe","country":"US","lat":32.510899,"lng":-92.037697},{"code":"MMH","name":"Mammoth Yosemite Airport","city":"Mammoth Lakes","country":"US","lat":37.625381,"lng":-118.84306},{"code":"MOB","name":"Mobile Regional Airport","city":"Mobile","country":"US","lat":30.6912,"lng":-88.242798},{"code":"MOT","name":"Minot International Airport","city":"Minot","country":"US","lat":48.25801,"lng":-101.279123},{"code":"MRY","name":"Monterey Regional Airport","city":"Monterey","country":"US","lat":36.586777,"lng":-121.844183},{"code":"MSL","name":"Northwest Alabama Regional Airport","city":"Muscle Shoals","country":"US","lat":34.745123,"lng":-87.612981},{"code":"MSN","name":"Dane County Regional Truax Field","city":"Madison","country":"US","lat":43.1399,"lng":-89.337502},{"code":"MSO","name":"Missoula Montana Airport","city":"Missoula","country":"US","lat":46.915816,"lng":-114.091107},{"code":"MSP","name":"Minneapolis–Saint Paul International Airport / Wold–Chamberlain Field","city":"Minneapolis","country":"US","lat":44.880081,"lng":-93.221741},{"code":"MSS","name":"Massena International Airport Richards Field","city":"Massena","country":"US","lat":44.936157,"lng":-74.844304},{"code":"MSY","name":"Louis Armstrong New Orleans International Airport","city":"New Orleans","country":"US","lat":29.993401,"lng":-90.264673},{"code":"MTJ","name":"Montrose Regional Airport","city":"Montrose","country":"US","lat":38.5098,"lng":-107.893997},{"code":"MTP","name":"Montauk Airport","city":"Montauk","country":"US","lat":41.0765,"lng":-71.920797},{"code":"MVY","name":"Martha's Vineyard Airport","city":"Martha's Vineyard","country":"US","lat":41.3931007385,"lng":-70.6143035889},{"code":"MWA","name":"Veterans Airport of Southern Illinois","city":"Marion","country":"US","lat":37.751208,"lng":-89.016568},{"code":"MWL","name":"Mineral Wells Regional Airport","city":"Mineral Wells","country":"US","lat":32.781601,"lng":-98.060204},{"code":"KMY","name":"Moser Bay Seaplane Base","city":"Moser Bay","country":"US","lat":57.0256,"lng":-154.145996},{"code":"MYL","name":"McCall Municipal Airport","city":"McCall","country":"US","lat":44.888837,"lng":-116.101112},{"code":"MYR","name":"Myrtle Beach International Airport","city":"Myrtle Beach","country":"US","lat":33.679699,"lng":-78.928299},{"code":"YUM","name":"Yuma International Airport / Marine Corps Air Station Yuma","city":"Yuma","country":"US","lat":32.650938,"lng":-114.609375},{"code":"OAJ","name":"Albert J Ellis Airport","city":"Richlands","country":"US","lat":34.829201,"lng":-77.612099},{"code":"OAK","name":"Oakland San Francisco Bay Airport","city":"Oakland","country":"US","lat":37.720085,"lng":-122.221184},{"code":"OGD","name":"Ogden Hinckley Airport","city":"Ogden","country":"US","lat":41.195899963379,"lng":-112.0120010376},{"code":"OGS","name":"Ogdensburg International Airport","city":"Ogdensburg","country":"US","lat":44.6819,"lng":-75.4655},{"code":"OKC","name":"OKC Will Rogers World Airport","city":"Oklahoma City","country":"US","lat":35.393388,"lng":-97.598248},{"code":"OLF","name":"L M Clayton Airport","city":"Wolf Point","country":"US","lat":48.094501,"lng":-105.574997},{"code":"OLM","name":"Olympia Regional Airport","city":"Olympia","country":"US","lat":46.969399,"lng":-122.903},{"code":"OMA","name":"Eppley Airfield","city":"Omaha","country":"US","lat":41.3032,"lng":-95.894096},{"code":"ONT","name":"Ontario International Airport","city":"Ontario","country":"US","lat":34.056,"lng":-117.600998},{"code":"OPF","name":"Miami-Opa Locka Executive Airport","city":"Miami","country":"US","lat":25.907,"lng":-80.278397},{"code":"ORD","name":"Chicago O'Hare International Airport","city":"Chicago","country":"US","lat":41.9786,"lng":-87.9048},{"code":"ORF","name":"Norfolk International Airport","city":"Norfolk","country":"US","lat":36.895341,"lng":-76.201},{"code":"ORH","name":"Worcester Regional Airport","city":"Worcester","country":"US","lat":42.2673,"lng":-71.875702},{"code":"ESD","name":"Orcas Island Airport","city":"Eastsound","country":"US","lat":48.708199,"lng":-122.910004},{"code":"OTH","name":"Southwest Oregon Regional Airport","city":"North Bend","country":"US","lat":43.41709899902344,"lng":-124.24600219726562},{"code":"OWB","name":"Owensboro Daviess County Airport","city":"Owensboro","country":"US","lat":37.74010086,"lng":-87.16680145},{"code":"OCE","name":"Ocean City Municipal Airport","city":"Ocean City","country":"US","lat":38.310398,"lng":-75.124001},{"code":"KOY","name":"Olga Bay Seaplane Base","city":"Olga Bay","country":"US","lat":57.161499,"lng":-154.229996},{"code":"PAE","name":"Seattle Paine Field International Airport","city":"Everett","country":"US","lat":47.9063,"lng":-122.281998},{"code":"PAH","name":"Barkley Regional Airport","city":"Paducah","country":"US","lat":37.06079864501953,"lng":-88.7738037109375},{"code":"KPB","name":"Point Baker Seaplane Base","city":"Point Baker","country":"US","lat":56.351898,"lng":-133.623001},{"code":"PBG","name":"Plattsburgh International Airport","city":"Plattsburgh","country":"US","lat":44.650902,"lng":-73.468102},{"code":"PBI","name":"President Donald J. Trump International Airport","city":"West Palm Beach","country":"US","lat":26.683201,"lng":-80.095596},{"code":"PDK","name":"DeKalb Peachtree Airport","city":"Atlanta","country":"US","lat":33.87633,"lng":-84.30213},{"code":"PDT","name":"Eastern Oregon Regional Airport at Pendleton","city":"Pendleton","country":"US","lat":45.695099,"lng":-118.841003},{"code":"PDX","name":"Portland International Airport","city":"Portland","country":"US","lat":45.588699,"lng":-122.598},{"code":"PGA","name":"Page Municipal Airport","city":"Page","country":"US","lat":36.924234,"lng":-111.447716},{"code":"PGD","name":"Punta Gorda Airport","city":"Punta Gorda","country":"US","lat":26.9202,"lng":-81.990501},{"code":"PGV","name":"Pitt-Greenville Airport","city":"Greenville","country":"US","lat":35.635535,"lng":-77.384262},{"code":"PHF","name":"Newport News Williamsburg International Airport","city":"Newport News","country":"US","lat":37.131901,"lng":-76.492996},{"code":"PHL","name":"Philadelphia International Airport","city":"Philadelphia","country":"US","lat":39.871899,"lng":-75.241096},{"code":"PHX","name":"Phoenix Sky Harbor International Airport","city":"Phoenix","country":"US","lat":33.435302,"lng":-112.005905},{"code":"PIA","name":"General Wayne A. Downing Peoria International Airport","city":"Peoria","country":"US","lat":40.663841,"lng":-89.692631},{"code":"PIB","name":"Hattiesburg Laurel Regional Airport","city":"Moselle","country":"US","lat":31.4671,"lng":-89.337097},{"code":"PIE","name":"St. Petersburg Clearwater International Airport","city":"Pinellas Park","country":"US","lat":27.9102,"lng":-82.687401},{"code":"PIH","name":"Pocatello Regional Airport","city":"Pocatello","country":"US","lat":42.9098014831543,"lng":-112.59600067138672},{"code":"PIR","name":"Pierre Regional Airport","city":"Pierre","country":"US","lat":44.38270187,"lng":-100.2860031},{"code":"PIT","name":"Pittsburgh International Airport","city":"Pittsburgh","country":"US","lat":40.491501,"lng":-80.232903},{"code":"PKB","name":"Mid Ohio Valley Regional Airport","city":"Parkersburg (Williamstown)","country":"US","lat":39.3451,"lng":-81.439201},{"code":"PLN","name":"Pellston Regional Airport of Emmet County Airport","city":"Pellston","country":"US","lat":45.57089996,"lng":-84.79669952},{"code":"PNS","name":"Pensacola International Airport","city":"Pensacola","country":"US","lat":30.472718,"lng":-87.186639},{"code":"PQI","name":"Presque Isle International Airport","city":"Presque Isle","country":"US","lat":46.688999,"lng":-68.0448},{"code":"KPR","name":"Port Williams Seaplane Base","city":"Port Williams","country":"US","lat":58.490101,"lng":-152.582001},{"code":"PRC","name":"Prescott Regional Airport - Ernest A. Love Field","city":"Prescott","country":"US","lat":34.653502,"lng":-112.419883},{"code":"PSC","name":"Tri Cities Airport","city":"Pasco","country":"US","lat":46.264702,"lng":-119.119003},{"code":"PSM","name":"Portsmouth International Airport at Pease","city":"Portsmouth","country":"US","lat":43.0779,"lng":-70.823303},{"code":"PSP","name":"Palm Springs International Airport","city":"Palm Springs","country":"US","lat":33.8297,"lng":-116.507004},{"code":"PUB","name":"Pueblo Memorial Airport","city":"Pueblo","country":"US","lat":38.289101,"lng":-104.497002},{"code":"PUW","name":"Pullman-Moscow Regional Airport","city":"Pullman","country":"US","lat":46.741601,"lng":-117.11163},{"code":"PVD","name":"Rhode Island T. F. Green International Airport","city":"Providence/Warwick","country":"US","lat":41.725038,"lng":-71.425668},{"code":"PVU","name":"Provo Municipal Airport","city":"Provo","country":"US","lat":40.218894,"lng":-111.722445},{"code":"PWM","name":"Portland International Jetport","city":"Portland","country":"US","lat":43.646198,"lng":-70.309303},{"code":"KPY","name":"Port Bailey Seaplane Base","city":"Port Bailey","country":"US","lat":57.931257,"lng":-153.040838},{"code":"RAP","name":"Rapid City Regional Airport","city":"Rapid City","country":"US","lat":44.0452995300293,"lng":-103.05699920654297},{"code":"RDD","name":"Redding Municipal Airport","city":"Redding","country":"US","lat":40.50899887,"lng":-122.2929993},{"code":"RDM","name":"Roberts Field","city":"Redmond","country":"US","lat":44.254101,"lng":-121.150002},{"code":"RDU","name":"Raleigh-Durham International Airport","city":"Raleigh/Durham","country":"US","lat":35.878659,"lng":-78.7873},{"code":"RFD","name":"Chicago Rockford International Airport","city":"Chicago/Rockford","country":"US","lat":42.1954,"lng":-89.097198},{"code":"RHI","name":"Rhinelander Oneida County Airport","city":"Rhinelander","country":"US","lat":45.63119888305664,"lng":-89.46749877929688},{"code":"RIC","name":"Richmond International Airport","city":"Richmond","country":"US","lat":37.505199,"lng":-77.319702},{"code":"RIW","name":"Central Wyoming Regional Airport","city":"Riverton","country":"US","lat":43.064201,"lng":-108.459999},{"code":"RKD","name":"Knox County Regional Airport","city":"Rockland","country":"US","lat":44.060101,"lng":-69.099197},{"code":"RKS","name":"Southwest Wyoming Regional Airport","city":"Rock Springs","country":"US","lat":41.5942,"lng":-109.065001},{"code":"RNO","name":"Reno Tahoe International Airport","city":"Reno","country":"US","lat":39.4991,"lng":-119.767998},{"code":"ROA","name":"Roanoke–Blacksburg Regional Airport","city":"Roanoke","country":"US","lat":37.3255,"lng":-79.975403},{"code":"ROC","name":"Frederick Douglass Greater Rochester International Airport","city":"Rochester","country":"US","lat":43.1189,"lng":-77.672401},{"code":"ROW","name":"Roswell Air Center Airport","city":"Roswell","country":"US","lat":33.301601,"lng":-104.530998},{"code":"RST","name":"Rochester International Airport","city":"Rochester","country":"US","lat":43.908298,"lng":-92.5},{"code":"RSW","name":"Southwest Florida International Airport","city":"Fort Myers","country":"US","lat":26.534685,"lng":-81.752816},{"code":"RUT","name":"Rutland - Southern Vermont Regional Airport","city":"Rutland","country":"US","lat":43.5294,"lng":-72.9496},{"code":"SAF","name":"Santa Fe Municipal Airport","city":"Santa Fe","country":"US","lat":35.6171,"lng":-106.088997},{"code":"SAN","name":"San Diego International Airport","city":"San Diego","country":"US","lat":32.733601,"lng":-117.190002},{"code":"SAT","name":"San Antonio International Airport","city":"San Antonio","country":"US","lat":29.533701,"lng":-98.469803},{"code":"SAV","name":"Savannah Hilton Head International Airport","city":"Savannah","country":"US","lat":32.126591,"lng":-81.19998},{"code":"MQT","name":"Marquette/Sawyer International Airport","city":"Gwinn","country":"US","lat":46.351498,"lng":-87.395884},{"code":"SBA","name":"Santa Barbara Municipal Airport","city":"Santa Barbara","country":"US","lat":34.426201,"lng":-119.839996},{"code":"SBD","name":"San Bernardino International Airport","city":"San Bernardino","country":"US","lat":34.096717,"lng":-117.236596},{"code":"SBN","name":"South Bend International Airport","city":"South Bend","country":"US","lat":41.708304,"lng":-86.316922},{"code":"SBP","name":"San Luis County Regional Airport","city":"San Luis Obispo","country":"US","lat":35.236801147499996,"lng":-120.641998291},{"code":"SBY","name":"Salisbury Ocean City Wicomico Regional Airport","city":"Salisbury","country":"US","lat":38.34049987792969,"lng":-75.51029968261719},{"code":"SCK","name":"Stockton Metropolitan Airport","city":"Stockton","country":"US","lat":37.893279,"lng":-121.238079},{"code":"SDF","name":"Louisville Muhammad Ali International Airport","city":"Louisville","country":"US","lat":38.1706,"lng":-85.735076},{"code":"SDY","name":"Sidney - Richland Regional Airport","city":"Sidney","country":"US","lat":47.705141,"lng":-104.194422},{"code":"SEA","name":"Seattle–Tacoma International Airport","city":"Seattle","country":"US","lat":47.447943,"lng":-122.310276},{"code":"SFB","name":"Orlando Sanford International Airport","city":"Orlando","country":"US","lat":28.774277,"lng":-81.234626},{"code":"SFO","name":"San Francisco International Airport","city":"San Francisco","country":"US","lat":37.619806,"lng":-122.374821},{"code":"SGF","name":"Springfield Branson National Airport","city":"Springfield","country":"US","lat":37.245047,"lng":-93.388596},{"code":"UST","name":"Northeast Florida Regional Airport","city":"St Augustine","country":"US","lat":29.9592,"lng":-81.339798},{"code":"SGU","name":"St George Regional Airport","city":"St George","country":"US","lat":37.036389,"lng":-113.510306},{"code":"SHD","name":"Shenandoah Valley Regional Airport","city":"Weyers Cave","country":"US","lat":38.263802,"lng":-78.8964},{"code":"SHR","name":"Sheridan County Airport","city":"Sheridan","country":"US","lat":44.76919937133789,"lng":-106.9800033569336},{"code":"SHV","name":"Shreveport Regional Airport","city":"Shreveport","country":"US","lat":32.444747,"lng":-93.826741},{"code":"SJC","name":"Norman Y. Mineta San Jose International Airport","city":"San Jose","country":"US","lat":37.362452,"lng":-121.929188},{"code":"SJT","name":"San Angelo Regional Mathis Field","city":"San Angelo","country":"US","lat":31.35770034790039,"lng":-100.49600219726562},{"code":"TSM","name":"Taos Regional Airport","city":"Taos","country":"US","lat":36.452494,"lng":-105.67749},{"code":"SLC","name":"Salt Lake City International Airport","city":"Salt Lake City","country":"US","lat":40.78886,"lng":-111.979866},{"code":"SLE","name":"Salem-Willamette Valley Airport/McNary Field","city":"Salem","country":"US","lat":44.9095,"lng":-123.002998},{"code":"SLK","name":"Adirondack Regional Airport","city":"Saranac Lake","country":"US","lat":44.38691,"lng":-74.204629},{"code":"SLN","name":"Salina Municipal Airport","city":"Salina","country":"US","lat":38.79100036621094,"lng":-97.6521987915039},{"code":"SMF","name":"Sacramento International Airport","city":"Sacramento","country":"US","lat":38.6954,"lng":-121.591003},{"code":"SMN","name":"Lemhi County Airport","city":"Salmon","country":"US","lat":45.122233,"lng":-113.88196},{"code":"SMX","name":"Santa Maria Public Airport Captain G Allan Hancock Field","city":"Santa Maria","country":"US","lat":34.898899,"lng":-120.457001},{"code":"SNA","name":"John Wayne Orange County International Airport","city":"Santa Ana","country":"US","lat":33.675063,"lng":-117.869281},{"code":"SOW","name":"Show Low Regional Airport","city":"Show Low","country":"US","lat":34.264062,"lng":-110.007091},{"code":"SPI","name":"Abraham Lincoln Capital Airport","city":"Springfield","country":"US","lat":39.844101,"lng":-89.677902},{"code":"SPS","name":"Wichita Falls Municipal Airport / Sheppard Air Force Base","city":"Wichita Falls","country":"US","lat":33.9888,"lng":-98.491898},{"code":"SQL","name":"San Carlos Airport","city":"San Carlos","country":"US","lat":37.51313,"lng":-122.250838},{"code":"SRQ","name":"Sarasota Bradenton International Airport","city":"Sarasota/Bradenton","country":"US","lat":27.394631,"lng":-82.554359},{"code":"STC","name":"Saint Cloud Regional Airport","city":"Saint Cloud","country":"US","lat":45.5466,"lng":-94.059898},{"code":"STL","name":"St. Louis Lambert International Airport","city":"St Louis","country":"US","lat":38.748697,"lng":-90.370003},{"code":"STS","name":"Charles M. Schulz Sonoma County Airport","city":"Santa Rosa","country":"US","lat":38.50899887,"lng":-122.8130035},{"code":"SUN","name":"Friedman Memorial Airport","city":"Hailey","country":"US","lat":43.50439835,"lng":-114.2959976},{"code":"SUX","name":"Sioux Gateway Airport / Brigadier General Bud Day Field","city":"Sioux City","country":"US","lat":42.397605,"lng":-96.382237},{"code":"SVC","name":"Grant County Airport","city":"Silver City","country":"US","lat":32.636701,"lng":-108.154736},{"code":"SWF","name":"New York Stewart International Airport","city":"Newburgh","country":"US","lat":41.50421,"lng":-74.108901},{"code":"SWO","name":"Stillwater Regional Airport","city":"Stillwater","country":"US","lat":36.162132,"lng":-97.085581},{"code":"SYR","name":"Syracuse Hancock International Airport","city":"Syracuse","country":"US","lat":43.11119842529297,"lng":-76.1063003540039},{"code":"KTB","name":"Thorne Bay Seaplane Base","city":"Thorne Bay","country":"US","lat":55.682762,"lng":-132.532883},{"code":"TBN","name":"Waynesville-St. Robert Regional Airport-Forney Field","city":"Fort Leonard Wood","country":"US","lat":37.7416,"lng":-92.140701},{"code":"TEB","name":"Teterboro Airport","city":"Teterboro","country":"US","lat":40.850101,"lng":-74.060799},{"code":"TEX","name":"Telluride Regional Airport","city":"Telluride","country":"US","lat":37.9538,"lng":-107.907997},{"code":"TIW","name":"Tacoma Narrows Airport","city":"Tacoma","country":"US","lat":47.267407,"lng":-122.577295},{"code":"TLH","name":"Tallahassee International Airport","city":"Tallahassee","country":"US","lat":30.401209,"lng":-84.35433},{"code":"TOL","name":"Eugene F. Kranz Toledo Express Airport","city":"Toledo","country":"US","lat":41.5868,"lng":-83.8078},{"code":"TPA","name":"Tampa International Airport","city":"Tampa","country":"US","lat":27.9755,"lng":-82.533203},{"code":"TRI","name":"Tri-Cities Regional TN/VA Airport","city":"Blountville","country":"US","lat":36.475201,"lng":-82.407401},{"code":"TKF","name":"Truckee Tahoe Airport","city":"Truckee","country":"US","lat":39.318628,"lng":-120.140562},{"code":"TTN","name":"Trenton Mercer Airport","city":"Ewing Township","country":"US","lat":40.276699,"lng":-74.813499},{"code":"TUL","name":"Tulsa International Airport","city":"Tulsa","country":"US","lat":36.197084,"lng":-95.886225},{"code":"TUP","name":"Tupelo Regional Airport","city":"Tupelo","country":"US","lat":34.26810073852539,"lng":-88.7698974609375},{"code":"TUS","name":"Tucson International Airport","city":"Tucson","country":"US","lat":32.115004,"lng":-110.938053},{"code":"TVC","name":"Cherry Capital Airport","city":"Traverse City","country":"US","lat":44.741402,"lng":-85.582199},{"code":"TVF","name":"Thief River Falls Regional Airport","city":"Thief River Falls","country":"US","lat":48.065701,"lng":-96.184998},{"code":"TWF","name":"Joslin Field Magic Valley Regional Airport","city":"Twin Falls","country":"US","lat":42.4818,"lng":-114.487999},{"code":"TXK","name":"Texarkana Regional Airport (Webb Field)","city":"Texarkana","country":"US","lat":33.453701,"lng":-93.990997},{"code":"TYR","name":"Tyler Pounds Regional Airport","city":"Tyler","country":"US","lat":32.35409927368164,"lng":-95.40239715576172},{"code":"TYS","name":"McGhee Tyson Airport","city":"Knoxville/Maryville","country":"US","lat":35.811001,"lng":-83.994003},{"code":"UIN","name":"Quincy Regional Airport Baldwin Field","city":"Quincy","country":"US","lat":39.942699,"lng":-91.194603},{"code":"SCE","name":"State College Regional Airport","city":"State College","country":"US","lat":40.849374,"lng":-77.84852},{"code":"NPT","name":"Newport State Airport","city":"Newport","country":"US","lat":41.532207,"lng":-71.281039},{"code":"VCT","name":"Victoria Regional Airport","city":"Victoria","country":"US","lat":28.85260009765625,"lng":-96.91850280761719},{"code":"VEL","name":"Vernal Regional Airport","city":"Vernal","country":"US","lat":40.436214,"lng":-109.511661},{"code":"VLD","name":"Valdosta Regional Airport","city":"Valdosta","country":"US","lat":30.782499313354492,"lng":-83.27670288085938},{"code":"VPS","name":"Destin-Fort Walton Beach Airport","city":"Valparaiso","country":"US","lat":30.481315,"lng":-86.515839},{"code":"VRB","name":"Vero Beach Regional Airport","city":"Vero Beach","country":"US","lat":27.6556,"lng":-80.417901},{"code":"KWP","name":"West Point Village Seaplane Base","city":"West Point","country":"US","lat":57.770099639899996,"lng":-153.548995972},{"code":"WST","name":"Westerly State Airport","city":"Westerly","country":"US","lat":41.349602,"lng":-71.803398},{"code":"WYS","name":"Yellowstone Airport","city":"West Yellowstone","country":"US","lat":44.6884,"lng":-111.117996},{"code":"KXA","name":"Kasaan Seaplane Base","city":"Kasaan","country":"US","lat":55.537399,"lng":-132.397995},{"code":"XNA","name":"Northwest Arkansas National Airport","city":"Fayetteville/Springdale/Rogers","country":"US","lat":36.281898,"lng":-94.306801},{"code":"XWA","name":"Williston Basin International Airport","city":"Williston","country":"US","lat":48.260863,"lng":-103.75116},{"code":"YKM","name":"Yakima Air Terminal McAllister Field","city":"Yakima","country":"US","lat":46.56819916,"lng":-120.5439987},{"code":"KZB","name":"Zachar Bay Seaplane Base","city":"Zachar Bay","country":"US","lat":57.553001,"lng":-153.746052},{"code":"BOR","name":"Bokeo International Airport","city":"Ton Phueng","country":"LA","lat":20.323986,"lng":100.165392},{"code":"TIA","name":"Tirana International Airport Mother Teresa","city":"Rinas","country":"AL","lat":41.4147,"lng":19.7206},{"code":"BOJ","name":"Burgas Airport","city":"Burgas","country":"BG","lat":42.569917,"lng":27.515173},{"code":"LBH","name":"Palm Beach Seaplane Base","city":"Sydney","country":"AU","lat":-33.5871,"lng":151.3234},{"code":"PDV","name":"Plovdiv International Airport","city":"Plovdiv","country":"BG","lat":42.067799,"lng":24.8508},{"code":"SOF","name":"Sofia Airport","city":"Sofia","country":"BG","lat":42.696357,"lng":23.417671},{"code":"VAR","name":"Varna Airport","city":"Varna","country":"BG","lat":43.232101,"lng":27.8251},{"code":"ECN","name":"Ercan International Airport","city":"Tymbou (Kirklar)","country":"CY","lat":35.153064,"lng":33.507352},{"code":"GEC","name":"Lefkoniko Airport / Geçitkale Air Base","city":"Lefkoniko (Geçitkale)","country":"CY","lat":35.23591,"lng":33.720088},{"code":"LCA","name":"Larnaca International Airport","city":"Larnaca","country":"CY","lat":34.875099,"lng":33.624901},{"code":"PFO","name":"Paphos International Airport","city":"Paphos","country":"CY","lat":34.717999,"lng":32.485699},{"code":"DBV","name":"Dubrovnik Ruđer Bošković Airport","city":"Dubrovnik","country":"HR","lat":42.562247,"lng":18.265543},{"code":"OSI","name":"Osijek Airport","city":"Osijek(Klisa)","country":"HR","lat":45.462355,"lng":18.811278},{"code":"PUY","name":"Pula Airport","city":"Pula","country":"HR","lat":44.893501,"lng":13.9222},{"code":"RJK","name":"Rijeka Airport","city":"Rijeka(Omišalj)","country":"HR","lat":45.216376,"lng":14.57085},{"code":"BWK","name":"Brač Airport","city":"Gornji Humac","country":"HR","lat":43.284454,"lng":16.678362},{"code":"SPU","name":"Split Saint Jerome Airport","city":"Split","country":"HR","lat":43.538898,"lng":16.298},{"code":"ZAG","name":"Zagreb Franjo Tuđman International Airport","city":"Velika Gorica","country":"HR","lat":45.742901,"lng":16.0688},{"code":"ZAD","name":"Zadar Airport","city":"Zadar","country":"HR","lat":44.096986,"lng":15.353565},{"code":"ALC","name":"Alicante-Elche Miguel Hernández Airport","city":"Alicante","country":"ES","lat":38.2822,"lng":-0.558156},{"code":"LEI","name":"Almería Airport","city":"Almería","country":"ES","lat":36.843899,"lng":-2.3701},{"code":"OVD","name":"Asturias Airport","city":"Ranón","country":"ES","lat":43.563599,"lng":-6.03462},{"code":"ODB","name":"Córdoba Airport","city":"Córdoba","country":"ES","lat":37.841999,"lng":-4.84888},{"code":"BIO","name":"Bilbao Airport","city":"Bilbao","country":"ES","lat":43.301102,"lng":-2.91061},{"code":"BCN","name":"Josep Tarradellas Barcelona-El Prat Airport","city":"Barcelona","country":"ES","lat":41.2971,"lng":2.07846},{"code":"BJZ","name":"Badajoz Airport","city":"Badajoz","country":"ES","lat":38.8913,"lng":-6.82133},{"code":"CDT","name":"Castellón-Costa Azahar Airport","city":"Castellón de la Plana","country":"ES","lat":40.213889,"lng":0.073333},{"code":"LCG","name":"A Coruña Airport","city":"Culleredo","country":"ES","lat":43.302101,"lng":-8.37726},{"code":"ILD","name":"Lleida-Alguaire Airport","city":"Lleida","country":"ES","lat":41.728185,"lng":0.535023},{"code":"GRO","name":"Girona-Costa Brava Airport","city":"Girona","country":"ES","lat":41.904639,"lng":2.761774},{"code":"GRX","name":"F.G.L. Airport Granada-Jaén Airport","city":"Granada","country":"ES","lat":37.188702,"lng":-3.77736},{"code":"IBZ","name":"Ibiza Airport","city":"Ibiza (Eivissa)","country":"ES","lat":38.872898,"lng":1.37312},{"code":"XRY","name":"Jerez Airport","city":"Jerez de la Frontera","country":"ES","lat":36.744598,"lng":-6.06011},{"code":"LEN","name":"León Airport","city":"La Virgen del Camino","country":"ES","lat":42.590709,"lng":-5.653406},{"code":"MAD","name":"Adolfo Suárez Madrid–Barajas Airport","city":"Madrid","country":"ES","lat":40.493407,"lng":-3.572249},{"code":"AGP","name":"Málaga-Costa del Sol Airport","city":"Málaga","country":"ES","lat":36.6749,"lng":-4.49911},{"code":"MAH","name":"Menorca Airport","city":"Mahón (Maó)","country":"ES","lat":39.862598,"lng":4.21865},{"code":"RMU","name":"Region of Murcia International Airport","city":"Corvera","country":"ES","lat":37.802852,"lng":-1.124884},{"code":"PMI","name":"Palma de Mallorca Airport","city":"Palma de Mallorca","country":"ES","lat":39.551701,"lng":2.73881},{"code":"PNA","name":"Pamplona Airport","city":"Pamplona","country":"ES","lat":42.77,"lng":-1.64633},{"code":"REU","name":"Reus Airport","city":"Reus","country":"ES","lat":41.147509,"lng":1.168354},{"code":"SLM","name":"Salamanca Airport","city":"Salamanca","country":"ES","lat":40.952099,"lng":-5.50199},{"code":"EAS","name":"San Sebastián Airport","city":"Hondarribia","country":"ES","lat":43.356499,"lng":-1.79061},{"code":"SCQ","name":"Santiago-Rosalía de Castro Airport","city":"Santiago de Compostela","country":"ES","lat":42.896301,"lng":-8.41514},{"code":"LEU","name":"Pirineus - la Seu d'Urgel Airport","city":"La Seu d'Urgell Pyrenees and Andorra","country":"ES","lat":42.3386,"lng":1.40917},{"code":"VLC","name":"Valencia Airport","city":"Valencia","country":"ES","lat":39.489162,"lng":-0.480961},{"code":"VLL","name":"Valladolid Airport","city":"Valladolid","country":"ES","lat":41.7061,"lng":-4.85194},{"code":"VIT","name":"Vitoria Airport","city":"Alava","country":"ES","lat":42.882801,"lng":-2.72447},{"code":"VGO","name":"Vigo Airport","city":"Vigo","country":"ES","lat":42.2318,"lng":-8.62677},{"code":"SDR","name":"Seve Ballesteros-Santander Airport","city":"Santander","country":"ES","lat":43.427101,"lng":-3.82001},{"code":"ZAZ","name":"Zaragoza Airport","city":"Zaragoza","country":"ES","lat":41.666199,"lng":-1.04155},{"code":"SVQ","name":"Seville Airport","city":"Seville","country":"ES","lat":37.417999,"lng":-5.89311},{"code":"BOD","name":"Bordeaux–Mérignac Airport","city":"Bordeaux","country":"FR","lat":44.82865,"lng":-0.715356},{"code":"EGC","name":"Bergerac Dordogne-Périgord airport","city":"Bergerac","country":"FR","lat":44.825298,"lng":0.518611},{"code":"LRH","name":"La Rochelle Île de Ré Airport","city":"La Rochelle","country":"FR","lat":46.179199,"lng":-1.19528},{"code":"PIS","name":"Poitiers-Biard Airport","city":"Poitiers/Biard","country":"FR","lat":46.5877,"lng":0.306666},{"code":"LIG","name":"Limoges Airport","city":"Limoges/Bellegarde","country":"FR","lat":45.862801,"lng":1.17944},{"code":"TLS","name":"Toulouse-Blagnac Airport","city":"Toulouse/Blagnac","country":"FR","lat":43.629101,"lng":1.36382},{"code":"PUF","name":"Pau Pyrénées Airport","city":"Pau/Pyrénées (Uzein)","country":"FR","lat":43.380001,"lng":-0.418611},{"code":"LDE","name":"Tarbes-Lourdes-Pyrénées Airport","city":"Tarbes/Lourdes/Pyrénées","country":"FR","lat":43.178699,"lng":-0.006439},{"code":"BIQ","name":"Biarritz Pays Basque airport","city":"Biarritz","country":"FR","lat":43.468372,"lng":-1.523223},{"code":"DCM","name":"Castres Mazamet Airport","city":"Castres","country":"FR","lat":43.556301,"lng":2.28918},{"code":"RDZ","name":"Rodez–Aveyron Airport","city":"Rodez/Marcillac","country":"FR","lat":44.407902,"lng":2.48267},{"code":"OUI","name":"Ouessant Airport","city":"Ushant","country":"FR","lat":48.4632,"lng":-5.06358},{"code":"DLE","name":"Dole Tavaux Airport","city":"Dole","country":"FR","lat":47.038955,"lng":5.427589},{"code":"LPY","name":"Le Puy-Loudes Airfield","city":"Chaspuzac, Haute-Loire","country":"FR","lat":45.0807,"lng":3.76289},{"code":"ETZ","name":"Metz-Nancy-Lorraine Airport","city":"Goin","country":"FR","lat":48.982101,"lng":6.25132},{"code":"BIA","name":"Bastia-Poretta International airport","city":"Bastia","country":"FR","lat":42.5527,"lng":9.48373},{"code":"CLY","name":"Calvi Sainte Catherine Airport","city":"Calvi","country":"FR","lat":42.530422,"lng":8.792984},{"code":"FSC","name":"Figari Sud-Corse Airport","city":"Figari","country":"FR","lat":41.50185,"lng":9.097092},{"code":"AJA","name":"Ajaccio Napoléon Bonaparte airport","city":"Ajaccio","country":"FR","lat":41.923599,"lng":8.80292},{"code":"CMF","name":"Chambéry Aix les Bains airport","city":"Chambéry","country":"FR","lat":45.6381,"lng":5.88023},{"code":"CFE","name":"Clermont-Ferrand Auvergne airport","city":"Clermont-Ferrand","country":"FR","lat":45.786701,"lng":3.16917},{"code":"LYS","name":"Lyon Saint-Exupéry Airport","city":"Colombier-Saugnieu, Rhône","country":"FR","lat":45.725996,"lng":5.090139},{"code":"NCY","name":"Annecy Meythet airport","city":"Annecy","country":"FR","lat":45.928947,"lng":6.0987},{"code":"GNB","name":"Grenoble Alpes Isère Airport","city":"Grenoble","country":"FR","lat":45.3629,"lng":5.32937},{"code":"AUR","name":"Aurillac airport","city":"Aurillac","country":"FR","lat":44.891399,"lng":2.42194},{"code":"CCF","name":"Carcassonne Salvaza Airport","city":"Carcassonne","country":"FR","lat":43.216,"lng":2.30632},{"code":"MRS","name":"Marseille Provence Airport","city":"Marignane, Bouches-du-Rhône","country":"FR","lat":43.438088,"lng":5.2125},{"code":"NCE","name":"Nice-Côte d'Azur Airport","city":"Nice, Alpes-Maritimes","country":"FR","lat":43.658401,"lng":7.21587},{"code":"PGF","name":"Perpignan-Rivesaltes (Llabanère) Airport","city":"Perpignan/Rivesaltes","country":"FR","lat":42.740398,"lng":2.87067},{"code":"MPL","name":"Montpellier-Méditerranée Airport","city":"Montpellier/Méditerranée","country":"FR","lat":43.576199,"lng":3.96301},{"code":"BZR","name":"Béziers Vias airport","city":"Béziers","country":"FR","lat":43.323502,"lng":3.3539},{"code":"AVN","name":"Avignon Caumont airport","city":"Avignon","country":"FR","lat":43.907299,"lng":4.90183},{"code":"BVA","name":"Beauvais-Tillé airport","city":"Beauvais","country":"FR","lat":49.454399,"lng":2.11278},{"code":"XCR","name":"Chalons Vatry airport","city":"Chalons en Champagne","country":"FR","lat":48.77333,"lng":4.20611},{"code":"TUF","name":"Tours Val de Loire Airport","city":"Tours, Indre-et-Loire","country":"FR","lat":47.432201,"lng":0.727606},{"code":"LBG","name":"Paris-Le Bourget International Airport","city":"Paris","country":"FR","lat":48.962276,"lng":2.436539},{"code":"CDG","name":"Charles de Gaulle International Airport","city":"Paris (Roissy-en-France, Val-d'Oise)","country":"FR","lat":49.00896,"lng":2.554117},{"code":"ORY","name":"Paris-Orly Airport","city":"Paris (Orly, Val-de-Marne)","country":"FR","lat":48.729499,"lng":2.358963},{"code":"LIL","name":"Lille Airport","city":"Lesquin","country":"FR","lat":50.566564,"lng":3.102429},{"code":"BES","name":"Brest Bretagne airport","city":"Brest","country":"FR","lat":48.447899,"lng":-4.41854},{"code":"DNR","name":"Dinard Pleurtuit Saint-Malo airport","city":"Dinard","country":"FR","lat":48.5877,"lng":-2.07996},{"code":"DOL","name":"Deauville Normandie airport","city":"Deauville","country":"FR","lat":49.365237,"lng":0.154481},{"code":"LRT","name":"Lorient South Brittany (Bretagne Sud) Airport","city":"Lorient/Lann/Bihoué","country":"FR","lat":47.760601,"lng":-3.44},{"code":"CFR","name":"Caen Carpiquet airport","city":"Caen","country":"FR","lat":49.17678,"lng":-0.454883},{"code":"LME","name":"Le Mans-Arnage Airport","city":"Le Mans, Sarthe","country":"FR","lat":47.948604,"lng":0.201702},{"code":"RNS","name":"Rennes-Saint-Jacques Airport","city":"Saint-Jacques-de-la-Lande, Ille-et-Vilaine","country":"FR","lat":48.0695,"lng":-1.73479},{"code":"NTE","name":"Nantes Atlantique Airport","city":"Nantes","country":"FR","lat":47.153198,"lng":-1.61073},{"code":"SNR","name":"Saint-Nazaire-Montoir Airport","city":"Saint-Nazaire/Montoir","country":"FR","lat":47.31142,"lng":-2.152634},{"code":"BSL","name":"EuroAirport Basel–Mulhouse–Freiburg","city":"Bâle / Mulhouse","country":"FR","lat":47.60068,"lng":7.521117},{"code":"DIJ","name":"Dijon Longvic airport","city":"Dijon","country":"FR","lat":47.268902,"lng":5.09},{"code":"BVE","name":"Brive Souillac airport","city":"Brive","country":"FR","lat":45.039722,"lng":1.485556},{"code":"SXB","name":"Strasbourg Airport","city":"Strasbourg","country":"FR","lat":48.5383,"lng":7.62823},{"code":"TLN","name":"Toulon-Hyères Airport","city":"Hyères, Var","country":"FR","lat":43.097301,"lng":6.14603},{"code":"FNI","name":"Nîmes-Arles-Camargue Airport","city":"Nîmes/Garons","country":"FR","lat":43.757401,"lng":4.41635},{"code":"LTT","name":"La Môle Airport","city":"Saint-Tropez","country":"FR","lat":43.205399,"lng":6.482},{"code":"MQC","name":"Miquelon Airport","city":"Miquelon","country":"PM","lat":47.095501,"lng":-56.380299},{"code":"FSP","name":"Saint-Pierre Pointe-Blanche Airport","city":"Saint-Pierre","country":"PM","lat":46.762651,"lng":-56.17499},{"code":"AXD","name":"Alexandroupoli Democritus Airport","city":"Alexandroupolis","country":"GR","lat":40.8559,"lng":25.956301},{"code":"ATH","name":"Athens Eleftherios Venizelos International Airport","city":"Spata-Artemida","country":"GR","lat":37.936401,"lng":23.9445},{"code":"VOL","name":"Nea Anchialos National Airport","city":"Nea Anchialos","country":"GR","lat":39.219601,"lng":22.7943},{"code":"JKH","name":"Chios Island National Airport","city":"Chios Island","country":"GR","lat":38.34320068359375,"lng":26.140600204467773},{"code":"JIK","name":"Ikaria Airport","city":"Ikaria Island","country":"GR","lat":37.6827011108,"lng":26.3470993042},{"code":"IOA","name":"Ioannina King Pyrrhus National Airport","city":"Ioannina","country":"GR","lat":39.6964,"lng":20.8225},{"code":"HER","name":"Heraklion International Nikos Kazantzakis Airport","city":"Heraklion","country":"GR","lat":35.339699,"lng":25.1803},{"code":"KSO","name":"Kastoria National Airport Aristotle","city":"Argos Orestiko","country":"GR","lat":40.446301,"lng":21.2822},{"code":"KIT","name":"Kithira Airport","city":"Kithira Island","country":"GR","lat":36.273978,"lng":23.016719},{"code":"EFL","name":"Kefallinia Airport","city":"Kefallinia Island","country":"GR","lat":38.12009811401367,"lng":20.500499725341797},{"code":"KZS","name":"Kastelorizo Airport","city":"Kastelorizo Island","country":"GR","lat":36.141701,"lng":29.576401},{"code":"KLX","name":"Kalamata Airport","city":"Kalamata","country":"GR","lat":37.06829833984375,"lng":22.02549934387207},{"code":"KGS","name":"Kos International Airport \"Ippokratis\"","city":"Kos Island","country":"GR","lat":36.794523,"lng":27.09115},{"code":"AOK","name":"Karpathos Airport","city":"Karpathos Island","country":"GR","lat":35.421398,"lng":27.146},{"code":"CFU","name":"Corfu Ioannis Kapodistrias International Airport","city":"Kerkyra (Corfu)","country":"GR","lat":39.60145,"lng":19.912179},{"code":"KSJ","name":"Kasos Airport","city":"Kasos Island","country":"GR","lat":35.4213981628,"lng":26.909999847399998},{"code":"KVA","name":"Kavala Alexander the Great International Airport","city":"Kavala","country":"GR","lat":40.9133,"lng":24.6192},{"code":"JKL","name":"Kalymnos Airport","city":"Kalymnos Island","country":"GR","lat":36.9632987976,"lng":26.9405994415},{"code":"KZI","name":"Kozani National Airport Filippos","city":"Kozani","country":"GR","lat":40.286098,"lng":21.840799},{"code":"LRS","name":"Leros Airport","city":"Leros Island","country":"GR","lat":37.184898,"lng":26.800301},{"code":"LXS","name":"Limnos Airport","city":"Limnos Island","country":"GR","lat":39.917098999,"lng":25.236299514799995},{"code":"JMK","name":"Mykonos Island National Airport","city":"Mykonos","country":"GR","lat":37.435101,"lng":25.348101},{"code":"MLO","name":"Milos Airport","city":"Milos Island","country":"GR","lat":36.696899,"lng":24.4769},{"code":"MJT","name":"Mytilene International Airport","city":"Mytilene","country":"GR","lat":39.057406,"lng":26.598629},{"code":"JNX","name":"Naxos Island National Airport","city":"Naxos","country":"GR","lat":37.0811,"lng":25.368099},{"code":"PAS","name":"Paros National Airport","city":"Paros","country":"GR","lat":37.020495,"lng":25.113195},{"code":"JTY","name":"Astypalaia Airport","city":"Astypalaia Island","country":"GR","lat":36.5798988342,"lng":26.3757991791},{"code":"PVK","name":"Aktion National Airport","city":"Preveza","country":"GR","lat":38.925499,"lng":20.765301},{"code":"RHO","name":"Rhodes International Airport \"Diagoras\"","city":"Rhodes","country":"GR","lat":36.405399,"lng":28.086201},{"code":"GPA","name":"Patras Araxos Agamemnon Airport","city":"Patras","country":"GR","lat":38.1511,"lng":21.4256},{"code":"CHQ","name":"Chania International Airport","city":"Souda","country":"GR","lat":35.531207,"lng":24.150673},{"code":"JSI","name":"Skiathos Island National Airport","city":"Skiathos","country":"GR","lat":39.177101,"lng":23.5037},{"code":"SMI","name":"Samos Airport","city":"Samos Island","country":"GR","lat":37.689999,"lng":26.911699},{"code":"JSY","name":"Syros Airport","city":"Syros Island","country":"GR","lat":37.4227981567,"lng":24.950899124099998},{"code":"JTR","name":"Santorini International Airport","city":"Santorini Island","country":"GR","lat":36.400045,"lng":25.478638},{"code":"JSH","name":"Sitia Airport","city":"Crete Island","country":"GR","lat":35.21609878540039,"lng":26.101299285888672},{"code":"SKU","name":"Skiros Airport","city":"Skiros Island","country":"GR","lat":38.9676017761,"lng":24.4871997833},{"code":"SKG","name":"Thessaloniki Macedonia International Airport","city":"Thessaloniki","country":"GR","lat":40.51928,"lng":22.970009},{"code":"ZTH","name":"Zakynthos International Airport Dionysios Solomos","city":"Zakynthos","country":"GR","lat":37.7509,"lng":20.8843},{"code":"BUD","name":"Budapest Liszt Ferenc International Airport","city":"Budapest","country":"HU","lat":47.43018,"lng":19.262393},{"code":"DEB","name":"Debrecen International Airport","city":"Debrecen","country":"HU","lat":47.489469,"lng":21.616278},{"code":"PEV","name":"Pécs-Pogány International Airport","city":"Pécs","country":"HU","lat":45.988891,"lng":18.242044},{"code":"SOB","name":"Hévíz–Balaton Airport","city":"Sármellék","country":"HU","lat":46.686391,"lng":17.159084},{"code":"CRV","name":"Crotone Sant'Anna Pythagoras Airport","city":"Isola di Capo Rizzuto (KR)","country":"IT","lat":38.9972,"lng":17.0802},{"code":"BRI","name":"Bari Karol Wojtyła International Airport","city":"Bari","country":"IT","lat":41.138901,"lng":16.760599},{"code":"FOG","name":"Foggia Gino Lisa Airport","city":"Foggia (FG)","country":"IT","lat":41.433596,"lng":15.534566},{"code":"PSR","name":"Abruzzo Airport","city":"Pescara","country":"IT","lat":42.431079,"lng":14.182981},{"code":"BDS","name":"Brindisi Airport","city":"Brindisi","country":"IT","lat":40.6576,"lng":17.947001},{"code":"SUF","name":"Lamezia Terme Sant'Eufemia International Airport","city":"Lamezia Terme (CZ)","country":"IT","lat":38.906214,"lng":16.246007},{"code":"CIY","name":"Comiso Airport","city":"Comiso","country":"IT","lat":36.99583,"lng":14.60889},{"code":"CTA","name":"Catania-Fontanarossa Airport","city":"Catania","country":"IT","lat":37.466801,"lng":15.0664},{"code":"LMP","name":"Lampedusa Airport","city":"Lampedusa","country":"IT","lat":35.497898,"lng":12.6181},{"code":"PNL","name":"Pantelleria Airport","city":"Pantelleria (TP)","country":"IT","lat":36.816502,"lng":11.9689},{"code":"PMO","name":"Falcone–Borsellino Airport","city":"Palermo","country":"IT","lat":38.175999,"lng":13.091},{"code":"REG","name":"Reggio Calabria Airport","city":"Reggio Calabria","country":"IT","lat":38.071201,"lng":15.6516},{"code":"TPS","name":"Vincenzo Florio Airport Trapani-Birgi","city":"Trapani (TP)","country":"IT","lat":37.9114,"lng":12.488},{"code":"AHO","name":"Alghero-Fertilia Airport","city":"Alghero","country":"IT","lat":40.632099,"lng":8.29077},{"code":"CAG","name":"Cagliari Elmas Airport","city":"Cagliari","country":"IT","lat":39.251499,"lng":9.05428},{"code":"OLB","name":"Olbia Costa Smeralda Airport","city":"Olbia (SS)","country":"IT","lat":40.898953,"lng":9.518457},{"code":"MXP","name":"Milan Malpensa International Airport","city":"Ferno (VA)","country":"IT","lat":45.6306,"lng":8.72811},{"code":"BGY","name":"Il Caravaggio International Airport","city":"Orio al Serio (BG)","country":"IT","lat":45.669362,"lng":9.708851},{"code":"TRN","name":"Turin Airport","city":"Caselle Torinese (TO)","country":"IT","lat":45.200802,"lng":7.64963},{"code":"GOA","name":"Genoa Cristoforo Colombo Airport","city":"Genova (GE)","country":"IT","lat":44.412039,"lng":8.840732},{"code":"LIN","name":"Milano Linate Airport","city":"Segrate (MI)","country":"IT","lat":45.445099,"lng":9.27674},{"code":"PMF","name":"Parma Airport","city":"Parma (PR)","country":"IT","lat":44.826351,"lng":10.29705},{"code":"CUF","name":"Cuneo International Airport","city":"Levaldigi (CN)","country":"IT","lat":44.547458,"lng":7.623045},{"code":"BZO","name":"Bolzano Airport","city":"Bolzano (BZ)","country":"IT","lat":46.459245,"lng":11.326125},{"code":"BLQ","name":"Bologna Guglielmo Marconi Airport","city":"Bologna","country":"IT","lat":44.5354,"lng":11.2887},{"code":"TSF","name":"Treviso Airport","city":"Treviso (TV)","country":"IT","lat":45.648399,"lng":12.1944},{"code":"FRL","name":"Forlì-Luigi Ridolfi International Airport","city":"Forlì (FC)","country":"IT","lat":44.194801,"lng":12.0701},{"code":"VBS","name":"Brescia Gabriele d'Annunzio Airport","city":"Montichiari (BS)","country":"IT","lat":45.428902,"lng":10.3306},{"code":"TRS","name":"Trieste Airport","city":"Ronchi dei Legionari/Trieste","country":"IT","lat":45.827862,"lng":13.466672},{"code":"RMI","name":"Federico Fellini International Airport","city":"Rimini (RN)","country":"IT","lat":44.020024,"lng":12.612198},{"code":"VRN","name":"Verona Villafranca Valerio Catullo Airport","city":"Caselle (VR)","country":"IT","lat":45.394955,"lng":10.887303},{"code":"AOI","name":"Marche Airport","city":"Falconara Marittima (AN)","country":"IT","lat":43.616299,"lng":13.3623},{"code":"VCE","name":"Venice Marco Polo Airport","city":"Venezia (VE)","country":"IT","lat":45.505299,"lng":12.3519},{"code":"CIA","name":"Ciampino–G. B. Pastine International Airport","city":"Rome","country":"IT","lat":41.798769,"lng":12.595331},{"code":"FCO","name":"Rome–Fiumicino Leonardo da Vinci International Airport","city":"Rome","country":"IT","lat":41.804532,"lng":12.251998},{"code":"QSR","name":"Salerno Costa d'Amalfi Airport","city":"Salerno","country":"IT","lat":40.620399,"lng":14.9113},{"code":"EBA","name":"Marina di Campo Airport","city":"Campo nell'Elba (LI)","country":"IT","lat":42.760871,"lng":10.239811},{"code":"NAP","name":"Naples International Airport","city":"Napoli","country":"IT","lat":40.886002,"lng":14.2908},{"code":"PSA","name":"Pisa International Airport","city":"Pisa (PI)","country":"IT","lat":43.683899,"lng":10.3927},{"code":"FLR","name":"Florence Airport, Peretola","city":"Firenze (FI)","country":"IT","lat":43.808558,"lng":11.202822},{"code":"PEG","name":"Perugia San Francesco d'Assisi – Umbria International Airport","city":"Perugia (PG)","country":"IT","lat":43.095901,"lng":12.5132},{"code":"LJU","name":"Ljubljana Jože Pučnik Airport","city":"Zgornji Brnik","country":"SI","lat":46.223701,"lng":14.4576},{"code":"MBX","name":"Maribor Edvard Rusjan Airport","city":"Maribor","country":"SI","lat":46.4799,"lng":15.6861},{"code":"JCL","name":"České Budějovice South Bohemian Airport","city":"České Budějovice","country":"CZ","lat":48.948171,"lng":14.42832},{"code":"KLV","name":"Karlovy Vary Airport","city":"Karlovy Vary","country":"CZ","lat":50.202999,"lng":12.915},{"code":"OSR","name":"Leoš Janáček Airport Ostrava","city":"Mošnov","country":"CZ","lat":49.696301,"lng":18.111099},{"code":"PED","name":"Pardubice Airport","city":"Pardubice","country":"CZ","lat":50.015049,"lng":15.73981},{"code":"PRG","name":"Václav Havel Airport Prague","city":"Prague","country":"CZ","lat":50.100874,"lng":14.259911},{"code":"BRQ","name":"Brno-Tuřany Airport","city":"Brno","country":"CZ","lat":49.151276,"lng":16.693972},{"code":"TLV","name":"Ben Gurion International Airport","city":"Tel Aviv","country":"IL","lat":32.011398,"lng":34.8867},{"code":"ETM","name":"Ramon International Airport","city":"Eilat","country":"IL","lat":29.727009,"lng":35.014116},{"code":"HFA","name":"Uri Michaeli Haifa International Airport","city":"Haifa","country":"IL","lat":32.810219,"lng":35.043719},{"code":"GZM","name":"Xewkija Heliport","city":"Gozo","country":"MT","lat":36.027199,"lng":14.2728},{"code":"MLA","name":"Malta International Airport","city":"Valletta","country":"MT","lat":35.845932,"lng":14.491546},{"code":"MCM","name":"Monaco Heliport","city":"Fontvieille","country":"MC","lat":43.725798,"lng":7.419673},{"code":"GRZ","name":"Graz Airport","city":"Feldkirchen bei Graz","country":"AT","lat":46.9911,"lng":15.4396},{"code":"INN","name":"Innsbruck Airport","city":"Innsbruck","country":"AT","lat":47.260201,"lng":11.344},{"code":"KLU","name":"Klagenfurt Airport","city":"Klagenfurt am Wörthersee","country":"AT","lat":46.642502,"lng":14.3377},{"code":"LNZ","name":"Linz-Hörsching Airport","city":"Linz","country":"AT","lat":48.235362,"lng":14.188128},{"code":"SZG","name":"Salzburg Airport","city":"Salzburg","country":"AT","lat":47.793301,"lng":13.0043},{"code":"VIE","name":"Vienna International Airport","city":"Vienna","country":"AT","lat":48.110298,"lng":16.5697},{"code":"SMA","name":"Santa Maria Airport","city":"Vila do Porto","country":"PT","lat":36.97140121459961,"lng":-25.17060089111328},{"code":"BGC","name":"Bragança Airport","city":"Bragança","country":"PT","lat":41.8578,"lng":-6.70713},{"code":"CVU","name":"Corvo Airport","city":"Corvo","country":"PT","lat":39.671501,"lng":-31.1136},{"code":"CAT","name":"Cascais Airport","city":"Cascais","country":"PT","lat":38.724998,"lng":-9.35523},{"code":"FLW","name":"Flores Airport","city":"Santa Cruz das Flores","country":"PT","lat":39.455299377441406,"lng":-31.131399154663086},{"code":"FAO","name":"Faro - Gago Coutinho International Airport","city":"Faro","country":"PT","lat":37.015909,"lng":-7.970939},{"code":"GRW","name":"Graciosa Airport","city":"Santa Cruz da Graciosa","country":"PT","lat":39.092201232910156,"lng":-28.029800415039062},{"code":"HOR","name":"Horta Airport","city":"Horta","country":"PT","lat":38.519901275634766,"lng":-28.715900421142578},{"code":"TER","name":"Lajes Airport","city":"Praia da Vitória","country":"PT","lat":38.761799,"lng":-27.090799},{"code":"FNC","name":"Cristiano Ronaldo International Airport","city":"Funchal","country":"PT","lat":32.697812,"lng":-16.774613},{"code":"PDL","name":"João Paulo II Airport","city":"Ponta Delgada","country":"PT","lat":37.7411994934,"lng":-25.6979007721},{"code":"PIX","name":"Pico Airport","city":"Pico Island","country":"PT","lat":38.554298,"lng":-28.441299},{"code":"PRM","name":"Portimão Airport","city":"Portimão","country":"PT","lat":37.1493,"lng":-8.58396},{"code":"OPO","name":"Francisco de Sá Carneiro Airport","city":"Porto","country":"PT","lat":41.2481002808,"lng":-8.68138980865},{"code":"PXO","name":"Porto Santo Airport","city":"Vila Baleira","country":"PT","lat":33.0733985901,"lng":-16.3500003815},{"code":"LIS","name":"Lisbon Humberto Delgado Airport","city":"Lisbon","country":"PT","lat":38.7813,"lng":-9.13592},{"code":"SJZ","name":"São Jorge Airport","city":"Velas","country":"PT","lat":38.665501,"lng":-28.1758},{"code":"VRL","name":"Vila Real Airport","city":"Vila Real","country":"PT","lat":41.2743,"lng":-7.72047},{"code":"VSE","name":"Aerodromo Goncalves Lobato (Viseu Airport)","city":"Viseu","country":"PT","lat":40.725498,"lng":-7.88899},{"code":"BNX","name":"Banja Luka International Airport","city":"Mahovljani","country":"BA","lat":44.941399,"lng":17.297501},{"code":"OMO","name":"Mostar International Airport","city":"Mostar","country":"BA","lat":43.282489,"lng":17.846101},{"code":"SJJ","name":"Sarajevo International Airport","city":"Sarajevo","country":"BA","lat":43.8246,"lng":18.331499},{"code":"TZL","name":"Tuzla International Airport","city":"Dubrave Gornje","country":"BA","lat":44.459858,"lng":18.723627},{"code":"ARW","name":"Arad International Airport","city":"Arad","country":"RO","lat":46.176136,"lng":21.264322},{"code":"BCM","name":"Bacău George Enescu International Airport","city":"Bacău","country":"RO","lat":46.5219,"lng":26.910299},{"code":"BAY","name":"Maramureș International Airport","city":"Tăuții-Măgherăuș","country":"RO","lat":47.658444,"lng":23.464445},{"code":"BBU","name":"Bucharest Băneasa Aurel Vlaicu International Airport","city":"Bucharest","country":"RO","lat":44.503133,"lng":26.102944},{"code":"GHV","name":"Brașov-Ghimbav International Airport","city":"Brașov (Ghimbav)","country":"RO","lat":45.70558,"lng":25.522871},{"code":"CND","name":"Mihail Kogălniceanu International Airport","city":"Constanța","country":"RO","lat":44.362202,"lng":28.4883},{"code":"CLJ","name":"Avram Iancu Cluj International Airport","city":"Cluj-Napoca","country":"RO","lat":46.786042,"lng":23.685733},{"code":"CRA","name":"Craiova International Airport","city":"Craiova","country":"RO","lat":44.3181,"lng":23.888599},{"code":"IAS","name":"Iaşi International Airport","city":"Iaşi","country":"RO","lat":47.179633,"lng":27.621431},{"code":"OMR","name":"Oradea International Airport","city":"Oradea","country":"RO","lat":47.025299,"lng":21.9025},{"code":"OTP","name":"Bucharest Henri Coandă International Airport","city":"Otopeni","country":"RO","lat":44.571792,"lng":26.103285},{"code":"SBZ","name":"Sibiu International Airport","city":"Sibiu","country":"RO","lat":45.785841,"lng":24.086666},{"code":"SUJ","name":"Satu Mare International Airport","city":"Satu Mare","country":"RO","lat":47.7033,"lng":22.8857},{"code":"SCV","name":"Suceava Ștefan cel Mare International Airport","city":"Suceava","country":"RO","lat":47.6875,"lng":26.354099},{"code":"TGM","name":"Târgu Mureş Transilvania International Airport","city":"Recea","country":"RO","lat":46.467701,"lng":24.4125},{"code":"TSR","name":"Timișoara Traian Vuia International Airport","city":"Timişoara","country":"RO","lat":45.809898,"lng":21.3379},{"code":"GVA","name":"Geneva International Airport","city":"Geneva","country":"CH","lat":46.238098,"lng":6.10895},{"code":"LUG","name":"Lugano Airport","city":"Agno","country":"CH","lat":46.004299,"lng":8.91058},{"code":"BRN","name":"Bern Airport","city":"Bern","country":"CH","lat":46.912736,"lng":7.498819},{"code":"ZRH","name":"Zürich Airport","city":"Zurich","country":"CH","lat":47.458056,"lng":8.548056},{"code":"ACH","name":"Sankt Gallen Altenrhein Airport","city":"St. Gallen","country":"CH","lat":47.485001,"lng":9.56077},{"code":"ESB","name":"Esenboğa International Airport","city":"Ankara","country":"TR","lat":40.128101,"lng":32.995098},{"code":"ADA","name":"Adana Şakirpaşa Airport","city":"Seyhan","country":"TR","lat":36.982201,"lng":35.280399},{"code":"AYT","name":"Antalya International Airport","city":"Antalya","country":"TR","lat":36.898701,"lng":30.800501},{"code":"GZT","name":"Gaziantep Oğuzeli International Airport","city":"Gaziantep","country":"TR","lat":36.947201,"lng":37.478699},{"code":"KYA","name":"Konya Airport","city":"Konya","country":"TR","lat":37.979,"lng":32.561901},{"code":"MZH","name":"Amasya Merzifon Airport","city":"Amasya","country":"TR","lat":40.829399,"lng":35.521999},{"code":"VAS","name":"Sivas Nuri Demirağ Airport","city":"Sivas","country":"TR","lat":39.813801,"lng":36.9035},{"code":"ONQ","name":"Zonguldak Çaycuma Airport","city":"Zonguldak","country":"TR","lat":41.506401,"lng":32.0886},{"code":"MLX","name":"Malatya Erhaç Airport","city":"Malatya","country":"TR","lat":38.435298919699996,"lng":38.0909996033},{"code":"ASR","name":"Kayseri Erkilet International Airport","city":"Kayseri","country":"TR","lat":38.770401,"lng":35.495399},{"code":"TJK","name":"Tokat Airport","city":"Tokat","country":"TR","lat":40.32472,"lng":36.39056},{"code":"DNZ","name":"Çardak Airport","city":"Denizli","country":"TR","lat":37.7855987549,"lng":29.7012996674},{"code":"NAV","name":"Nevşehir Kapadokya Airport","city":"Nevşehir","country":"TR","lat":38.7719,"lng":34.5345},{"code":"BZI","name":"Balıkesir Airport","city":"Balıkesir","country":"TR","lat":39.619301,"lng":27.926001},{"code":"CKZ","name":"Çanakkale Airport","city":"Çanakkale","country":"TR","lat":40.1376991272,"lng":26.4267997742},{"code":"ADB","name":"Adnan Menderes International Airport","city":"Gaziemir","country":"TR","lat":38.2924,"lng":27.157},{"code":"YEI","name":"Bursa Yenişehir Airport","city":"Yenişehir","country":"TR","lat":40.255199,"lng":29.562599},{"code":"DLM","name":"Dalaman International Airport","city":"Dalaman","country":"TR","lat":36.7131,"lng":28.7925},{"code":"TEQ","name":"Tekirdağ Çorlu Airport","city":"Çorlu","country":"TR","lat":41.138199,"lng":27.9191},{"code":"AOE","name":"Hasan Polatkan Airport","city":"Eskişehir","country":"TR","lat":39.811644,"lng":30.519268},{"code":"KZR","name":"Zafer Airport","city":"Altıntaş","country":"TR","lat":39.111107,"lng":30.130405},{"code":"EZS","name":"Elazığ Airport","city":"Elazığ","country":"TR","lat":38.597974,"lng":39.28348},{"code":"OGU","name":"Ordu–Giresun Airport","city":"Ordu","country":"TR","lat":40.966872,"lng":38.085995},{"code":"DIY","name":"Diyarbakır Airport","city":"Diyarbakır","country":"TR","lat":37.893902,"lng":40.201},{"code":"ERC","name":"Erzincan Airport","city":"Erzincan","country":"TR","lat":39.7102012634,"lng":39.527000427199994},{"code":"ERZ","name":"Erzurum International Airport","city":"Erzurum","country":"TR","lat":39.956501,"lng":41.1702},{"code":"KSY","name":"Kars Airport","city":"Kars","country":"TR","lat":40.562198638916016,"lng":43.1150016784668},{"code":"TZX","name":"Trabzon International Airport","city":"Trabzon","country":"TR","lat":40.995098,"lng":39.7897},{"code":"VAN","name":"Van Ferit Melen Airport","city":"Van","country":"TR","lat":38.46820068359375,"lng":43.332298278808594},{"code":"BAL","name":"Batman Airport","city":"Batman","country":"TR","lat":37.929001,"lng":41.1166},{"code":"MSR","name":"Muş Airport","city":"Muş","country":"TR","lat":38.747798919677734,"lng":41.66120147705078},{"code":"NOP","name":"Sinop Airport","city":"Sinop","country":"TR","lat":42.018313,"lng":35.071774},{"code":"KCM","name":"Kahramanmaraş Airport","city":"Kahramanmaraş","country":"TR","lat":37.5388259888,"lng":36.9535217285},{"code":"AJI","name":"Ağrı Airport","city":"Ağrı","country":"TR","lat":39.655642,"lng":43.025742},{"code":"ADF","name":"Adıyaman Airport","city":"Adıyaman","country":"TR","lat":37.7314,"lng":38.468899},{"code":"MQM","name":"Mardin Airport","city":"Mardin","country":"TR","lat":37.223300933800004,"lng":40.6316986084},{"code":"GNY","name":"Şanlıurfa GAP Airport","city":"Şanlıurfa","country":"TR","lat":37.445663,"lng":38.895592},{"code":"IGD","name":"Iğdır Airport","city":"Iğdır","country":"TR","lat":39.976627,"lng":43.876648},{"code":"BGG","name":"Bingöl Airport","city":"Bingöl","country":"TR","lat":38.860131,"lng":40.594489},{"code":"NKT","name":"Şırnak Şerafettin Elçi Airport","city":"Şırnak","country":"TR","lat":37.3647,"lng":42.0582},{"code":"YKO","name":"Hakkari Yüksekova Airport","city":"Hakkari","country":"TR","lat":37.5497,"lng":44.2381},{"code":"HTY","name":"Hatay Airport","city":"Antakya","country":"TR","lat":36.36079,"lng":36.285589},{"code":"COV","name":"Çukurova International Airport","city":"Tarsus","country":"TR","lat":36.891478,"lng":35.071235},{"code":"ISE","name":"Süleyman Demirel International Airport","city":"Isparta","country":"TR","lat":37.8554,"lng":30.368401},{"code":"EDO","name":"Balıkesir Koca Seyit Airport","city":"Edremit","country":"TR","lat":39.552533,"lng":27.010192},{"code":"BJV","name":"Milas Bodrum International Airport","city":"Bodrum","country":"TR","lat":37.249314,"lng":27.66401},{"code":"GZP","name":"Gazipaşa-Alanya Airport","city":"Gazipaşa","country":"TR","lat":36.298832,"lng":32.297017},{"code":"SZF","name":"Samsun-Çarşamba Airport","city":"Samsun","country":"TR","lat":41.253988,"lng":36.567546},{"code":"SAW","name":"Istanbul Sabiha Gökçen International Airport","city":"Pendik, Istanbul","country":"TR","lat":40.898602,"lng":29.3092},{"code":"IST","name":"İstanbul Airport","city":"Istanbul","country":"TR","lat":41.274874,"lng":28.732136},{"code":"RZV","name":"Rize–Artvin Airport","city":"Rize","country":"TR","lat":41.179766,"lng":40.848802},{"code":"RMO","name":"Chişinău International Airport","city":"Chişinău","country":"MD","lat":46.92774,"lng":28.931704},{"code":"OHD","name":"Ohrid St. Paul the Apostle Airport","city":"Ohrid","country":"MK","lat":41.18,"lng":20.7423},{"code":"SKP","name":"Skopje International Airport","city":"Ilinden","country":"MK","lat":41.958087,"lng":21.622581},{"code":"GIB","name":"Gibraltar Airport","city":"Gibraltar","country":"GI","lat":36.151679,"lng":-5.34978},{"code":"BEG","name":"Belgrade Nikola Tesla Airport","city":"Belgrade","country":"RS","lat":44.818401,"lng":20.309099},{"code":"KVO","name":"Morava Airport","city":"Kraljevo","country":"RS","lat":43.817519,"lng":20.586662},{"code":"INI","name":"Niš Constantine the Great Airport","city":"Niš","country":"RS","lat":43.336538,"lng":21.856242},{"code":"TGD","name":"Podgorica Airport / Podgorica Golubovci Airbase","city":"Podgorica","country":"ME","lat":42.359402,"lng":19.2519},{"code":"TIV","name":"Tivat Airport","city":"Tivat","country":"ME","lat":42.404701,"lng":18.723301},{"code":"LZG","name":"Langzhong Gucheng Airport","city":"Nanchong (Langzhong)","country":"CN","lat":31.50191,"lng":106.034417},{"code":"BTS","name":"M. R. Štefánik Airport","city":"Bratislava","country":"SK","lat":48.1702,"lng":17.2127},{"code":"KSC","name":"Košice International Airport","city":"Košice","country":"SK","lat":48.663101,"lng":21.2411},{"code":"SLD","name":"Sliač Airport","city":"Sliač","country":"SK","lat":48.63779830932617,"lng":19.13409996032715},{"code":"TAT","name":"Poprad-Tatry Airport","city":"Poprad","country":"SK","lat":49.070994,"lng":20.241394},{"code":"GDT","name":"JAGS McCartney International Airport","city":"Cockburn Town","country":"TC","lat":21.4445,"lng":-71.142303},{"code":"NCA","name":"North Caicos Airport","city":"North Caicos","country":"TC","lat":21.916094,"lng":-71.942954},{"code":"PLS","name":"Providenciales International Airport","city":"Providenciales","country":"TC","lat":21.773697,"lng":-72.268321},{"code":"XSC","name":"South Caicos Airport","city":"South Caicos","country":"TC","lat":21.515699,"lng":-71.528503},{"code":"SLX","name":"Salt Cay Airport","city":"Salt Cay","country":"TC","lat":21.333000183099998,"lng":-71.1999969482},{"code":"BRX","name":"Maria Montez International Airport","city":"Barahona","country":"DO","lat":18.25149917602539,"lng":-71.12039947509766},{"code":"AZS","name":"Samaná El Catey International Airport","city":"Samana","country":"DO","lat":19.269259,"lng":-69.737405},{"code":"JBQ","name":"La Isabela International Airport","city":"La Isabela","country":"DO","lat":18.572500228881836,"lng":-69.98560333251953},{"code":"LRM","name":"Casa De Campo International Airport","city":"La Romana","country":"DO","lat":18.452222,"lng":-68.911111},{"code":"PUJ","name":"Punta Cana International Airport","city":"Punta Cana","country":"DO","lat":18.567093,"lng":-68.364558},{"code":"POP","name":"Gregorio Luperon International Airport","city":"Puerto Plata","country":"DO","lat":19.7579,"lng":-70.57},{"code":"SDQ","name":"Las Américas International Airport","city":"Santo Domingo","country":"DO","lat":18.42970085144,"lng":-69.668899536133},{"code":"STI","name":"Cibao International Airport","city":"Santiago","country":"DO","lat":19.40406,"lng":-70.604431},{"code":"GUA","name":"La Aurora International Airport","city":"Guatemala City","country":"GT","lat":14.582896,"lng":-90.527515},{"code":"HUG","name":"Huehuetenango Airport","city":"Huehuetenango","country":"GT","lat":15.316667,"lng":-91.505556},{"code":"PBR","name":"Puerto Barrios Airport","city":"Puerto Barrios","country":"GT","lat":15.7309,"lng":-88.583801},{"code":"AAZ","name":"Quezaltenango Airport","city":"Quezaltenango","country":"GT","lat":14.8656,"lng":-91.501999},{"code":"RER","name":"Retalhuleu Airport","city":"Retalhuleu","country":"GT","lat":14.521431,"lng":-91.697001},{"code":"FRS","name":"Mundo Maya International Airport","city":"San Benito","country":"GT","lat":16.913707,"lng":-89.866143},{"code":"UJE","name":"Ujae Atoll Airport","city":"Ujae Atoll","country":"MH","lat":8.92806,"lng":165.761993},{"code":"AUL","name":"Aur Island Airport","city":"Aur Atoll","country":"MH","lat":8.14528,"lng":171.173004},{"code":"JEJ","name":"Jeh Airport","city":"Ailinglapalap Atoll","country":"MH","lat":7.565350055690001,"lng":168.962005615},{"code":"LIK","name":"Likiep Airport","city":"Likiep Island","country":"MH","lat":9.82316,"lng":169.307999},{"code":"MJE","name":"Majkin Airport","city":"Majkin","country":"MH","lat":8.163362,"lng":168.173862},{"code":"RNP","name":"Rongelap Island Airport","city":"Rongelap Island","country":"MH","lat":11.1572,"lng":166.886993},{"code":"UIT","name":"Jaluit Airport","city":"Jabor Jaluit Atoll","country":"MH","lat":5.909213,"lng":169.637256},{"code":"WTE","name":"Wotje Airport","city":"Wotje","country":"MH","lat":9.458333,"lng":170.238611},{"code":"WTO","name":"Wotho Island Airport","city":"Wotho Island","country":"MH","lat":10.1733,"lng":166.003006},{"code":"LCE","name":"Golosón International Airport","city":"La Ceiba","country":"HN","lat":15.7425,"lng":-86.852997},{"code":"SAP","name":"Ramón Villeda Morales International Airport","city":"San Pedro Sula","country":"HN","lat":15.4526,"lng":-87.923599},{"code":"GJA","name":"La Laguna Airport","city":"Guanaja","country":"HN","lat":16.4454,"lng":-85.906601},{"code":"PEU","name":"Puerto Lempira Airport","city":"Puerto Lempira","country":"HN","lat":15.2622,"lng":-83.781197},{"code":"RTB","name":"Juan Manuel Gálvez International Airport","city":"Coxen Hole","country":"HN","lat":16.316799,"lng":-86.523003},{"code":"XPL","name":"Palmerola International Airport","city":"Palmerola","country":"HN","lat":14.3824,"lng":-87.621201},{"code":"TGU","name":"Toncontín Airport","city":"Tegucigalpa","country":"HN","lat":14.0609,"lng":-87.217201},{"code":"UII","name":"Utila Airport","city":"Utila Island","country":"HN","lat":16.1131,"lng":-86.880302},{"code":"OCJ","name":"Ian Fleming International Airport","city":"Boscobel","country":"JM","lat":18.404079,"lng":-76.969754},{"code":"KIN","name":"Norman Manley International Airport","city":"Kingston","country":"JM","lat":17.935699462890625,"lng":-76.7874984741211},{"code":"MBJ","name":"Sangster International Airport","city":"Montego Bay","country":"JM","lat":18.50342,"lng":-77.91323},{"code":"KTP","name":"Tinson Pen Airport","city":"Tinson Pen","country":"JM","lat":17.98859977722168,"lng":-76.82379913330078},{"code":"MIJ","name":"Mili Island Airport","city":"Mili Island","country":"MH","lat":6.084754,"lng":171.731419},{"code":"ACA","name":"General Juan N. Álvarez International Airport","city":"Acapulco","country":"MX","lat":16.757126,"lng":-99.753113},{"code":"AGU","name":"Aguascalientes International Airport","city":"Aguascalientes","country":"MX","lat":21.699589,"lng":-102.318372},{"code":"HUX","name":"Bahías de Huatulco International Airport","city":"Huatulco","country":"MX","lat":15.775437,"lng":-96.260456},{"code":"CME","name":"Ciudad del Carmen International Airport","city":"Ciudad del Carmen","country":"MX","lat":18.651516,"lng":-91.799355},{"code":"CUL","name":"Bachigualato Federal International Airport","city":"Culiacán","country":"MX","lat":24.76504,"lng":-107.475228},{"code":"CTM","name":"Chetumal International Airport","city":"Chetumal","country":"MX","lat":18.504985,"lng":-88.328018},{"code":"CEN","name":"Ciudad Obregón International Airport","city":"Ciudad Obregón","country":"MX","lat":27.39259910583496,"lng":-109.83300018310547},{"code":"CPE","name":"Ingeniero Alberto Acuña Ongay International Airport","city":"Campeche","country":"MX","lat":19.815969,"lng":-90.500125},{"code":"CJS","name":"Abraham González International Airport","city":"Ciudad Juárez","country":"MX","lat":31.636673,"lng":-106.428533},{"code":"CUU","name":"General Roberto Fierro Villalobos International Airport","city":"Chihuahua","country":"MX","lat":28.702649,"lng":-105.963764},{"code":"CVM","name":"General Pedro Jose Mendez International Airport","city":"Ciudad Victoria","country":"MX","lat":23.7033,"lng":-98.956497},{"code":"CZM","name":"Cozumel International Airport","city":"Cozumel","country":"MX","lat":20.514922,"lng":-86.928538},{"code":"CUA","name":"Ciudad Constitución National Airport","city":"Comondú","country":"MX","lat":25.053801,"lng":-111.614998},{"code":"DGO","name":"General Guadalupe Victoria International Airport","city":"Durango","country":"MX","lat":24.125468,"lng":-104.527938},{"code":"TPQ","name":"Amado Nervo National Airport","city":"Tepic","country":"MX","lat":21.419833,"lng":-104.842472},{"code":"GDL","name":"Guadalajara International Airport","city":"Guadalajara","country":"MX","lat":20.523342,"lng":-103.310108},{"code":"GYM","name":"General José María Yáñez International Airport","city":"Guaymas","country":"MX","lat":27.9689998626709,"lng":-110.92500305175781},{"code":"GUB","name":"Guerrero Negro Airport","city":"San Quintín","country":"MX","lat":28.0261,"lng":-114.024002},{"code":"HMO","name":"General Ignacio L. Pesqueira International Airport","city":"Hermosillo","country":"MX","lat":29.092811,"lng":-111.053015},{"code":"CLQ","name":"Licenciado Miguel de la Madrid International Airport","city":"Colima","country":"MX","lat":19.277,"lng":-103.577002},{"code":"SLW","name":"Plan de Guadalupe International Airport","city":"Saltillo","country":"MX","lat":25.53786,"lng":-100.928346},{"code":"IZT","name":"General Antonio Cárdenas Rodríguez National Airport / Ixtepec Air Base","city":"Ixtepec","country":"MX","lat":16.445961,"lng":-95.093708},{"code":"LMM","name":"Valle del Fuerte International Airport","city":"Los Mochis","country":"MX","lat":25.685503,"lng":-109.081159},{"code":"BJX","name":"Guanajuato International Airport","city":"Silao","country":"MX","lat":20.992695,"lng":-101.480284},{"code":"LAP","name":"Manuel Márquez de León International Airport","city":"La Paz","country":"MX","lat":24.072331,"lng":-110.362662},{"code":"LTO","name":"Loreto International Airport","city":"Loreto","country":"MX","lat":25.989549,"lng":-111.348408},{"code":"MAM","name":"General Servando Canales International Airport","city":"Matamoros","country":"MX","lat":25.7698993683,"lng":-97.5252990723},{"code":"MID","name":"Manuel Crescencio Rejón International Airport","city":"Mérida","country":"MX","lat":20.930485,"lng":-89.645462},{"code":"MXL","name":"General Rodolfo Sánchez Taboada International Airport","city":"Mexicali","country":"MX","lat":32.630629,"lng":-115.242805},{"code":"MLM","name":"General Francisco J. Mujica International Airport","city":"Morelia","country":"MX","lat":19.849899,"lng":-101.025002},{"code":"MTT","name":"Minatitlán/Coatzacoalcos International Airport","city":"Cosoleacaque","country":"MX","lat":18.103399,"lng":-94.580704},{"code":"MEX","name":"Mexico City Benito Juárez International Airport","city":"Mexico City","country":"MX","lat":19.435822,"lng":-99.07033},{"code":"MTY","name":"Monterrey International Airport","city":"Monterrey","country":"MX","lat":25.778521,"lng":-100.106989},{"code":"MZT","name":"General Rafael Buelna International Airport","city":"Mazatlàn","country":"MX","lat":23.162823,"lng":-106.264517},{"code":"NLD","name":"Quetzalcóatl International Airport","city":"Nuevo Laredo","country":"MX","lat":27.443899,"lng":-99.570503},{"code":"OAX","name":"Xoxocotlán International Airport","city":"Oaxaca","country":"MX","lat":16.998773,"lng":-96.726092},{"code":"PAZ","name":"El Tajín National Airport","city":"Poza Rica","country":"MX","lat":20.6026992798,"lng":-97.46080017090001},{"code":"PBC","name":"Hermanos Serdán International Airport","city":"Puebla","country":"MX","lat":19.158465,"lng":-98.371582},{"code":"PPE","name":"Mar de Cortés International Airport","city":"Puerto Peñasco","country":"MX","lat":31.351987,"lng":-113.305177},{"code":"PDS","name":"Piedras Negras International Airport","city":"Piedras Negras","country":"MX","lat":28.627873,"lng":-100.53524},{"code":"UPN","name":"Uruapan - Licenciado y General Ignacio Lopez Rayon International Airport","city":"Uruapan","country":"MX","lat":19.3967,"lng":-102.039001},{"code":"PVR","name":"Puerto Vallarta International Airport","city":"Puerto Vallarta","country":"MX","lat":20.679913,"lng":-105.254391},{"code":"PXM","name":"Puerto Escondido International Airport","city":"Puerto Escondido","country":"MX","lat":15.876808,"lng":-97.0891},{"code":"QRO","name":"Querétaro Intercontinental Airport","city":"Querétaro","country":"MX","lat":20.618779,"lng":-100.186449},{"code":"REX","name":"General Lucio Blanco International Airport","city":"Reynosa","country":"MX","lat":26.0089,"lng":-98.2285},{"code":"SJD","name":"Los Cabos International Airport","city":"San José del Cabo","country":"MX","lat":23.151919,"lng":-109.720731},{"code":"CSW","name":"Cabo San Lucas International Airport","city":"Cabo San Lucas","country":"MX","lat":22.949072,"lng":-109.939479},{"code":"NLU","name":"Felipe Ángeles International Airport","city":"Mexico City","country":"MX","lat":19.743824,"lng":-99.01507},{"code":"SLP","name":"Ponciano Arriaga International Airport","city":"San Luis Potosí","country":"MX","lat":22.262026,"lng":-100.935692},{"code":"TRC","name":"Francisco Sarabia Tinoco International Airport","city":"Torreón","country":"MX","lat":25.562265,"lng":-103.404608},{"code":"TGZ","name":"Angel Albino Corzo International Airport","city":"Tuxtla Gutiérrez","country":"MX","lat":16.561611,"lng":-93.025731},{"code":"TIJ","name":"General Abelardo L. Rodriguez International Airport","city":"Tijuana","country":"MX","lat":32.541043,"lng":-116.969976},{"code":"TQO","name":"Felipe Carrillo Puerto International Airport Tulum","city":"Tulum","country":"MX","lat":20.172098,"lng":-87.660272},{"code":"TAM","name":"General Francisco Javier Mina International Airport","city":"Ciudad Madero","country":"MX","lat":22.292622,"lng":-97.867058},{"code":"TLC","name":"Adolfo López Mateos International Airport","city":"Toluca","country":"MX","lat":19.3369,"lng":-99.56583},{"code":"TAP","name":"Tapachula International Airport","city":"Tapachula","country":"MX","lat":14.794549,"lng":-92.369871},{"code":"CUN","name":"Cancún International Airport","city":"Cancún","country":"MX","lat":21.040817,"lng":-86.87347},{"code":"VSA","name":"Carlos Rovirosa Pérez International Airport","city":"Villahermosa","country":"MX","lat":17.994283,"lng":-92.818199},{"code":"VER","name":"General Heriberto Jara International Airport","city":"Veracruz","country":"MX","lat":19.139589,"lng":-96.188602},{"code":"ZCL","name":"General Leobardo C. Ruiz International Airport","city":"Zacatecas","country":"MX","lat":22.894942,"lng":-102.687154},{"code":"ZIH","name":"Ixtapa-Zihuatanejo International Airport","city":"Ixtapa","country":"MX","lat":17.601771,"lng":-101.460629},{"code":"ZLO","name":"Playa de Oro International Airport","city":"Manzanillo","country":"MX","lat":19.1448,"lng":-104.558998},{"code":"BEF","name":"Bluefields Airport","city":"Bluefields","country":"NI","lat":11.991,"lng":-83.774101},{"code":"RNI","name":"Corn Island Airport","city":"Corn Island","country":"NI","lat":12.174785,"lng":-83.059445},{"code":"MGA","name":"Augusto C. Sandino (Managua) International Airport","city":"Managua","country":"NI","lat":12.1415,"lng":-86.168198},{"code":"PUZ","name":"Puerto Cabezas Airport","city":"Puerto Cabezas","country":"NI","lat":14.047200202941895,"lng":-83.38670349121094},{"code":"MOH","name":"Maleo Airport","city":"Morowali","country":"ID","lat":-2.201143,"lng":121.662884},{"code":"BOC","name":"Bocas del Toro \"Isla Colón\" International Airport","city":"Isla Colón","country":"PA","lat":9.34085,"lng":-82.250801},{"code":"CTD","name":"Alonso Valderrama Airport","city":"Chitré","country":"PA","lat":7.98784,"lng":-80.409837},{"code":"CHX","name":"Changuinola Captain Manuel Niño International Airport","city":"Changuinola","country":"PA","lat":9.458962,"lng":-82.515062},{"code":"DAV","name":"Enrique Malek International Airport","city":"David","country":"PA","lat":8.388983,"lng":-82.436425},{"code":"ONX","name":"Enrique Adolfo Jimenez Airport","city":"Colón","country":"PA","lat":9.35664,"lng":-79.867401},{"code":"BLB","name":"Panamá Pacífico International Airport","city":"Panamá City","country":"PA","lat":8.91479,"lng":-79.599602},{"code":"PAC","name":"Marcos A. Gelabert International Airport","city":"Albrook","country":"PA","lat":8.97334,"lng":-79.555603},{"code":"PDM","name":"Capt. J. Montenegro Airport","city":"Pedasí","country":"PA","lat":7.534801,"lng":-80.043347},{"code":"RIH","name":"Scarlett Martinez International Airport","city":"Río Hato","country":"PA","lat":8.375880241394,"lng":-80.127899169922},{"code":"PTY","name":"Tocumen International Airport","city":"Tocumen","country":"PA","lat":9.07136,"lng":-79.383499},{"code":"FON","name":"La Fortuna Arenal Airport","city":"La Fortuna","country":"CR","lat":10.469311,"lng":-84.579073},{"code":"GLF","name":"Golfito Airport","city":"Golfito","country":"CR","lat":8.65401,"lng":-83.182198},{"code":"LIR","name":"Daniel Oduber Quirós International Airport","city":"Liberia","country":"CR","lat":10.5933,"lng":-85.544403},{"code":"LIO","name":"Limón International Airport","city":"Limón","country":"CR","lat":9.95796,"lng":-83.022003},{"code":"NOB","name":"Nosara Airport","city":"Nicoya","country":"CR","lat":9.976490020750001,"lng":-85.65299987790002},{"code":"SJO","name":"Juan Santamaría International Airport","city":"San José (Alajuela)","country":"CR","lat":9.99386,"lng":-84.208801},{"code":"PJM","name":"Puerto Jimenez Airport","city":"Puerto Jimenez","country":"CR","lat":8.533329963684082,"lng":-83.30000305175781},{"code":"SYQ","name":"Tobías Bolaños International Airport","city":"San Jose","country":"CR","lat":9.95705,"lng":-84.139801},{"code":"XQP","name":"Quepos Managua Airport","city":"Quepos","country":"CR","lat":9.44316,"lng":-84.129799},{"code":"SAL","name":"El Salvador International Airport Saint Óscar Arnulfo Romero y Galdámez","city":"San Salvador (San Luis Talpa)","country":"SV","lat":13.44447,"lng":-89.055784},{"code":"ILS","name":"Ilopango International Airport","city":"San Salvador","country":"SV","lat":13.6995,"lng":-89.119904},{"code":"CYA","name":"Antoine-Simon International Airport","city":"Les Cayes","country":"HT","lat":18.271099,"lng":-73.7883},{"code":"CAP","name":"Cap Haitien International Airport","city":"Cap Haitien","country":"HT","lat":19.725547,"lng":-72.20071},{"code":"JEE","name":"Jérémie Airport","city":"Carrefour Sanon","country":"HT","lat":18.663099,"lng":-74.170303},{"code":"PAP","name":"Toussaint Louverture International Airport","city":"Port-au-Prince","country":"HT","lat":18.580023,"lng":-72.292596},{"code":"BCA","name":"Gustavo Rizo Airport","city":"Baracoa","country":"CU","lat":20.365299224853516,"lng":-74.5062026977539},{"code":"BYM","name":"Carlos Manuel de Cespedes Airport","city":"Bayamo","country":"CU","lat":20.3964,"lng":-76.621399},{"code":"CCC","name":"Jardines Del Rey Airport","city":"Cayo Coco","country":"CU","lat":22.461000442499998,"lng":-78.32839965820001},{"code":"CFG","name":"Jaime Gonzalez Airport","city":"Cienfuegos","country":"CU","lat":22.149999618530273,"lng":-80.41419982910156},{"code":"CYO","name":"Vilo Acuña International Airport","city":"Cayo Largo del Sur","country":"CU","lat":21.6165008545,"lng":-81.5459976196},{"code":"CMW","name":"Ignacio Agramonte International Airport","city":"Camaguey","country":"CU","lat":21.419909,"lng":-77.848039},{"code":"SCU","name":"Antonio Maceo International Airport","city":"Santiago","country":"CU","lat":19.97474,"lng":-75.835504},{"code":"HAV","name":"José Martí International Airport","city":"Havana","country":"CU","lat":22.989200592041016,"lng":-82.40910339355469},{"code":"HOG","name":"Frank Pais International Airport","city":"Holguin","country":"CU","lat":20.785148,"lng":-76.315526},{"code":"MZO","name":"Sierra Maestra International Airport","city":"Manzanillo","country":"CU","lat":20.288605,"lng":-77.087545},{"code":"GER","name":"Rafael Cabrera Airport","city":"Nueva Gerona","country":"CU","lat":21.834699630737305,"lng":-82.78379821777344},{"code":"SNU","name":"Abel Santamaria International Airport","city":"Santa Clara","country":"CU","lat":22.492215,"lng":-79.943119},{"code":"TND","name":"Alberto Delgado Airport","city":"Trinidad","country":"CU","lat":21.7883,"lng":-79.9972},{"code":"VRA","name":"Juan Gualberto Gomez International Airport","city":"Matanzas","country":"CU","lat":23.034401,"lng":-81.435303},{"code":"VTU","name":"Hermanos Ameijeiras Airport","city":"Las Tunas","country":"CU","lat":20.9876,"lng":-76.935799},{"code":"FMT","name":"Faresmaathoda Airport","city":"Faresmaathodaa","country":"MV","lat":0.192647,"lng":73.196755},{"code":"CYB","name":"Charles Kirkconnell International Airport","city":"West End","country":"KY","lat":19.687,"lng":-79.882797},{"code":"GCM","name":"Owen Roberts International Airport","city":"George Town","country":"KY","lat":19.292801,"lng":-81.357697},{"code":"LBP","name":"Long Banga Airport","city":"Long Banga","country":"MY","lat":3.202138,"lng":115.401811},{"code":"ASD","name":"Andros Town Airport","city":"Andros Town","country":"BS","lat":24.697901,"lng":-77.795601},{"code":"TZN","name":"Congo Town Airport","city":"Andros","country":"BS","lat":24.158701,"lng":-77.589798},{"code":"MHH","name":"Leonard M. Thompson International Airport","city":"Marsh Harbour","country":"BS","lat":26.510711,"lng":-77.084262},{"code":"SAQ","name":"San Andros Airport","city":"Andros Island","country":"BS","lat":25.053800582885742,"lng":-78.04900360107422},{"code":"AXP","name":"Spring Point Airport","city":"Spring Point","country":"BS","lat":22.441799,"lng":-73.970901},{"code":"TCB","name":"Treasure Cay Airport","city":"Treasure Cay","country":"BS","lat":26.745300293,"lng":-77.3912963867},{"code":"CCZ","name":"Chub Cay Airport","city":"Chub Cay","country":"BS","lat":25.417101,"lng":-77.880898},{"code":"BIM","name":"South Bimini Airport","city":"South Bimini","country":"BS","lat":25.6998996735,"lng":-79.2647018433},{"code":"ATC","name":"Arthur's Town Airport","city":"Arthur's Town","country":"BS","lat":24.6294,"lng":-75.673797},{"code":"TBI","name":"New Bight Airport","city":"Cat Island","country":"BS","lat":24.3153,"lng":-75.452301},{"code":"CRI","name":"Colonel Hill Airport","city":"Colonel Hill","country":"BS","lat":22.7456,"lng":-74.182404},{"code":"GGT","name":"Exuma International Airport","city":"Moss Town","country":"BS","lat":23.562599,"lng":-75.877998},{"code":"ELH","name":"North Eleuthera Airport","city":"North Eleuthera","country":"BS","lat":25.475823,"lng":-76.680784},{"code":"GHB","name":"Governor's Harbour Airport","city":"Governor's Harbour","country":"BS","lat":25.2847,"lng":-76.331001},{"code":"RSD","name":"Rock Sound International Airport","city":"Rock Sound","country":"BS","lat":24.89165,"lng":-76.177602},{"code":"FPO","name":"Grand Bahama International Airport","city":"Freeport","country":"BS","lat":26.557996,"lng":-78.695583},{"code":"IGA","name":"Inagua Airport","city":"Matthew Town","country":"BS","lat":20.975,"lng":-73.666901},{"code":"MYK","name":"May Creek Airport","city":"May Creek","country":"US","lat":61.335701,"lng":-142.686996},{"code":"LGI","name":"Deadman's Cay Airport","city":"Deadman's Cay","country":"BS","lat":23.1790008545,"lng":-75.09359741210001},{"code":"SML","name":"Stella Maris Airport","city":"Stella Maris","country":"BS","lat":23.582317,"lng":-75.268621},{"code":"MYG","name":"Mayaguana Airport","city":"Abraham Bay Settlement","country":"BS","lat":22.379499,"lng":-73.013494},{"code":"NAS","name":"Lynden Pindling International Airport","city":"Nassau","country":"BS","lat":25.039,"lng":-77.466202},{"code":"ZSA","name":"San Salvador International Airport","city":"San Salvador","country":"BS","lat":24.063041,"lng":-74.523233},{"code":"BZE","name":"Philip S. W. Goldson International Airport","city":"Belize City","country":"BZ","lat":17.539951,"lng":-88.303556},{"code":"PLJ","name":"Placencia Airport","city":"Placencia","country":"BZ","lat":16.536957,"lng":-88.361511},{"code":"AIT","name":"Aitutaki Airport","city":"Aitutaki","country":"CK","lat":-18.8309,"lng":-159.764008},{"code":"AIU","name":"Enua Airport","city":"Atiu Island","country":"CK","lat":-19.96780014038086,"lng":-158.11900329589844},{"code":"MHX","name":"Manihiki Island Airport","city":"Manihiki Island","country":"CK","lat":-10.38091,"lng":-160.999424},{"code":"MUK","name":"Mauke Airport","city":"Mauke Island","country":"CK","lat":-20.13610076904297,"lng":-157.34500122070312},{"code":"MOI","name":"Mitiaro Island Airport","city":"Mitiaro Island","country":"CK","lat":-19.842501,"lng":-157.703003},{"code":"RAR","name":"Rarotonga International Airport","city":"Avarua","country":"CK","lat":-21.2027,"lng":-159.806},{"code":"ICI","name":"Cicia Airport","city":"Cicia","country":"FJ","lat":-17.7432994843,"lng":-179.341995239},{"code":"NAN","name":"Nadi International Airport","city":"Nadi","country":"FJ","lat":-17.761822,"lng":177.437843},{"code":"PTF","name":"Malolo Lailai Island Airport","city":"Malolo Lailai Island","country":"FJ","lat":-17.778448,"lng":177.196996},{"code":"KDV","name":"Vunisea Airport","city":"Vunisea","country":"FJ","lat":-19.058099746699998,"lng":178.156997681},{"code":"MNF","name":"Mana Island Airport","city":"Mana Island","country":"FJ","lat":-17.672844,"lng":177.0985},{"code":"MFJ","name":"Moala Airport","city":"Moala","country":"FJ","lat":-18.566699981699998,"lng":179.951004028},{"code":"SUV","name":"Nausori International Airport","city":"Nausori","country":"FJ","lat":-18.044226,"lng":178.561486},{"code":"LEV","name":"Levuka Airfield","city":"Bureta","country":"FJ","lat":-17.7110996246,"lng":178.759002686},{"code":"NGI","name":"Ngau Airport","city":"Ngau","country":"FJ","lat":-18.115600585899998,"lng":179.339996338},{"code":"LKB","name":"Lakeba Island Airport","city":"Lakeba Island","country":"FJ","lat":-18.1991996765,"lng":-178.817001343},{"code":"LBS","name":"Labasa Airport","city":"Labasa","country":"FJ","lat":-16.4667,"lng":179.339996},{"code":"TVU","name":"Matei Airport","city":"Matei","country":"FJ","lat":-16.690599,"lng":-179.876999},{"code":"KXF","name":"Koro Island Airport","city":"Koro Island","country":"FJ","lat":-17.3458003998,"lng":179.42199707},{"code":"RTA","name":"Rotuma Airport","city":"Rotuma","country":"FJ","lat":-12.482500076293945,"lng":177.0709991455078},{"code":"SVU","name":"Savusavu Airport","city":"Savusavu","country":"FJ","lat":-16.803394,"lng":179.340587},{"code":"YAS","name":"Yasawa Island Airport","city":"Yasawa Island","country":"FJ","lat":-16.7589,"lng":177.544998},{"code":"EUA","name":"Kaufana Airport","city":"Eua Island","country":"TO","lat":-21.378299713100002,"lng":-174.957992554},{"code":"TBU","name":"Fua'amotu International Airport","city":"Nuku'alofa","country":"TO","lat":-21.241438,"lng":-175.149164},{"code":"HPA","name":"Lifuka Island Airport","city":"Lifuka","country":"TO","lat":-19.777000427246094,"lng":-174.34100341796875},{"code":"NTT","name":"Kuini Lavenia Airport","city":"Niuatoputapu","country":"TO","lat":-15.977297,"lng":-173.791089},{"code":"VAV","name":"Vava'u International Airport","city":"Vava'u Island","country":"TO","lat":-18.5853,"lng":-173.962006},{"code":"VBV","name":"Vanua Balavu Airport","city":"Vanua Balavu","country":"FJ","lat":-17.268999099731445,"lng":-178.9759979248047},{"code":"PHG","name":"Port Harcourt City Airport / Port Harcourt Air Force Base","city":"Port Harcourt","country":"NG","lat":4.846111,"lng":7.021389},{"code":"FUN","name":"Funafuti International Airport","city":"Funafuti","country":"TV","lat":-8.523885,"lng":179.196976},{"code":"TRW","name":"Bonriki International Airport","city":"South Tarawa","country":"KI","lat":1.38164,"lng":173.147003},{"code":"BBG","name":"Butaritari Airport","city":"Butaritari","country":"KI","lat":3.08583,"lng":172.811005},{"code":"AAK","name":"Aranuka Airport","city":"Buariki","country":"KI","lat":0.185278,"lng":173.636993},{"code":"IUE","name":"Niue International Airport","city":"Alofi","country":"NU","lat":-19.080109,"lng":-169.923493},{"code":"NLH","name":"Ninglang Luguhu Airport","city":"Ninglang","country":"CN","lat":27.5403,"lng":100.7593},{"code":"FUT","name":"Pointe Vele Airport","city":"Futuna Island","country":"WF","lat":-14.311565,"lng":-178.066492},{"code":"WLS","name":"Hihifo Airport","city":"Wallis Island","country":"WF","lat":-13.239445,"lng":-176.19865},{"code":"PHH","name":"Pokhara International Airport","city":"Pokhara","country":"NP","lat":28.1838,"lng":84.0147},{"code":"OFU","name":"Ofu Airport","city":"Ofu","country":"AS","lat":-14.1844,"lng":-169.669998},{"code":"APW","name":"Faleolo International Airport","city":"Apia","country":"WS","lat":-13.83,"lng":-172.007996},{"code":"FTI","name":"Fitiuta Airport","city":"Fitiuta Village","country":"AS","lat":-14.216131,"lng":-169.423771},{"code":"PPG","name":"Pago Pago International Airport","city":"Pago Pago","country":"AS","lat":-14.331,"lng":-170.710007},{"code":"PPT","name":"Fa'a'ā International Airport","city":"Papeete","country":"PF","lat":-17.553472,"lng":-149.606936},{"code":"RMT","name":"Rimatara Airport","city":"Rimatara Island","country":"PF","lat":-22.63725,"lng":-152.8059},{"code":"RUR","name":"Rurutu Airport","city":"","country":"PF","lat":-22.434099197387695,"lng":-151.36099243164062},{"code":"TUB","name":"Tubuai Airport","city":"","country":"PF","lat":-23.365400314331055,"lng":-149.5240020751953},{"code":"RVV","name":"Raivavae Airport","city":"","country":"PF","lat":-23.885201,"lng":-147.662003},{"code":"AAA","name":"Anaa Airport","city":"Anaa","country":"PF","lat":-17.3526,"lng":-145.509995},{"code":"FGU","name":"Fangatau Airport","city":"Fangatau","country":"PF","lat":-15.820044,"lng":-140.888071},{"code":"TIH","name":"Tikehau Airport","city":"Tuherahera","country":"PF","lat":-15.1196,"lng":-148.231003},{"code":"APK","name":"Apataki Airport","city":"Apataki","country":"PF","lat":-15.5736,"lng":-146.414993},{"code":"FAV","name":"Fakarava Airport","city":"","country":"PF","lat":-16.054100036621094,"lng":-145.65699768066406},{"code":"HHZ","name":"Hikueru Airport","city":"Hikueru","country":"PF","lat":-17.548297,"lng":-142.612037},{"code":"XMH","name":"Manihi Airport","city":"","country":"PF","lat":-14.436800003051758,"lng":-146.07000732421875},{"code":"GMR","name":"Totegegie Airport","city":"","country":"PF","lat":-23.07990074157715,"lng":-134.88999938964844},{"code":"KKR","name":"Kaukura Airport","city":"Raitahiti","country":"PF","lat":-15.6633,"lng":-146.884995},{"code":"MKP","name":"Makemo Airport","city":"Makemo","country":"PF","lat":-16.5839,"lng":-143.658005},{"code":"NAU","name":"Napuka Island Airport","city":"Napuka Island","country":"PF","lat":-14.177397,"lng":-141.266112},{"code":"TKV","name":"Tatakoto Airport","city":"Tatakoto","country":"PF","lat":-17.355226,"lng":-138.44734},{"code":"PKP","name":"Puka Puka Airport","city":"","country":"PF","lat":-14.80955,"lng":-138.812621},{"code":"TKP","name":"Takapoto Airport","city":"","country":"PF","lat":-14.709500312805176,"lng":-145.24600219726562},{"code":"AXR","name":"Arutua Airport","city":"","country":"PF","lat":-15.248299598693848,"lng":-146.61700439453125},{"code":"MVT","name":"Mataiva Airport","city":"","country":"PF","lat":-14.8681001663208,"lng":-148.7169952392578},{"code":"AHE","name":"Ahe Airport","city":"Ahe Atoll","country":"PF","lat":-14.428099632263184,"lng":-146.2570037841797},{"code":"KHZ","name":"Kauehi Airport","city":"Kauehi","country":"PF","lat":-15.780622,"lng":-145.128443},{"code":"FHZ","name":"Fakahina Airport","city":"Fakahina","country":"PF","lat":-15.992259,"lng":-140.164246},{"code":"NIU","name":"Naiu Airport","city":"Naiu Atoll","country":"PF","lat":-16.119296,"lng":-146.368768},{"code":"RRR","name":"Raroia Airport","city":"Raroia","country":"PF","lat":-16.050485,"lng":-142.476568},{"code":"TKX","name":"Takaroa Airport","city":"","country":"PF","lat":-14.45580005645752,"lng":-145.02499389648438},{"code":"NHV","name":"Nuku Hiva Airport","city":"Nuku Hiva","country":"PF","lat":-8.7956,"lng":-140.229004},{"code":"AUQ","name":"Hiva Oa-Atuona Airport","city":"Hiva Oa Island","country":"PF","lat":-9.76879024506,"lng":-139.011001587},{"code":"UAP","name":"Ua Pou Airport","city":"Ua Pou","country":"PF","lat":-9.35167,"lng":-140.078003},{"code":"UAH","name":"Ua Huka Airport","city":"Ua Huka","country":"PF","lat":-8.936228,"lng":-139.554062},{"code":"BOB","name":"Bora Bora Airport","city":"Motu Mute","country":"PF","lat":-16.444400787353516,"lng":-151.75100708007812},{"code":"RGI","name":"Rangiroa Airport","city":"","country":"PF","lat":-14.954299926757812,"lng":-147.66099548339844},{"code":"HUH","name":"Huahine-Fare Airport","city":"Fare","country":"PF","lat":-16.687084,"lng":-151.021593},{"code":"MOZ","name":"Moorea Temae Airport","city":"Moorea-Maiao","country":"PF","lat":-17.48979,"lng":-149.761777},{"code":"HOI","name":"Hao Airport","city":"Otepa","country":"PF","lat":-18.0748,"lng":-140.945999},{"code":"MAU","name":"Maupiti Airport","city":"","country":"PF","lat":-16.42650032043457,"lng":-152.24400329589844},{"code":"RFP","name":"Raiatea Airport","city":"Uturoa","country":"PF","lat":-16.7229,"lng":-151.466003},{"code":"SLH","name":"Sola Airport","city":"Sola","country":"VU","lat":-13.8516998291,"lng":167.537002563},{"code":"CCV","name":"Craig Cove Airport","city":"Craig Cove","country":"VU","lat":-16.264999,"lng":167.923996},{"code":"LOD","name":"Longana Airport","city":"Longana","country":"VU","lat":-15.3066997528,"lng":167.966995239},{"code":"SSR","name":"Sara Airport","city":"Pentecost Island","country":"VU","lat":-15.4708003998,"lng":168.151992798},{"code":"PBJ","name":"Tavie Airport","city":"Paama Island","country":"VU","lat":-16.431955,"lng":168.235511},{"code":"LPM","name":"Lamap Airport","city":"Lamap","country":"VU","lat":-16.4611228,"lng":167.829253},{"code":"LNB","name":"Lamen Bay Airport","city":"Lamen Bay","country":"VU","lat":-16.584199905400002,"lng":168.158996582},{"code":"LNE","name":"Lonorore Airport","city":"Lonorore","country":"VU","lat":-15.865599632299999,"lng":168.17199707},{"code":"NUS","name":"Norsup Airport","city":"Norsup","country":"VU","lat":-16.0797,"lng":167.401001},{"code":"SON","name":"Santo Pekoa International Airport","city":"Luganville","country":"VU","lat":-15.505000114399998,"lng":167.220001221},{"code":"TGH","name":"Tongoa Airport","city":"Tongoa Island","country":"VU","lat":-16.8910999298,"lng":168.550994873},{"code":"VLS","name":"Valesdir Airport","city":"Epi Island","country":"VU","lat":-16.796100616500002,"lng":168.177001953},{"code":"WLH","name":"Walaha Airport","city":"Walaha","country":"VU","lat":-15.412,"lng":167.690994},{"code":"OLJ","name":"North West Santo Airport","city":"Olpoi","country":"VU","lat":-14.881699562099998,"lng":166.557998657},{"code":"FTA","name":"Futuna Airport","city":"Futuna Island","country":"VU","lat":-19.516399383499998,"lng":170.231994629},{"code":"VLI","name":"Bauerfield International Airport","city":"Port Vila","country":"VU","lat":-17.699301,"lng":168.320007},{"code":"TAH","name":"Whitegrass Airport","city":"Tanna Island","country":"VU","lat":-19.455099,"lng":169.223999},{"code":"TGJ","name":"Tiga Airport","city":"Tiga","country":"NC","lat":-21.096407,"lng":167.80379},{"code":"BMY","name":"Île Art - Waala Airport","city":"Waala","country":"NC","lat":-19.720521,"lng":163.661077},{"code":"KNQ","name":"Koné Airport","city":"Koné","country":"NC","lat":-21.053551,"lng":164.838768},{"code":"ILP","name":"Île des Pins Airport","city":"Île des Pins","country":"NC","lat":-22.5889,"lng":167.455994},{"code":"KOC","name":"Koumac Airport","city":"Koumac","country":"NC","lat":-20.546323,"lng":164.255648},{"code":"LIF","name":"Lifou Airport","city":"Lifou","country":"NC","lat":-20.774563,"lng":167.239328},{"code":"GEA","name":"Nouméa Magenta Airport","city":"Nouméa","country":"NC","lat":-22.257706,"lng":166.472822},{"code":"MEE","name":"Maré Airport","city":"Maré","country":"NC","lat":-21.482438,"lng":168.038465},{"code":"TOU","name":"Touho Airport","city":"Touho","country":"NC","lat":-20.790127,"lng":165.259524},{"code":"UVE","name":"Ouvéa Airport","city":"Ouvéa","country":"NC","lat":-20.640932,"lng":166.573024},{"code":"NOU","name":"La Tontouta International Airport","city":"Nouméa (La Tontouta)","country":"NC","lat":-22.014601,"lng":166.212997},{"code":"AKL","name":"Auckland International Airport","city":"Auckland","country":"NZ","lat":-37.01199,"lng":174.786331},{"code":"TUO","name":"Taupo Airport","city":"Taupo","country":"NZ","lat":-38.73970031738281,"lng":176.08399963378906},{"code":"CHC","name":"Christchurch International Airport","city":"Christchurch","country":"NZ","lat":-43.489029,"lng":172.532065},{"code":"CHT","name":"Inia William Tuuta Memorial Airport","city":"Te One","country":"NZ","lat":-43.81189,"lng":-176.46514},{"code":"DUD","name":"Dunedin International Airport","city":"Dunedin","country":"NZ","lat":-45.929017,"lng":170.197758},{"code":"GBZ","name":"Great Barrier Aerodrome","city":"Claris","country":"NZ","lat":-36.241402,"lng":175.472},{"code":"GIS","name":"Gisborne Airport","city":"Gisborne","country":"NZ","lat":-38.6633,"lng":177.977997},{"code":"HKK","name":"Hokitika Airfield","city":"","country":"NZ","lat":-42.713600158691406,"lng":170.98500061035156},{"code":"HLZ","name":"Hamilton International Airport","city":"Hamilton","country":"NZ","lat":-37.866961,"lng":175.331952},{"code":"KKE","name":"Kerikeri Airport","city":"Kerikeri","country":"NZ","lat":-35.259148,"lng":173.913317},{"code":"KAT","name":"Kaitaia Airport","city":"Awanui","country":"NZ","lat":-35.069837,"lng":173.287053},{"code":"NPL","name":"New Plymouth Airport","city":"New Plymouth","country":"NZ","lat":-39.00859832763672,"lng":174.1790008544922},{"code":"NPE","name":"Hawke's Bay Airport","city":"Napier","country":"NZ","lat":-39.465801,"lng":176.869995},{"code":"NSN","name":"Nelson Airport","city":"Nelson","country":"NZ","lat":-41.298302,"lng":173.220993},{"code":"IVC","name":"Invercargill Airport","city":"Invercargill","country":"NZ","lat":-46.41239929199219,"lng":168.31300354003906},{"code":"PMR","name":"Palmerston North Airport","city":"Palmerston North","country":"NZ","lat":-40.320599,"lng":175.617004},{"code":"PCN","name":"Picton Aerodrome","city":"Koromiko","country":"NZ","lat":-41.346099853516,"lng":173.95599365234},{"code":"ZQN","name":"Queenstown Airport","city":"Queenstown","country":"NZ","lat":-45.019205,"lng":168.746379},{"code":"ROT","name":"Rotorua Regional Airport","city":"Rotorua","country":"NZ","lat":-38.1092,"lng":176.317001},{"code":"TRG","name":"Tauranga Airport","city":"Tauranga","country":"NZ","lat":-37.67190170288086,"lng":176.1959991455078},{"code":"TIU","name":"Timaru Airport","city":"","country":"NZ","lat":-44.302799224853516,"lng":171.22500610351562},{"code":"BHE","name":"Woodbourne Airport","city":"Blenheim","country":"NZ","lat":-41.5182991027832,"lng":173.8699951171875},{"code":"WKA","name":"Wanaka Airport","city":"Wanaka","country":"NZ","lat":-44.722091,"lng":169.246287},{"code":"WHK","name":"Whakatāne Airport","city":"Whakatāne","country":"NZ","lat":-37.922224,"lng":176.91696},{"code":"WLG","name":"Wellington International Airport","city":"Wellington","country":"NZ","lat":-41.326839,"lng":174.806862},{"code":"WRE","name":"Whangarei Airport","city":"Whangarei","country":"NZ","lat":-35.769253,"lng":174.363713},{"code":"WSZ","name":"Westport Airport","city":"Westport","country":"NZ","lat":-41.737111,"lng":171.579033},{"code":"WAG","name":"Wanganui Airport","city":"Wanganui","country":"NZ","lat":-39.963467,"lng":175.02444},{"code":"FBD","name":"Fayzabad Airport","city":"Fayzabad","country":"AF","lat":37.122148,"lng":70.5201},{"code":"HEA","name":"Herat - Khwaja Abdullah Ansari International Airport","city":"Guzara","country":"AF","lat":34.209999,"lng":62.228298},{"code":"KBL","name":"Kabul International Airport","city":"Kabul","country":"AF","lat":34.565899,"lng":69.212303},{"code":"KDH","name":"Ahmad Shah Baba International Airport","city":"Kandahar","country":"AF","lat":31.505769,"lng":65.847963},{"code":"KHT","name":"Khost International Airport","city":"Khost","country":"AF","lat":33.284605,"lng":69.80734},{"code":"MMZ","name":"Maymana Zahiraddin Faryabi Airport","city":"Maymana","country":"AF","lat":35.930801,"lng":64.760902},{"code":"MZR","name":"Mazar-i-Sharif International Airport","city":"Mazar-i-Sharif","country":"AF","lat":36.70412,"lng":67.210459},{"code":"BAH","name":"Bahrain International Airport","city":"Manama","country":"BH","lat":26.267295,"lng":50.63764},{"code":"OCS","name":"Corisco International Airport","city":"Corisco Island","country":"GQ","lat":0.910937,"lng":9.33027},{"code":"AHB","name":"Abha International Airport","city":"Abha","country":"SA","lat":18.2404,"lng":42.656601},{"code":"HOF","name":"Al-Ahsa International Airport","city":"Hofuf","country":"SA","lat":25.285299,"lng":49.485199},{"code":"ULH","name":"Al-Ula International Airport","city":"Al-Ula","country":"SA","lat":26.483634,"lng":38.117048},{"code":"ABT","name":"King Saud Bin Abdulaziz (Al Baha) Airport","city":"Al-Baha","country":"SA","lat":20.298506,"lng":41.636153},{"code":"BHH","name":"Bisha Airport","city":"Bisha","country":"SA","lat":19.9844,"lng":42.620899},{"code":"DMM","name":"King Fahd International Airport","city":"Ad Dammam","country":"SA","lat":26.4691,"lng":49.798209},{"code":"DWD","name":"Dawadmi Domestic Airport","city":"Dawadmi","country":"SA","lat":24.4499,"lng":44.121201},{"code":"GIZ","name":"Jizan Regional Airport / King Abdullah bin Abdulaziz Airport","city":"Jizan","country":"SA","lat":16.9011,"lng":42.5858},{"code":"ELQ","name":"Prince Naif bin Abdulaziz International Airport","city":"Qassim","country":"SA","lat":26.302799,"lng":43.774399},{"code":"URY","name":"Gurayat Domestic Airport","city":"Gurayat","country":"SA","lat":31.412413,"lng":37.278898},{"code":"HAS","name":"Hail International Airport","city":"Hail","country":"SA","lat":27.437901,"lng":41.686298},{"code":"JED","name":"King Abdulaziz International Airport","city":"Jeddah","country":"SA","lat":21.680241,"lng":39.157436},{"code":"KMC","name":"King Khaled Military City Airport","city":"King Khaled Military City","country":"SA","lat":27.9009,"lng":45.528198},{"code":"MED","name":"Prince Mohammad Bin Abdulaziz Airport","city":"Medina","country":"SA","lat":24.5534,"lng":39.705101},{"code":"EAM","name":"Najran Domestic Airport","city":"Najran","country":"SA","lat":17.611401,"lng":44.419201},{"code":"NUM","name":"Neom Bay Airport","city":"Sharma","country":"SA","lat":27.924261,"lng":35.29358},{"code":"AQI","name":"Qaisumah–Hafar Al-Batin International Airport","city":"Qaisumah","country":"SA","lat":28.335726,"lng":46.127108},{"code":"RAH","name":"Rafha Domestic Airport","city":"Rafha","country":"SA","lat":29.626399993896484,"lng":43.4906005859375},{"code":"RUH","name":"King Khalid International Airport","city":"Riyadh","country":"SA","lat":24.9576,"lng":46.698799},{"code":"RAE","name":"Arar Domestic Airport","city":"Arar","country":"SA","lat":30.906600952148438,"lng":41.13819885253906},{"code":"SHW","name":"Sharurah Domestic Airport","city":"Sharurah","country":"SA","lat":17.4669,"lng":47.121399},{"code":"AJF","name":"Al-Jawf International Airport","city":"Al-Jawf","country":"SA","lat":29.783301,"lng":40.100905},{"code":"TUU","name":"Prince Sultan bin Abdulaziz International Airport","city":"Tabuk","country":"SA","lat":28.3711,"lng":36.624865},{"code":"TIF","name":"Taif International Airport","city":"Taif","country":"SA","lat":21.484739,"lng":40.544074},{"code":"TUI","name":"Turaif Domestic Airport","city":"Turaif","country":"SA","lat":31.692188,"lng":38.731544},{"code":"WAE","name":"Wadi Al Dawasir Domestic Airport","city":"Wadi Al Dawasir","country":"SA","lat":20.504299,"lng":45.1996},{"code":"EJH","name":"Al Wajh Domestic Airport","city":"Al Wajh","country":"SA","lat":26.198601,"lng":36.476398},{"code":"YNB","name":"Prince Abdulmohsen Bin Abdulaziz International Airport","city":"Yanbu","country":"SA","lat":24.144199,"lng":38.0634},{"code":"ABD","name":"Abadan Ayatollah Jami International Airport","city":"Abadan","country":"IR","lat":30.367887,"lng":48.230075},{"code":"DEF","name":"Dezful Airport","city":"Dezful","country":"IR","lat":32.434399,"lng":48.397598},{"code":"GCH","name":"Gachsaran Airport","city":"Gachsaran","country":"IR","lat":30.333869,"lng":50.833796},{"code":"MRX","name":"Mahshahr Airport","city":"Mahshahr","country":"IR","lat":30.5562,"lng":49.151901},{"code":"AWZ","name":"Qasem Soleimani International Airport","city":"Ahvaz","country":"IR","lat":31.3364,"lng":48.763789},{"code":"AEU","name":"Abu Musa Island Airport","city":"Abu Musa","country":"IR","lat":25.8757,"lng":55.033001},{"code":"BUZ","name":"Bushehr Airport","city":"Bushehr","country":"IR","lat":28.9447994232,"lng":50.8345985413},{"code":"KIH","name":"Kish International Airport","city":"Kish Island","country":"IR","lat":26.525427,"lng":53.980462},{"code":"BDH","name":"Bandar Lengeh International Airport","city":"Bandar Lengeh","country":"IR","lat":26.532283,"lng":54.82484},{"code":"PGU","name":"Persian Gulf International Airport","city":"Khiyaroo","country":"IR","lat":27.379601,"lng":52.737701},{"code":"KHK","name":"Khark Airport","city":"Khark","country":"IR","lat":29.260503,"lng":50.322193},{"code":"KSH","name":"Shahid Ashrafi Esfahani Airport","city":"Kermanshah","country":"IR","lat":34.3459014893,"lng":47.1581001282},{"code":"KHD","name":"Khoram Abad Airport","city":"","country":"IR","lat":33.43539810180664,"lng":48.282901763916016},{"code":"SDG","name":"Sanandaj Airport","city":"","country":"IR","lat":35.24589920043945,"lng":47.00920104980469},{"code":"KKS","name":"Kashan Airport","city":"Kashan","country":"IR","lat":33.895302,"lng":51.577},{"code":"IFN","name":"Isfahan Shahid Beheshti International Airport","city":"Isfahan","country":"IR","lat":32.755142,"lng":51.883877},{"code":"RAS","name":"Sardar-e-Jangal Airport","city":"Rasht","country":"IR","lat":37.323333,"lng":49.617778},{"code":"HDM","name":"Hamadan Airport","city":"Hamadan","country":"IR","lat":34.866416,"lng":48.56069},{"code":"IKA","name":"Imam Khomeini International Airport","city":"Tehran","country":"IR","lat":35.416099548339844,"lng":51.152198791503906},{"code":"THR","name":"Mehrabad International Airport","city":"Tehran","country":"IR","lat":35.689175,"lng":51.314392},{"code":"PYK","name":"Payam International Airport","city":"Karaj","country":"IR","lat":35.7761,"lng":50.826698},{"code":"SNX","name":"Semnan Municipal Airport","city":"Semnan","country":"IR","lat":35.591099,"lng":53.495098},{"code":"BND","name":"Bandar Abbas International Airport","city":"Bandar Abbas","country":"IR","lat":27.2183,"lng":56.3778},{"code":"KER","name":"Ayatollah Hashemi Rafsanjani International Airport","city":"Kerman","country":"IR","lat":30.271276,"lng":56.949692},{"code":"BXR","name":"Bam Airport","city":"Bam","country":"IR","lat":29.0842,"lng":58.450001},{"code":"GSM","name":"Qeshm International Airport","city":"Qeshm(Dayrestan)","country":"IR","lat":26.754639,"lng":55.902353},{"code":"RJN","name":"Rafsanjan Airport","city":"Rafsanjan","country":"IR","lat":30.298345,"lng":56.048979},{"code":"XBJ","name":"Birjand International Airport","city":"Birjand","country":"IR","lat":32.896525,"lng":59.281258},{"code":"MHD","name":"Mashhad International Airport","city":"Mashhad","country":"IR","lat":36.234821,"lng":59.642949},{"code":"BJB","name":"Bojnord Airport","city":"Bojnord","country":"IR","lat":37.49300003051758,"lng":57.30820083618164},{"code":"AFZ","name":"Sabzevar National Airport","city":"Sabzevar","country":"IR","lat":36.16809844970703,"lng":57.59519958496094},{"code":"NSH","name":"Nowshahr Airport","city":"Nowshahr","country":"IR","lat":36.664286,"lng":51.46273},{"code":"RZR","name":"Ramsar Airport","city":"Ramsar","country":"IR","lat":36.90701,"lng":50.687316},{"code":"SRY","name":"Sari Dasht-e Naz International Airport","city":"Sari","country":"IR","lat":36.644408,"lng":53.188761},{"code":"LRR","name":"Lar Airport","city":"Lar","country":"IR","lat":27.6747,"lng":54.383301},{"code":"LFM","name":"Lamerd Airport","city":"Lamerd","country":"IR","lat":27.3727,"lng":53.188801},{"code":"SYZ","name":"Shiraz Shahid Dastghaib International Airport","city":"Shiraz","country":"IR","lat":29.5392,"lng":52.589802},{"code":"ADU","name":"Ardabil Airport","city":"Ardabil","country":"IR","lat":38.3256988525,"lng":48.4244003296},{"code":"PFQ","name":"Parsabad-Moghan Airport","city":"Parsabad","country":"IR","lat":39.6036,"lng":47.8815},{"code":"OMH","name":"Urmia Airport","city":"Urmia","country":"IR","lat":37.6680984497,"lng":45.0686988831},{"code":"TBZ","name":"Tabriz International Airport","city":"Tabriz","country":"IR","lat":38.1339,"lng":46.235001},{"code":"AZD","name":"Shahid Sadooghi Airport","city":"Yazd","country":"IR","lat":31.9048995972,"lng":54.2765007019},{"code":"ZBR","name":"Chabahar Konarak International Airport","city":"Konarak","country":"IR","lat":25.44319,"lng":60.382167},{"code":"ZAH","name":"Zahedan International Airport","city":"Zahedan","country":"IR","lat":29.4757,"lng":60.9062},{"code":"JSK","name":"Jask Airport","city":"Bandar-e-Jask","country":"IR","lat":25.654757,"lng":57.801718},{"code":"AMM","name":"Queen Alia International Airport","city":"Amman","country":"JO","lat":31.7226009369,"lng":35.9931983948},{"code":"ADJ","name":"Marka International (Amman Civil) Airport","city":"Amman","country":"JO","lat":31.9727,"lng":35.9916},{"code":"AQJ","name":"King Hussein International Airport","city":"Aqaba","country":"JO","lat":29.611601,"lng":35.018101},{"code":"KWI","name":"Kuwait International Airport","city":"Kuwait City","country":"KW","lat":29.224487,"lng":47.969813},{"code":"BEY","name":"Beirut Rafic Hariri International Airport","city":"Beirut","country":"LB","lat":33.819833,"lng":35.487443},{"code":"OLH","name":"Old Harbor Airport","city":"Old Harbor","country":"US","lat":57.218102,"lng":-153.270004},{"code":"DQM","name":"Duqm International Airport","city":"Duqm","country":"OM","lat":19.501944,"lng":57.634167},{"code":"AUH","name":"Zayed International Airport","city":"Abu Dhabi","country":"AE","lat":24.440966,"lng":54.649237},{"code":"AAN","name":"Al Ain International Airport","city":"Al Ain","country":"AE","lat":24.2617,"lng":55.6092},{"code":"DXB","name":"Dubai International Airport","city":"Dubai","country":"AE","lat":25.24979,"lng":55.370992},{"code":"ZDY","name":"Delma Airport","city":"Delma Island","country":"AE","lat":24.51,"lng":52.3352},{"code":"DWC","name":"Al Maktoum International Airport","city":"Dubai(Jebel Ali)","country":"AE","lat":24.896171,"lng":55.16235},{"code":"FJR","name":"Fujairah International Airport","city":"Fujairah","country":"AE","lat":25.108411,"lng":56.328061},{"code":"RKT","name":"Ras Al Khaimah International Airport","city":"Ras Al Khaimah","country":"AE","lat":25.613501,"lng":55.938801},{"code":"SHJ","name":"Sharjah International Airport","city":"Sharjah","country":"AE","lat":25.3286,"lng":55.5172},{"code":"KHS","name":"Khasab Airport","city":"Khasab","country":"OM","lat":26.171,"lng":56.240601},{"code":"MCT","name":"Muscat International Airport","city":"Muscat/Seeb","country":"OM","lat":23.600218,"lng":58.285268},{"code":"SLL","name":"Salalah International Airport","city":"Salalah","country":"OM","lat":17.0387,"lng":54.091301},{"code":"OHS","name":"Suhar International Airport","city":"Suhar","country":"OM","lat":24.38604,"lng":56.62541},{"code":"BHV","name":"Bahawalpur Airport","city":"Bahawalpur","country":"PK","lat":29.348100662231445,"lng":71.71800231933594},{"code":"CJL","name":"Chitral Airport","city":"Chitral","country":"PK","lat":35.886213,"lng":71.799922},{"code":"DBA","name":"Dalbandin Airport","city":"Dalbandin","country":"PK","lat":28.878299713100002,"lng":64.3998031616},{"code":"DEA","name":"Dera Ghazi Khan Airport","city":"Dera Ghazi Khan","country":"PK","lat":29.961000442504883,"lng":70.48590087890625},{"code":"LYP","name":"Faisalabad International Airport","city":"Faisalabad","country":"PK","lat":31.364923,"lng":72.995319},{"code":"GWD","name":"New Gwadar International Airport","city":"Gurandani","country":"PK","lat":25.296733,"lng":62.498822},{"code":"GIL","name":"Gilgit Airport","city":"Gilgit","country":"PK","lat":35.918800354003906,"lng":74.33360290527344},{"code":"ISB","name":"Islamabad International Airport","city":"Attock","country":"PK","lat":33.549,"lng":72.82566},{"code":"KHI","name":"Jinnah International Airport","city":"Karachi","country":"PK","lat":24.9065,"lng":67.160797},{"code":"HDD","name":"Hyderabad Airport","city":"Hyderabad","country":"PK","lat":25.3181,"lng":68.366096},{"code":"KDD","name":"Khuzdar Airport","city":"Khuzdar","country":"PK","lat":27.790599823,"lng":66.6473007202},{"code":"LHE","name":"Allama Iqbal International Airport","city":"Lahore","country":"PK","lat":31.521601,"lng":74.403603},{"code":"MFG","name":"Muzaffarabad Airport","city":"Muzaffarabad","country":"PK","lat":34.338835,"lng":73.508886},{"code":"MUX","name":"Multan International Airport","city":"Multan","country":"PK","lat":30.203199,"lng":71.419098},{"code":"WNS","name":"Shaheed Benazirabad Airport","city":"Nawabashah","country":"PK","lat":26.2194,"lng":68.390099},{"code":"PEW","name":"Bacha Khan International Airport","city":"Peshawar","country":"PK","lat":33.9939,"lng":71.514603},{"code":"UET","name":"Quetta International Airport","city":"Quetta","country":"PK","lat":30.2514,"lng":66.937798},{"code":"RYK","name":"Shaikh Zaid Airport","city":"Rahim Yar Khan","country":"PK","lat":28.383899688720703,"lng":70.27960205078125},{"code":"KDU","name":"Skardu International Airport","city":"Skardu","country":"PK","lat":35.33866,"lng":75.538648},{"code":"SKZ","name":"Begum Nusrat Bhutto International Airport Sukkur","city":"Sukkur","country":"PK","lat":27.722,"lng":68.791702},{"code":"SKT","name":"Sialkot International Airport","city":"Sialkot","country":"PK","lat":32.535941,"lng":74.364623},{"code":"TUK","name":"Turbat International Airport","city":"Turbat","country":"PK","lat":25.984767,"lng":63.028856},{"code":"PZH","name":"Zhob Airport","city":"Fort Sandeman","country":"PK","lat":31.358400344848633,"lng":69.4636001586914},{"code":"BGW","name":"Baghdad International Airport / New Al Muthana Air Base","city":"Baghdad","country":"IQ","lat":33.262501,"lng":44.2346},{"code":"EBL","name":"Erbil International Airport","city":"Arbil","country":"IQ","lat":36.236047,"lng":43.946615},{"code":"ORI","name":"Port Lions Airport","city":"Port Lions","country":"US","lat":57.884905,"lng":-152.847719},{"code":"KIK","name":"Kirkuk International Airport","city":"Kirkuk","country":"IQ","lat":35.469501,"lng":44.3489},{"code":"BSR","name":"Basra International Airport","city":"Basra","country":"IQ","lat":30.549101,"lng":47.662102},{"code":"NJF","name":"Al Najaf International Airport","city":"Najaf","country":"IQ","lat":31.991116,"lng":44.405019},{"code":"ISU","name":"Jalal Talabani International Airport","city":"Sulaymaniyah","country":"IQ","lat":35.560487,"lng":45.315137},{"code":"ALP","name":"Aleppo International Airport","city":"Aleppo","country":"SY","lat":36.181262,"lng":37.226869},{"code":"DAM","name":"Damascus International Airport","city":"Damascus","country":"SY","lat":33.4114990234375,"lng":36.51559829711914},{"code":"KAC","name":"Qamishli International Airport","city":"Qamishli","country":"SY","lat":37.020599,"lng":41.191399},{"code":"LTK","name":"Latakia International Airport","city":"Latakia","country":"SY","lat":35.4011,"lng":35.9487},{"code":"DIA","name":"Doha International Airport","city":"Doha","country":"QA","lat":25.259431,"lng":51.565528},{"code":"DOH","name":"Hamad International Airport","city":"Doha","country":"QA","lat":25.273056,"lng":51.608056},{"code":"ADE","name":"Aden International Airport","city":"Aden","country":"YE","lat":12.829564,"lng":45.029976},{"code":"AAY","name":"Al Ghaydah International Airport","city":"Al Ghaydah","country":"YE","lat":16.193341,"lng":52.174186},{"code":"RIY","name":"Riyan International Airport","city":"Mukalla(Riyan)","country":"YE","lat":14.662241,"lng":49.375289},{"code":"SAH","name":"Sanaa International Airport","city":"Sanaa","country":"YE","lat":15.4763,"lng":44.2197},{"code":"SCT","name":"Socotra Airport","city":"Mori","country":"YE","lat":12.632093,"lng":53.906173},{"code":"GXF","name":"Seiyun Hadhramaut International Airport","city":"Seiyun","country":"YE","lat":15.965887,"lng":48.788146},{"code":"TAI","name":"Taiz International Airport","city":"Taiz","country":"YE","lat":13.686,"lng":44.139099},{"code":"BFQ","name":"Bahia Piña Airport","city":"Puerto Piña","country":"PA","lat":7.58737,"lng":-78.179939},{"code":"OTD","name":"Raul Arias Espinoza Airport","city":"Contadora Island","country":"PA","lat":8.62876,"lng":-79.034698},{"code":"AKB","name":"Atka Airport","city":"Atka","country":"US","lat":52.220299,"lng":-174.205994},{"code":"PTD","name":"Port Alexander Seaplane Base","city":"Port Alexander","country":"US","lat":56.246799,"lng":-134.647995},{"code":"BTI","name":"Barter Island Long Range Radar Station Airport","city":"Barter Island","country":"US","lat":70.134003,"lng":-143.582001},{"code":"BET","name":"Bethel Airport","city":"Bethel","country":"US","lat":60.77980042,"lng":-161.8379974},{"code":"BKC","name":"Buckland Airport","city":"Buckland","country":"US","lat":65.981598,"lng":-161.149002},{"code":"BRW","name":"Wiley Post Will Rogers Memorial Airport","city":"Utqiaġvik","country":"US","lat":71.285402,"lng":-156.766008},{"code":"BTT","name":"Bettles Airport","city":"Bettles","country":"US","lat":66.913902,"lng":-151.529007},{"code":"CDB","name":"Cold Bay Airport","city":"Cold Bay","country":"US","lat":55.207871,"lng":-162.725029},{"code":"CEM","name":"Central Airport","city":"Central","country":"US","lat":65.573815,"lng":-144.780707},{"code":"CIK","name":"Chalkyitsik Airport","city":"Chalkyitsik","country":"US","lat":66.644997,"lng":-143.740005},{"code":"CYF","name":"Chefornak Airport","city":"Chefornak","country":"US","lat":60.136667,"lng":-164.279167},{"code":"SCM","name":"Scammon Bay Airport","city":"Scammon Bay","country":"US","lat":61.84471,"lng":-165.575209},{"code":"IRC","name":"Circle City (New) Airport","city":"Circle","country":"US","lat":65.827673,"lng":-144.076195},{"code":"CDV","name":"Merle K (Mudhole) Smith Airport","city":"Cordova","country":"US","lat":60.491798,"lng":-145.477997},{"code":"CYT","name":"Yakataga Airport","city":"Yakataga","country":"US","lat":60.080974,"lng":-142.494541},{"code":"DRG","name":"Deering Airport","city":"Deering","country":"US","lat":66.068901,"lng":-162.766914},{"code":"ADK","name":"Adak Airport","city":"Adak","country":"US","lat":51.883564,"lng":-176.642783},{"code":"DLG","name":"Dillingham Airport","city":"Dillingham","country":"US","lat":59.04470062,"lng":-158.5050049},{"code":"MLL","name":"Marshall Don Hunter Sr Airport","city":"Marshall","country":"US","lat":61.8643,"lng":-162.026001},{"code":"ADQ","name":"Kodiak Airport","city":"Kodiak","country":"US","lat":57.75,"lng":-152.4940033},{"code":"DUT","name":"Tom Madsen (Dutch Harbor) Airport","city":"Unalaska","country":"US","lat":53.89881,"lng":-166.544996},{"code":"KKH","name":"Kongiganak Airport","city":"Kongiganak","country":"US","lat":59.9608,"lng":-162.880997},{"code":"EEK","name":"Eek Airport","city":"Eek","country":"US","lat":60.213173,"lng":-162.044048},{"code":"EAA","name":"Eagle Airport","city":"Eagle","country":"US","lat":64.778003,"lng":-141.150455},{"code":"ELV","name":"Elfin Cove Seaplane Base","city":"Elfin Cove","country":"US","lat":58.195201873799995,"lng":-136.347000122},{"code":"EMK","name":"Emmonak Airport","city":"Emmonak","country":"US","lat":62.78609848,"lng":-164.4909973},{"code":"ENA","name":"Kenai Municipal Airport","city":"Kenai","country":"US","lat":60.570946,"lng":-151.245174},{"code":"WWT","name":"Mertarvik Airport","city":"Mertarvik","country":"US","lat":60.81037,"lng":-164.499511},{"code":"FAI","name":"Fairbanks International Airport","city":"Fairbanks","country":"US","lat":64.815102,"lng":-147.856003},{"code":"ABL","name":"Ambler Airport","city":"Ambler","country":"US","lat":67.105529,"lng":-157.855339},{"code":"NIB","name":"Nikolai Airport","city":"Nikolai","country":"US","lat":63.017949,"lng":-154.360399},{"code":"GAL","name":"Edward G. Pitka Sr Airport","city":"Galena","country":"US","lat":64.736198,"lng":-156.936997},{"code":"KWK","name":"Kwigillingok Airport","city":"Kwigillingok","country":"US","lat":59.876499,"lng":-163.169005},{"code":"SHG","name":"Shungnak Airport","city":"Shungnak","country":"US","lat":66.88809967041,"lng":-157.16200256348},{"code":"GKN","name":"Gulkana Airport","city":"Gulkana","country":"US","lat":62.155859,"lng":-145.454662},{"code":"GLV","name":"Golovin Airport","city":"Golovin","country":"US","lat":64.550499,"lng":-163.007004},{"code":"GAM","name":"Gambell Airport","city":"Gambell","country":"US","lat":63.767694,"lng":-171.733303},{"code":"AGN","name":"Angoon Seaplane Base","city":"Angoon","country":"US","lat":57.497097,"lng":-134.56722},{"code":"GST","name":"Gustavus Airport","city":"Gustavus","country":"US","lat":58.4253006,"lng":-135.7070007},{"code":"NME","name":"Nightmute Airport","city":"Nightmute","country":"US","lat":60.469129,"lng":-164.70407},{"code":"SGY","name":"Skagway Airport","city":"Skagway","country":"US","lat":59.460343,"lng":-135.316651},{"code":"HCR","name":"Holy Cross Airport","city":"Holy Cross","country":"US","lat":62.18830108642578,"lng":-159.77499389648438},{"code":"HSL","name":"Huslia Airport","city":"Huslia","country":"US","lat":65.697899,"lng":-156.350998},{"code":"HNS","name":"Haines Airport","city":"Haines","country":"US","lat":59.243921,"lng":-135.523925},{"code":"HOM","name":"Homer Airport","city":"Homer","country":"US","lat":59.644456,"lng":-151.479235},{"code":"HPB","name":"Hooper Bay Airport","city":"Hooper Bay","country":"US","lat":61.523899,"lng":-166.147003},{"code":"HUS","name":"Hughes Airport","city":"Hughes","country":"US","lat":66.04109955,"lng":-154.2630005},{"code":"HYG","name":"Hydaburg Seaplane Base","city":"Hydaburg","country":"US","lat":55.205227,"lng":-132.830286},{"code":"IGG","name":"Igiugig Airport","city":"Igiugig","country":"US","lat":59.324001,"lng":-155.901993},{"code":"EGX","name":"Egegik Airport","city":"Egegik","country":"US","lat":58.184386,"lng":-157.374873},{"code":"IAN","name":"Bob Baker Memorial Airport","city":"Kiana","country":"US","lat":66.976128,"lng":-160.439014},{"code":"ILI","name":"Iliamna Airport","city":"Iliamna","country":"US","lat":59.754398,"lng":-154.910996},{"code":"UTO","name":"Indian Mountain LRRS Airport","city":"Utopia Creek","country":"US","lat":65.992798,"lng":-153.703995},{"code":"WAA","name":"Wales Airport","city":"Wales","country":"US","lat":65.622593,"lng":-168.095},{"code":"KCG","name":"Chignik Airport","city":"Chignik","country":"US","lat":56.311501,"lng":-158.373001},{"code":"JNU","name":"Juneau International Airport","city":"Juneau","country":"US","lat":58.354935,"lng":-134.574416},{"code":"KGK","name":"Koliganek Airport","city":"Koliganek","country":"US","lat":59.726600647,"lng":-157.259002686},{"code":"KFP","name":"False Pass Airport","city":"False Pass","country":"US","lat":54.84751,"lng":-163.407168},{"code":"AKK","name":"Akhiok Airport","city":"Akhiok","country":"US","lat":56.938702,"lng":-154.182999},{"code":"KPN","name":"Kipnuk Airport","city":"Kipnuk","country":"US","lat":59.931796,"lng":-164.028196},{"code":"KKA","name":"Koyuk Alfred Adams Airport","city":"Koyuk","country":"US","lat":64.939499,"lng":-161.154007},{"code":"AKN","name":"King Salmon Airport","city":"King Salmon","country":"US","lat":58.677845,"lng":-156.651965},{"code":"IKO","name":"Nikolski Air Station","city":"Nikolski","country":"US","lat":52.941552,"lng":-168.849478},{"code":"AKP","name":"Anaktuvuk Pass Airport","city":"Anaktuvuk Pass","country":"US","lat":68.133598,"lng":-151.742996},{"code":"KTN","name":"Ketchikan International Airport","city":"Ketchikan","country":"US","lat":55.35559845,"lng":-131.7140045},{"code":"KAL","name":"Kaltag Airport","city":"Kaltag","country":"US","lat":64.31864,"lng":-158.74197},{"code":"KLW","name":"Klawock Airport","city":"Klawock","country":"US","lat":55.579201,"lng":-133.076004},{"code":"KYK","name":"Karluk Airport","city":"Karluk","country":"US","lat":57.565919,"lng":-154.453762},{"code":"KLN","name":"Larsen Bay Airport","city":"Larsen Bay","country":"US","lat":57.53516,"lng":-153.976455},{"code":"KLG","name":"Kalskag Airport","city":"Kalskag","country":"US","lat":61.53630065917969,"lng":-160.34100341796875},{"code":"PTA","name":"Port Alsworth Airport","city":"Port Alsworth","country":"US","lat":60.201681,"lng":-154.325863},{"code":"LUR","name":"Cape Lisburne LRRS Airport","city":"Cape Lisburne","country":"US","lat":68.875099,"lng":-166.110001},{"code":"KMO","name":"Manokotak Airport","city":"Manokotak","country":"US","lat":58.932056,"lng":-158.901886},{"code":"MCG","name":"McGrath Airport","city":"McGrath","country":"US","lat":62.95289993,"lng":-155.6060028},{"code":"LMA","name":"Minchumina Airport","city":"Minchumina","country":"US","lat":63.886002,"lng":-152.302002},{"code":"SMK","name":"St Michael Airport","city":"St Michael","country":"US","lat":63.49010086,"lng":-162.1100006},{"code":"MLY","name":"Manley Hot Springs Airport","city":"Manley Hot Springs","country":"US","lat":64.99188,"lng":-150.644102},{"code":"MTM","name":"Metlakatla Seaplane Base","city":"Metlakatla","country":"US","lat":55.131001,"lng":-131.578003},{"code":"MOU","name":"Mountain Village Airport","city":"Mountain Village","country":"US","lat":62.095401763916016,"lng":-163.6820068359375},{"code":"MRI","name":"Merrill Field","city":"Anchorage","country":"US","lat":61.212792,"lng":-149.843988},{"code":"MYU","name":"Mekoryuk Airport","city":"Mekoryuk","country":"US","lat":60.372338,"lng":-166.269751},{"code":"WNA","name":"Napakiak Airport","city":"Napakiak","country":"US","lat":60.6903,"lng":-161.979004},{"code":"ANC","name":"Ted Stevens Anchorage International Airport","city":"Anchorage","country":"US","lat":61.179004,"lng":-149.992561},{"code":"ANI","name":"Aniak Airport","city":"Aniak","country":"US","lat":61.581600189208984,"lng":-159.54299926757812},{"code":"FNR","name":"Funter Bay Seaplane Base","city":"Funter Bay","country":"US","lat":58.2543983459,"lng":-134.897994995},{"code":"NUL","name":"Nulato Airport","city":"Nulato","country":"US","lat":64.729301,"lng":-158.074005},{"code":"ANV","name":"Anvik Airport","city":"Anvik","country":"US","lat":62.646702,"lng":-160.190994},{"code":"KNW","name":"New Stuyahok Airport","city":"New Stuyahok","country":"US","lat":59.451838,"lng":-157.373829},{"code":"OBU","name":"Kobuk Airport","city":"Kobuk","country":"US","lat":66.9123,"lng":-156.897003},{"code":"HNH","name":"Hoonah Airport","city":"Hoonah","country":"US","lat":58.0961,"lng":-135.410111},{"code":"OME","name":"Nome Airport","city":"Nome","country":"US","lat":64.51219940185547,"lng":-165.44500732421875},{"code":"OOK","name":"Toksook Bay Airport","city":"Toksook Bay","country":"US","lat":60.541401,"lng":-165.087006},{"code":"ORT","name":"Northway Airport","city":"Northway","country":"US","lat":62.9613,"lng":-141.929001},{"code":"OTZ","name":"Ralph Wien Memorial Airport","city":"Kotzebue","country":"US","lat":66.88469696,"lng":-162.598999},{"code":"NLG","name":"Nelson Lagoon Airport","city":"Nelson Lagoon","country":"US","lat":56.0075,"lng":-161.160004},{"code":"STG","name":"St George Airport","city":"St George","country":"US","lat":56.577345,"lng":-169.663823},{"code":"KPV","name":"Perryville Airport","city":"Perryville","country":"US","lat":55.9065,"lng":-159.16091},{"code":"PSG","name":"Petersburg James A Johnson Airport","city":"Petersburg","country":"US","lat":56.80170059,"lng":-132.9450073},{"code":"PTH","name":"Port Heiden Airport","city":"Port Heiden","country":"US","lat":56.957893,"lng":-158.630219},{"code":"PKA","name":"Napaskiak Airport","city":"Napaskiak","country":"US","lat":60.7029,"lng":-161.778},{"code":"PTU","name":"Platinum Airport","city":"Platinum","country":"US","lat":59.017675,"lng":-161.827927},{"code":"PIP","name":"Pilot Point Airport","city":"Pilot Point","country":"US","lat":57.580399,"lng":-157.572006},{"code":"PHO","name":"Point Hope Airport","city":"Point Hope","country":"US","lat":68.348801,"lng":-166.798996},{"code":"KWN","name":"Quinhagak Airport","city":"Quinhagak","country":"US","lat":59.7551,"lng":-161.845},{"code":"NUI","name":"Nuiqsut Airport","city":"Nuiqsut","country":"US","lat":70.209999,"lng":-151.005998},{"code":"ARC","name":"Arctic Village Airport","city":"Arctic Village","country":"US","lat":68.1147,"lng":-145.578995},{"code":"RSH","name":"Russian Mission Airport","city":"Russian Mission","country":"US","lat":61.775071,"lng":-161.319466},{"code":"RBY","name":"Ruby Airport","city":"Ruby","country":"US","lat":64.727203,"lng":-155.470001},{"code":"SVA","name":"Savoonga Airport","city":"Savoonga","country":"US","lat":63.686401,"lng":-170.492996},{"code":"SCC","name":"Deadhorse Airport","city":"Deadhorse","country":"US","lat":70.19470215,"lng":-148.4649963},{"code":"SDP","name":"Sand Point Airport","city":"Sand Point","country":"US","lat":55.313908,"lng":-160.522099},{"code":"SHH","name":"Shishmaref Airport","city":"Shishmaref","country":"US","lat":66.249604,"lng":-166.089112},{"code":"SIT","name":"Sitka Rocky Gutierrez Airport","city":"Sitka","country":"US","lat":57.04710006713867,"lng":-135.36199951171875},{"code":"WLK","name":"Selawik Airport","city":"Selawik","country":"US","lat":66.600098,"lng":-159.985992},{"code":"SLQ","name":"Sleetmute Airport","city":"Sleetmute","country":"US","lat":61.7005,"lng":-157.166},{"code":"KSM","name":"St Mary's Airport","city":"St Mary's","country":"US","lat":62.060501,"lng":-163.302002},{"code":"SNP","name":"St Paul Island Airport","city":"St Paul Island","country":"US","lat":57.166311,"lng":-170.222555},{"code":"SOV","name":"Seldovia Airport","city":"Seldovia","country":"US","lat":59.442981,"lng":-151.705227},{"code":"TAL","name":"Ralph M Calhoun Memorial Airport","city":"Tanana","country":"US","lat":65.1744,"lng":-152.108994},{"code":"TNC","name":"Tin City Long Range Radar Station Airport","city":"Tin City","country":"US","lat":65.563103,"lng":-167.921997},{"code":"TLA","name":"Teller Airport","city":"Teller","country":"US","lat":65.240402,"lng":-166.339005},{"code":"TOG","name":"Togiak Airport","city":"Togiak Village","country":"US","lat":59.052799,"lng":-160.397003},{"code":"ATK","name":"Atqasuk Edward Burnell Sr Memorial Airport","city":"Atqasuk","country":"US","lat":70.46704,"lng":-157.436013},{"code":"AUK","name":"Alakanuk Airport","city":"Alakanuk","country":"US","lat":62.682653,"lng":-164.722481},{"code":"UNK","name":"Unalakleet Airport","city":"Unalakleet","country":"US","lat":63.888401,"lng":-160.798996},{"code":"KQA","name":"Akutan Airport","city":"Akutan","country":"US","lat":54.14459,"lng":-165.604332},{"code":"VAK","name":"Chevak Airport","city":"Chevak","country":"US","lat":61.5409,"lng":-165.6005},{"code":"KVC","name":"King Cove Airport","city":"King Cove","country":"US","lat":55.116299,"lng":-162.266006},{"code":"VDZ","name":"Valdez Pioneer Field","city":"Valdez","country":"US","lat":61.132712,"lng":-146.246567},{"code":"VEE","name":"Venetie Airport","city":"Venetie","country":"US","lat":67.008698,"lng":-146.365997},{"code":"KVL","name":"Kivalina Airport","city":"Kivalina","country":"US","lat":67.734565,"lng":-164.559917},{"code":"WBQ","name":"Beaver Airport","city":"Beaver","country":"US","lat":66.362198,"lng":-147.406998},{"code":"WRG","name":"Wrangell Airport","city":"Wrangell","country":"US","lat":56.48429871,"lng":-132.3699951},{"code":"AIN","name":"Wainwright Airport","city":"Wainwright","country":"US","lat":70.638,"lng":-159.994995},{"code":"WMO","name":"White Mountain Airport","city":"White Mountain","country":"US","lat":64.689201,"lng":-163.412994},{"code":"WTK","name":"Noatak Airport","city":"Noatak","country":"US","lat":67.561241,"lng":-162.980461},{"code":"YAK","name":"Yakutat Airport","city":"Yakutat","country":"US","lat":59.508717,"lng":-139.660435},{"code":"PEC","name":"Pelican Seaplane Base","city":"Pelican","country":"US","lat":57.9552,"lng":-136.235992},{"code":"AKI","name":"Akiak Airport","city":"Akiak","country":"US","lat":60.902646,"lng":-161.23106},{"code":"AET","name":"Allakaket Airport","city":"Allakaket","country":"US","lat":66.551804,"lng":-152.621994},{"code":"NCN","name":"Chenega Bay Airport","city":"Chenega","country":"US","lat":60.077602,"lng":-147.99468},{"code":"CLP","name":"Clarks Point Airport","city":"Clarks Point","country":"US","lat":58.833698,"lng":-158.529007},{"code":"ELI","name":"Elim Airport","city":"Elim","country":"US","lat":64.6147,"lng":-162.272003},{"code":"KUK","name":"Kasigluk Airport","city":"Kasigluk","country":"US","lat":60.872747,"lng":-162.5247},{"code":"KNK","name":"Kokhanok Airport","city":"Kokhanok","country":"US","lat":59.433248,"lng":-154.80217},{"code":"KOT","name":"Kotlik Airport","city":"Kotlik","country":"US","lat":63.030602,"lng":-163.533005},{"code":"KTS","name":"Brevig Mission Airport","city":"Brevig Mission","country":"US","lat":65.331826,"lng":-166.467848},{"code":"KYU","name":"Koyukuk Airport","city":"Koyukuk","country":"US","lat":64.876099,"lng":-157.727005},{"code":"KWT","name":"Kwethluk Airport","city":"Kwethluk","country":"US","lat":60.790298,"lng":-161.444},{"code":"ORV","name":"Robert (Bob) Curtis Memorial Airport","city":"Noorvik","country":"US","lat":66.817902,"lng":-161.018997},{"code":"SKK","name":"Shaktoolik Airport","city":"Shaktoolik","country":"US","lat":64.371101,"lng":-161.223999},{"code":"TKJ","name":"Tok Junction Airport","city":"Tok","country":"US","lat":63.32949829,"lng":-142.9539948},{"code":"WSN","name":"South Naknek Number 2 Airport","city":"South Naknek","country":"US","lat":58.702362,"lng":-157.005444},{"code":"FYU","name":"Fort Yukon Airport","city":"Fort Yukon","country":"US","lat":66.571651,"lng":-145.249987},{"code":"LMY","name":"Lake Murray Airport","city":"Lake Murray","country":"PG","lat":-7.00992,"lng":141.494003},{"code":"PGM","name":"Port Graham Airport","city":"Port Graham","country":"US","lat":59.348355,"lng":-151.830025},{"code":"ROP","name":"Rota International Airport","city":"Rota Island","country":"MP","lat":14.173267,"lng":145.241103},{"code":"SPN","name":"Saipan International Airport","city":"I Fadang, Saipan","country":"MP","lat":15.119384,"lng":145.728848},{"code":"GUM","name":"Antonio B. Won Pat International Airport","city":"Hagåtña","country":"GU","lat":13.485,"lng":144.797282},{"code":"TIQ","name":"Francisco Manglona Borja / Tinian International Airport","city":"Tinian Island","country":"MP","lat":14.999207,"lng":145.619355},{"code":"HNM","name":"Hana Airport","city":"Hana","country":"US","lat":20.795601,"lng":-156.014008},{"code":"JHM","name":"Kapalua Airport","city":"Lahaina","country":"US","lat":20.9629,"lng":-156.673004},{"code":"KOA","name":"Ellison Onizuka Kona International Airport at Keāhole","city":"Kailua-Kona","country":"US","lat":19.738783,"lng":-156.045603},{"code":"LIH","name":"Lihue Airport","city":"Lihue, Kauai","country":"US","lat":21.974393,"lng":-159.337146},{"code":"LUP","name":"Kalaupapa Airport","city":"Kalaupapa","country":"US","lat":21.211,"lng":-156.973999},{"code":"MKK","name":"Molokai Airport","city":"Kaunakakai","country":"US","lat":21.152901,"lng":-157.095993},{"code":"MUE","name":"Waimea Kohala Airport","city":"Waimea (Kamuela)","country":"US","lat":20.001301,"lng":-155.667999},{"code":"HNL","name":"Daniel K. Inouye International Airport","city":"Honolulu, Oahu","country":"US","lat":21.318387,"lng":-157.92567},{"code":"LNY","name":"Lanai Airport","city":"Lanai City","country":"US","lat":20.785675,"lng":-156.951324},{"code":"OGG","name":"Kahului International Airport","city":"Kahului","country":"US","lat":20.896263,"lng":-156.431837},{"code":"ITO","name":"Hilo International Airport","city":"Hilo","country":"US","lat":19.72135,"lng":-155.045428},{"code":"ENT","name":"Eniwetok Airport","city":"Eniwetok Atoll","country":"MH","lat":11.340700149536133,"lng":162.3280029296875},{"code":"MAJ","name":"Marshall Islands International Airport","city":"Majuro Atoll","country":"MH","lat":7.06511,"lng":171.271656},{"code":"KWA","name":"Bucholz Army Air Field","city":"Kwajalein","country":"MH","lat":8.720120429992676,"lng":167.73199462890625},{"code":"CXI","name":"Cassidy International Airport","city":"Kiritimati","country":"KI","lat":1.986278,"lng":-157.350011},{"code":"PIZ","name":"Point Lay LRRS Airport","city":"Point Lay","country":"US","lat":69.732903,"lng":-163.005005},{"code":"TKK","name":"Chuuk International Airport","city":"Weno Island","country":"FM","lat":7.46187,"lng":151.843002},{"code":"PNI","name":"Pohnpei International Airport","city":"Pohnpei Island","country":"FM","lat":6.985082,"lng":158.20992},{"code":"ROR","name":"Roman Tmetuchl International Airport","city":"Babelthuap Island","country":"PW","lat":7.366965,"lng":134.544085},{"code":"KSA","name":"Kosrae International Airport","city":"Okat","country":"FM","lat":5.35698,"lng":162.957993},{"code":"YAP","name":"Yap International Airport","city":"Yap Island","country":"FM","lat":9.49891,"lng":138.082993},{"code":"AWK","name":"Wake Island Airfield","city":"Wake Island","country":"UM","lat":19.282437,"lng":166.636637},{"code":"KIO","name":"Kili Airport","city":"Kili Island","country":"MH","lat":5.64386,"lng":169.124586},{"code":"QFX","name":"Igaliku Heliport","city":"Igaliku","country":"GL","lat":60.99229,"lng":-45.431428},{"code":"KNH","name":"Kinmen Airport","city":"Shang-I","country":"TW","lat":24.4279,"lng":118.359001},{"code":"LZN","name":"Matsu Nangan Airport","city":"Matsu (Nangan)","country":"TW","lat":26.159656,"lng":119.958376},{"code":"TTT","name":"Taitung Airport","city":"Taitung City","country":"TW","lat":22.754856,"lng":121.101794},{"code":"KHH","name":"Kaohsiung International Airport","city":"Kaohsiung (Xiaogang)","country":"TW","lat":22.577101,"lng":120.349998},{"code":"CYI","name":"Chiayi Airport","city":"Shuishang","country":"TW","lat":23.462577,"lng":120.390544},{"code":"KYD","name":"Lanyu Airport","city":"Orchid Island","country":"TW","lat":22.027000427246094,"lng":121.53500366210938},{"code":"RMQ","name":"Taichung International Airport / Ching Chuang Kang Air Base","city":"Taichung (Qingshui)","country":"TW","lat":24.2647,"lng":120.621002},{"code":"MFK","name":"Matsu Beigan Airport","city":"Matsu (Beigan)","country":"TW","lat":26.22414,"lng":120.002679},{"code":"TNN","name":"Tainan International Airport / Tainan Air Base","city":"Tainan (Rende)","country":"TW","lat":22.950399,"lng":120.206001},{"code":"MZG","name":"Penghu Magong Airport","city":"Huxi","country":"TW","lat":23.568701,"lng":119.627998},{"code":"TSA","name":"Taipei Songshan International Airport","city":"Taipei (Songshan)","country":"TW","lat":25.067244,"lng":121.552822},{"code":"TPE","name":"Taiwan Taoyuan International Airport","city":"Taoyuan","country":"TW","lat":25.0777,"lng":121.233002},{"code":"HUN","name":"Hualien Chiashan Airport","city":"Hualien City","country":"TW","lat":24.023163,"lng":121.617991},{"code":"RDV","name":"Red Devil Airport","city":"Red Devil","country":"US","lat":61.788101,"lng":-157.350006},{"code":"RHT","name":"Alxa Right Banner Badanjilin Airport","city":"Badanjilin","country":"CN","lat":39.225,"lng":101.546},{"code":"NRT","name":"Narita International Airport","city":"Narita","country":"JP","lat":35.76858,"lng":140.388714},{"code":"MMJ","name":"Shinshu-Matsumoto Airport","city":"Matsumoto","country":"JP","lat":36.166801,"lng":137.923004},{"code":"IBR","name":"Ibaraki Airport","city":"Omitama","country":"JP","lat":36.181456,"lng":140.414434},{"code":"KIX","name":"Kansai International Airport","city":"Osaka","country":"JP","lat":34.427299,"lng":135.244003},{"code":"SHM","name":"Nanki Shirahama Airport","city":"Shirahama","country":"JP","lat":33.6622009277,"lng":135.363998413},{"code":"UKB","name":"Kobe Airport","city":"Kobe","country":"JP","lat":34.632801,"lng":135.223999},{"code":"TJH","name":"Konotori Tajima Airport","city":"Toyooka","country":"JP","lat":35.512798,"lng":134.787003},{"code":"OBO","name":"Tokachi-Obihiro Airport","city":"Obihiro","country":"JP","lat":42.7332992554,"lng":143.216995239},{"code":"CTS","name":"New Chitose Airport","city":"Sapporo","country":"JP","lat":42.774753,"lng":141.690414},{"code":"HKD","name":"Hakodate Airport","city":"Hakodate","country":"JP","lat":41.77,"lng":140.822006},{"code":"KUH","name":"Kushiro Airport","city":"Kushiro","country":"JP","lat":43.041,"lng":144.192993},{"code":"MMB","name":"Memanbetsu Airport","city":"Ōzora","country":"JP","lat":43.8805999756,"lng":144.164001465},{"code":"SHB","name":"Nakashibetsu Airport","city":"Nakashibetsu","country":"JP","lat":43.5774993896,"lng":144.960006714},{"code":"OKD","name":"Sapporo Okadama Airport","city":"Sapporo","country":"JP","lat":43.117447,"lng":141.38134},{"code":"WKJ","name":"Wakkanai Airport","city":"Wakkanai","country":"JP","lat":45.4042015076,"lng":141.800994873},{"code":"AXJ","name":"Amakusa Airport","city":"Amakusa","country":"JP","lat":32.482498,"lng":130.158997},{"code":"IKI","name":"Iki Airport","city":"Iki","country":"JP","lat":33.7490005493,"lng":129.785003662},{"code":"UBJ","name":"Yamaguchi Ube Airport","city":"Ube","country":"JP","lat":33.93,"lng":131.279007},{"code":"TSJ","name":"Tsushima Airport","city":"Tsushima","country":"JP","lat":34.2849006653,"lng":129.330993652},{"code":"MBE","name":"Monbetsu Airport","city":"Monbetsu","country":"JP","lat":44.303901672399995,"lng":143.404006958},{"code":"AKJ","name":"Asahikawa Airport","city":"Higashikagura","country":"JP","lat":43.670799,"lng":142.447006},{"code":"OIR","name":"Okushiri Airport","city":"Okushiri Island","country":"JP","lat":42.0717010498,"lng":139.432998657},{"code":"RIS","name":"Rishiri Airport","city":"Rishiri","country":"JP","lat":45.2420005798,"lng":141.186004639},{"code":"KUM","name":"Yakushima Airport","city":"Yakushima","country":"JP","lat":30.385599,"lng":130.658997},{"code":"FUJ","name":"Fukue Airport","city":"Goto","country":"JP","lat":32.666302,"lng":128.832993},{"code":"FUK","name":"Fukuoka Airport","city":"Fukuoka","country":"JP","lat":33.585899353027344,"lng":130.4510040283203},{"code":"TNE","name":"New Tanegashima Airport","city":"Tanegashima","country":"JP","lat":30.605100631699997,"lng":130.990997314},{"code":"KOJ","name":"Kagoshima Airport","city":"Kagoshima","country":"JP","lat":31.8034,"lng":130.718994},{"code":"KMI","name":"Miyazaki Airport","city":"Miyazaki","country":"JP","lat":31.877199,"lng":131.449005},{"code":"OIT","name":"Oita Airport","city":"Oita","country":"JP","lat":33.479400634799994,"lng":131.736999512},{"code":"KKJ","name":"Kitakyushu Airport","city":"Kitakyushu","country":"JP","lat":33.845901,"lng":131.035004},{"code":"HSG","name":"Kyushu Saga International Airport","city":"Saga","country":"JP","lat":33.1497,"lng":130.302002},{"code":"KMJ","name":"Kumamoto Airport","city":"Kumamoto","country":"JP","lat":32.837299,"lng":130.854996},{"code":"NGS","name":"Nagasaki Airport","city":"Nagasaki","country":"JP","lat":32.916901,"lng":129.914001},{"code":"NGO","name":"Chubu Centrair International Airport","city":"Tokoname","country":"JP","lat":34.858398,"lng":136.804993},{"code":"ASJ","name":"Amami Airport","city":"Amami","country":"JP","lat":28.430599212646484,"lng":129.71299743652344},{"code":"OKE","name":"Okinoerabu Airport","city":"Wadomari","country":"JP","lat":27.431604,"lng":128.705564},{"code":"KKX","name":"Kikai Airport","city":"Kikai","country":"JP","lat":28.321300506599997,"lng":129.927993774},{"code":"TKN","name":"Tokunoshima Airport","city":"Amagi","country":"JP","lat":27.836382,"lng":128.881356},{"code":"NKM","name":"Nagoya Airport / JASDF Komaki Air Base","city":"Nagoya","country":"JP","lat":35.255759,"lng":136.924095},{"code":"KMQ","name":"Komatsu Airport / JASDF Komatsu Air Base","city":"Kanazawa","country":"JP","lat":36.393407,"lng":136.406895},{"code":"OKI","name":"Oki Global Geopark Airport","city":"Okinoshima","country":"JP","lat":36.178388,"lng":133.323566},{"code":"FSZ","name":"Mount Fuji Shizuoka Airport","city":"Makinohara / Shimada","country":"JP","lat":34.795022,"lng":138.190976},{"code":"TOY","name":"Toyama Kitokito Airport","city":"Toyama","country":"JP","lat":36.648416,"lng":137.187359},{"code":"NTQ","name":"Noto Satoyama Airport","city":"Wajima","country":"JP","lat":37.293098,"lng":136.962006},{"code":"HIJ","name":"Hiroshima Airport","city":"Hiroshima","country":"JP","lat":34.4361,"lng":132.919006},{"code":"OKJ","name":"Okayama Momotaro Airport","city":"Okayama","country":"JP","lat":34.756901,"lng":133.854996},{"code":"IZO","name":"Izumo Enmusubi Airport","city":"Izumo","country":"JP","lat":35.413601,"lng":132.889999},{"code":"YGJ","name":"Yonago Kitaro Airport / JASDF Miho Air Base","city":"Yonago","country":"JP","lat":35.492199,"lng":133.235992},{"code":"IWK","name":"Iwakuni Kintaikyo Airport","city":"Iwakuni","country":"JP","lat":34.146333,"lng":132.247238},{"code":"KCZ","name":"Kochi Ryoma Airport","city":"Nankoku","country":"JP","lat":33.545217,"lng":133.670166},{"code":"MYJ","name":"Matsuyama Airport","city":"Matsuyama","country":"JP","lat":33.826893,"lng":132.700114},{"code":"ITM","name":"Osaka Itami International Airport","city":"Osaka","country":"JP","lat":34.780853,"lng":135.440829},{"code":"TTJ","name":"Tottori Sand Dunes Conan Airport","city":"Tottori","country":"JP","lat":35.530058,"lng":134.165023},{"code":"TKS","name":"Tokushima Awaodori Airport / JMSDF Tokushima Air Base","city":"Tokushima","country":"JP","lat":34.132559,"lng":134.607816},{"code":"TAK","name":"Takamatsu Airport","city":"Takamatsu","country":"JP","lat":34.214963,"lng":134.015454},{"code":"IWJ","name":"Iwami Airport","city":"Masuda","country":"JP","lat":34.676399231,"lng":131.789993286},{"code":"AOJ","name":"Aomori Airport","city":"Aomori","country":"JP","lat":40.733777,"lng":140.689477},{"code":"GAJ","name":"Yamagata Airport","city":"Higashine","country":"JP","lat":38.4119,"lng":140.371002},{"code":"SDS","name":"Sado Airport","city":"Sado","country":"JP","lat":38.0601997375,"lng":138.414001465},{"code":"FKS","name":"Fukushima Airport","city":"Sukagawa","country":"JP","lat":37.22740173339844,"lng":140.43099975585938},{"code":"HNA","name":"Iwate Hanamaki Airport","city":"Hanamaki","country":"JP","lat":39.4286,"lng":141.134995},{"code":"AXT","name":"Akita Airport","city":"Akita","country":"JP","lat":39.615601,"lng":140.218994},{"code":"MSJ","name":"Misawa Airport / Misawa Air Base","city":"Misawa","country":"JP","lat":40.703201,"lng":141.367996},{"code":"KIJ","name":"Niigata Airport","city":"Niigata","country":"JP","lat":37.954166,"lng":139.112189},{"code":"ONJ","name":"Odate Noshiro Airport","city":"Kitaakita","country":"JP","lat":40.191898,"lng":140.371002},{"code":"SDJ","name":"Sendai Airport","city":"Natori","country":"JP","lat":38.139702,"lng":140.917007},{"code":"SYO","name":"Shonai Airport","city":"Shonai","country":"JP","lat":38.812199,"lng":139.787003},{"code":"HAC","name":"Hachijojima Airport","city":"Hachijojima","country":"JP","lat":33.114843,"lng":139.785645},{"code":"OIM","name":"Oshima Airport","city":"Izu Oshima","country":"JP","lat":34.782001495399996,"lng":139.36000061},{"code":"MYE","name":"Miyakejima Airport","city":"Miyakejima","country":"JP","lat":34.073600769,"lng":139.559997559},{"code":"HND","name":"Tokyo Haneda International Airport","city":"Tokyo","country":"JP","lat":35.549678,"lng":139.786958},{"code":"MWX","name":"Muan International Airport","city":"Muan (Piseo-ri)","country":"KR","lat":34.991406,"lng":126.382814},{"code":"KWJ","name":"Gwangju Airport","city":"Gwangju","country":"KR","lat":35.123173,"lng":126.805444},{"code":"KUV","name":"Gunsan Airport / Gunsan Air Base","city":"Gunsan","country":"KR","lat":35.903801,"lng":126.615997},{"code":"RSU","name":"Yeosu Airport","city":"Yeosu","country":"KR","lat":34.84230041503906,"lng":127.61699676513672},{"code":"WJU","name":"Wonju Airport / Hoengseong Air Base (K-38/K-46)","city":"Wonju","country":"KR","lat":37.437113,"lng":127.960051},{"code":"YNY","name":"Yangyang International Airport","city":"Gonghang-ro","country":"KR","lat":38.060481,"lng":128.669822},{"code":"CJU","name":"Jeju International Airport","city":"Jeju City","country":"KR","lat":33.512058,"lng":126.492548},{"code":"PUS","name":"Gimhae International Airport","city":"Busan","country":"KR","lat":35.179501,"lng":128.938004},{"code":"HIN","name":"Sacheon Airport / Sacheon Air Base","city":"Sacheon","country":"KR","lat":35.088591,"lng":128.071747},{"code":"USN","name":"Ulsan Airport","city":"Ulsan","country":"KR","lat":35.59349823,"lng":129.352005005},{"code":"ICN","name":"Incheon International Airport","city":"Seoul","country":"KR","lat":37.469101,"lng":126.450996},{"code":"GMP","name":"Seoul Gimpo International Airport","city":"Seoul","country":"KR","lat":37.5583,"lng":126.791},{"code":"KPO","name":"Pohang Airport (G-815/K-3)","city":"Pohang","country":"KR","lat":35.987955,"lng":129.420383},{"code":"TAE","name":"Daegu International Airport","city":"Daegu","country":"KR","lat":35.894394,"lng":128.656989},{"code":"CJJ","name":"Cheongju International Airport/Cheongju Air Base (K-59/G-513)","city":"Cheongju","country":"KR","lat":36.71556,"lng":127.500289},{"code":"RMP","name":"Rampart Airport","city":"Rampart","country":"US","lat":65.507896,"lng":-150.141007},{"code":"OKA","name":"Naha International Airport","city":"Naha","country":"JP","lat":26.192437,"lng":127.639804},{"code":"ISG","name":"New Ishigaki Airport","city":"Ishigaki","country":"JP","lat":24.396389,"lng":124.245},{"code":"UEO","name":"Kumejima Airport","city":"Kumejima","country":"JP","lat":26.363445,"lng":126.71384},{"code":"MMD","name":"Minamidaito Airport","city":"Minamidaito","country":"JP","lat":25.8465,"lng":131.263},{"code":"MMY","name":"Miyako Airport","city":"Miyakojima","country":"JP","lat":24.782801,"lng":125.294998},{"code":"AGJ","name":"Aguni Airport","city":"Aguni","country":"JP","lat":26.592755,"lng":127.240335},{"code":"KTD","name":"Kitadaito Airport","city":"Kitadaitōjima","country":"JP","lat":25.9447002411,"lng":131.32699585},{"code":"SHI","name":"Shimojishima Airport","city":"Miyakojima","country":"JP","lat":24.8267,"lng":125.144997},{"code":"TRA","name":"Tarama Airport","city":"Tarama","country":"JP","lat":24.653793,"lng":124.675453},{"code":"RNJ","name":"Yoron Airport","city":"Yoron","country":"JP","lat":27.043847,"lng":128.401648},{"code":"OGN","name":"Yonaguni Airport","city":"Yonaguni","country":"JP","lat":24.467298,"lng":122.979827},{"code":"ENI","name":"El Nido Airport","city":"El Nido","country":"PH","lat":11.202399,"lng":119.416087},{"code":"SFS","name":"Subic Bay International Airport / Naval Air Station Cubi Point","city":"Olongapo","country":"PH","lat":14.794833,"lng":120.271883},{"code":"CRK","name":"Clark International Airport / Clark Air Base","city":"Mabalacat","country":"PH","lat":15.186,"lng":120.559998},{"code":"LAO","name":"Laoag International Airport","city":"Laoag City","country":"PH","lat":18.175089,"lng":120.531006},{"code":"DRP","name":"Bicol International Airport","city":"Legazpi","country":"PH","lat":13.111915,"lng":123.676829},{"code":"MNL","name":"Ninoy Aquino International Airport","city":"Manila (Pasay)","country":"PH","lat":14.5086,"lng":121.019997},{"code":"CYU","name":"Cuyo Airport","city":"Cuyo","country":"PH","lat":10.8581,"lng":121.069},{"code":"CBO","name":"Cotabato (Awang) Airport","city":"Datu Odin Sinsuat","country":"PH","lat":7.164753,"lng":124.209938},{"code":"DVO","name":"Francisco Bangoy International Airport","city":"Davao","country":"PH","lat":7.12552,"lng":125.646004},{"code":"BXU","name":"Bancasi Airport","city":"Butuan","country":"PH","lat":8.9515,"lng":125.4788},{"code":"DPL","name":"Dipolog Airport","city":"Dipolog","country":"PH","lat":8.601983,"lng":123.341875},{"code":"CGM","name":"Camiguin Airport","city":"Mambajao","country":"PH","lat":9.253894,"lng":124.709115},{"code":"JOL","name":"Jolo Airport","city":"Jolo","country":"PH","lat":6.05367,"lng":121.011002},{"code":"TWT","name":"Sanga Sanga Airport","city":"Bongao","country":"PH","lat":5.048196,"lng":119.743338},{"code":"OZC","name":"Labo Airport","city":"Ozamiz","country":"PH","lat":8.17851,"lng":123.842003},{"code":"PAG","name":"Pagadian Airport","city":"Pagadian","country":"PH","lat":7.825632,"lng":123.459635},{"code":"GES","name":"General Santos International Airport","city":"General Santos","country":"PH","lat":6.057208,"lng":125.096243},{"code":"SUG","name":"Surigao Airport","city":"Surigao City","country":"PH","lat":9.755838325629998,"lng":125.480947495},{"code":"CGY","name":"Laguindingan International Airport","city":"Laguindingan","country":"PH","lat":8.612203,"lng":124.456496},{"code":"ZAM","name":"Zamboanga International Airport","city":"Zamboanga","country":"PH","lat":6.92242,"lng":122.059998},{"code":"IAO","name":"Siargao Airport","city":"Del Carmen","country":"PH","lat":9.8591,"lng":126.014},{"code":"TAG","name":"Bohol-Panglao International Airport","city":"Panglao","country":"PH","lat":9.573045,"lng":123.770143},{"code":"SWL","name":"San Vicente Airport","city":"San Vicente","country":"PH","lat":10.524298,"lng":119.272899},{"code":"SJI","name":"San Jose Airport","city":"San Jose","country":"PH","lat":12.361499786399998,"lng":121.04699707},{"code":"WNP","name":"Naga Airport","city":"Naga","country":"PH","lat":13.58489990234375,"lng":123.2699966430664},{"code":"BSO","name":"Basco Airport","city":"Basco","country":"PH","lat":20.4513,"lng":121.980003},{"code":"TUG","name":"Tuguegarao Airport","city":"Tuguegarao City","country":"PH","lat":17.6433676823,"lng":121.733150482},{"code":"VRC","name":"Virac Airport","city":"Virac","country":"PH","lat":13.576399803161621,"lng":124.20600128173828},{"code":"CYZ","name":"Cauayan Airport","city":"Cauayan City","country":"PH","lat":16.9298992157,"lng":121.752998352},{"code":"TAC","name":"Daniel Z. Romualdez Airport","city":"Tacloban City","country":"PH","lat":11.227761,"lng":125.027783},{"code":"BCD","name":"Bacolod-Silay International Airport","city":"Bacolod City","country":"PH","lat":10.776237,"lng":123.018879},{"code":"CYP","name":"Calbayog Airport","city":"Calbayog City","country":"PH","lat":12.072574,"lng":124.54508},{"code":"DGT","name":"Sibulan Airport","city":"Dumaguete City","country":"PH","lat":9.334183,"lng":123.30191},{"code":"MPH","name":"Godofredo P. Ramos Airport","city":"Caticlan","country":"PH","lat":11.9245,"lng":121.954002},{"code":"CRM","name":"Catarman National Airport","city":"Catarman","country":"PH","lat":12.50161,"lng":124.635258},{"code":"ILO","name":"Iloilo International Airport","city":"Cabatuan","country":"PH","lat":10.833017,"lng":122.493358},{"code":"MBT","name":"Moises R. Espinosa Airport","city":"Masbate","country":"PH","lat":12.369682,"lng":123.630095},{"code":"KLO","name":"Kalibo International Airport","city":"Kalibo","country":"PH","lat":11.6794,"lng":122.375999},{"code":"CEB","name":"Mactan Cebu International Airport","city":"Cebu City/Lapu-Lapu City","country":"PH","lat":10.309261,"lng":123.97974},{"code":"PPS","name":"Puerto Princesa International Airport / PAF Antonio Bautista Air Base","city":"Puerto Princesa","country":"PH","lat":9.742044,"lng":118.75911},{"code":"RXS","name":"Roxas Airport","city":"Roxas City","country":"PH","lat":11.597700119018555,"lng":122.75199890136719},{"code":"TBH","name":"Tugdan Airport","city":"Tablas Island","country":"PH","lat":12.3109998703,"lng":122.084999084},{"code":"USU","name":"Francisco B. Reyes (Busuanga) Airport","city":"Coron","country":"PH","lat":12.121865,"lng":120.100801},{"code":"RSI","name":"Red Sea International Airport","city":"Hanak","country":"SA","lat":25.627975,"lng":37.088914},{"code":"TKM","name":"Taksimo Airport","city":"Taksimo","country":"RU","lat":56.361668,"lng":114.93},{"code":"MJY","name":"Motygino Airport","city":"Motygino","country":"RU","lat":58.180921,"lng":94.745272},{"code":"LPS","name":"Lopez Island Airport","city":"Lopez","country":"US","lat":48.4838981628418,"lng":-122.93800354003906},{"code":"KEH","name":"Kenmore Air Harbor LLC Seaplane Base","city":"Kenmore","country":"US","lat":47.754799,"lng":-122.259003},{"code":"PRA","name":"General Urquiza Airport","city":"Parana","country":"AR","lat":-31.7948,"lng":-60.4804},{"code":"ROS","name":"Rosario Islas Malvinas International Airport","city":"Rosario","country":"AR","lat":-32.9036,"lng":-60.785},{"code":"SFN","name":"Sauce Viejo Airport","city":"Santa Fe","country":"AR","lat":-31.7117,"lng":-60.8117},{"code":"AEP","name":"Aeroparque Jorge Newbery","city":"Buenos Aires","country":"AR","lat":-34.559419,"lng":-58.415536},{"code":"COR","name":"Ingeniero Aeronáutico Ambrosio L.V. Taravella International Airport","city":"Cordoba","country":"AR","lat":-31.312346,"lng":-64.208329},{"code":"EZE","name":"Ezeiza International Airport - Ministro Pistarini","city":"Buenos Aires (Ezeiza)","country":"AR","lat":-34.8222,"lng":-58.5358},{"code":"MDZ","name":"Governor Francisco Gabrielli International Airport","city":"Mendoza","country":"AR","lat":-32.831699,"lng":-68.7929},{"code":"AFA","name":"Suboficial Ay Santiago Germano Airport","city":"San Rafael","country":"AR","lat":-34.588299,"lng":-68.4039},{"code":"CTC","name":"Coronel Felipe Varela International Airport","city":"Catamarca","country":"AR","lat":-28.593117,"lng":-65.7512},{"code":"SDE","name":"Vicecomodoro Angel D. La Paz Aragonés Airport","city":"Santiago del Estero","country":"AR","lat":-27.765556335399996,"lng":-64.3099975586},{"code":"IRJ","name":"Capitan V A Almonacid Airport","city":"La Rioja","country":"AR","lat":-29.381599,"lng":-66.795799},{"code":"RHD","name":"Termas de Río Hondo international Airport","city":"Termas de Río Hondo","country":"AR","lat":-27.4966,"lng":-64.93595},{"code":"TUC","name":"Teniente Benjamín Matienzo International Airport","city":"San Miguel de Tucumán","country":"AR","lat":-26.837397,"lng":-65.104195},{"code":"UAQ","name":"Domingo Faustino Sarmiento Airport","city":"San Juan","country":"AR","lat":-31.571501,"lng":-68.418198},{"code":"LUQ","name":"Brigadier Mayor D Cesar Raul Ojeda Airport","city":"San Luis","country":"AR","lat":-33.273201,"lng":-66.3564},{"code":"CNQ","name":"Corrientes Airport","city":"Corrientes","country":"AR","lat":-27.4455,"lng":-58.7619},{"code":"RES","name":"Resistencia International Airport","city":"Resistencia","country":"AR","lat":-27.44991,"lng":-59.056125},{"code":"FMA","name":"Formosa National Airport","city":"Formosa","country":"AR","lat":-26.2127,"lng":-58.2281},{"code":"IGR","name":"Cataratas Del Iguazú International Airport","city":"Puerto Iguazu","country":"AR","lat":-25.737301,"lng":-54.4734},{"code":"PSS","name":"Libertador Gral D Jose De San Martin Airport","city":"Posadas","country":"AR","lat":-27.3858,"lng":-55.9707},{"code":"SLA","name":"Martín Miguel de Güemes International Airport","city":"Salta","country":"AR","lat":-24.856001,"lng":-65.486198},{"code":"JUJ","name":"Gobernador Horacio Guzman International Airport","city":"San Salvador de Jujuy","country":"AR","lat":-24.392799,"lng":-65.097801},{"code":"CRD","name":"General Enrique Mosconi International Airport","city":"Comodoro Rivadavia","country":"AR","lat":-45.786941,"lng":-67.463355},{"code":"EQS","name":"Esquel Brigadier Antonio Parodi International Airport","city":"Esquel","country":"AR","lat":-42.908001,"lng":-71.139503},{"code":"LHS","name":"Las Heras Airport","city":"Las Heras","country":"AR","lat":-46.538517,"lng":-68.965323},{"code":"OES","name":"Antoine de Saint Exupéry Airport","city":"San Antonio Oeste","country":"AR","lat":-40.7512,"lng":-65.0343},{"code":"REL","name":"Almirante Marco Andres Zar Airport","city":"Rawson","country":"AR","lat":-43.2105,"lng":-65.2703},{"code":"VDM","name":"Gobernador Castello Airport","city":"Viedma / Carmen de Patagones","country":"AR","lat":-40.8692,"lng":-63.0004},{"code":"PMY","name":"El Tehuelche Airport","city":"Puerto Madryn","country":"AR","lat":-42.7592,"lng":-65.1027},{"code":"FTE","name":"El Calafate - Commander Armando Tola International Airport","city":"El Calafate","country":"AR","lat":-50.280267,"lng":-72.053401},{"code":"PUD","name":"Puerto Deseado Airport","city":"Puerto Deseado","country":"AR","lat":-47.7353,"lng":-65.9041},{"code":"RGA","name":"Gobernador Ramón Trejo Noel International Airport","city":"Rio Grande","country":"AR","lat":-53.7777,"lng":-67.7494},{"code":"RGL","name":"Piloto Civil Norberto Fernández International Airport","city":"Rio Gallegos","country":"AR","lat":-51.608791,"lng":-69.308948},{"code":"USH","name":"Ushuaia - Malvinas Argentinas International Airport","city":"Ushuaia","country":"AR","lat":-54.8433,"lng":-68.2958},{"code":"PMQ","name":"Perito Moreno Jalil Hamer Airport","city":"Perito Moreno","country":"AR","lat":-46.537899,"lng":-70.978699},{"code":"GGS","name":"Gobernador Gregores Airport","city":"Gobernador Gregores","country":"AR","lat":-48.7831,"lng":-70.150002},{"code":"RYO","name":"28 de Noviembre Airport","city":"Rio Turbio","country":"AR","lat":-51.605,"lng":-72.2203},{"code":"BHI","name":"Comandante Espora Airport","city":"Bahía Blanca","country":"AR","lat":-38.725,"lng":-62.1693},{"code":"SST","name":"Santa Teresita Airport","city":"Santa Teresita","country":"AR","lat":-36.5423,"lng":-56.7218},{"code":"MDQ","name":"Ástor Piazzola International Airport","city":"Mar del Plata","country":"AR","lat":-37.9342,"lng":-57.5733},{"code":"NQN","name":"Presidente Perón International Airport","city":"Neuquén","country":"AR","lat":-38.949001,"lng":-68.155701},{"code":"NEC","name":"Necochea Airport","city":"Necochea","country":"AR","lat":-38.490746,"lng":-58.816337},{"code":"RSA","name":"Santa Rosa Airport","city":"Santa Rosa","country":"AR","lat":-36.588299,"lng":-64.275703},{"code":"BRC","name":"Teniente Luis Candelaria International Airport","city":"San Carlos de Bariloche","country":"AR","lat":-41.151199,"lng":-71.157501},{"code":"CPC","name":"Aviador C. Campos Airport","city":"Chapelco/San Martin de los Andes","country":"AR","lat":-40.075401,"lng":-71.137299},{"code":"JTC","name":"Bauru/Arealva–Moussa Nakhal Tobias State Airport","city":"Bauru","country":"BR","lat":-22.160755,"lng":-49.070325},{"code":"AQA","name":"Araraquara Airport","city":"Araraquara","country":"BR","lat":-21.812,"lng":-48.132999},{"code":"AJU","name":"Aracaju - Santa Maria Airport","city":"Aracaju","country":"BR","lat":-10.98394,"lng":-37.072873},{"code":"AFL","name":"Piloto Osvaldo Marques Dias Airport","city":"Alta Floresta","country":"BR","lat":-9.866389,"lng":-56.106298},{"code":"ARU","name":"Araçatuba Airport","city":"Araçatuba","country":"BR","lat":-21.141479,"lng":-50.424575},{"code":"AAX","name":"Romeu Zema Airport","city":"Araxá","country":"BR","lat":-19.5632,"lng":-46.9604},{"code":"BEL","name":"Val de Cans/Júlio Cezar Ribeiro International Airport","city":"Belém","country":"BR","lat":-1.379279,"lng":-48.476207},{"code":"BSB","name":"Presidente Juscelino Kubitschek International Airport","city":"Brasília","country":"BR","lat":-15.869167,"lng":-47.920834},{"code":"BVB","name":"Atlas Brasil Cantanhede International Airport","city":"Boa Vista","country":"BR","lat":2.84615,"lng":-60.690648},{"code":"CAC","name":"Coronel Adalberto Mendes da Silva Airport","city":"Cascavel","country":"BR","lat":-25.000323,"lng":-53.501208},{"code":"CFB","name":"Cabo Frio Airport","city":"Cabo Frio","country":"BR","lat":-22.921485,"lng":-42.071874},{"code":"CNF","name":"Tancredo Neves International Airport","city":"Belo Horizonte","country":"BR","lat":-19.63571,"lng":-43.966928},{"code":"CGR","name":"Campo Grande Airport","city":"Campo Grande","country":"BR","lat":-20.469998,"lng":-54.673988},{"code":"XAP","name":"Serafin Enoss Bertaso Airport","city":"Chapecó","country":"BR","lat":-27.134199,"lng":-52.656601},{"code":"CKS","name":"Carajás Airport","city":"Parauapebas","country":"BR","lat":-6.117841,"lng":-50.003372},{"code":"CLV","name":"Nelson Ribeiro Guimarães Airport","city":"Caldas Novas","country":"BR","lat":-17.725096,"lng":-48.606324},{"code":"CAW","name":"Bartolomeu Lisandro Airport","city":"Campos dos Goytacazes","country":"BR","lat":-21.698299,"lng":-41.301701},{"code":"CMG","name":"Corumbá International Airport","city":"Corumbá","country":"BR","lat":-19.01193,"lng":-57.672772},{"code":"CWB","name":"Curitiba-Afonso Pena International Airport","city":"Curitiba","country":"BR","lat":-25.5285,"lng":-49.1758},{"code":"CXJ","name":"Hugo Cantergiani Regional Airport","city":"Caxias Do Sul","country":"BR","lat":-29.197183,"lng":-51.187647},{"code":"CGB","name":"Várzea Grande–Marechal Rondon International Airport","city":"Cuiabá","country":"BR","lat":-15.6529,"lng":-56.116699},{"code":"CZS","name":"Cruzeiro do Sul Airport","city":"Cruzeiro Do Sul","country":"BR","lat":-7.59991,"lng":-72.769501},{"code":"BYO","name":"Bonito Airport","city":"Bonito","country":"BR","lat":-21.2473,"lng":-56.452399},{"code":"PPB","name":"Presidente Prudente Airport","city":"Presidente Prudente","country":"BR","lat":-22.1751,"lng":-51.424599},{"code":"MAO","name":"Eduardo Gomes International Airport","city":"Manaus","country":"BR","lat":-3.03861,"lng":-60.049702},{"code":"IGU","name":"Cataratas International Airport","city":"Foz do Iguaçu","country":"BR","lat":-25.594167,"lng":-54.489444},{"code":"FLN","name":"Hercílio Luz International Airport","city":"Florianópolis","country":"BR","lat":-27.670279,"lng":-48.552502},{"code":"FEN","name":"Fernando de Noronha Airport","city":"Fernando de Noronha","country":"BR","lat":-3.854534,"lng":-32.423017},{"code":"FOR","name":"Pinto Martins International Airport","city":"Fortaleza","country":"BR","lat":-3.775833,"lng":-38.532222},{"code":"GIG","name":"Rio Galeão – Tom Jobim International Airport","city":"Rio De Janeiro","country":"BR","lat":-22.809999,"lng":-43.250557},{"code":"GYN","name":"Santa Genoveva International Airport","city":"Goiânia","country":"BR","lat":-16.632,"lng":-49.220699},{"code":"GRU","name":"São Paulo/Guarulhos–Governor André Franco Montoro International Airport","city":"São Paulo","country":"BR","lat":-23.431274,"lng":-46.469954},{"code":"GVR","name":"Coronel Altino Machado Airport","city":"Governador Valadares","country":"BR","lat":-18.895882,"lng":-41.982869},{"code":"ATM","name":"Altamira Interstate Airport","city":"Altamira","country":"BR","lat":-3.253144,"lng":-52.253938},{"code":"ITB","name":"Itaituba Airport","city":"Itaituba","country":"BR","lat":-4.242131,"lng":-56.000651},{"code":"IOS","name":"Bahia - Jorge Amado Airport","city":"Ilhéus","country":"BR","lat":-14.815929,"lng":-39.033458},{"code":"IPN","name":"Usiminas Airport","city":"Ipatinga","country":"BR","lat":-19.470699,"lng":-42.487598},{"code":"IMP","name":"Prefeito Renato Moreira Airport","city":"Imperatriz","country":"BR","lat":-5.53129,"lng":-47.459999},{"code":"JJG","name":"Humberto Ghizzo Bortoluzzi Regional Airport","city":"Jaguaruna","country":"BR","lat":-28.6753,"lng":-49.0596},{"code":"JJD","name":"Comandante Ariston Pessoa Airport","city":"Cruz","country":"BR","lat":-2.906425,"lng":-40.357338},{"code":"JDF","name":"Francisco de Assis Airport","city":"Juiz de Fora","country":"BR","lat":-21.791482,"lng":-43.386072},{"code":"JPR","name":"Ji-Paraná Airport","city":"Ji-Paraná","country":"BR","lat":-10.870743,"lng":-61.846675},{"code":"JPA","name":"Presidente Castro Pinto International Airport","city":"João Pessoa","country":"BR","lat":-7.148691,"lng":-34.950554},{"code":"RRJ","name":"Jacarepaguá - Roberto Marinho Airport","city":"Rio de Janeiro","country":"BR","lat":-22.986773,"lng":-43.372194},{"code":"JDO","name":"Orlando Bezerra de Menezes Airport","city":"Juazeiro do Norte","country":"BR","lat":-7.21932,"lng":-39.269096},{"code":"JOI","name":"Lauro Carneiro de Loyola Airport","city":"Joinville","country":"BR","lat":-26.224501,"lng":-48.797401},{"code":"CPV","name":"Presidente João Suassuna Airport","city":"Campina Grande","country":"BR","lat":-7.269662,"lng":-35.896057},{"code":"VCP","name":"Viracopos International Airport","city":"Campinas","country":"BR","lat":-23.007404,"lng":-47.134502},{"code":"LEC","name":"Coronel Horácio de Mattos Airport","city":"Lençóis","country":"BR","lat":-12.4823,"lng":-41.277},{"code":"LAJ","name":"Lages Airport","city":"Lages","country":"BR","lat":-27.782101,"lng":-50.281502},{"code":"LDB","name":"Governor José Richa Airport","city":"Londrina","country":"BR","lat":-23.334375,"lng":-51.128365},{"code":"MAB","name":"João Correa da Rocha Airport","city":"Marabá","country":"BR","lat":-5.36859,"lng":-49.138},{"code":"MGF","name":"Regional de Maringá - Sílvio Name Júnior Airport","city":"Maringá","country":"BR","lat":-23.47606,"lng":-52.016187},{"code":"MOC","name":"Mário Ribeiro Airport","city":"Montes Claros","country":"BR","lat":-16.706919,"lng":-43.818901},{"code":"MII","name":"Frank Miloye Milenkowichi–Marília State Airport","city":"Marília","country":"BR","lat":-22.196899,"lng":-49.926485},{"code":"MCZ","name":"Zumbi dos Palmares International Airport","city":"Maceió","country":"BR","lat":-9.512559,"lng":-35.791839},{"code":"MCP","name":"Macapá - Alberto Alcolumbre International Airport","city":"Macapá","country":"BR","lat":0.050664,"lng":-51.072201},{"code":"MVF","name":"Dix-Sept Rosado Airport","city":"Mossoró","country":"BR","lat":-5.20192,"lng":-37.3643},{"code":"MNX","name":"Manicoré Airport","city":"Manicoré","country":"BR","lat":-5.81138,"lng":-61.278301},{"code":"NVT","name":"Ministro Victor Konder International Airport","city":"Navegantes","country":"BR","lat":-26.879431,"lng":-48.650979},{"code":"GEL","name":"Santo Ângelo Airport","city":"Santo Ângelo","country":"BR","lat":-28.282503,"lng":-54.169623},{"code":"POA","name":"Porto Alegre-Salgado Filho International Airport","city":"Porto Alegre","country":"BR","lat":-29.993984,"lng":-51.167482},{"code":"PHB","name":"Parnaíba - Prefeito Doutor João Silva Filho International Airport","city":"Parnaíba","country":"BR","lat":-2.89375,"lng":-41.731998},{"code":"PFB","name":"Lauro Kurtz Airport","city":"Passo Fundo","country":"BR","lat":-28.243976,"lng":-52.32777},{"code":"PGZ","name":"Ponta Grossa Airport - Comandante Antonio Amilton Beraldo","city":"Ponta Grossa","country":"BR","lat":-25.184476,"lng":-50.143822},{"code":"PMW","name":"Brigadeiro Lysias Rodrigues Airport","city":"Palmas","country":"BR","lat":-10.2915,"lng":-48.356998},{"code":"PET","name":"João Simões Lopes Neto International Airport","city":"Pelotas","country":"BR","lat":-31.717189,"lng":-52.327796},{"code":"PNZ","name":"Senador Nilo Coelho Airport","city":"Petrolina","country":"BR","lat":-9.362422,"lng":-40.569098},{"code":"PTO","name":"Juvenal Loureiro Cardoso Airport","city":"Pato Branco","country":"BR","lat":-26.217184,"lng":-52.694463},{"code":"PMG","name":"Ponta Porã Airport","city":"Ponta Porã","country":"BR","lat":-22.549601,"lng":-55.702599},{"code":"BPS","name":"Porto Seguro International Airport","city":"Porto Seguro","country":"BR","lat":-16.438426,"lng":-39.080584},{"code":"PVH","name":"Governador Jorge Teixeira de Oliveira International Airport","city":"Porto Velho","country":"BR","lat":-8.708491,"lng":-63.902338},{"code":"RBR","name":"Rio Branco-Plácido de Castro International Airport","city":"Rio Branco","country":"BR","lat":-9.869031,"lng":-67.893984},{"code":"ROO","name":"Maestro Marinho Franco Airport","city":"Rondonópolis","country":"BR","lat":-16.584292,"lng":-54.724803},{"code":"REC","name":"Recife/Guararapes - Gilberto Freyre International Airport","city":"Recife","country":"BR","lat":-8.12747,"lng":-34.922961},{"code":"SDU","name":"Santos Dumont Airport","city":"Rio de Janeiro","country":"BR","lat":-22.910397,"lng":-43.16282},{"code":"RAO","name":"Leite Lopes Airport","city":"Ribeirão Preto","country":"BR","lat":-21.134314,"lng":-47.774053},{"code":"NAT","name":"Rio Grande do Norte/São Gonçalo do Amarante–Governador Aluízio Alves International Airport","city":"Natal","country":"BR","lat":-5.769822,"lng":-35.36661},{"code":"OPS","name":"Presidente João Batista Figueiredo Airport","city":"Sinop","country":"BR","lat":-11.885001,"lng":-55.586109},{"code":"SJK","name":"Professor Urbano Ernesto Stumpf Airport","city":"São José Dos Campos","country":"BR","lat":-23.2292,"lng":-45.8615},{"code":"SLZ","name":"Marechal Cunha Machado International Airport","city":"São Luís","country":"BR","lat":-2.586386,"lng":-44.235008},{"code":"RIA","name":"Santa Maria Airport","city":"Santa Maria","country":"BR","lat":-29.711399,"lng":-53.688202},{"code":"STM","name":"Santarém - Maestro Wilson Fonseca International Airport","city":"Santarém","country":"BR","lat":-2.422423,"lng":-54.79306},{"code":"SMT","name":"Adolino Bedin Regional Airport","city":"Sorriso","country":"BR","lat":-12.479177,"lng":-55.672341},{"code":"CGH","name":"Congonhas–Deputado Freitas Nobre Airport","city":"São Paulo","country":"BR","lat":-23.627657,"lng":-46.654601},{"code":"SJP","name":"Prof. Eribelto Manoel Reino State Airport","city":"São José do Rio Preto","country":"BR","lat":-20.817113,"lng":-49.406963},{"code":"SSA","name":"Deputado Luiz Eduardo Magalhães International Airport","city":"Salvador","country":"BR","lat":-12.908611,"lng":-38.322498},{"code":"TMT","name":"Trombetas Airport","city":"Oriximiná","country":"BR","lat":-1.4896,"lng":-56.396801},{"code":"UNA","name":"Una-Comandatuba Airport","city":"Una","country":"BR","lat":-15.354358,"lng":-38.998729},{"code":"TOW","name":"Toledo - Luiz Dalcanale Filho Municipal Airport","city":"Toledo","country":"BR","lat":-24.6863,"lng":-53.697498},{"code":"THE","name":"Senador Petrônio Portela Airport","city":"Teresina","country":"BR","lat":-5.06025,"lng":-42.823712},{"code":"TFF","name":"Tefé Airport","city":"Tefé","country":"BR","lat":-3.38294,"lng":-64.724098},{"code":"TJL","name":"Plínio Alarcom Airport","city":"Três Lagoas","country":"BR","lat":-20.752643,"lng":-51.682026},{"code":"TBT","name":"Tabatinga International Airport","city":"Tabatinga","country":"BR","lat":-4.25567,"lng":-69.935799},{"code":"TUR","name":"Tucuruí Airport","city":"Tucuruí","country":"BR","lat":-3.78601,"lng":-49.720299},{"code":"SJL","name":"São Gabriel da Cachoeira Airport","city":"São Gabriel da Cachoeira","country":"BR","lat":-0.14835,"lng":-66.9855},{"code":"PAV","name":"Paulo Afonso Airport","city":"Paulo Afonso","country":"BR","lat":-9.40088,"lng":-38.250599},{"code":"URG","name":"Rubem Berta Airport","city":"Uruguaiana","country":"BR","lat":-29.7822,"lng":-57.0382},{"code":"UDI","name":"Ten. Cel. Aviador César Bombonato Airport","city":"Uberlândia","country":"BR","lat":-18.883579,"lng":-48.225936},{"code":"UBA","name":"Mário de Almeida Franco Airport","city":"Uberaba","country":"BR","lat":-19.765036,"lng":-47.964805},{"code":"BVH","name":"Brigadeiro Camarão Airport","city":"Vilhena","country":"BR","lat":-12.6944,"lng":-60.098301},{"code":"VIX","name":"Eurico de Aguiar Salles International Airport","city":"Vitória","country":"BR","lat":-20.258,"lng":-40.285},{"code":"IZA","name":"Presidente Itamar Franco Airport","city":"Juiz de Fora","country":"BR","lat":-21.513086,"lng":-43.173069},{"code":"ARI","name":"Chacalluta International Airport","city":"Arica","country":"CL","lat":-18.348499,"lng":-70.338699},{"code":"CPO","name":"Desierto de Atacama Airport","city":"Copiapo","country":"CL","lat":-27.2612,"lng":-70.779198},{"code":"BBA","name":"Balmaceda Airport","city":"Balmaceda","country":"CL","lat":-45.915968,"lng":-71.689499},{"code":"CJC","name":"El Loa Airport","city":"Calama","country":"CL","lat":-22.498199,"lng":-68.903603},{"code":"PUQ","name":"President Carlos Ibáñez International Airport","city":"Punta Arenas","country":"CL","lat":-53.002602,"lng":-70.854599},{"code":"IQQ","name":"Diego Aracena International Airport","city":"Iquique","country":"CL","lat":-20.536273,"lng":-70.181373},{"code":"SCL","name":"Comodoro Arturo Merino Benítez International Airport","city":"Santiago","country":"CL","lat":-33.393001556396484,"lng":-70.78579711914062},{"code":"ESR","name":"Ricardo García Posada Airport","city":"El Salvador","country":"CL","lat":-26.3111,"lng":-69.765198},{"code":"ANF","name":"Andrés Sabella Gálvez International Airport","city":"Antofagasta","country":"CL","lat":-23.445274,"lng":-70.445232},{"code":"CCP","name":"Carriel Sur International Airport","city":"Concepcion","country":"CL","lat":-36.77235,"lng":-73.062828},{"code":"IPC","name":"Mataveri International Airport","city":"Isla De Pascua","country":"CL","lat":-27.165411,"lng":-109.421027},{"code":"ZOS","name":"Cañal Bajo Carlos Hott Siebert Airport","city":"Osorno","country":"CL","lat":-40.611198,"lng":-73.060997},{"code":"PNT","name":"Lieutenant Julio Gallardo Airport","city":"Puerto Natales","country":"CL","lat":-51.67067,"lng":-72.529078},{"code":"ZPC","name":"Pucón Airport","city":"Pucon","country":"CL","lat":-39.292801,"lng":-71.915901},{"code":"MHC","name":"Mocopulli Airport","city":"Dalcahue","country":"CL","lat":-42.340388,"lng":-73.715693},{"code":"ZCO","name":"La Araucanía International Airport","city":"Temuco","country":"CL","lat":-38.9259,"lng":-72.6515},{"code":"TNM","name":"Teniente Rodolfo Marsh Martin Airport","city":"Villa Las Estrellas","country":"AQ","lat":-62.191026,"lng":-58.98669},{"code":"LSC","name":"La Florida Airport","city":"La Serena-Coquimbo","country":"CL","lat":-29.916201,"lng":-71.199501},{"code":"PMC","name":"El Tepual International Airport","city":"Puerto Montt","country":"CL","lat":-41.443093,"lng":-73.094065},{"code":"ZAL","name":"Pichoy Airport","city":"Valdivia","country":"CL","lat":-39.6500015259,"lng":-73.0860977173},{"code":"SOD","name":"Sorocaba Airport","city":"Sorocaba","country":"BR","lat":-23.478001,"lng":-47.490002},{"code":"OCC","name":"Francisco De Orellana Airport","city":"Coca","country":"EC","lat":-0.462886,"lng":-76.986801},{"code":"CUE","name":"Mariscal Lamar Airport","city":"Cuenca","country":"EC","lat":-2.88947,"lng":-78.984398},{"code":"GPS","name":"Seymour Galapagos Ecological Airport","city":"Isla Baltra","country":"EC","lat":-0.453758,"lng":-90.2659},{"code":"GYE","name":"José Joaquín de Olmedo International Airport","city":"Guayaquil","country":"EC","lat":-2.15742,"lng":-79.883598},{"code":"IBB","name":"General Villamil Airport","city":"Puerto Villamil","country":"EC","lat":-0.942628,"lng":-90.953003},{"code":"LTX","name":"Cotopaxi International Airport","city":"Latacunga","country":"EC","lat":-0.906833,"lng":-78.615799},{"code":"XMS","name":"Coronel E Carvajal Airport","city":"Macas","country":"EC","lat":-2.2991700172424316,"lng":-78.12079620361328},{"code":"MEC","name":"Eloy Alfaro International Airport","city":"Manta","country":"EC","lat":-0.9460780024528503,"lng":-80.67880249023438},{"code":"UIO","name":"Mariscal Sucre International Airport","city":"Quito","country":"EC","lat":-0.125399,"lng":-78.354306},{"code":"ETR","name":"Santa Rosa - Artillery Colonel Victor Larrea International Airport","city":"Santa Rosa","country":"EC","lat":-3.441986,"lng":-79.996957},{"code":"SNC","name":"General Ulpiano Paez International Airport","city":"Salinas/La Libertad","country":"EC","lat":-2.210057,"lng":-80.985067},{"code":"SCY","name":"San Cristóbal Airport","city":"Puerto Baquerizo Moreno","country":"EC","lat":-0.910206,"lng":-89.617401},{"code":"LOH","name":"Ciudad de Catamayo Airport","city":"La Toma (Catamayo)","country":"EC","lat":-3.995606,"lng":-79.371902},{"code":"ESM","name":"Carlos Concha Torres International Airport","city":"Tachina","country":"EC","lat":0.978519,"lng":-79.626602},{"code":"TUA","name":"Lieutenant Colonel Luis A. Mantilla International Airport","city":"Tulcán","country":"EC","lat":0.809506,"lng":-77.708099},{"code":"PSY","name":"Port Stanley Airport","city":"Stanley","country":"FK","lat":-51.685699,"lng":-57.777599},{"code":"ASU","name":"Silvio Pettirossi International Airport","city":"Asunción","country":"PY","lat":-25.240156,"lng":-57.519227},{"code":"ENO","name":"Teniente Ramon A. Ayub Gonzalez International Airport","city":"Encarnación","country":"PY","lat":-27.227537,"lng":-55.837584},{"code":"AGT","name":"Guaraní International Airport","city":"Ciudad del Este","country":"PY","lat":-25.457186,"lng":-54.839544},{"code":"AXM","name":"El Eden Airport","city":"Armenia","country":"CO","lat":4.45278,"lng":-75.7664},{"code":"PUU","name":"Tres De Mayo Airport","city":"Puerto Asís","country":"CO","lat":0.505228,"lng":-76.5008},{"code":"BGA","name":"Palonegro Airport","city":"Bucaramanga","country":"CO","lat":7.1265,"lng":-73.1848},{"code":"BOG","name":"El Dorado International Airport","city":"Bogota","country":"CO","lat":4.70159,"lng":-74.1469},{"code":"BAQ","name":"Ernesto Cortissoz International Airport","city":"Barranquilla","country":"CO","lat":10.8896,"lng":-74.7808},{"code":"BSC","name":"José Celestino Mutis Airport","city":"Bahía Solano","country":"CO","lat":6.20292,"lng":-77.3947},{"code":"BUN","name":"Gerardo Tobar López Airport","city":"Buenaventura","country":"CO","lat":3.81963,"lng":-76.9898},{"code":"CUC","name":"Camilo Daza International Airport","city":"Cúcuta","country":"CO","lat":7.92757,"lng":-72.5115},{"code":"CTG","name":"Rafael Nuñez International Airport","city":"Cartagena","country":"CO","lat":10.4424,"lng":-75.513},{"code":"CLO","name":"Alfonso Bonilla Aragon International Airport","city":"Cali","country":"CO","lat":3.542717,"lng":-76.381898},{"code":"TCO","name":"La Florida Airport","city":"Tumaco","country":"CO","lat":1.81442,"lng":-78.7492},{"code":"CZU","name":"Las Brujas Airport","city":"Corozal","country":"CO","lat":9.33274,"lng":-75.2856},{"code":"EJA","name":"Yariguíes Airport","city":"Barrancabermeja","country":"CO","lat":7.02433,"lng":-73.8068},{"code":"FLA","name":"Gustavo Artunduaga Paredes Airport","city":"Florencia","country":"CO","lat":1.58919,"lng":-75.5644},{"code":"GPI","name":"Guapi Airport","city":"Guapi","country":"CO","lat":2.57013,"lng":-77.897969},{"code":"IBE","name":"Perales Airport","city":"Ibagué","country":"CO","lat":4.42161,"lng":-75.1333},{"code":"IPI","name":"San Luis Airport","city":"Ipiales","country":"CO","lat":0.861946,"lng":-77.671783},{"code":"APO","name":"Antonio Roldán Betancur Airport","city":"Carepa","country":"CO","lat":7.81196,"lng":-76.7164},{"code":"LQM","name":"Caucaya Airport","city":"Puerto Leguízamo","country":"CO","lat":-0.182278,"lng":-74.7708},{"code":"LPD","name":"La Pedrera Airport","city":"La Pedrera","country":"CO","lat":-1.324882,"lng":-69.581294},{"code":"LET","name":"Alfredo Vásquez Cobo International Airport","city":"Leticia","country":"CO","lat":-4.191147,"lng":-69.942003},{"code":"EOH","name":"Enrique Olaya Herrera Airport","city":"Medellín","country":"CO","lat":6.219686,"lng":-75.590597},{"code":"MTR","name":"Los Garzones Airport","city":"Montería","country":"CO","lat":8.82374,"lng":-75.8258},{"code":"MVP","name":"Fabio Alberto Leon Bentley Airport","city":"Mitú","country":"CO","lat":1.25366,"lng":-70.2339},{"code":"MZL","name":"La Nubia Airport","city":"Manizales","country":"CO","lat":5.0296,"lng":-75.4647},{"code":"LMC","name":"La Macarena Airport","city":"La Macarena","country":"CO","lat":2.17565,"lng":-73.78674},{"code":"NQU","name":"Reyes Murillo Airport","city":"Nuquí","country":"CO","lat":5.710348,"lng":-77.261868},{"code":"NVA","name":"Benito Salas Airport","city":"Neiva","country":"CO","lat":2.95015,"lng":-75.294},{"code":"PCR","name":"German Olano Airport","city":"Puerto Carreño","country":"CO","lat":6.18472,"lng":-67.4932},{"code":"PDA","name":"Obando Cesar Gaviria Trujillo Airport","city":"Puerto Inírida","country":"CO","lat":3.85353,"lng":-67.9062},{"code":"PEI","name":"Matecaña International Airport","city":"Pereira","country":"CO","lat":4.81267,"lng":-75.7395},{"code":"PPN","name":"Guillermo León Valencia Airport","city":"Popayán","country":"CO","lat":2.454359,"lng":-76.609253},{"code":"PSO","name":"Antonio Nariño Airport","city":"Chachagüí","country":"CO","lat":1.396722,"lng":-77.29093},{"code":"PVA","name":"El Embrujo Airport","city":"Providencia","country":"CO","lat":13.357461,"lng":-81.357977},{"code":"TCD","name":"Tarapacá Airport","city":"Tarapacá","country":"CO","lat":-2.895802,"lng":-69.749762},{"code":"MDE","name":"Jose Maria Córdova International Airport","city":"Medellín","country":"CO","lat":6.16454,"lng":-75.4231},{"code":"RCH","name":"Almirante Padilla Airport","city":"Riohacha","country":"CO","lat":11.5262,"lng":-72.926},{"code":"RVE","name":"Los Colonizadores Airport","city":"Saravena","country":"CO","lat":6.951868,"lng":-71.857179},{"code":"SJE","name":"Jorge E. Gonzalez Torres Airport","city":"San José Del Guaviare","country":"CO","lat":2.57969,"lng":-72.6394},{"code":"SMR","name":"Simón Bolívar International Airport","city":"Santa Marta","country":"CO","lat":11.1196,"lng":-74.2306},{"code":"ADZ","name":"Gustavo Rojas Pinilla International Airport","city":"San Andrés","country":"CO","lat":12.5836,"lng":-81.7112},{"code":"SVI","name":"Eduardo Falla Solano Airport","city":"San Vicente Del Caguán","country":"CO","lat":2.15217,"lng":-74.7663},{"code":"TLU","name":"Golfo de Morrosquillo Airport","city":"Santiago de Tolú","country":"CO","lat":9.50945,"lng":-75.5854},{"code":"TME","name":"Gustavo Vargas Airport","city":"Tame","country":"CO","lat":6.45108,"lng":-71.7603},{"code":"AUC","name":"Santiago Perez Airport","city":"Arauca","country":"CO","lat":7.06888,"lng":-70.7369},{"code":"UIB","name":"El Caraño Airport","city":"Quibdó","country":"CO","lat":5.69076,"lng":-76.6412},{"code":"VUP","name":"Alfonso López Pumarejo Airport","city":"Valledupar","country":"CO","lat":10.435,"lng":-73.2495},{"code":"VVC","name":"Vanguardia Airport","city":"Villavicencio","country":"CO","lat":4.16787,"lng":-73.6138},{"code":"EYP","name":"El Alcaravan - Yopal Airport","city":"Yopal","country":"CO","lat":5.31911,"lng":-72.384},{"code":"SRE","name":"Alcantarí International Airport","city":"Sucre","country":"BO","lat":-19.246835,"lng":-65.149611},{"code":"CBB","name":"Jorge Wilsterman International Airport","city":"Cochabamba","country":"BO","lat":-17.421105,"lng":-66.177102},{"code":"CIJ","name":"Capitán Aníbal Arab Airport","city":"Cobija","country":"BO","lat":-11.039109,"lng":-68.782774},{"code":"SRZ","name":"El Trompillo Airport","city":"Santa Cruz","country":"BO","lat":-17.8115997314,"lng":-63.1715011597},{"code":"GYA","name":"Guayaramerín Airport","city":"Guayaramerín","country":"BO","lat":-10.88856,"lng":-65.380955},{"code":"CCA","name":"Chimore Airport","city":"Chimore","country":"BO","lat":-16.976834,"lng":-65.145568},{"code":"LPB","name":"El Alto International Airport","city":"La Paz / El Alto","country":"BO","lat":-16.510272,"lng":-68.189416},{"code":"ORU","name":"Juan Mendoza International Airport","city":"Oruro","country":"BO","lat":-17.956151,"lng":-67.075829},{"code":"PUR","name":"Puerto Rico Airport","city":"Puerto Rico/Manuripi","country":"BO","lat":-11.107663,"lng":-67.551155},{"code":"PSZ","name":"Capitán Av. Salvador Ogaya G. airport","city":"Puerto Suárez","country":"BO","lat":-18.975301,"lng":-57.820599},{"code":"RIB","name":"Capitán Av. Selin Zeitun Lopez Airport","city":"Riberalta","country":"BO","lat":-11.009355,"lng":-66.075468},{"code":"RBQ","name":"Rurrenabaque Airport","city":"Rurrenabaque","country":"BO","lat":-14.4279,"lng":-67.496803},{"code":"TJA","name":"Capitan Oriel Lea Plaza Airport","city":"Tarija","country":"BO","lat":-21.5557,"lng":-64.701302},{"code":"TDD","name":"Teniente Av. Jorge Henrich Arauz Airport","city":"Trinidad","country":"BO","lat":-14.8187,"lng":-64.917999},{"code":"UYU","name":"Joya Andina International Airport","city":"Quijarro","country":"BO","lat":-20.441304,"lng":-66.857553},{"code":"VVI","name":"Viru Viru International Airport","city":"Santa Cruz","country":"BO","lat":-17.6448,"lng":-63.135399},{"code":"DRJ","name":"Drietabbetje Airport","city":"Drietabbetje","country":"SR","lat":4.111359,"lng":-54.672766},{"code":"PBM","name":"Johan Adolf Pengel International Airport","city":"Paramaribo","country":"SR","lat":5.45283,"lng":-55.187801},{"code":"MOJ","name":"Moengo Airstrip","city":"Moengo","country":"SR","lat":5.6076,"lng":-54.4003},{"code":"AGI","name":"Wageningen Airstrip","city":"Wageningen","country":"SR","lat":5.841252,"lng":-56.673247},{"code":"ORG","name":"Zorg en Hoop Airport","city":"Paramaribo","country":"SR","lat":5.811211,"lng":-55.192299},{"code":"BRA","name":"Dom Ricardo Weberberger Airport","city":"Barreiras","country":"BR","lat":-12.0789,"lng":-45.008999},{"code":"JPE","name":"Nagib Demachki Airport","city":"Paragominas","country":"BR","lat":-3.019798,"lng":-47.316474},{"code":"GUZ","name":"Guarapari Airport","city":"Guarapari","country":"BR","lat":-20.6465,"lng":-40.491901},{"code":"SET","name":"Santa Magalhães Airport","city":"Serra Talhada","country":"BR","lat":-8.061433,"lng":-38.328888},{"code":"CAU","name":"Caruaru Airport","city":"Caruaru","country":"BR","lat":-8.28239,"lng":-36.0135},{"code":"OPP","name":"Salinópolis Airport","city":"Salinópolis","country":"BR","lat":-0.696111,"lng":-47.336111},{"code":"VAL","name":"Valença Airport","city":"Valença","country":"BR","lat":-13.2965,"lng":-38.992401},{"code":"BVS","name":"Breves Airport","city":"Breves","country":"BR","lat":-1.63653,"lng":-50.4436},{"code":"GGF","name":"Almeirim Airport","city":"Almeirim","country":"BR","lat":-1.479524,"lng":-52.578214},{"code":"PYT","name":"Pedro Rabelo de Souza Airport","city":"Paracatu","country":"BR","lat":-17.242599,"lng":-46.883099},{"code":"CAY","name":"Cayenne – Félix Eboué Airport","city":"Matoury","country":"GF","lat":4.819964,"lng":-52.361326},{"code":"MPY","name":"Maripasoula Airport","city":"Maripasoula","country":"GF","lat":3.655907,"lng":-54.039431},{"code":"LDX","name":"Saint-Laurent-du-Maroni Airport","city":"Saint-Laurent-du-Maroni","country":"GF","lat":5.481891,"lng":-54.034964},{"code":"PCL","name":"Cap FAP David Abenzur Rengifo International Airport","city":"Pucallpa","country":"PE","lat":-8.378064,"lng":-74.574495},{"code":"CHM","name":"FAP Lieutenant Jaime Andres de Montreuil Morales Airport","city":"Chimbote","country":"PE","lat":-9.14961,"lng":-78.523804},{"code":"CIX","name":"Capitán FAP José A. Quiñones González International Airport","city":"Chiclayo","country":"PE","lat":-6.789223,"lng":-79.828254},{"code":"AYP","name":"Air Force Colonel Alfredo Mendivil Duarte Airport","city":"Ayacucho","country":"PE","lat":-13.1548,"lng":-74.204399},{"code":"ANS","name":"Andahuaylas Airport","city":"Andahuaylas","country":"PE","lat":-13.706399917602539,"lng":-73.35040283203125},{"code":"LIM","name":"Jorge Chávez International Airport","city":"Lima","country":"PE","lat":-12.0219,"lng":-77.114305},{"code":"JAE","name":"Shumba Airport","city":"Jaén","country":"PE","lat":-5.59248,"lng":-78.774002},{"code":"JAU","name":"Francisco Carle Airport","city":"Jauja","country":"PE","lat":-11.7831001282,"lng":-75.47339630130001},{"code":"JUL","name":"Inca Manco Capac International Airport","city":"Juliaca","country":"PE","lat":-15.467689,"lng":-70.15653},{"code":"CJA","name":"Mayor General FAP Armando Revoredo Iglesias Airport","city":"Cajamarca","country":"PE","lat":-7.13918,"lng":-78.489403},{"code":"ILQ","name":"General Jorge Fernandez Maldon Airport","city":"Ilo","country":"PE","lat":-17.695,"lng":-71.344002},{"code":"TBP","name":"Captain Pedro Canga Rodríguez International Airport","city":"Tumbes","country":"PE","lat":-3.552074,"lng":-80.381086},{"code":"YMS","name":"Moises Benzaquen Rengifo Airport","city":"Yurimaguas","country":"PE","lat":-5.89377,"lng":-76.118202},{"code":"HUU","name":"Alferez Fap David Figueroa Fernandini Airport","city":"Huánuco","country":"PE","lat":-9.878809928894043,"lng":-76.20480346679688},{"code":"CHH","name":"Chachapoyas Airport","city":"Chachapoyas","country":"PE","lat":-6.201911,"lng":-77.856195},{"code":"IQT","name":"Coronel FAP Francisco Secada Vignetta International Airport","city":"Iquitos","country":"PE","lat":-3.78474,"lng":-73.3088},{"code":"AQP","name":"Rodríguez Ballón International Airport","city":"Arequipa","country":"PE","lat":-16.340786,"lng":-71.569485},{"code":"TRU","name":"Capitán FAP Carlos Martínez de Pinillos International Airport","city":"Trujillo","country":"PE","lat":-8.082382,"lng":-79.108821},{"code":"TPP","name":"Cadete FAP Guillermo Del Castillo Paredes Airport","city":"Tarapoto","country":"PE","lat":-6.508739948272705,"lng":-76.37319946289062},{"code":"TCQ","name":"Coronel FAP Carlos Ciriani Santa Rosa International Airport","city":"Tacna","country":"PE","lat":-18.053300857500002,"lng":-70.2758026123},{"code":"PEM","name":"Padre Aldamiz International Airport","city":"Puerto Maldonado","country":"PE","lat":-12.6135997772,"lng":-69.2285995483},{"code":"PIU","name":"PAF Captain Guillermo Concha Iberico International Airport","city":"Piura","country":"PE","lat":-5.20575,"lng":-80.616402},{"code":"TYL","name":"Captain Victor Montes Arias International Airport","city":"Talara","country":"PE","lat":-4.57664,"lng":-81.254097},{"code":"NZC","name":"Maria Reiche Neuman Airport","city":"Nazca","country":"PE","lat":-14.854,"lng":-74.961502},{"code":"CUZ","name":"Alejandro Velasco Astete International Airport","city":"Cusco","country":"PE","lat":-13.535699844400002,"lng":-71.9387969971},{"code":"EAX","name":"Eduard Alexander Gummels Airport","city":"Kwatta","country":"SR","lat":5.863807,"lng":-55.189504},{"code":"SRL","name":"Palo Verde Airport","city":"Mulegé","country":"MX","lat":27.0927,"lng":-112.0985},{"code":"SRV","name":"Stony River 2 Airport","city":"Stony River","country":"US","lat":61.7897,"lng":-156.589005},{"code":"CEL","name":"Canela Airport","city":"Canela","country":"BR","lat":-29.370091,"lng":-50.831729},{"code":"FBE","name":"Paulo Abdala Airport","city":"Francisco Beltrão","country":"BR","lat":-26.05938,"lng":-53.063827},{"code":"GGJ","name":"Guaíra Airport","city":"Guaíra","country":"BR","lat":-24.0811,"lng":-54.1917},{"code":"OAL","name":"Cacoal Airport","city":"Cacoal","country":"BR","lat":-11.496,"lng":-61.4508},{"code":"UMU","name":"Orlando de Carvalho Airport","city":"Umuarama","country":"BR","lat":-23.7987,"lng":-53.313801},{"code":"UVI","name":"José Cleto Airport","city":"União da Vitória","country":"BR","lat":-26.233224,"lng":-51.066754},{"code":"SRA","name":"Luis Alberto Lehr Airport","city":"Santa Rosa","country":"BR","lat":-27.90658,"lng":-54.520628},{"code":"PDP","name":"Capitan Corbeta CA Curbelo International Airport","city":"Punta del Este","country":"UY","lat":-34.855099,"lng":-55.094299},{"code":"MVD","name":"Carrasco General Cesáreo L. Berisso International Airport","city":"Ciudad de la Costa","country":"UY","lat":-34.835647,"lng":-56.026497},{"code":"RVY","name":"Pres. Gral. Óscar D. Gestido Binational Airport","city":"Rivera/Santana do Livramento","country":"UY","lat":-30.9746,"lng":-55.4762},{"code":"BLA","name":"General José Antonio Anzoategui International Airport","city":"Barcelona","country":"VE","lat":10.111111,"lng":-64.692222},{"code":"BNS","name":"Barinas Airport","city":"Barinas","country":"VE","lat":8.615,"lng":-70.214167},{"code":"BRM","name":"Jacinto Lara International Airport","city":"Barquisimeto","country":"VE","lat":10.042747,"lng":-69.35862},{"code":"CAJ","name":"Canaima Airport","city":"Canaima","country":"VE","lat":6.231989,"lng":-62.85485},{"code":"CUP","name":"General Francisco Bermúdez Airport","city":"Carúpano","country":"VE","lat":10.660014,"lng":-63.261681},{"code":"CZE","name":"José Leonardo Chirinos Airport","city":"Coro","country":"VE","lat":11.414944,"lng":-69.680901},{"code":"CUM","name":"Antonio José de Sucre Airport","city":"Cumaná","country":"VE","lat":10.450333,"lng":-64.13047},{"code":"ICC","name":"Andrés Miguel Salazar Marcano Airport","city":"Isla de Coche","country":"VE","lat":10.794432,"lng":-63.98159},{"code":"LSP","name":"Josefa Camejo International Airport","city":"Paraguaná","country":"VE","lat":11.780775,"lng":-70.151497},{"code":"MAR","name":"La Chinita International Airport","city":"Maracaibo","country":"VE","lat":10.557542,"lng":-71.729307},{"code":"PMV","name":"Del Caribe Santiago Mariño International Airport","city":"Isla Margarita","country":"VE","lat":10.912603,"lng":-63.966599},{"code":"CCS","name":"Maiquetía Simón Bolívar International Airport","city":"Maiquetía","country":"VE","lat":10.602214,"lng":-66.991174},{"code":"MUN","name":"José Tadeo Monagas International Airport","city":"Maturín","country":"VE","lat":9.749023,"lng":-63.153348},{"code":"PZO","name":"General Manuel Carlos Piar International Airport","city":"Guyana City","country":"VE","lat":8.28853,"lng":-62.760399},{"code":"LRV","name":"Los Roques Airport","city":"Gran Roque Island","country":"VE","lat":11.946935,"lng":-66.668333},{"code":"SVS","name":"Stevens Village Airport","city":"Stevens Village","country":"US","lat":66.016727,"lng":-149.056835},{"code":"SVZ","name":"Juan Vicente Gómez International Airport","city":"San Antonio del Tachira","country":"VE","lat":7.840809,"lng":-72.439878},{"code":"SNV","name":"Santa Elena de Uairén Airport","city":"Santa Elena de Uairén","country":"VE","lat":4.554699,"lng":-61.145234},{"code":"STD","name":"Mayor Buenaventura Vivas International Airport","city":"Santo Domingo","country":"VE","lat":7.56538,"lng":-72.035103},{"code":"SOM","name":"San Tomé Airport","city":"El Tigre","country":"VE","lat":8.9451465606689,"lng":-64.151084899902},{"code":"VLN","name":"Arturo Michelena International Airport","city":"Valencia","country":"VE","lat":10.149733,"lng":-67.928398},{"code":"VIG","name":"Juan Pablo Pérez Alfonso Airport","city":"El Vigía","country":"VE","lat":8.624139,"lng":-71.672668},{"code":"VLV","name":"Dr. Antonio Nicolás Briceño Airport","city":"Valera","country":"VE","lat":9.340478,"lng":-70.584061},{"code":"BAZ","name":"Barcelos Airport","city":"Barcelos","country":"BR","lat":-0.981191,"lng":-62.918603},{"code":"RBB","name":"Borba Airport","city":"Borba","country":"BR","lat":-4.40634,"lng":-59.602402},{"code":"CAF","name":"Carauari Airport","city":"Carauari","country":"BR","lat":-4.871473,"lng":-66.897552},{"code":"AUX","name":"Araguaína Airport","city":"Araguaína","country":"BR","lat":-7.22787,"lng":-48.240501},{"code":"LBR","name":"Lábrea Airport","city":"Lábrea","country":"BR","lat":-7.27897,"lng":-64.769501},{"code":"IRZ","name":"Tapuruquara Airport","city":"Santa Isabel do Rio Negro","country":"BR","lat":-0.378604,"lng":-64.992558},{"code":"TGQ","name":"Tangará da Serra Airport","city":"Tangará da Serra","country":"BR","lat":-14.662,"lng":-57.443744},{"code":"SXP","name":"Nunam Iqua Airport","city":"Nunam Iqua","country":"US","lat":62.520599,"lng":-164.848006},{"code":"SYB","name":"Seal Bay Seaplane Base","city":"Seal Bay","country":"US","lat":58.373291,"lng":-152.201797},{"code":"GEO","name":"Cheddi Jagan International Airport","city":"Georgetown","country":"GY","lat":6.49855,"lng":-58.254101},{"code":"SYF","name":"Silva Bay Seaplane Base","city":"Gabriola Island","country":"CA","lat":49.151,"lng":-123.698},{"code":"OGL","name":"Eugene F. Correia International Airport","city":"Ogle","country":"GY","lat":6.805881,"lng":-58.107657},{"code":"KAI","name":"Kaieteur Airport","city":"Kaieteur Falls","country":"GY","lat":5.177349,"lng":-59.48898},{"code":"LTM","name":"Lethem Airport","city":"Lethem","country":"GY","lat":3.37276,"lng":-59.789398},{"code":"ANU","name":"V. C. Bird International Airport","city":"Osbourn","country":"AG","lat":17.1367,"lng":-61.792702},{"code":"BGI","name":"Grantley Adams International Airport","city":"Bridgetown","country":"BB","lat":13.074667,"lng":-59.491034},{"code":"TCT","name":"Takotna Airport","city":"Takotna","country":"US","lat":62.993206,"lng":-156.029026},{"code":"DCF","name":"Canefield Airport","city":"Canefield","country":"DM","lat":15.336693,"lng":-61.392108},{"code":"DOM","name":"Douglas-Charles Airport","city":"Marigot","country":"DM","lat":15.546673,"lng":-61.301082},{"code":"DSD","name":"La Désirade Airport","city":"Grande Anse","country":"GP","lat":16.296902,"lng":-61.0844},{"code":"BBR","name":"Basse-Terre Baillif Airport","city":"Basse-Terre","country":"GP","lat":16.013648,"lng":-61.742928},{"code":"SFC","name":"St-François Airport","city":"St-François","country":"GP","lat":16.257799,"lng":-61.262501},{"code":"FDF","name":"Martinique Aimé Césaire International Airport","city":"Fort-de-France","country":"MQ","lat":14.591,"lng":-61.003201},{"code":"SFG","name":"Grand Case-l'Espérance Airport","city":"Grand Case","country":"MF","lat":18.100057,"lng":-63.047444},{"code":"SBH","name":"St. Jean Airport","city":"Gustavia","country":"BL","lat":17.904398,"lng":-62.843333},{"code":"GBJ","name":"Marie-Galante Airport","city":"Grand-Bourg","country":"GP","lat":15.868925,"lng":-61.270087},{"code":"PTP","name":"Maryse Condé International Airport","city":"Pointe-à-Pitre","country":"GP","lat":16.265443,"lng":-61.532754},{"code":"GND","name":"Maurice Bishop International Airport","city":"Saint George's","country":"GD","lat":12.003996,"lng":-61.785302},{"code":"CRU","name":"Lauriston Airport","city":"Carriacou Island","country":"GD","lat":12.4761,"lng":-61.472801},{"code":"STT","name":"Cyril E. King Airport","city":"Charlotte Amalie","country":"VI","lat":18.337091,"lng":-64.977251},{"code":"STX","name":"Henry E. Rohlsen Airport","city":"Christiansted","country":"VI","lat":17.701413,"lng":-64.802608},{"code":"BQN","name":"Rafael Hernández International Airport","city":"Aguadilla","country":"PR","lat":18.4949,"lng":-67.129402},{"code":"CPX","name":"Benjamin Rivera Noriega Airport","city":"Culebra","country":"PR","lat":18.312954,"lng":-65.303893},{"code":"SIG","name":"Fernando Luis Ribas Dominicci Airport","city":"San Juan","country":"PR","lat":18.45680046081543,"lng":-66.09809875488281},{"code":"MAZ","name":"Eugenio Maria De Hostos Airport","city":"Mayaguez","country":"PR","lat":18.255699157714844,"lng":-67.14849853515625},{"code":"PSE","name":"Mercedita International Airport","city":"Ponce","country":"PR","lat":18.008301,"lng":-66.563004},{"code":"NRR","name":"José Aponte de la Torre Airport","city":"Ceiba","country":"PR","lat":18.247262,"lng":-65.63979},{"code":"SJU","name":"Luis Munoz Marin International Airport","city":"San Juan","country":"PR","lat":18.4394,"lng":-66.001801},{"code":"VQS","name":"Antonio Rivera Rodriguez Airport","city":"Vieques","country":"PR","lat":18.1348,"lng":-65.493599},{"code":"TKE","name":"Tenakee Seaplane Base","city":"Tenakee Springs","country":"US","lat":57.779701,"lng":-135.218002},{"code":"SKB","name":"Robert L. Bradshaw International Airport","city":"Basseterre","country":"KN","lat":17.310845,"lng":-62.719117},{"code":"NEV","name":"Vance W. Amory International Airport","city":"Charlestown","country":"KN","lat":17.2057,"lng":-62.589901},{"code":"SLU","name":"George F. L. Charles Airport","city":"Castries","country":"LC","lat":14.0202,"lng":-60.992901},{"code":"UVF","name":"Hewanorra International Airport","city":"Vieux Fort","country":"LC","lat":13.7332,"lng":-60.952599},{"code":"TLT","name":"Tuluksak Airport","city":"Tuluksak","country":"US","lat":61.086973,"lng":-160.922842},{"code":"BKN","name":"Balkanabat International Airport","city":"Balkanabat","country":"TM","lat":39.681052,"lng":54.206008},{"code":"AUA","name":"Queen Beatrix International Airport","city":"Oranjestad","country":"AW","lat":12.501056,"lng":-70.014281},{"code":"BON","name":"Flamingo International Airport","city":"Kralendijk","country":"BQ","lat":12.131,"lng":-68.268501},{"code":"CUR","name":"Hato International Airport","city":"Willemstad","country":"CW","lat":12.1889,"lng":-68.959801},{"code":"EUX","name":"F. D. Roosevelt Airport","city":"Oranjestad","country":"BQ","lat":17.4965,"lng":-62.979401},{"code":"SXM","name":"Princess Juliana International Airport","city":"Sint Maarten","country":"SX","lat":18.041,"lng":-63.108898},{"code":"SAB","name":"Juancho E. Yrausquin Airport","city":"Zion's Hill","country":"BQ","lat":17.64533,"lng":-63.220514},{"code":"AXA","name":"Clayton J. Lloyd International Airport","city":"The Valley","country":"AI","lat":18.204773,"lng":-63.05383},{"code":"MNI","name":"John A. Osborne Airport","city":"Gerald's Park","country":"MS","lat":16.791835,"lng":-62.193204},{"code":"TAB","name":"A.N.R. Robinson International Airport","city":"Scarborough","country":"TT","lat":11.149571,"lng":-60.831298},{"code":"POS","name":"Piarco International Airport","city":"Port of Spain","country":"TT","lat":10.59535,"lng":-61.337568},{"code":"TTW","name":"Tissa Tank Waterdrome","city":"Tissamaharama","country":"LK","lat":6.2876,"lng":81.2906},{"code":"EIS","name":"Terrance B. Lettsome International Airport","city":"Beef Island","country":"VG","lat":18.445492,"lng":-64.541707},{"code":"VIJ","name":"Virgin Gorda Airport","city":"Spanish Town","country":"VG","lat":18.446634,"lng":-64.427932},{"code":"SVD","name":"Argyle International Airport","city":"Kingstown","country":"VC","lat":13.159725,"lng":-61.148801},{"code":"BQU","name":"J F Mitchell Airport","city":"Bequia","country":"VC","lat":12.9884,"lng":-61.262001},{"code":"CIW","name":"Canouan Airport","city":"Canouan","country":"VC","lat":12.699,"lng":-61.3424},{"code":"MQS","name":"Mustique Airport","city":"Lovell","country":"VC","lat":12.8879,"lng":-61.180199},{"code":"UNI","name":"Union Island International Airport","city":"Union Island","country":"VC","lat":12.600135,"lng":-61.411945},{"code":"TWC","name":"Tumxuk Tangwangcheng Airport","city":"Tumxuk","country":"CN","lat":39.886663,"lng":79.233408},{"code":"BDA","name":"L.F. Wade International Airport","city":"Hamilton","country":"BM","lat":32.363802,"lng":-64.67824},{"code":"ALA","name":"Almaty International Airport","city":"Almaty","country":"KZ","lat":43.354267,"lng":77.042828},{"code":"BXH","name":"Balkhash Airport","city":"Balkhash","country":"KZ","lat":46.894246,"lng":75.004533},{"code":"USJ","name":"Usharal Airport","city":"Usharal","country":"KZ","lat":46.190767,"lng":80.830922},{"code":"TDK","name":"Taldykorgan Airport","city":"Taldykorgan","country":"KZ","lat":45.12255,"lng":78.442758},{"code":"NQZ","name":"Nursultan Nazarbayev International Airport","city":"Astana","country":"KZ","lat":51.027035,"lng":71.467094},{"code":"KOV","name":"Kokshetau International Airport","city":"Kokshetau","country":"KZ","lat":53.329102,"lng":69.594597},{"code":"PPK","name":"Petropavl International Airport","city":"Petropavl","country":"KZ","lat":54.775622,"lng":69.187415},{"code":"DMB","name":"Taraz International Airport","city":"Taraz","country":"KZ","lat":42.8536,"lng":71.303596},{"code":"BSZ","name":"Manas International Airport","city":"Bishkek","country":"KG","lat":43.061298,"lng":74.4776},{"code":"OSS","name":"Osh International Airport","city":"Osh","country":"KG","lat":40.609001,"lng":72.793297},{"code":"IKG","name":"Karakol International Airport","city":"Karakol","country":"KG","lat":42.508099,"lng":78.407799},{"code":"CIT","name":"Shymkent International Airport","city":"Shymkent","country":"KZ","lat":42.365021,"lng":69.47564},{"code":"HSA","name":"Hazrat Sultan International Airport","city":"Turkıstan","country":"KZ","lat":43.311087,"lng":68.550352},{"code":"DZN","name":"Zhezkazgan National Airport","city":"Zhezkazgan","country":"KZ","lat":47.708953,"lng":67.738094},{"code":"KGF","name":"Sary-Arka Airport","city":"Karaganda","country":"KZ","lat":49.670799,"lng":73.334396},{"code":"BXY","name":"Baikonur Krayniy International Airport","city":"Baikonur","country":"KZ","lat":45.621994,"lng":63.210773},{"code":"KZO","name":"Korkyt Ata International Airport","city":"Kyzylorda","country":"KZ","lat":44.706902,"lng":65.592499},{"code":"URA","name":"Manshuk Mametova International Airport","city":"Uralsk","country":"KZ","lat":51.151976,"lng":51.543652},{"code":"UKK","name":"Oskemen International Airport","city":"Ust-Kamenogorsk (Oskemen)","country":"KZ","lat":50.035033,"lng":82.496057},{"code":"PWQ","name":"Pavlodar International Airport","city":"Pavlodar","country":"KZ","lat":52.194982,"lng":77.073088},{"code":"PLX","name":"Semei International Airport","city":"Semey","country":"KZ","lat":50.351295,"lng":80.234398},{"code":"SZI","name":"Zaysan Airport","city":"Zaysan","country":"KZ","lat":47.487491,"lng":84.887675},{"code":"SCO","name":"Aktau International Airport","city":"Aktau","country":"KZ","lat":43.860093,"lng":51.09086},{"code":"GUW","name":"Atyrau International Airport","city":"Atyrau","country":"KZ","lat":47.121318,"lng":51.820343},{"code":"AKX","name":"Aktobe International Airport","city":"Aktobe","country":"KZ","lat":50.248116,"lng":57.204144},{"code":"KSN","name":"Kostanay International Airport","city":"Kostanay","country":"KZ","lat":53.206902,"lng":63.550301},{"code":"GYD","name":"Heydar Aliyev International Airport","city":"Baku","country":"AZ","lat":40.47278,"lng":50.050931},{"code":"GNJ","name":"Ganja International Airport","city":"Ganja","country":"AZ","lat":40.738692,"lng":46.320383},{"code":"LLK","name":"Lankaran International Airport","city":"Lankaran","country":"AZ","lat":38.757919,"lng":48.807042},{"code":"NAJ","name":"Nakhchivan International Airport","city":"Nakhchivan","country":"AZ","lat":39.188801,"lng":45.458401},{"code":"GBB","name":"Gabala International Airport","city":"Gabala","country":"AZ","lat":40.808617,"lng":47.725389},{"code":"LHL","name":"Lachin International Airport","city":"Lachin","country":"AZ","lat":39.883787,"lng":46.363137},{"code":"IKU","name":"Issyk-Kul International Airport","city":"Tamchy","country":"KG","lat":42.585584,"lng":76.701181},{"code":"LWN","name":"Shirak International Airport","city":"Gyumri","country":"AM","lat":40.750401,"lng":43.859299},{"code":"EVN","name":"Zvartnots International Airport","city":"Yerevan","country":"AM","lat":40.148941,"lng":44.397887},{"code":"BQJ","name":"Batagay Airport","city":"Batagay","country":"RU","lat":67.648003,"lng":134.695007},{"code":"SUK","name":"Sakkyryr Airport","city":"Batagay-Alyta","country":"RU","lat":67.792,"lng":130.394},{"code":"UKG","name":"Ust-Kuyga Airport","city":"Ust-Kuyga","country":"RU","lat":70.011002,"lng":135.645004},{"code":"YKS","name":"Platon Oyunsky Yakutsk International Airport","city":"Yakutsk","country":"RU","lat":62.0933,"lng":129.770996},{"code":"NER","name":"Chulman Airport","city":"Neryungri","country":"RU","lat":56.913898468018,"lng":124.91400146484},{"code":"MQJ","name":"Moma Airport","city":"Khonuu","country":"RU","lat":66.450861,"lng":143.261551},{"code":"OLZ","name":"Olyokminsk Airport","city":"Olyokminsk","country":"RU","lat":60.401833,"lng":120.476094},{"code":"USR","name":"Ust-Nera Airport","city":"Ust-Nera","country":"RU","lat":64.550003,"lng":143.115005},{"code":"UMS","name":"Ust-Maya Airport","city":"Ust-Maya","country":"RU","lat":60.356998443604,"lng":134.43499755859},{"code":"VHV","name":"Verkhnevilyuisk Airport","city":"Verkhnevilyuisk","country":"RU","lat":63.458057403564,"lng":120.26916503906},{"code":"SUY","name":"Suntar Airport","city":"Suntar","country":"RU","lat":62.185001373291,"lng":117.63500213623},{"code":"VYI","name":"Vilyuisk Airport","city":"Vilyuisk","country":"RU","lat":63.756668,"lng":121.693336},{"code":"ULK","name":"Lensk Airport","city":"Lensk","country":"RU","lat":60.7206001282,"lng":114.825996399},{"code":"PYJ","name":"Polyarny Airport","city":"Yakutia","country":"RU","lat":66.400398,"lng":112.029999},{"code":"MJZ","name":"Mirny Airport","city":"Mirny","country":"RU","lat":62.534698486328125,"lng":114.03900146484375},{"code":"SYS","name":"Saskylakh Airport","city":"Saskylakh","country":"RU","lat":71.927902,"lng":114.080002},{"code":"SEK","name":"Srednekolymsk Airport","city":"Srednekolymsk","country":"RU","lat":67.4805,"lng":153.7364},{"code":"CKH","name":"Chokurdakh Airport","city":"Chokurdah","country":"RU","lat":70.6231,"lng":147.901993},{"code":"CYX","name":"Cherskiy Airport","city":"Cherskiy","country":"RU","lat":68.7406005859375,"lng":161.33799743652344},{"code":"IKS","name":"Tiksi Airport","city":"Tiksi","country":"RU","lat":71.697700500488,"lng":128.90299987793},{"code":"ZKP","name":"Zyryanka Airport","city":"Zyryanka","country":"RU","lat":65.7485,"lng":150.8889},{"code":"ZIX","name":"Zhigansk Airport","city":"Zhigansk","country":"RU","lat":66.7965011597,"lng":123.361000061},{"code":"KXO","name":"Kisoro Airport","city":"Kisoro","country":"UG","lat":-1.2837,"lng":29.7197},{"code":"KHX","name":"Savannah Airstrip","city":"Kihihi","country":"UG","lat":-0.7165,"lng":29.6997},{"code":"KUT","name":"David the Builder Kutaisi International Airport","city":"Kopitnari","country":"GE","lat":42.177448,"lng":42.485353},{"code":"BUS","name":"Alexander Kartveli Batumi International Airport","city":"Batumi","country":"GE","lat":41.60939,"lng":41.600315},{"code":"SUI","name":"Vladislav Ardzinba Sukhum International Airport","city":"Sukhumi","country":"GE","lat":42.8582,"lng":41.128101},{"code":"TBS","name":"Tbilisi International Airport","city":"Tbilisi","country":"GE","lat":41.669201,"lng":44.9547},{"code":"BQS","name":"Ignatyevo Airport","city":"Blagoveschensk","country":"RU","lat":50.42539978027344,"lng":127.41200256347656},{"code":"KHV","name":"Khabarovsk Novy Airport","city":"Khabarovsk","country":"RU","lat":48.528338,"lng":135.188588},{"code":"KXK","name":"Komsomolsk-on-Amur Airport","city":"Komsomolsk-on-Amur","country":"RU","lat":50.409000396728516,"lng":136.9340057373047},{"code":"DYR","name":"Ugolny Yuri Ryktheu Airport","city":"Anadyr","country":"RU","lat":64.734902,"lng":177.740997},{"code":"KPW","name":"Keperveem Airport","city":"Keperveem","country":"RU","lat":67.845001,"lng":166.139999},{"code":"GDX","name":"Sokol Airport","city":"Magadan","country":"RU","lat":59.9109992980957,"lng":150.72000122070312},{"code":"KVM","name":"Markovo Airport","city":"Markovo","country":"RU","lat":64.665446,"lng":170.417626},{"code":"PWE","name":"Pevek Airport","city":"Apapelgino","country":"RU","lat":69.783302,"lng":170.597},{"code":"BQG","name":"Bogorodskoye Airport","city":"Bogorodskoye","country":"RU","lat":52.381041,"lng":140.449378},{"code":"NLI","name":"Nikolayevsk-na-Amure Airport","city":"Nikolayevsk-na-Amure Airport","country":"RU","lat":53.154999,"lng":140.649994},{"code":"OHO","name":"Okhotsk Airport","city":"Okhotsk","country":"RU","lat":59.410065,"lng":143.056503},{"code":"PKC","name":"Yelizovo Airport","city":"Petropavlovsk-Kamchatsky","country":"RU","lat":53.168716,"lng":158.451068},{"code":"ITU","name":"Iturup Airport","city":"Kurilsk","country":"RU","lat":45.256389,"lng":147.95549},{"code":"EKS","name":"Shakhtyorsk Airport","city":"Shakhtyorsk","country":"RU","lat":49.1903,"lng":142.082993},{"code":"DEE","name":"Yuzhno-Kurilsk Mendeleyevo Airport","city":"Yuzhno-Kurilsk","country":"RU","lat":43.961066,"lng":145.684977},{"code":"NGK","name":"Nogliki Airport","city":"Nogliki","country":"RU","lat":51.783955,"lng":143.141785},{"code":"UUS","name":"Yuzhno-Sakhalinsk International Airport","city":"Yuzhno-Sakhalinsk","country":"RU","lat":46.885461,"lng":142.717466},{"code":"TLY","name":"Plastun Airport","city":"Plastun","country":"RU","lat":44.814999,"lng":136.292007},{"code":"VVO","name":"Vladivostok International Airport","city":"Artyom","country":"RU","lat":43.396256,"lng":132.148155},{"code":"HTA","name":"Chita-Kadala International Airport","city":"Chita","country":"RU","lat":52.024841,"lng":113.305839},{"code":"BTK","name":"Bratsk Airport","city":"Bratsk","country":"RU","lat":56.3696,"lng":101.701822},{"code":"IKT","name":"Irkutsk International Airport","city":"Irkutsk","country":"RU","lat":52.266707,"lng":104.395563},{"code":"ODO","name":"Bodaybo Airport","city":"Bodaybo","country":"RU","lat":57.866100311299995,"lng":114.242996216},{"code":"UKX","name":"Ust-Kut Airport","city":"Ust-Kut","country":"RU","lat":56.856701,"lng":105.730003},{"code":"NZG","name":"Nizhneangarsk International Airport","city":"Nizhneangarsk","country":"RU","lat":55.8008,"lng":109.595001},{"code":"UUD","name":"Baikal International Airport","city":"Ulan Ude","country":"RU","lat":51.808614,"lng":107.439652},{"code":"MPW","name":"Mariupol International Airport","city":"Mariupol","country":"UA","lat":47.07609939575195,"lng":37.44960021972656},{"code":"DNK","name":"Dnipro International Airport","city":"Dnipro","country":"UA","lat":48.357201,"lng":35.100601},{"code":"KWG","name":"Kryvyi Rih International Airport","city":"Kryvyi Rih","country":"UA","lat":48.04330062866211,"lng":33.209999084472656},{"code":"SIP","name":"Simferopol International Airport","city":"Simferopol","country":"UA","lat":45.0522,"lng":33.975101},{"code":"IFO","name":"Ivano-Frankivsk International Airport","city":"Ivano-Frankivsk","country":"UA","lat":48.88420104980469,"lng":24.686100006103516},{"code":"LWO","name":"Lviv International Airport","city":"Lviv","country":"UA","lat":49.8125,"lng":23.9561},{"code":"CWC","name":"Chernivtsi International Airport","city":"Chernivtsi","country":"UA","lat":48.259300231933594,"lng":25.98080062866211},{"code":"RWN","name":"Rivne International Airport","city":"Rivne","country":"UA","lat":50.60710144042969,"lng":26.141599655151367},{"code":"UDJ","name":"Uzhhorod International Airport","city":"Uzhhorod","country":"UA","lat":48.6343,"lng":22.263399},{"code":"KHE","name":"Kherson International Airport","city":"Kherson","country":"UA","lat":46.6758,"lng":32.506401},{"code":"ARH","name":"Talagi Airport","city":"Archangelsk","country":"RU","lat":64.60030364990234,"lng":40.71670150756836},{"code":"LDG","name":"Leshukonskoye Airport","city":"Leshukonskoye","country":"RU","lat":64.896004,"lng":45.723},{"code":"NNM","name":"Naryan Mar Airport","city":"Naryan Mar","country":"RU","lat":67.639999,"lng":53.121899},{"code":"CSH","name":"Solovki Airport","city":"Solovetsky Islands","country":"RU","lat":65.029996,"lng":35.730558},{"code":"AMV","name":"Amderma Airport","city":"Amderma","country":"RU","lat":69.76329803466797,"lng":61.556400299072266},{"code":"KSZ","name":"Kotlas Airport","city":"Kotlas","country":"RU","lat":61.235801696777344,"lng":46.6974983215332},{"code":"LED","name":"Pulkovo Airport","city":"St. Petersburg","country":"RU","lat":59.800301,"lng":30.262501},{"code":"MMK","name":"Emperor Nicholas II Murmansk Airport","city":"Murmansk","country":"RU","lat":68.7817,"lng":32.750801},{"code":"PKV","name":"Princess Olga Pskov International Airport","city":"Pskov","country":"RU","lat":57.781316,"lng":28.39384},{"code":"PES","name":"Petrozavodsk Airport","city":"Petrozavodsk","country":"RU","lat":61.885201,"lng":34.154701},{"code":"CEE","name":"Cherepovets Airport","city":"Cherepovets","country":"RU","lat":59.273601532,"lng":38.015800476100004},{"code":"VUS","name":"Velikiy Ustyug Airport","city":"Velikiy Ustyug","country":"RU","lat":60.7883,"lng":46.259998},{"code":"BQT","name":"Brest International Airport","city":"Brest","country":"BY","lat":52.108138,"lng":23.89676},{"code":"GME","name":"Gomel Airport","city":"Gomel","country":"BY","lat":52.527000427246094,"lng":31.016700744628906},{"code":"KGD","name":"Khrabrovo Airport","city":"Kaliningrad","country":"RU","lat":54.89161,"lng":20.598559},{"code":"MSQ","name":"Minsk National Airport","city":"Minsk","country":"BY","lat":53.888071,"lng":28.039964},{"code":"MVQ","name":"Mogilev Airport","city":"Mogilev","country":"BY","lat":53.954898834228516,"lng":30.09510040283203},{"code":"ABA","name":"Abakan International Airport","city":"Abakan","country":"RU","lat":53.740002,"lng":91.385002},{"code":"BAX","name":"Barnaul Gherman Titov International Airport","city":"Barnaul","country":"RU","lat":53.361285,"lng":83.539701},{"code":"KEJ","name":"Alexei Leonov Kemerovo International Airport","city":"Kemerovo","country":"RU","lat":55.2701,"lng":86.107201},{"code":"EIE","name":"Yeniseysk Airport","city":"Yeniseysk","country":"RU","lat":58.474201,"lng":92.112503},{"code":"VEO","name":"Severo-Yeniseysk Airport","city":"Severo-Yeniseysk","country":"RU","lat":60.373299,"lng":93.011703},{"code":"GOY","name":"Tura Mountain Airport","city":"Tura","country":"RU","lat":64.333511,"lng":100.432892},{"code":"VAQ","name":"Vanavara Airport","city":"Vanavara","country":"RU","lat":60.356229,"lng":102.309641},{"code":"KJA","name":"Krasnoyarsk International Airport","city":"Krasnoyarsk","country":"RU","lat":56.175743,"lng":92.485788},{"code":"KCY","name":"Krasnoyarsk Cheremshanka Airport","city":"Krasnoyarsk","country":"RU","lat":56.177584,"lng":92.545881},{"code":"KYZ","name":"Kyzyl Airport","city":"Kyzyl","country":"RU","lat":51.669399,"lng":94.400597},{"code":"OVB","name":"Novosibirsk Tolmachevo Airport","city":"Novosibirsk","country":"RU","lat":55.019756,"lng":82.618675},{"code":"OMS","name":"Omsk Central Airport","city":"Omsk","country":"RU","lat":54.963124,"lng":73.312418},{"code":"TOF","name":"Tomsk Kamov Airport","city":"Tomsk","country":"RU","lat":56.380299,"lng":85.208298},{"code":"NOZ","name":"Spichenkovo Airport","city":"Novokuznetsk","country":"RU","lat":53.811401,"lng":86.877197},{"code":"HTG","name":"Khatanga Airport","city":"Khatanga","country":"RU","lat":71.97810363769531,"lng":102.49099731445312},{"code":"IAA","name":"Igarka Airport","city":"Igarka","country":"RU","lat":67.437202,"lng":86.621902},{"code":"NSK","name":"Alykel International Airport","city":"Norilsk","country":"RU","lat":69.307951,"lng":87.325906},{"code":"THX","name":"Turukhansk Airport","city":"Turukhansk","country":"RU","lat":65.797203,"lng":87.935303},{"code":"AAQ","name":"Anapa Vityazevo Airport","city":"Krasnyi Kurgan","country":"RU","lat":45.002102,"lng":37.347301},{"code":"GDZ","name":"Gelendzhik Airport","city":"Gelendzhik","country":"RU","lat":44.5820926295,"lng":38.0124807358},{"code":"KRR","name":"Krasnodar Pashkovsky International Airport","city":"Krasnodar","country":"RU","lat":45.034465,"lng":39.174215},{"code":"GRV","name":"Akhmat Kadyrov Grozny International Airport","city":"Grozny","country":"RU","lat":43.388106,"lng":45.699772},{"code":"MCX","name":"Makhachkala Uytash International Airport","city":"Makhachkala","country":"RU","lat":42.816799,"lng":47.652302},{"code":"MRV","name":"Mineralnye Vody Airport","city":"Mineralnyye Vody","country":"RU","lat":44.225101,"lng":43.081902},{"code":"NAL","name":"Nalchik Airport","city":"Nalchik","country":"RU","lat":43.512901,"lng":43.6366},{"code":"OGZ","name":"Vladikavkaz Beslan International Airport","city":"Beslan","country":"RU","lat":43.205101,"lng":44.606602},{"code":"IGT","name":"Magas Airport","city":"Sunzha","country":"RU","lat":43.323268,"lng":45.012568},{"code":"STW","name":"Stavropol Shpakovskoye Airport","city":"Stavropol","country":"RU","lat":45.10919952392578,"lng":42.11280059814453},{"code":"ROV","name":"Platov International Airport","city":"Rostov-on-Don","country":"RU","lat":47.493888,"lng":39.924722},{"code":"TGK","name":"Taganrog Yuzhny Airport","city":"Taganrog","country":"RU","lat":47.1983333,"lng":38.8491667},{"code":"AER","name":"Sochi International Airport","city":"Sochi","country":"RU","lat":43.449902,"lng":39.9566},{"code":"ASF","name":"Astrakhan Narimanovo Boris M. Kustodiev International Airport","city":"Astrakhan","country":"RU","lat":46.282843,"lng":48.010511},{"code":"ESL","name":"Elista Airport","city":"Elista","country":"RU","lat":46.373901,"lng":44.330898},{"code":"VOG","name":"Volgograd International Airport","city":"Volgograd","country":"RU","lat":48.78134,"lng":44.339194},{"code":"PQS","name":"Pilot Station Airport","city":"Pilot Station","country":"US","lat":61.96169,"lng":-162.941944},{"code":"CEK","name":"Kurchatov Chelyabinsk International Airport","city":"Chelyabinsk","country":"RU","lat":55.303141,"lng":61.504927},{"code":"MQF","name":"Magnitogorsk International Airport","city":"Magnitogorsk","country":"RU","lat":53.391991,"lng":58.755235},{"code":"SBT","name":"Sabetta International Airport","city":"Sabetta","country":"RU","lat":71.219167,"lng":72.052222},{"code":"BVJ","name":"Bovanenkovo Airport","city":"Bovanenkovo","country":"RU","lat":70.315278,"lng":68.333611},{"code":"SLY","name":"Salekhard Airport","city":"Salekhard","country":"RU","lat":66.5907974243164,"lng":66.61100006103516},{"code":"HMA","name":"Khanty Mansiysk Airport","city":"Khanty-Mansiysk","country":"RU","lat":61.0285,"lng":69.086098},{"code":"NYA","name":"Nyagan Airport","city":"Nyagan","country":"RU","lat":62.110001,"lng":65.614998},{"code":"EYK","name":"Beloyarskiy Airport","city":"","country":"RU","lat":63.686901,"lng":66.698601},{"code":"OVS","name":"Sovetskiy Airport","city":"Sovetskiy","country":"RU","lat":61.326622009277344,"lng":63.60191345214844},{"code":"URJ","name":"Uray Airport","city":"Uray","country":"RU","lat":60.103298,"lng":64.826698},{"code":"IJK","name":"Izhevsk Airport","city":"Izhevsk","country":"RU","lat":56.834524,"lng":53.462172},{"code":"KVX","name":"Pobedilovo Airport","city":"Kirov","country":"RU","lat":58.503883,"lng":49.347831},{"code":"NYM","name":"Nadym Airport","city":"Nadym","country":"RU","lat":65.48090362548828,"lng":72.69889831542969},{"code":"NUX","name":"Novy Urengoy Airport","city":"Novy Urengoy","country":"RU","lat":66.06939697265625,"lng":76.52030181884766},{"code":"NJC","name":"Nizhnevartovsk Airport","city":"Nizhnevartovsk","country":"RU","lat":60.949299,"lng":76.483597},{"code":"PEE","name":"Perm International Airport","city":"Perm","country":"RU","lat":57.914501,"lng":56.021198},{"code":"KGP","name":"Kogalym International Airport","city":"Kogalym","country":"RU","lat":62.190399169921875,"lng":74.53379821777344},{"code":"NFG","name":"Nefteyugansk Airport","city":"Nefteyugansk","country":"RU","lat":61.108299,"lng":72.650002},{"code":"NOJ","name":"Noyabrsk Airport","city":"Noyabrsk","country":"RU","lat":63.18330001831055,"lng":75.2699966430664},{"code":"SGC","name":"Surgut International Airport","city":"Surgut","country":"RU","lat":61.340501,"lng":73.405782},{"code":"SVX","name":"Koltsovo Airport","city":"Yekaterinburg","country":"RU","lat":56.743099212646,"lng":60.802700042725},{"code":"RMZ","name":"Tobolsk Remezov Airport","city":"Tobolsk","country":"RU","lat":58.059712,"lng":68.347997},{"code":"TJM","name":"Roshchino International Airport","city":"Tyumen","country":"RU","lat":57.178984,"lng":65.327696},{"code":"KRO","name":"Kurgan Airport","city":"Kurgan","country":"RU","lat":55.4753,"lng":65.415604},{"code":"ASB","name":"Ashgabat International Airport","city":"Ashgabat","country":"TM","lat":37.986801,"lng":58.361},{"code":"KRW","name":"Turkmenbaşy International Airport","city":"Turkmenbaşy","country":"TM","lat":40.062833,"lng":53.005085},{"code":"MYP","name":"Mary International Airport","city":"Mary","country":"TM","lat":37.62353,"lng":61.895668},{"code":"TAZ","name":"Dashoguz International Airport","city":"Daşoguz","country":"TM","lat":41.759853,"lng":59.836149},{"code":"CRZ","name":"Türkmenabat International Airport","city":"Türkmenabat","country":"TM","lat":38.930662,"lng":63.563982},{"code":"DYU","name":"Dushanbe International Airport","city":"Dushanbe","country":"TJ","lat":38.543746,"lng":68.822977},{"code":"TJU","name":"Kulob International Airport","city":"Kulob","country":"TJ","lat":37.988098,"lng":69.805},{"code":"LBD","name":"Khujand International Airport","city":"Khujand","country":"TJ","lat":40.215401,"lng":69.694702},{"code":"KQT","name":"Bokhtar International Airport","city":"Bokhtar","country":"TJ","lat":37.866315,"lng":68.864475},{"code":"NMA","name":"Namangan International Airport","city":"Namangan","country":"UZ","lat":40.984622,"lng":71.5578},{"code":"AZN","name":"Andijan International Airport","city":"Andijan","country":"UZ","lat":40.727699,"lng":72.293999},{"code":"FEG","name":"Fergana International Airport","city":"Fergana","country":"UZ","lat":40.358799,"lng":71.745003},{"code":"NCU","name":"Nukus International Airport","city":"Nukus","country":"UZ","lat":42.4884,"lng":59.623299},{"code":"UGC","name":"Urgench International Airport","city":"Urgench","country":"UZ","lat":41.58274,"lng":60.64338},{"code":"NVI","name":"Navoi International Airport","city":"Navoi","country":"UZ","lat":40.117599,"lng":65.172658},{"code":"BHK","name":"Bukhara International Airport","city":"Bukhara","country":"UZ","lat":39.775319,"lng":64.482279},{"code":"KSQ","name":"Karshi Airport","city":"Karshi","country":"UZ","lat":38.802246,"lng":65.773068},{"code":"SKD","name":"Samarkand International Airport","city":"Samarkand","country":"UZ","lat":39.701842,"lng":66.981467},{"code":"TMJ","name":"Termez Airport","city":"Termez","country":"UZ","lat":37.287261,"lng":67.311869},{"code":"TVT","name":"Tashkent-Khumo International Airport","city":"Tashkent","country":"UZ","lat":41.312889,"lng":69.395535},{"code":"TAS","name":"Tashkent International Airport","city":"Tashkent","country":"UZ","lat":41.2579,"lng":69.281197},{"code":"OMN","name":"Zomin Airport","city":"Zomin","country":"UZ","lat":40.01402,"lng":68.41103},{"code":"KMW","name":"Kostroma Sokerkino Airport","city":"Kostroma","country":"RU","lat":57.796902,"lng":41.019402},{"code":"IWA","name":"Ivanovo South Airport","city":"Ivanovo","country":"RU","lat":56.93939971923828,"lng":40.940799713134766},{"code":"RYB","name":"Staroselye Airport","city":"Rybinsk","country":"RU","lat":58.104198,"lng":38.929401},{"code":"BZK","name":"Bryansk International Airport","city":"Bryansk","country":"RU","lat":53.214403,"lng":34.175965},{"code":"ZIA","name":"Zhukovsky International Airport","city":"Moscow","country":"RU","lat":55.553299,"lng":38.150002},{"code":"DME","name":"Domodedovo International Airport","city":"Moscow","country":"RU","lat":55.408798,"lng":37.9063},{"code":"IAR","name":"Golden Ring Yaroslavl International Airport","city":"Tunoshna","country":"RU","lat":57.560699,"lng":40.157398},{"code":"SVO","name":"Sheremetyevo International Airport","city":"Moscow","country":"RU","lat":55.976858,"lng":37.41121},{"code":"EGO","name":"Belgorod International Airport","city":"Belgorod","country":"RU","lat":50.643798828125,"lng":36.5900993347168},{"code":"URS","name":"Kursk East Airport","city":"Kursk","country":"RU","lat":51.7505989074707,"lng":36.29560089111328},{"code":"LPK","name":"Lipetsk Airport","city":"Lipetsk","country":"RU","lat":52.70280075073242,"lng":39.53779983520508},{"code":"VOZ","name":"Voronezh International Airport","city":"Voronezh","country":"RU","lat":51.814324,"lng":39.230948},{"code":"VKO","name":"Vnukovo International Airport","city":"Moscow","country":"RU","lat":55.591499,"lng":37.261501},{"code":"UCT","name":"Ukhta Airport","city":"Ukhta","country":"RU","lat":63.566898,"lng":53.804699},{"code":"PEX","name":"Pechora Airport","city":"Pechora","country":"RU","lat":65.121101,"lng":57.130798},{"code":"USK","name":"Usinsk Airport","city":"Usinsk","country":"RU","lat":66.0047,"lng":57.367199},{"code":"VKT","name":"Vorkuta Airport","city":"Vorkuta","country":"RU","lat":67.488602,"lng":63.993099},{"code":"SCW","name":"Syktyvkar Airport","city":"Syktyvkar","country":"RU","lat":61.64699935913086,"lng":50.84510040283203},{"code":"GOJ","name":"Nizhny Novgorod / Strigino International Airport","city":"Nizhny Novgorod","country":"RU","lat":56.227351,"lng":43.785152},{"code":"UUA","name":"Bugulma Airport","city":"Bugulma","country":"RU","lat":54.641184,"lng":52.800229},{"code":"KZN","name":"Kazan International Airport","city":"Kazan","country":"RU","lat":55.606201,"lng":49.278702},{"code":"NBC","name":"Begishevo Airport","city":"Nizhnekamsk","country":"RU","lat":55.564701,"lng":52.092499},{"code":"CSY","name":"Cheboksary Airport","city":"Cheboksary","country":"RU","lat":56.090302,"lng":47.347301},{"code":"ULV","name":"Ulyanovsk Baratayevka Airport","city":"Ulyanovsk","country":"RU","lat":54.27016,"lng":48.225569},{"code":"ULY","name":"Ulyanovsk Vostochny Airport","city":"Cherdakly","country":"RU","lat":54.401001,"lng":48.8027},{"code":"REN","name":"Orenburg Central Airport","city":"Orenburg","country":"RU","lat":51.792668,"lng":55.457229},{"code":"OSW","name":"Orsk Airport","city":"Orsk","country":"RU","lat":51.072498,"lng":58.5956},{"code":"PEZ","name":"Penza Airport","city":"Penza","country":"RU","lat":53.1106,"lng":45.021099},{"code":"SKX","name":"Saransk International Airport","city":"Saransk","country":"RU","lat":54.12513,"lng":45.212257},{"code":"BWO","name":"Balakovo Airport","city":"Balakovo","country":"RU","lat":51.858299,"lng":47.745602},{"code":"GSV","name":"Gagarin International Airport","city":"Saratov","country":"RU","lat":51.712778,"lng":46.171111},{"code":"UFA","name":"Ufa International Airport","city":"Ufa","country":"RU","lat":54.557498931885,"lng":55.874401092529},{"code":"KUF","name":"Kurumoch International Airport","city":"Samara","country":"RU","lat":53.504902,"lng":50.164299},{"code":"UZR","name":"Urzhar Airport","city":"Urzhar","country":"KZ","lat":47.091786,"lng":81.668216},{"code":"REW","name":"Rewa Airport, Chorhata, REWA","city":"Rewa","country":"IN","lat":24.503401,"lng":81.220299},{"code":"DIU","name":"Diu Airport","city":"Diu","country":"IN","lat":20.714185,"lng":70.921855},{"code":"GDB","name":"Gondia Airport","city":"Gondia","country":"IN","lat":21.526817,"lng":80.290347},{"code":"AMD","name":"Sardar Vallabh Patel International Airport","city":"Ahmedabad","country":"IN","lat":23.0772,"lng":72.634697},{"code":"IXU","name":"Aurangabad Airport","city":"Aurangabad","country":"IN","lat":19.862875,"lng":75.396312},{"code":"BOM","name":"Chhatrapati Shivaji Maharaj International Airport","city":"Mumbai","country":"IN","lat":19.088699,"lng":72.867897},{"code":"BHJ","name":"Bhuj Airport","city":"Bhuj","country":"IN","lat":23.2877998352,"lng":69.6701965332},{"code":"IXG","name":"Belagavi Airport","city":"Belgaum","country":"IN","lat":15.8593,"lng":74.618301},{"code":"BDQ","name":"Vadodara International Airport","city":"Vadodara","country":"IN","lat":22.336201,"lng":73.226303},{"code":"BHO","name":"Raja Bhoj International Airport","city":"Bhopal","country":"IN","lat":23.2875,"lng":77.337402},{"code":"BHU","name":"Bhavnagar Airport","city":"Bhavnagar","country":"IN","lat":21.752199173,"lng":72.1852035522},{"code":"HBX","name":"Hubballi Airport","city":"Hubballi","country":"IN","lat":15.361084,"lng":75.082096},{"code":"HSR","name":"Rajkot International Airport","city":"Rajkot","country":"IN","lat":22.378824,"lng":71.039391},{"code":"IDR","name":"Devi Ahilya Bai Holkar International Airport","city":"Indore","country":"IN","lat":22.721404,"lng":75.80051},{"code":"JLR","name":"Jabalpur Airport","city":"Jabalpur","country":"IN","lat":23.177799,"lng":80.052002},{"code":"JLG","name":"Jalgaon Airport","city":"Jalgaon","country":"IN","lat":20.962678,"lng":75.627492},{"code":"JGA","name":"Jamnagar Airport","city":"Jamnagar","country":"IN","lat":22.465499877929688,"lng":70.01260375976562},{"code":"IXY","name":"Kandla Airport","city":"Kandla","country":"IN","lat":23.1127,"lng":70.100304},{"code":"HJR","name":"Khajuraho Airport","city":"Khajuraho","country":"IN","lat":24.8172,"lng":79.918602},{"code":"KLH","name":"Kolhapur Airport","city":"Kolhapur","country":"IN","lat":16.6647,"lng":74.289398},{"code":"IXK","name":"Keshod Airport","city":"Keshod","country":"IN","lat":21.317101,"lng":70.270401},{"code":"LTU","name":"Murod Kond Airport","city":"Latur","country":"IN","lat":18.411501,"lng":76.464699},{"code":"NDC","name":"Nanded Airport","city":"Nanded","country":"IN","lat":19.1833,"lng":77.316704},{"code":"NAG","name":"Dr. Babasaheb Ambedkar International Airport","city":"Nagpur","country":"IN","lat":21.092199,"lng":79.047203},{"code":"ISK","name":"Nashik International Airport","city":"Nashik","country":"IN","lat":20.119101,"lng":73.912903},{"code":"PNQ","name":"Pune International Airport","city":"Pune","country":"IN","lat":18.5821,"lng":73.919701},{"code":"PBD","name":"Porbandar Airport","city":"Porbandar","country":"IN","lat":21.649524,"lng":69.656405},{"code":"RPR","name":"Swami Vivekananda Airport","city":"Raipur","country":"IN","lat":21.180401,"lng":81.7388},{"code":"SAG","name":"Shirdi International Airport","city":"Kakadi","country":"IN","lat":19.689211,"lng":74.373655},{"code":"STV","name":"Surat International Airport","city":"Surat","country":"IN","lat":21.115531,"lng":72.743251},{"code":"UDR","name":"Maharana Pratap Airport","city":"Udaipur","country":"IN","lat":24.617700576799997,"lng":73.89610290530001},{"code":"CMB","name":"Bandaranaike International Colombo Airport","city":"Colombo","country":"LK","lat":7.180759906768799,"lng":79.88410186767578},{"code":"BTC","name":"Batticaloa International Airport","city":"Batticaloa","country":"LK","lat":7.705073,"lng":81.677163},{"code":"RML","name":"Colombo Ratmalana International Airport","city":"Colombo","country":"LK","lat":6.821638,"lng":79.885859},{"code":"JAF","name":"Jaffna International Airport","city":"Jaffna","country":"LK","lat":9.79233,"lng":80.070099},{"code":"KCT","name":"Koggala Airport","city":"Galle","country":"LK","lat":5.993680000305176,"lng":80.32029724121094},{"code":"TRR","name":"China Bay Airport","city":"Trincomalee","country":"LK","lat":8.539159,"lng":81.18128},{"code":"HRI","name":"Mattala Rajapaksa International Airport","city":"Mattala","country":"LK","lat":6.283878,"lng":81.124163},{"code":"BBM","name":"Battambang Airport","city":"Battambang","country":"KH","lat":13.0956,"lng":103.223999},{"code":"SAI","name":"Siem Reap-Angkor International Airport","city":"Siem Reap","country":"KH","lat":13.36974,"lng":104.223831},{"code":"KOS","name":"Sihanouk International Airport","city":"Preah Sihanouk","country":"KH","lat":10.570557,"lng":103.632067},{"code":"IXA","name":"Agartala - Maharaja Bir Bikram Airport","city":"Agartala","country":"IN","lat":23.886999,"lng":91.240402},{"code":"IXB","name":"Bagdogra Airport","city":"Siliguri","country":"IN","lat":26.6812,"lng":88.328598},{"code":"SHL","name":"Shillong Airport","city":"Shillong","country":"IN","lat":25.70359992980957,"lng":91.97869873046875},{"code":"VNS","name":"Lal Bahadur Shastri International Airport","city":"Varanasi","country":"IN","lat":25.452171,"lng":82.862549},{"code":"BBI","name":"Biju Patnaik International Airport","city":"Bhubaneswar","country":"IN","lat":20.251021,"lng":85.814747},{"code":"PAB","name":"Bilaspur Airport","city":"Bilaspur","country":"IN","lat":21.9884,"lng":82.111},{"code":"CCU","name":"Netaji Subhash Chandra Bose International Airport","city":"Kolkata","country":"IN","lat":22.654012,"lng":88.44765},{"code":"RDP","name":"Kazi Nazrul Islam Airport","city":"Durgapur","country":"IN","lat":23.6225,"lng":87.243},{"code":"DBR","name":"Darbhanga Airport","city":"Darbhanga","country":"IN","lat":26.192801,"lng":85.916901},{"code":"DGH","name":"Deoghar Airport","city":"Deoghar","country":"IN","lat":24.446842,"lng":86.704955},{"code":"GOP","name":"Gorakhpur Airport","city":"Gorakhpur","country":"IN","lat":26.739700317399997,"lng":83.4496994019},{"code":"GAU","name":"Lokpriya Gopinath Bordoloi International Airport","city":"Guwahati","country":"IN","lat":26.106654,"lng":91.585226},{"code":"GAY","name":"Gaya Airport","city":"Gaya","country":"IN","lat":24.744301,"lng":84.951202},{"code":"IMF","name":"Bir Tikendrajit International Airport","city":"Imphal","country":"IN","lat":24.76,"lng":93.896698},{"code":"JRG","name":"Jharsuguda Airport","city":"","country":"IN","lat":21.9135,"lng":84.0504},{"code":"JRH","name":"Jorhat Airport","city":"Jorhat","country":"IN","lat":26.730456,"lng":94.175416},{"code":"IXS","name":"Silchar Airport","city":"Silchar","country":"IN","lat":24.9129009247,"lng":92.97869873050001},{"code":"AJL","name":"Lengpui Airport","city":"Aizawl (Lengpui)","country":"IN","lat":23.840599,"lng":92.619698},{"code":"IXI","name":"Lilabari North Lakhimpur Airport","city":"Lilabari","country":"IN","lat":27.295682,"lng":94.097266},{"code":"DIB","name":"Dibrugarh Airport","city":"Dibrugarh","country":"IN","lat":27.4839000702,"lng":95.0168991089},{"code":"DMU","name":"Dimapur Airport","city":"Dimapur","country":"IN","lat":25.883899688699998,"lng":93.77110290530001},{"code":"PAT","name":"Jay Prakash Narayan Airport","city":"Patna","country":"IN","lat":25.591299,"lng":85.087997},{"code":"IXR","name":"Birsa Munda Airport","city":"Ranchi","country":"IN","lat":23.314300537100003,"lng":85.3217010498},{"code":"TEZ","name":"Tezpur Airport","city":"","country":"IN","lat":26.7091007232666,"lng":92.78469848632812},{"code":"UKE","name":"Utkela Airport","city":"Bhawanipatna","country":"IN","lat":20.097778,"lng":83.183333},{"code":"VTZ","name":"Visakhapatnam International Airport","city":"Visakhapatnam","country":"IN","lat":17.723506,"lng":83.227729},{"code":"BZL","name":"Barisal Airport","city":"Barisal","country":"BD","lat":22.801001,"lng":90.301201},{"code":"CXB","name":"Cox's Bazar Airport","city":"Cox's Bazar","country":"BD","lat":21.457517,"lng":91.963263},{"code":"CGP","name":"Shah Amanat International Airport","city":"Chattogram (Chittagong)","country":"BD","lat":22.249599,"lng":91.813301},{"code":"DAC","name":"Hazrat Shahjalal International Airport","city":"Dhaka","country":"BD","lat":23.843347,"lng":90.397783},{"code":"JSR","name":"Jessore Airport","city":"Jashore (Jessore)","country":"BD","lat":23.1838,"lng":89.160797},{"code":"RJH","name":"Shah Makhdum Airport","city":"Rajshahi","country":"BD","lat":24.437201,"lng":88.616501},{"code":"SPD","name":"Saidpur Airport","city":"Saidpur","country":"BD","lat":25.759199,"lng":88.908897},{"code":"ZYL","name":"Osmany International Airport","city":"Sylhet","country":"BD","lat":24.963993,"lng":91.864749},{"code":"HKG","name":"Hong Kong International Airport","city":"Hong Kong","country":"HK","lat":22.31184,"lng":113.914862},{"code":"HHP","name":"Shun Tak Heliport","city":"Central and Western","country":"HK","lat":22.288868,"lng":114.152369},{"code":"SPB","name":"Charlotte Amalie Harbor Seaplane Base","city":"Charlotte Amalie","country":"VI","lat":18.338204,"lng":-64.939675},{"code":"SSB","name":"Christiansted Harbor Seaplane Base","city":"Christiansted","country":"VI","lat":17.747908,"lng":-64.707928},{"code":"AGR","name":"Agra Airport / Agra Air Force Station","city":"Agra","country":"IN","lat":27.157975,"lng":77.961025},{"code":"HRH","name":"Aligarh Airport","city":"Aligarh","country":"IN","lat":27.861437,"lng":78.145404},{"code":"IXD","name":"Prayagraj Airport","city":"Allahabad","country":"IN","lat":25.4401,"lng":81.733902},{"code":"ATQ","name":"Sri Guru Ram Das Ji International Airport","city":"Amritsar","country":"IN","lat":31.7096,"lng":74.797302},{"code":"AIP","name":"Adampur Airport","city":"Adampur","country":"IN","lat":31.4338,"lng":75.758797},{"code":"KUU","name":"Kullu Manali Airport","city":"Bhuntar","country":"IN","lat":31.876699,"lng":77.154404},{"code":"BEK","name":"Bareilly Air Force Station","city":"Bareilly","country":"IN","lat":28.4221,"lng":79.450798},{"code":"IXC","name":"Shaheed Bhagat Singh International Airport","city":"Chandigarh","country":"IN","lat":30.6735,"lng":76.788498},{"code":"KNU","name":"Kanpur Airport","city":"Kanpur","country":"IN","lat":26.404301,"lng":80.410103},{"code":"DED","name":"Dehradun Jolly Grant Airport","city":"Dehradun (Jauligrant)","country":"IN","lat":30.189243,"lng":78.176651},{"code":"DEL","name":"Indira Gandhi International Airport","city":"New Delhi","country":"IN","lat":28.55563,"lng":77.09519},{"code":"HDO","name":"Hindon Airport / Hindon Air Force Station","city":"Ghaziabad","country":"IN","lat":28.707701,"lng":77.358902},{"code":"DHM","name":"Kangra Airport","city":"Kangra","country":"IN","lat":32.164902,"lng":76.263018},{"code":"GWL","name":"Gwalior Airport","city":"Gwalior","country":"IN","lat":26.29330062866211,"lng":78.22779846191406},{"code":"HSS","name":"Maharaja Agrasen International Airport","city":"Hisar","country":"IN","lat":29.186065,"lng":75.74142},{"code":"HWR","name":"Halwara International Airport","city":"Halwara","country":"IN","lat":30.748501,"lng":75.629799},{"code":"JDH","name":"Jodhpur Airport","city":"Jodhpur","country":"IN","lat":26.251100540161133,"lng":73.04889678955078},{"code":"JAI","name":"Jaipur International Airport","city":"Jaipur","country":"IN","lat":26.8242,"lng":75.812202},{"code":"JSA","name":"Jaisalmer Airport","city":"","country":"IN","lat":26.8887,"lng":70.864998},{"code":"IXJ","name":"Jammu Airport","city":"Jammu","country":"IN","lat":32.688849,"lng":74.838152},{"code":"KQH","name":"Kishangarh Airport Ajmer","city":"Ajmer (Kishangarh)","country":"IN","lat":26.591007,"lng":74.812956},{"code":"IXL","name":"Leh Kushok Bakula Rimpochee Airport","city":"Leh","country":"IN","lat":34.135899,"lng":77.546501},{"code":"LKO","name":"Chaudhary Charan Singh International Airport","city":"Lucknow","country":"IN","lat":26.760599,"lng":80.889297},{"code":"MZS","name":"Moradabad Airport","city":"Moradabad","country":"IN","lat":28.81746,"lng":78.92187},{"code":"IXP","name":"Pathankot Airport","city":"Pathankot","country":"IN","lat":32.233611,"lng":75.634444},{"code":"PGH","name":"Pantnagar Airport","city":"Pantnagar","country":"IN","lat":29.0334,"lng":79.473701},{"code":"SXR","name":"Sheikh ul Alam International Airport","city":"Srinagar","country":"IN","lat":33.987099,"lng":74.7742},{"code":"VSV","name":"Shravasti Airport","city":"Shravasti","country":"IN","lat":27.499732,"lng":82.032927},{"code":"LPQ","name":"Luang Phabang International Airport","city":"Luang Phabang","country":"LA","lat":19.904273,"lng":102.167191},{"code":"LXG","name":"Luang Namtha Airport","city":"Luang Namtha","country":"LA","lat":20.966999,"lng":101.400002},{"code":"ODY","name":"Oudomsay Airport","city":"Oudomsay","country":"LA","lat":20.68269920349121,"lng":101.99400329589844},{"code":"PKZ","name":"Pakse International Airport","city":"Pakse","country":"LA","lat":15.133973,"lng":105.779871},{"code":"VTE","name":"Wattay International Airport","city":"Vientiane","country":"LA","lat":17.985052,"lng":102.566692},{"code":"XKH","name":"Xieng Khouang Airport","city":"Xieng Khouang","country":"LA","lat":19.450001,"lng":103.157997},{"code":"MFM","name":"Macau International Airport","city":"Nossa Senhora do Carmo","country":"MO","lat":22.149599,"lng":113.592003},{"code":"BHR","name":"Bharatpur Airport","city":"Bharatpur","country":"NP","lat":27.678101,"lng":84.429398},{"code":"BWA","name":"Gautam Buddha International Airport","city":"Siddharthanagar (Bhairahawa)","country":"NP","lat":27.504636,"lng":83.410381},{"code":"BDP","name":"Bhadrapur Airport","city":"Bhadrapur","country":"NP","lat":26.5708007812,"lng":88.07959747310001},{"code":"DOP","name":"Dolpa Airport","city":"Dolpa","country":"NP","lat":28.985659,"lng":82.818703},{"code":"SIH","name":"Silgadi Doti Airport","city":"Silgadi Doti","country":"NP","lat":29.262226,"lng":80.935968},{"code":"JUM","name":"Jumla Airport","city":"Jumla","country":"NP","lat":29.2742,"lng":82.193298},{"code":"JKR","name":"Janakpur Airport","city":"Janakpur","country":"NP","lat":26.708799,"lng":85.922401},{"code":"JMO","name":"Jomsom Airport","city":"Jomsom","country":"NP","lat":28.780426,"lng":83.723},{"code":"KTM","name":"Tribhuvan International Airport","city":"Kathmandu","country":"NP","lat":27.6966,"lng":85.3591},{"code":"LUA","name":"Tenzing-Hillary Airport","city":"Lukla","country":"NP","lat":27.686768,"lng":86.729479},{"code":"KEP","name":"Nepalgunj Airport","city":"Nepalgunj","country":"NP","lat":28.1036,"lng":81.667},{"code":"PKR","name":"Pokhara Domestic Airport","city":"Pokhara","country":"NP","lat":28.200621,"lng":83.981203},{"code":"SIF","name":"Simara Airport","city":"Simara","country":"NP","lat":27.159803,"lng":84.980021},{"code":"SKH","name":"Surkhet Airport","city":"Surkhet","country":"NP","lat":28.586,"lng":81.636002},{"code":"IMK","name":"Simikot Airport","city":"Simikot","country":"NP","lat":29.9711,"lng":81.818901},{"code":"TPJ","name":"Taplejung Airport","city":"Taplejung","country":"NP","lat":27.3509,"lng":87.69525},{"code":"TMI","name":"Tumling Tar Airport","city":"Tumling Tar","country":"NP","lat":27.315001,"lng":87.193298},{"code":"BIR","name":"Biratnagar Airport","city":"Biratnagar","country":"NP","lat":26.48150062561035,"lng":87.26399993896484},{"code":"AGX","name":"Agatti Airport","city":"Agatti","country":"IN","lat":10.8237,"lng":72.176003},{"code":"BLR","name":"Kempegowda International Airport Bengaluru","city":"Bengaluru","country":"IN","lat":13.1979,"lng":77.706299},{"code":"VGA","name":"Vijayawada International Airport","city":"Vijayawada","country":"IN","lat":16.530011,"lng":80.804888},{"code":"CJB","name":"Coimbatore International Airport","city":"Coimbatore","country":"IN","lat":11.03,"lng":77.043404},{"code":"COK","name":"Cochin International Airport","city":"Kochi","country":"IN","lat":10.151047,"lng":76.400838},{"code":"CCJ","name":"Calicut International Airport","city":"Calicut","country":"IN","lat":11.135996,"lng":75.955152},{"code":"CDP","name":"Kadapa Airport","city":"Kadapa","country":"IN","lat":14.513154,"lng":78.769183},{"code":"GOX","name":"Manohar International Airport","city":"Mopa","country":"IN","lat":15.744257,"lng":73.860625},{"code":"GBI","name":"Kalaburagi Airport","city":"Kalaburagi","country":"IN","lat":17.308154,"lng":76.965246},{"code":"GOI","name":"Goa Dabolim International Airport","city":"Vasco da Gama","country":"IN","lat":15.380062,"lng":73.833328},{"code":"HYD","name":"Rajiv Gandhi International Airport","city":"Hyderabad","country":"IN","lat":17.231318,"lng":78.429855},{"code":"CNN","name":"Kannur International Airport","city":"Kannur","country":"IN","lat":11.916343,"lng":75.544979},{"code":"KJB","name":"Kurnool Airport","city":"Orvakal","country":"IN","lat":15.716288,"lng":78.16923},{"code":"IXM","name":"Madurai Airport","city":"Madurai","country":"IN","lat":9.83450984955,"lng":78.09339904790001},{"code":"IXE","name":"Mangaluru International Airport","city":"Mangaluru","country":"IN","lat":12.95471,"lng":74.886812},{"code":"MAA","name":"Chennai International Airport","city":"Chennai","country":"IN","lat":12.990005,"lng":80.169296},{"code":"MYQ","name":"Mysore Airport","city":"Mysore","country":"IN","lat":12.229751,"lng":76.653683},{"code":"IXZ","name":"Veer Savarkar International Airport / INS Utkrosh","city":"Port Blair","country":"IN","lat":11.640194,"lng":92.72902},{"code":"PNY","name":"Pondicherry Airport","city":"Puducherry (Pondicherry)","country":"IN","lat":11.968,"lng":79.812},{"code":"RJA","name":"Rajahmundry Airport","city":"Madhurapudi","country":"IN","lat":17.105799,"lng":81.813204},{"code":"TCR","name":"Tuticorin Airport","city":"Vagaikulam","country":"IN","lat":8.724241,"lng":78.025803},{"code":"TIR","name":"Tirupati International Airport","city":"Tirupati","country":"IN","lat":13.631988,"lng":79.539869},{"code":"TRZ","name":"Tiruchirappalli International Airport","city":"Tiruchirappalli","country":"IN","lat":10.762915,"lng":78.717741},{"code":"TRV","name":"Thiruvananthapuram International Airport","city":"Thiruvananthapuram","country":"IN","lat":8.481889,"lng":76.920029},{"code":"BUT","name":"Bathpalathang Airport","city":"Jakar","country":"BT","lat":27.5622,"lng":90.7471},{"code":"PBH","name":"Paro International Airport","city":"Paro","country":"BT","lat":27.4032,"lng":89.424599},{"code":"YON","name":"Yongphulla Airport","city":"Yongphulla","country":"BT","lat":27.25627,"lng":91.514633},{"code":"HRF","name":"Hoarafushi Airport","city":"","country":"MV","lat":6.96967,"lng":72.896287},{"code":"HDK","name":"Kulhudhuffushi Airport","city":"Kulhudhuffushi","country":"MV","lat":6.630762,"lng":73.067815},{"code":"NMF","name":"Maafaru International Airport","city":"Noonu Atoll","country":"MV","lat":5.817407,"lng":73.468404},{"code":"DRV","name":"Dharavandhoo Airport","city":"Baa Atoll","country":"MV","lat":5.1561,"lng":73.1302},{"code":"GAN","name":"Gan International Airport","city":"Gan","country":"MV","lat":-0.692975,"lng":73.152627},{"code":"HAQ","name":"Hanimaadhoo International Airport","city":"Haa Dhaalu Atoll","country":"MV","lat":6.743235,"lng":73.167072},{"code":"KDO","name":"Kadhdhoo Airport","city":"Kadhdhoo","country":"MV","lat":1.8591699600219727,"lng":73.52189636230469},{"code":"MLE","name":"Velana International Airport","city":"Malé","country":"MV","lat":4.19183,"lng":73.529099},{"code":"GKK","name":"Kooddoo Airport","city":"Huvadhu Atoll","country":"MV","lat":0.7324,"lng":73.4336},{"code":"KDM","name":"Kaadedhdhoo Airport","city":"Huvadhu Atoll","country":"MV","lat":0.488131,"lng":72.996902},{"code":"VAM","name":"Villa International Airport Maamigili","city":"Maamigili","country":"MV","lat":3.471751,"lng":72.832633},{"code":"TMF","name":"Thimarafushi Airport","city":"Thimarafushi","country":"MV","lat":2.211,"lng":73.1533},{"code":"RUL","name":"Maavaarulaa Airport","city":"Maavaarulu","country":"MV","lat":0.338085,"lng":73.512869},{"code":"DMK","name":"Don Mueang International Airport","city":"Bangkok","country":"TH","lat":13.9126,"lng":100.607002},{"code":"TDX","name":"Trat Airport","city":"Laem Ngop","country":"TH","lat":12.2746,"lng":102.319},{"code":"BKK","name":"Suvarnabhumi Airport","city":"Bangkok","country":"TH","lat":13.6811,"lng":100.747002},{"code":"UTP","name":"U-Tapao–Rayong–Pattaya International Airport","city":"Rayong","country":"TH","lat":12.6799,"lng":101.004997},{"code":"CNX","name":"Chiang Mai International Airport","city":"Chiang Mai","country":"TH","lat":18.766799926799997,"lng":98.962600708},{"code":"HGN","name":"Mae Hong Son Airport","city":"Mae Hong Son","country":"TH","lat":19.3013,"lng":97.9758},{"code":"LPT","name":"Lampang Airport","city":"","country":"TH","lat":18.27090072631836,"lng":99.50420379638672},{"code":"NNT","name":"Nan Airport","city":"","country":"TH","lat":18.807899475097656,"lng":100.78299713134766},{"code":"CEI","name":"Mae Fah Luang - Chiang Rai International Airport","city":"Chiang Rai","country":"TH","lat":19.952299,"lng":99.882896},{"code":"PHY","name":"Phetchabun Airport","city":"","country":"TH","lat":16.6760005951,"lng":101.194999695},{"code":"HHQ","name":"Hua Hin Airport","city":"Hua Hin","country":"TH","lat":12.6361999512,"lng":99.951499939},{"code":"MAQ","name":"Mae Sot Airport","city":"","country":"TH","lat":16.699899673461914,"lng":98.54509735107422},{"code":"THS","name":"Sukhothai Airport","city":"","country":"TH","lat":17.238000869750977,"lng":99.81819915771484},{"code":"PHS","name":"Phitsanulok Airport","city":"Phitsanulok","country":"TH","lat":16.7829,"lng":100.278999},{"code":"URT","name":"Surat Thani Airport","city":"Surat Thani","country":"TH","lat":9.13259983063,"lng":99.135597229},{"code":"NAW","name":"Narathiwat Airport","city":"","country":"TH","lat":6.5199198722839355,"lng":101.74299621582031},{"code":"CJM","name":"Chumphon Airport","city":"Chumphon","country":"TH","lat":10.7112,"lng":99.361702},{"code":"NST","name":"Nakhon Si Thammarat Airport","city":"Nakhon Si Thammarat","country":"TH","lat":8.539620399475098,"lng":99.9447021484375},{"code":"KBV","name":"Krabi International Airport","city":"Krabi","country":"TH","lat":8.095591,"lng":98.988955},{"code":"USM","name":"Samui International Airport","city":"Na Thon (Ko Samui Island)","country":"TH","lat":9.54779,"lng":100.061996},{"code":"HKT","name":"Phuket International Airport","city":"Phuket","country":"TH","lat":8.113257,"lng":98.3174},{"code":"UNN","name":"Ranong Airport","city":"Ranong","country":"TH","lat":9.77762,"lng":98.585503},{"code":"HDY","name":"Hat Yai International Airport","city":"Hat Yai","country":"TH","lat":6.93321,"lng":100.392998},{"code":"TST","name":"Trang Airport","city":"Trang","country":"TH","lat":7.50874,"lng":99.6166},{"code":"UTH","name":"Udon Thani International Airport","city":"Udon Thani","country":"TH","lat":17.386186,"lng":102.788577},{"code":"SNO","name":"Sakon Nakhon Airport","city":"","country":"TH","lat":17.195100784301758,"lng":104.11900329589844},{"code":"PXR","name":"Surin Airport","city":"Surin","country":"TH","lat":14.868300437927,"lng":103.49800109863},{"code":"KKC","name":"Khon Kaen Airport","city":"Khon Kaen","country":"TH","lat":16.466600418099997,"lng":102.783996582},{"code":"LOE","name":"Loei Airport","city":"","country":"TH","lat":17.43910026550293,"lng":101.72200012207031},{"code":"BFV","name":"Buri Ram Airport","city":"Buriram","country":"TH","lat":15.2295,"lng":103.252998},{"code":"UBP","name":"Ubon Ratchathani Airport","city":"Ubon Ratchathani","country":"TH","lat":15.2512998581,"lng":104.870002747},{"code":"ROI","name":"Roi Et Airport","city":"Roi Et","country":"TH","lat":16.1168,"lng":103.774002},{"code":"KOP","name":"Nakhon Phanom Airport","city":"Nakhon Phanom","country":"TH","lat":17.383801,"lng":104.642998},{"code":"BMV","name":"Buon Ma Thuot Airport","city":"Buon Ma Thuot","country":"VN","lat":12.668299675,"lng":108.120002747},{"code":"VCL","name":"Chu Lai Airport","city":"Tam Nghĩa","country":"VN","lat":15.4033,"lng":108.706001},{"code":"HPH","name":"Cat Bi International Airport","city":"Haiphong (Hai An)","country":"VN","lat":20.817428,"lng":106.724315},{"code":"CAH","name":"Cà Mau Airport","city":"Ca Mau City","country":"VN","lat":9.177667,"lng":105.177778},{"code":"CXR","name":"Cam Ranh International Airport / Cam Ranh Air Base","city":"Nha Trang/nha Trang aiurportCam Ranh","country":"VN","lat":11.9982,"lng":109.219002},{"code":"VCS","name":"Con Dao Airport","city":"Con Dao","country":"VN","lat":8.73183,"lng":106.633003},{"code":"VCA","name":"Can Tho International Airport","city":"Can Tho","country":"VN","lat":10.083397,"lng":105.709371},{"code":"DIN","name":"Dien Bien Phu Airport","city":"Dien Bien Phu","country":"VN","lat":21.3974990845,"lng":103.008003235},{"code":"VDH","name":"Dong Hoi Airport","city":"Dong Hoi","country":"VN","lat":17.515,"lng":106.590556},{"code":"DLI","name":"Lien Khuong Airport","city":"Da Lat","country":"VN","lat":11.750556,"lng":108.366997},{"code":"DAD","name":"Da Nang International Airport","city":"Da Nang","country":"VN","lat":16.0439,"lng":108.198997},{"code":"HAN","name":"Noi Bai International Airport","city":"Hanoi (Soc Son)","country":"VN","lat":21.221201,"lng":105.806999},{"code":"HUI","name":"Phu Bai International Airport","city":"Huế","country":"VN","lat":16.400628,"lng":107.703094},{"code":"UIH","name":"Phu Cat Airport","city":"Quy Nohn","country":"VN","lat":13.955,"lng":109.042},{"code":"PXU","name":"Pleiku Airport","city":"Pleiku","country":"VN","lat":14.004500389099121,"lng":108.01699829101562},{"code":"PQC","name":"Phú Quốc International Airport","city":"Phu Quoc Island","country":"VN","lat":10.169783,"lng":103.99353},{"code":"VKG","name":"Rach Gia Airport","city":"Rach Gia","country":"VN","lat":9.95802997234,"lng":105.132379532},{"code":"TBB","name":"Dong Tac Airport","city":"Tuy Hoa","country":"VN","lat":13.0496,"lng":109.334},{"code":"SGN","name":"Tan Son Nhat International Airport","city":"Ho Chi Minh City","country":"VN","lat":10.8188,"lng":106.652},{"code":"THD","name":"Tho Xuan Airport","city":"Thanh Hóa","country":"VN","lat":19.901667,"lng":105.467778},{"code":"VDO","name":"Van Don International Airport","city":"Van Don","country":"VN","lat":21.120693,"lng":107.41539},{"code":"VII","name":"Vinh Airport","city":"Vinh","country":"VN","lat":18.7376003265,"lng":105.67099762},{"code":"NYU","name":"Bagan Airport","city":"Nyaung U","country":"MM","lat":21.178800582885742,"lng":94.9301986694336},{"code":"BMO","name":"Banmaw Airport","city":"Banmaw","country":"MM","lat":24.27044,"lng":97.2476},{"code":"TVY","name":"Dawei Airport","city":"Dawei","country":"MM","lat":14.1039,"lng":98.203598},{"code":"HEH","name":"Heho Airport","city":"Heho","country":"MM","lat":20.74714,"lng":96.792029},{"code":"KET","name":"Kengtung Airport","city":"Kengtung","country":"MM","lat":21.301599502563477,"lng":99.63600158691406},{"code":"KHM","name":"Kanti Airport","city":"Kanti","country":"MM","lat":25.9883,"lng":95.6744},{"code":"KYP","name":"Kyaukpyu Airport","city":"Kyaukpyu","country":"MM","lat":19.42639923095703,"lng":93.53479766845703},{"code":"KAW","name":"Kawthoung Airport","city":"Kawthoung","country":"MM","lat":10.049300193786621,"lng":98.53800201416016},{"code":"LIW","name":"Loikaw Airport","city":"Loikaw","country":"MM","lat":19.691499710083008,"lng":97.21479797363281},{"code":"LSH","name":"Lashio Airport","city":"Lashio","country":"MM","lat":22.9778995513916,"lng":97.752197265625},{"code":"MDL","name":"Mandalay International Airport","city":"Mandalay","country":"MM","lat":21.702199935913086,"lng":95.97789764404297},{"code":"MGZ","name":"Myeik Airport","city":"Mkeik","country":"MM","lat":12.439800262451172,"lng":98.62149810791016},{"code":"MYT","name":"Myitkyina Airport","city":"Myitkyina","country":"MM","lat":25.3836,"lng":97.351898},{"code":"MNU","name":"Mawlamyine Airport","city":"Mawlamyine","country":"MM","lat":16.444700241088867,"lng":97.66069793701172},{"code":"MOG","name":"Mong Hsat Airport","city":"Mong Hsat","country":"MM","lat":20.516799926757812,"lng":99.25679779052734},{"code":"MWQ","name":"Magway Airport","city":"Magway","country":"MM","lat":20.1656,"lng":94.941399},{"code":"NYT","name":"Nay Pyi Taw International Airport","city":"Naypyitaw","country":"MM","lat":19.623501,"lng":96.200996},{"code":"BSX","name":"Pathein Airport","city":"Pathein","country":"MM","lat":16.815201,"lng":94.7799},{"code":"PBU","name":"Putao Airport","city":"Putao","country":"MM","lat":27.32990074157715,"lng":97.42630004882812},{"code":"AKY","name":"Sittwe Airport","city":"Sittwe","country":"MM","lat":20.133165,"lng":92.870693},{"code":"SNW","name":"Thandwe Airport","city":"Thandwe","country":"MM","lat":18.4606990814209,"lng":94.30010223388672},{"code":"THL","name":"Tachileik Airport","city":"Tachileik","country":"MM","lat":20.483800888061523,"lng":99.9354019165039},{"code":"RGN","name":"Yangon International Airport","city":"Yangon","country":"MM","lat":16.907300949099998,"lng":96.1332015991},{"code":"FBS","name":"Friday Harbor Seaplane Base","city":"Friday Harbor","country":"US","lat":48.5373,"lng":-123.010002},{"code":"RSJ","name":"Rosario Seaplane Base","city":"Rosario","country":"US","lat":48.645699,"lng":-122.867996},{"code":"LKE","name":"Kenmore Air Harbor Seaplane Base","city":"Seattle","country":"US","lat":47.629002,"lng":-122.338997},{"code":"RCE","name":"Roche Harbor Airport","city":"Roche Harbor","country":"US","lat":48.612301,"lng":-123.139},{"code":"WSX","name":"Westsound/WSX Seaplane Base","city":"West Sound","country":"US","lat":48.617901,"lng":-122.956903},{"code":"UPG","name":"Sultan Hasanuddin International Airport","city":"Makassar","country":"ID","lat":-5.075539,"lng":119.553702},{"code":"BIK","name":"Frans Kaisiepo Airport","city":"Biak","country":"ID","lat":-1.19002,"lng":136.108002},{"code":"TIM","name":"Mozes Kilangin Airport","city":"Timika","country":"ID","lat":-4.52978,"lng":136.887388},{"code":"BMU","name":"Sultan Muhammad Salahuddin Airport","city":"Bima","country":"ID","lat":-8.537184,"lng":118.685002},{"code":"DPS","name":"Denpasar I Gusti Ngurah Rai International Airport","city":"Kuta, Badung","country":"ID","lat":-8.748409,"lng":115.167123},{"code":"LOP","name":"Lombok International Airport","city":"Mataram (Pujut, Lombok Tengah)","country":"ID","lat":-8.759962,"lng":116.278169},{"code":"SWQ","name":"Sultan Muhammad Kaharuddin III Airport","city":"Sumbawa Besar","country":"ID","lat":-8.48904,"lng":117.412003},{"code":"TMC","name":"Tambolaka Airport","city":"Radamata","country":"ID","lat":-9.409224,"lng":119.242558},{"code":"WGP","name":"Umbu Mehang Kunda Airport","city":"Waingapu-Sumba Island","country":"ID","lat":-9.66922,"lng":120.302002},{"code":"BWX","name":"Banyuwangi Airport","city":"Rogojampi, Banyuwangi","country":"ID","lat":-8.31015,"lng":114.3401},{"code":"TTE","name":"Sultan Babullah Airport","city":"Ternate","country":"ID","lat":0.831012,"lng":127.381611},{"code":"TRT","name":"Toraja Airport","city":"Toraja","country":"ID","lat":-3.185833,"lng":119.91775},{"code":"LLO","name":"Bua - Palopo Lagaligo Airport","city":"Palopo","country":"ID","lat":-3.08227,"lng":120.244823},{"code":"PLW","name":"Mutiara - SIS Al-Jufrie Airport","city":"Palu","country":"ID","lat":-0.916462,"lng":119.908647},{"code":"TLI","name":"Sultan Bantilan Airport","city":"Toli Toli-Celebes Island","country":"ID","lat":1.123428,"lng":120.793658},{"code":"OJU","name":"Tanjung Api Airport","city":"Tojo Una-Una","country":"ID","lat":-0.865437,"lng":121.628625},{"code":"HMS","name":"Haji Muhammad Sidik Airport","city":"Muara Teweh","country":"ID","lat":-1.02581,"lng":114.92881},{"code":"KLP","name":"Seruyan Kuala Pembuang Airport","city":"Seruyan","country":"ID","lat":-3.376062,"lng":112.536521},{"code":"PKY","name":"Tjilik Riwut Airport","city":"Palangkaraya","country":"ID","lat":-2.227146,"lng":113.943388},{"code":"YIA","name":"Yogyakarta International Airport","city":"Yogyakarta","country":"ID","lat":-7.905338,"lng":110.057264},{"code":"CXP","name":"Tunggul Wulung Airport","city":"Cilacap","country":"ID","lat":-7.64506,"lng":109.033997},{"code":"KWB","name":"Dewadaru Airport","city":"Karimunjawa","country":"ID","lat":-5.80091,"lng":110.47838},{"code":"BUI","name":"Bokondini Airport","city":"Bokondini","country":"ID","lat":-3.68455,"lng":138.677437},{"code":"DJJ","name":"Dortheys Hiyo Eluay International Airport","city":"Sentani","country":"ID","lat":-2.579627,"lng":140.519857},{"code":"OKL","name":"Oksibil Airport","city":"Oksibil","country":"ID","lat":-4.9071,"lng":140.6277},{"code":"MKQ","name":"Mopah International Airport","city":"Merauke","country":"ID","lat":-8.523898,"lng":140.419693},{"code":"TMH","name":"Tanah Merah Airport","city":"Tanah Merah","country":"ID","lat":-6.09669,"lng":140.303521},{"code":"TJS","name":"Tanjung Harapan Airport","city":"Tanjung Selor-Borneo Island","country":"ID","lat":2.835833,"lng":117.373611},{"code":"DTD","name":"Datadawai Airport","city":"Datadawai-Borneo Island","country":"ID","lat":0.807028,"lng":114.529052},{"code":"BPN","name":"Sultan Aji Muhammad Sulaiman Sepinggan International Airport","city":"Balikpapan","country":"ID","lat":-1.268342,"lng":116.89452},{"code":"TRK","name":"Juwata International Airport / Suharnoko Harbani AFB","city":"Tarakan","country":"ID","lat":3.325145,"lng":117.564169},{"code":"AAP","name":"Aji Pangeran Tumenggung Pranoto International Airport","city":"Samarinda","country":"ID","lat":-0.374478,"lng":117.250128},{"code":"GTO","name":"Jalaluddin Airport","city":"Gorontalo","country":"ID","lat":0.637119,"lng":122.849998},{"code":"NAH","name":"Naha Airport","city":"Tabukan Utara, Sangihe Islands","country":"ID","lat":3.68478,"lng":125.527162},{"code":"LKM","name":"Bolaang Mongondow Airport","city":"Lolak","country":"ID","lat":0.88855,"lng":124.033634},{"code":"MDC","name":"Sam Ratulangi International Airport","city":"Manado","country":"ID","lat":1.548591,"lng":124.92625},{"code":"UOL","name":"Buol - Pogogul Airport","city":"Buol","country":"ID","lat":1.102318,"lng":121.413358},{"code":"BTW","name":"Bersujud Airport","city":"Batu Licin","country":"ID","lat":-3.41241,"lng":115.995003},{"code":"PKN","name":"Iskandar Airport","city":"Pangkalanbun","country":"ID","lat":-2.7052,"lng":111.672997},{"code":"KBU","name":"Gusti Syamsir Alam Airport","city":"Stagen","country":"ID","lat":-3.294796,"lng":116.163815},{"code":"TJG","name":"Warukin Airport","city":"Tanta-Tabalong","country":"ID","lat":-2.21656,"lng":115.435997},{"code":"BDJ","name":"Syamsudin Noor International Airport","city":"Banjarbaru","country":"ID","lat":-3.440112,"lng":114.761209},{"code":"SMQ","name":"Sampit (H.Asan) Airport","city":"Sampit","country":"ID","lat":-2.499198,"lng":112.974987},{"code":"TBM","name":"Tumbang Samba Airport","city":"Tumbang Samba-Borneo Island","country":"ID","lat":-1.469024,"lng":113.080984},{"code":"LUV","name":"Karel Sadsuitubun Airport","city":"Langgur","country":"ID","lat":-5.760278,"lng":132.759444},{"code":"NAM","name":"Namniwel Airport","city":"Namniwel","country":"ID","lat":-3.143159,"lng":126.976469},{"code":"AMQ","name":"Pattimura International Airport","city":"Ambon","country":"ID","lat":-3.71026,"lng":128.089005},{"code":"LNU","name":"Robert Atty Bessing","city":"Malinau","country":"ID","lat":3.575256,"lng":116.619769},{"code":"BEJ","name":"Kalimarau Airport","city":"Tanjung Redeb - Borneo Island","country":"ID","lat":2.14776,"lng":117.430705},{"code":"MLG","name":"Abdul Rachman Saleh Airport","city":"Malang","country":"ID","lat":-7.929099,"lng":112.714233},{"code":"JBB","name":"Notohadinegoro Airport","city":"Jember","country":"ID","lat":-8.242494,"lng":113.693476},{"code":"JOG","name":"Adisutjipto International Airport","city":"Yogyakarta","country":"ID","lat":-7.78818,"lng":110.431999},{"code":"SOC","name":"Adisoemarmo International Airport","city":"Surakarta","country":"ID","lat":-7.516044,"lng":110.757492},{"code":"SUB","name":"Juanda International Airport","city":"Surabaya","country":"ID","lat":-7.37983,"lng":112.787003},{"code":"SRG","name":"Jenderal Ahmad Yani Airport","city":"Semarang","country":"ID","lat":-6.970732,"lng":110.373244},{"code":"FKQ","name":"Fakfak Airport","city":"Fakfak","country":"ID","lat":-2.920508,"lng":132.267011},{"code":"KNG","name":"Utarom Airport","city":"Kaimana","country":"ID","lat":-3.6446,"lng":133.695116},{"code":"RJM","name":"Marinda Airport","city":"Waisai","country":"ID","lat":-0.424969,"lng":130.773535},{"code":"MKW","name":"Rendani Airport","city":"Manokwari","country":"ID","lat":-0.891833,"lng":134.048996},{"code":"SOQ","name":"Domine Eduard Osok Airport","city":"Sorong","country":"ID","lat":-0.894,"lng":131.287},{"code":"ABU","name":"AA Bere Tallo (Haliwen) Airport","city":"Atambua","country":"ID","lat":-9.074841,"lng":124.903285},{"code":"MOF","name":"Frans Xavier Seda Airport","city":"Waioti","country":"ID","lat":-8.639514,"lng":122.238135},{"code":"ENE","name":"H. Hasan Aroeboesman (Ende) Airport","city":"Ende","country":"ID","lat":-8.849114,"lng":121.66101},{"code":"RTG","name":"Frans Sales Lega Airport","city":"Satar Tacik, Manggarai","country":"ID","lat":-8.596171,"lng":120.478177},{"code":"LKA","name":"Larantuka Gewayentana Airport","city":"Tiwatobi","country":"ID","lat":-8.274424,"lng":123.002},{"code":"ARD","name":"Alor Island - Mali Airport","city":"Kabola","country":"ID","lat":-8.13234,"lng":124.597},{"code":"LBJ","name":"Komodo Airport","city":"Labuan Bajo, Manggarai Barat","country":"ID","lat":-8.480694,"lng":119.888306},{"code":"RTI","name":"David Constantijn Saudale Airport","city":"Ba'a - Rote Island","country":"ID","lat":-10.765834,"lng":123.076046},{"code":"KOE","name":"El Tari Airport","city":"Kupang","country":"ID","lat":-10.1716,"lng":123.670998},{"code":"WMX","name":"Wamena Airport","city":"Wamena","country":"ID","lat":-4.097324,"lng":138.952417},{"code":"WNI","name":"Matahora Airport","city":"Wangi-wangi Island","country":"ID","lat":-5.292123,"lng":123.636233},{"code":"KSR","name":"Selayar - Haji Aroeppala Airport","city":"Benteng","country":"ID","lat":-6.17921,"lng":120.437479},{"code":"KXB","name":"Sangia Nibandera Airport","city":"Kolaka","country":"ID","lat":-4.338158,"lng":121.524047},{"code":"KDI","name":"Haluoleo Airport","city":"Kendari","country":"ID","lat":-4.08161,"lng":122.417999},{"code":"WBB","name":"Stebbins Airport","city":"Stebbins","country":"US","lat":63.515999,"lng":-162.278},{"code":"BTU","name":"Bintulu Airport","city":"Bintulu","country":"MY","lat":3.12385010719,"lng":113.019996643},{"code":"LGL","name":"Long Lellang Airport","city":"Long Datih","country":"MY","lat":3.4210000038099997,"lng":115.153999329},{"code":"KCH","name":"Kuching International Airport","city":"Kuching","country":"MY","lat":1.487364,"lng":110.352859},{"code":"ODN","name":"Long Seridan Airport","city":"Long Seridan","country":"MY","lat":3.976087,"lng":115.067303},{"code":"LMN","name":"Limbang Airport","city":"Limbang","country":"MY","lat":4.808300018310547,"lng":115.01000213623047},{"code":"MKM","name":"Mukah Airport","city":"Mukah","country":"MY","lat":2.881944,"lng":112.043333},{"code":"MUR","name":"Marudi Airport","city":"Marudi","country":"MY","lat":4.178734,"lng":114.330318},{"code":"BKM","name":"Bakalalan Airport","city":"Bakalalan","country":"MY","lat":3.974,"lng":115.617996},{"code":"MYY","name":"Miri Airport","city":"Miri","country":"MY","lat":4.32201,"lng":113.987},{"code":"SBW","name":"Sibu Airport","city":"Sibu","country":"MY","lat":2.2616,"lng":111.985001},{"code":"LWY","name":"Lawas Airport","city":"Lawas","country":"MY","lat":4.84917,"lng":115.407997},{"code":"BBN","name":"Bario Airport","city":"Bario","country":"MY","lat":3.734648,"lng":115.478548},{"code":"LDU","name":"Lahad Datu Airport","city":"Lahad Datu","country":"MY","lat":5.03241,"lng":118.32376},{"code":"BKI","name":"Kota Kinabalu International Airport","city":"Kota Kinabalu","country":"MY","lat":5.932743,"lng":116.049324},{"code":"LBU","name":"Labuan Airport","city":"Labuan","country":"MY","lat":5.301667,"lng":115.248333},{"code":"TMG","name":"Tomanggong Airport","city":"Tomanggong","country":"MY","lat":5.40257,"lng":118.65763},{"code":"SDK","name":"Sandakan Airport","city":"Sandakan","country":"MY","lat":5.900899887084961,"lng":118.05899810791016},{"code":"KUD","name":"Kudat Airport","city":"Kudat","country":"MY","lat":6.924111,"lng":116.837068},{"code":"TWU","name":"Tawau Airport","city":"Tawau","country":"MY","lat":4.31338,"lng":118.12152},{"code":"MZV","name":"Mulu Airport","city":"Mulu","country":"MY","lat":4.048329830169678,"lng":114.80500030517578},{"code":"BWN","name":"Brunei International Airport","city":"Bandar Seri Begawan","country":"BN","lat":4.9442,"lng":114.928001},{"code":"PKU","name":"Sultan Syarif Kasim II International Airport / Roesmin Nurjadin AFB","city":"Pekanbaru","country":"ID","lat":0.458647,"lng":101.444321},{"code":"DUM","name":"Pinang Kampai Airport","city":"Dumai","country":"ID","lat":1.609026,"lng":101.433493},{"code":"KJT","name":"Kertajati International Airport","city":"Kertajati","country":"ID","lat":-6.647384,"lng":108.165561},{"code":"BDO","name":"Husein Sastranegara International Airport","city":"Bandung","country":"ID","lat":-6.90063,"lng":107.575996},{"code":"CJN","name":"Nusawiru Airport","city":"Cijulang","country":"ID","lat":-7.719895,"lng":108.488995},{"code":"BTH","name":"Hang Nadim International Airport","city":"Batam","country":"ID","lat":1.12103,"lng":104.119003},{"code":"TNJ","name":"Raja Haji Fisabilillah International Airport","city":"Tanjung Pinang-Bintan Island","country":"ID","lat":0.92397,"lng":104.53341},{"code":"MPC","name":"Muko Muko Airport","city":"Muko Muko","country":"ID","lat":-2.541058,"lng":101.087651},{"code":"HLP","name":"Halim Perdanakusuma International Airport","city":"Jakarta","country":"ID","lat":-6.266989,"lng":106.890318},{"code":"CGK","name":"Soekarno-Hatta International Airport","city":"Jakarta","country":"ID","lat":-6.12557,"lng":106.655998},{"code":"TKG","name":"Radin Inten II International Airport","city":"Bandar Lampung","country":"ID","lat":-5.246803,"lng":105.182531},{"code":"LSW","name":"Malikus Saleh Airport","city":"Lhok Seumawe-Sumatra Island","country":"ID","lat":5.22668,"lng":96.950302},{"code":"GNS","name":"Binaka Airport","city":"Gunungsitoli","country":"ID","lat":1.166275,"lng":97.705196},{"code":"KNO","name":"Kualanamu International Airport","city":"Beringin","country":"ID","lat":3.637847,"lng":98.870566},{"code":"DTB","name":"Silangit Airport","city":"Siborong-Borong","country":"ID","lat":2.25973,"lng":98.991898},{"code":"FLZ","name":"Dr. Ferdinand Lumban Tobing Airport","city":"Sibolga (Pinangsori)","country":"ID","lat":1.557127,"lng":98.887145},{"code":"LSR","name":"Alas Leuser Airport","city":"Kutacane","country":"ID","lat":3.391456,"lng":97.863726},{"code":"TJQ","name":"H A S Hanandjoeddin International Airport","city":"Tanjung Pandan","country":"ID","lat":-2.744109,"lng":107.754535},{"code":"NPO","name":"Nanga Pinoh Airport","city":"Nanga Pinoh-Borneo Island","country":"ID","lat":-0.34864,"lng":111.746155},{"code":"KTG","name":"Rahadi Osman Airport","city":"Ketapang","country":"ID","lat":-1.817165,"lng":109.963469},{"code":"NTX","name":"Ranai Airport","city":"Ranai-Natuna Besar Island","country":"ID","lat":3.90871,"lng":108.388},{"code":"PNK","name":"Supadio International Airport","city":"Pontianak","country":"ID","lat":-0.152258,"lng":109.404493},{"code":"PSU","name":"Pangsuma Airport","city":"Putussibau-Borneo Island","country":"ID","lat":0.834579,"lng":112.940217},{"code":"SQG","name":"Tebelian Airport","city":"Sintang","country":"ID","lat":-0.045214,"lng":111.458049},{"code":"DJB","name":"Sultan Thaha Airport","city":"Jambi","country":"ID","lat":-1.638685,"lng":103.645063},{"code":"BUU","name":"Muara Bungo Airport","city":"Muara Bungo","country":"ID","lat":-1.541635,"lng":102.181613},{"code":"PGK","name":"Depati Amir Airport","city":"Pangkal Pinang","country":"ID","lat":-2.1622,"lng":106.139},{"code":"BKS","name":"Fatmawati Soekarno Airport","city":"Bengkulu","country":"ID","lat":-3.8637,"lng":102.338997},{"code":"PLM","name":"Sultan Mahmud Badaruddin II Airport","city":"Palembang","country":"ID","lat":-2.897653,"lng":104.698147},{"code":"PDO","name":"Pendopo Airport","city":"Talang Gudang-Sumatra Island","country":"ID","lat":-3.2860701084136963,"lng":103.87999725341797},{"code":"PDG","name":"Minangkabau International Airport","city":"Padang (Katapiang)","country":"ID","lat":-0.785964,"lng":100.280384},{"code":"MEQ","name":"Cut Nyak Dhien Airport","city":"Kuala Pesisir","country":"ID","lat":4.040998,"lng":96.253312},{"code":"LKI","name":"Lasikin Airport","city":"Lubang","country":"ID","lat":2.411328,"lng":96.327722},{"code":"TXE","name":"Rembele Airport","city":"Takengon","country":"ID","lat":4.72113,"lng":96.851943},{"code":"BTJ","name":"Sultan Iskandar Muda International Airport","city":"Banda Aceh","country":"ID","lat":5.525087,"lng":95.41997},{"code":"TOD","name":"Tioman Airport","city":"Tioman Island","country":"MY","lat":2.81818,"lng":104.160004},{"code":"AOR","name":"Sultan Abdul Halim Airport","city":"Alor Satar","country":"MY","lat":6.189670085906982,"lng":100.39800262451172},{"code":"KBR","name":"Sultan Ismail Petra Airport","city":"Kota Baharu","country":"MY","lat":6.1668500900268555,"lng":102.29299926757812},{"code":"KUA","name":"Kuantan Airport","city":"Kuantan","country":"MY","lat":3.7753899097442627,"lng":103.20899963378906},{"code":"IPH","name":"Sultan Azlan Shah Airport","city":"Ipoh","country":"MY","lat":4.56733,"lng":101.091643},{"code":"JHB","name":"Senai International Airport","city":"Johor Bahru","country":"MY","lat":1.64131,"lng":103.669998},{"code":"KUL","name":"Kuala Lumpur International Airport","city":"Sepang","country":"MY","lat":2.74558,"lng":101.709999},{"code":"LGK","name":"Langkawi International Airport","city":"Langkawi","country":"MY","lat":6.32973,"lng":99.728699},{"code":"MKZ","name":"Malacca International Airport","city":"Malacca","country":"MY","lat":2.265613,"lng":102.252778},{"code":"TGG","name":"Sultan Mahmud Airport","city":"Kuala Terengganu","country":"MY","lat":5.3826398849487305,"lng":103.10299682617188},{"code":"PEN","name":"Penang International Airport","city":"Penang","country":"MY","lat":5.296303,"lng":100.276185},{"code":"PKG","name":"Pulau Pangkor Airport","city":"Pangkor Island","country":"MY","lat":4.244719982147217,"lng":100.5530014038086},{"code":"SZB","name":"Sultan Abdul Aziz Shah International Airport","city":"Subang","country":"MY","lat":3.13058,"lng":101.549004},{"code":"DTR","name":"Decatur Shores Airport","city":"Decatur","country":"US","lat":48.500171,"lng":-122.813973},{"code":"SXK","name":"Mathilda Batlayeri Airport","city":"Saumlaki-Yamdena Island","country":"ID","lat":-7.848354,"lng":131.337068},{"code":"UAI","name":"Commander in Chief of FALINTIL, Kay Rala Xanana Gusmão, International Airport","city":"Suai","country":"TL","lat":-9.301856,"lng":125.286302},{"code":"DIL","name":"Presidente Nicolau Lobato International Airport","city":"Dili","country":"TL","lat":-8.546562,"lng":125.524507},{"code":"BCH","name":"Baucau Airport","city":"Baucau","country":"TL","lat":-8.486479,"lng":126.399981},{"code":"OEC","name":"Oecusse Route of the Sandalwood International Airport","city":"Oecussi-Ambeno","country":"TL","lat":-9.198419,"lng":124.337873},{"code":"LBW","name":"Yuvai Semaring Airport","city":"Long Bawan","country":"ID","lat":3.900495,"lng":115.690089},{"code":"BXT","name":"LNG Badak Airport","city":"Bontang-Borneo Island","country":"ID","lat":0.121656,"lng":117.476558},{"code":"LPU","name":"Long Apung Airport","city":"Long Apung-Borneo Island","country":"ID","lat":1.706932,"lng":114.969934},{"code":"UGI","name":"San Juan /Uganik/ Seaplane Base","city":"San Juan","country":"US","lat":57.7304,"lng":-153.320999},{"code":"XSP","name":"Seletar Airport","city":"Seletar","country":"SG","lat":1.41555,"lng":103.86673},{"code":"SIN","name":"Singapore Changi Airport","city":"Singapore","country":"SG","lat":1.35019,"lng":103.994003},{"code":"ALH","name":"Albany Airport","city":"Albany","country":"AU","lat":-34.943333,"lng":117.808889},{"code":"YAJ","name":"Lyall Harbour Seaplane Base","city":"Saturna Island","country":"CA","lat":48.795274,"lng":-123.181801},{"code":"YAQ","name":"Maple Bay Seaplane Base","city":"Maple Bay","country":"CA","lat":48.8167,"lng":-123.6084},{"code":"ARM","name":"Armidale Airport","city":"Armidale","country":"AU","lat":-30.528099060099997,"lng":151.617004395},{"code":"AUU","name":"Aurukun Airport","city":"Aurukun","country":"AU","lat":-13.354019,"lng":141.720028},{"code":"AYQ","name":"Ayers Rock Connellan Airport","city":"Yulara","country":"AU","lat":-25.185913,"lng":130.97703},{"code":"BCI","name":"Barcaldine Airport","city":"Barcaldine","country":"AU","lat":-23.566268,"lng":145.302086},{"code":"ASP","name":"Alice Springs Airport","city":"Alice Springs","country":"AU","lat":-23.806588,"lng":133.903427},{"code":"BDD","name":"Badu Island Airport","city":"Badu Island","country":"AU","lat":-10.14955,"lng":142.173665},{"code":"BNE","name":"Brisbane International Airport","city":"Brisbane","country":"AU","lat":-27.384199142456055,"lng":153.11700439453125},{"code":"OOL","name":"Gold Coast Airport","city":"Gold Coast","country":"AU","lat":-28.165962,"lng":153.506641},{"code":"BKQ","name":"Blackall Airport","city":"Blackall","country":"AU","lat":-24.431678,"lng":145.429716},{"code":"CNS","name":"Cairns International Airport","city":"Cairns","country":"AU","lat":-16.878921,"lng":145.74948},{"code":"CTL","name":"Charleville Airport","city":"Charleville","country":"AU","lat":-26.4132995605,"lng":146.261993408},{"code":"BXG","name":"Bendigo Airport","city":"","country":"AU","lat":-36.739399,"lng":144.330002},{"code":"BVI","name":"Birdsville Airport","city":"","country":"AU","lat":-25.897499084472656,"lng":139.34800720214844},{"code":"BHQ","name":"Broken Hill Airport","city":"Broken Hill","country":"AU","lat":-32.0013999939,"lng":141.472000122},{"code":"HTI","name":"Hamilton Island Airport","city":"Hamilton Island","country":"AU","lat":-20.3581008911,"lng":148.95199585},{"code":"BEU","name":"Bedourie Airport","city":"Bedourie","country":"AU","lat":-24.3461,"lng":139.460007},{"code":"BRK","name":"Bourke Airport","city":"","country":"AU","lat":-30.039199829101562,"lng":145.95199584960938},{"code":"BUC","name":"Burketown Airport","city":"","country":"AU","lat":-17.748600006103516,"lng":139.53399658203125},{"code":"BQB","name":"Busselton Margaret River Regional Airport","city":"Busselton","country":"AU","lat":-33.687222,"lng":115.400278},{"code":"ISA","name":"Mount Isa Airport","city":"Mount Isa","country":"AU","lat":-20.66638,"lng":139.488468},{"code":"MKY","name":"Mackay Airport","city":"Mackay","country":"AU","lat":-21.170816,"lng":149.182588},{"code":"BNK","name":"Ballina Byron Gateway Airport","city":"Ballina","country":"AU","lat":-28.833236,"lng":153.561471},{"code":"GIC","name":"Boigu Island Airport","city":"Boigu Island","country":"AU","lat":-9.23278,"lng":142.218002},{"code":"OKY","name":"Oakey Army Aviation Centre","city":"","country":"AU","lat":-27.409262,"lng":151.736727},{"code":"BQL","name":"Boulia Airport","city":"","country":"AU","lat":-22.913299560546875,"lng":139.89999389648438},{"code":"PPP","name":"Proserpine Whitsunday Coast Airport","city":"Proserpine","country":"AU","lat":-20.494416,"lng":148.553583},{"code":"YBQ","name":"Telegraph Harbour Seaplane Base","city":"Thetis Island","country":"CA","lat":48.97,"lng":-123.664},{"code":"ROK","name":"Rockhampton Airport","city":"Rockhampton","country":"AU","lat":-23.380019,"lng":150.475359},{"code":"BME","name":"Broome International Airport","city":"Broome","country":"AU","lat":-17.949194,"lng":122.2283},{"code":"MCY","name":"Sunshine Coast Airport","city":"Maroochydore","country":"AU","lat":-26.593324,"lng":153.08319},{"code":"BHS","name":"Bathurst Airport","city":"Bathurst","country":"AU","lat":-33.406817,"lng":149.651161},{"code":"TSV","name":"Townsville Airport / RAAF Base Townsville","city":"Townsville","country":"AU","lat":-19.252904,"lng":146.766512},{"code":"BDB","name":"Bundaberg Airport","city":"Bundaberg","country":"AU","lat":-24.905039,"lng":152.322612},{"code":"WEI","name":"Weipa Airport","city":"Weipa","country":"AU","lat":-12.677493,"lng":141.922618},{"code":"WTB","name":"Toowoomba Wellcamp Airport","city":"Toowoomba","country":"AU","lat":-27.558332,"lng":151.793335},{"code":"CVQ","name":"Carnarvon Airport","city":"Carnarvon","country":"AU","lat":-24.884337,"lng":113.666393},{"code":"CAZ","name":"Cobar Airport","city":"","country":"AU","lat":-31.538299560546875,"lng":145.79400634765625},{"code":"CPD","name":"Coober Pedy Airport","city":"Coober Pedy","country":"AU","lat":-29.038312,"lng":134.722166},{"code":"CNC","name":"Coconut Island Airport","city":"","country":"AU","lat":-10.050139,"lng":143.070007},{"code":"CNJ","name":"Cloncurry Airport","city":"Cloncurry","country":"AU","lat":-20.668600082399998,"lng":140.503997803},{"code":"CED","name":"Ceduna Airport","city":"","country":"AU","lat":-32.13059997558594,"lng":133.7100067138672},{"code":"CFS","name":"Coffs Harbour Airport","city":"Coffs Harbour","country":"AU","lat":-30.320601,"lng":153.115997},{"code":"CKW","name":"Christmas Creek Airport","city":"Christmas Creek Mine","country":"AU","lat":-22.354302,"lng":119.642603},{"code":"CTN","name":"Cooktown Airport","city":"","country":"AU","lat":-15.443649,"lng":145.183221},{"code":"CMA","name":"Cunnamulla Airport","city":"","country":"AU","lat":-28.030000686645508,"lng":145.6219940185547},{"code":"CNB","name":"Coonamble Airport","city":"","country":"AU","lat":-30.980946,"lng":148.377833},{"code":"CUQ","name":"Coen Airport","city":"Coen","country":"AU","lat":-13.761133,"lng":143.113311},{"code":"OOM","name":"Cooma Snowy Mountains Airport","city":"Cooma","country":"AU","lat":-36.300445,"lng":148.972408},{"code":"DMD","name":"Doomadgee Airport","city":"","country":"AU","lat":-17.9403,"lng":138.822006},{"code":"NLF","name":"Darnley Island Airport","city":"Darnley Island","country":"AU","lat":-9.579176,"lng":143.780276},{"code":"DPO","name":"Devonport Airport","city":"Devonport","country":"AU","lat":-41.1697006226,"lng":146.429992676},{"code":"ELC","name":"Elcho Island Airport","city":"Elcho Island","country":"AU","lat":-12.019399642899998,"lng":135.570999146},{"code":"EMD","name":"Emerald Airport","city":"Emerald","country":"AU","lat":-23.5674991608,"lng":148.179000854},{"code":"EPR","name":"Esperance Airport","city":"Esperance","country":"AU","lat":-33.684399,"lng":121.822998},{"code":"FLS","name":"Flinders Island Airport","city":"Whitemark","country":"AU","lat":-40.091702,"lng":147.992996},{"code":"FIZ","name":"Fitzroy Crossing Airport","city":"","country":"AU","lat":-18.183596,"lng":125.559783},{"code":"GET","name":"Geraldton Airport","city":"Moonyoonooka","country":"AU","lat":-28.796101,"lng":114.707001},{"code":"GLT","name":"Gladstone Airport","city":"Gladstone","country":"AU","lat":-23.869763,"lng":151.225439},{"code":"YGN","name":"Greenway Sound Seaplane Base","city":"Broughton Island","country":"CA","lat":50.839,"lng":-126.775},{"code":"GTE","name":"Groote Eylandt Airport","city":"Groote Eylandt","country":"AU","lat":-13.972405,"lng":136.458553},{"code":"GFF","name":"Griffith Airport","city":"Griffith","country":"AU","lat":-34.2508010864,"lng":146.067001343},{"code":"HVB","name":"Hervey Bay Airport","city":"Hervey Bay","country":"AU","lat":-25.320127,"lng":152.880662},{"code":"HID","name":"Horn Island Airport","city":"Horn","country":"AU","lat":-10.585636,"lng":142.29277},{"code":"HOK","name":"Hooker Creek Airport","city":"Lajamanu","country":"AU","lat":-18.3367004395,"lng":130.638000488},{"code":"MHU","name":"Mount Hotham Airport","city":"Mount Hotham","country":"AU","lat":-37.0475006104,"lng":147.333999634},{"code":"HGD","name":"Hughenden Airport","city":"Hughenden","country":"AU","lat":-20.815001,"lng":144.225006},{"code":"JCK","name":"Julia Creek Airport","city":"","country":"AU","lat":-20.66830062866211,"lng":141.72300720214844},{"code":"KQR","name":"Karara Airport","city":"Karara","country":"AU","lat":-29.217223,"lng":116.687154},{"code":"KAX","name":"Kalbarri Airport","city":"Kalbarri","country":"AU","lat":-27.692813,"lng":114.259169},{"code":"KNS","name":"King Island Airport","city":"","country":"AU","lat":-39.877498626708984,"lng":143.8780059814453},{"code":"KFG","name":"Kalkgurung Airport","city":"","country":"AU","lat":-17.431900024414062,"lng":130.80799865722656},{"code":"KRB","name":"Karumba Airport","city":"","country":"AU","lat":-17.45669937133789,"lng":140.8300018310547},{"code":"KWM","name":"Kowanyama Airport","city":"Kowanyama","country":"AU","lat":-15.485369,"lng":141.752572},{"code":"KGC","name":"Kingscote Airport","city":"","country":"AU","lat":-35.71390151977539,"lng":137.52099609375},{"code":"KUG","name":"Kubin Island Airport","city":"Kubin Island","country":"AU","lat":-10.22648,"lng":142.219619},{"code":"LNO","name":"Leonora Airport","city":"Leonora","country":"AU","lat":-28.87809944152832,"lng":121.31500244140625},{"code":"LEL","name":"Lake Evella Airport","city":"","country":"AU","lat":-12.498900413513184,"lng":135.80599975585938},{"code":"LDH","name":"Lord Howe Island Airport","city":"Lord Howe Island","country":"AU","lat":-31.538204,"lng":159.07546},{"code":"IRG","name":"Lockhart River Airport","city":"Lockhart River","country":"AU","lat":-12.7869,"lng":143.304993},{"code":"LSY","name":"Lismore Airport","city":"Lismore","country":"AU","lat":-28.830689,"lng":153.258419},{"code":"LHG","name":"Lightning Ridge Airport","city":"","country":"AU","lat":-29.452857,"lng":147.977146},{"code":"LRE","name":"Longreach Airport","city":"Longreach","country":"AU","lat":-23.4342,"lng":144.279999},{"code":"LER","name":"Leinster Airport","city":"","country":"AU","lat":-27.843299865722656,"lng":120.7030029296875},{"code":"LVO","name":"Laverton Airport","city":"Laverton","country":"AU","lat":-28.614267,"lng":122.428673},{"code":"UBB","name":"Mabuiag Island Airport","city":"Mabuiag Island","country":"AU","lat":-9.950168,"lng":142.19523},{"code":"MYI","name":"Murray Island Airport","city":"Murray Island","country":"AU","lat":-9.915105,"lng":144.054644},{"code":"AVV","name":"Melbourne Avalon International Airport","city":"Geelong/Melbourne","country":"AU","lat":-38.040269,"lng":144.467196},{"code":"ABX","name":"Albury Airport","city":"East Albury","country":"AU","lat":-36.066758,"lng":146.959148},{"code":"MKR","name":"Meekatharra Airport","city":"","country":"AU","lat":-26.6117000579834,"lng":118.5479965209961},{"code":"MEB","name":"Melbourne Essendon Airport","city":"Essendon Fields","country":"AU","lat":-37.7281,"lng":144.901993},{"code":"MIM","name":"Merimbula Airport","city":"Merimbula","country":"AU","lat":-36.9085998535,"lng":149.901000977},{"code":"MGT","name":"Milingimbi Airport","city":"Milingimbi Island","country":"AU","lat":-12.0944004059,"lng":134.893997192},{"code":"MNG","name":"Maningrida Airport","city":"Maningrida","country":"AU","lat":-12.0560998917,"lng":134.23399353},{"code":"HBA","name":"Hobart International Airport","city":"Hobart (Cambridge)","country":"AU","lat":-42.837032,"lng":147.513022},{"code":"MCV","name":"McArthur River Mine Airport","city":"McArthur River Mine","country":"AU","lat":-16.4424991608,"lng":136.083999634},{"code":"MQL","name":"Mildura Airport","city":"Mildura","country":"AU","lat":-34.229198455799995,"lng":142.085998535},{"code":"LST","name":"Launceston Airport","city":"Launceston (Western Junction)","country":"AU","lat":-41.544935,"lng":147.210785},{"code":"MBW","name":"Melbourne Moorabbin Airport","city":"Melbourne","country":"AU","lat":-37.977765,"lng":145.099799},{"code":"MEL","name":"Melbourne Airport","city":"Melbourne","country":"AU","lat":-37.670732,"lng":144.837898},{"code":"MMG","name":"Mount Magnet Airport","city":"","country":"AU","lat":-28.116100311279297,"lng":117.84200286865234},{"code":"MRZ","name":"Moree Airport","city":"Moree","country":"AU","lat":-29.498899459799997,"lng":149.845001221},{"code":"MOV","name":"Moranbah Airport","city":"Moranbah","country":"AU","lat":-22.057800293,"lng":148.07699585},{"code":"MYA","name":"Moruya Airport","city":"Moruya","country":"AU","lat":-35.8978004456,"lng":150.143997192},{"code":"MGB","name":"Mount Gambier Airport","city":"Mount Gambier","country":"AU","lat":-37.744382,"lng":140.780561},{"code":"ONG","name":"Mornington Island Airport","city":"","country":"AU","lat":-16.662500381469727,"lng":139.17799377441406},{"code":"NRA","name":"Narrandera Airport","city":"Narrandera","country":"AU","lat":-34.7022018433,"lng":146.511993408},{"code":"NAA","name":"Narrabri Airport","city":"Narrabri","country":"AU","lat":-30.3192005157,"lng":149.82699585},{"code":"ABM","name":"Northern Peninsula Airport","city":"Bamaga","country":"AU","lat":-10.946194,"lng":142.455081},{"code":"NTN","name":"Normanton Airport","city":"Normanton","country":"AU","lat":-17.68409,"lng":141.069664},{"code":"ZNE","name":"Newman Airport","city":"Newman","country":"AU","lat":-23.417800903299998,"lng":119.803001404},{"code":"OLP","name":"Olympic Dam Airport","city":"Olympic Dam","country":"AU","lat":-30.483861,"lng":136.876699},{"code":"ADL","name":"Adelaide International Airport","city":"Adelaide","country":"AU","lat":-34.947512,"lng":138.533393},{"code":"PUG","name":"Port Augusta Airport","city":"","country":"AU","lat":-32.506900787353516,"lng":137.7169952392578},{"code":"PMK","name":"Palm Island Airport","city":"","country":"AU","lat":-18.755301,"lng":146.580994},{"code":"PBO","name":"Paraburdoo Airport","city":"Paraburdoo","country":"AU","lat":-23.1711006165,"lng":117.745002747},{"code":"CCK","name":"Cocos (Keeling) Islands Airport","city":"West Island","country":"CC","lat":-12.192218,"lng":96.834103},{"code":"DRW","name":"Darwin International Airport / RAAF Darwin","city":"Darwin","country":"AU","lat":-12.41497,"lng":130.88185},{"code":"GOV","name":"Gove Airport","city":"Nhulunbuy","country":"AU","lat":-12.269399642899998,"lng":136.817993164},{"code":"KTA","name":"Karratha Airport","city":"Karratha","country":"AU","lat":-20.712200164799995,"lng":116.773002625},{"code":"KGI","name":"Kalgoorlie Boulder Airport","city":"Broadwood","country":"AU","lat":-30.791543,"lng":121.464586},{"code":"PKE","name":"Parkes Airport","city":"Parkes","country":"AU","lat":-33.131401062,"lng":148.238998413},{"code":"KNX","name":"East Kimberley Regional (Kununurra) Airport","city":"Kununurra","country":"AU","lat":-15.7781,"lng":128.707993},{"code":"PLO","name":"Port Lincoln Airport","city":"Port Lincoln","country":"AU","lat":-34.6053009033,"lng":135.880004883},{"code":"LEA","name":"Learmonth Airport","city":"Exmouth","country":"AU","lat":-22.235201,"lng":114.090024},{"code":"PXH","name":"Prominent Hill Airport","city":"Mount Eba","country":"AU","lat":-29.716,"lng":135.5244},{"code":"EDR","name":"Pormpuraaw Airport","city":"Pormpuraaw","country":"AU","lat":-14.896451,"lng":141.60908},{"code":"PQQ","name":"Port Macquarie Airport","city":"Port Macquarie","country":"AU","lat":-31.4358005524,"lng":152.863006592},{"code":"PTJ","name":"Portland Airport","city":"","country":"AU","lat":-38.31809997558594,"lng":141.4709930419922},{"code":"PHE","name":"Port Hedland International Airport","city":"Port Hedland","country":"AU","lat":-20.382787,"lng":118.629789},{"code":"PER","name":"Perth International Airport","city":"Perth","country":"AU","lat":-31.94029998779297,"lng":115.96700286865234},{"code":"XCH","name":"Christmas Island International Airport","city":"Flying Fish Cove","country":"CX","lat":-10.450387,"lng":105.6911},{"code":"ULP","name":"Quilpie Airport","city":"","country":"AU","lat":-26.609176,"lng":144.253746},{"code":"RCM","name":"Richmond Airport","city":"","country":"AU","lat":-20.701900482177734,"lng":143.11500549316406},{"code":"RAM","name":"Ramingining Airport","city":"","country":"AU","lat":-12.356399536132812,"lng":134.8979949951172},{"code":"RMA","name":"Roma Airport","city":"Roma","country":"AU","lat":-26.545000076300003,"lng":148.774993896},{"code":"CBR","name":"Canberra Airport","city":"Canberra","country":"AU","lat":-35.3069,"lng":149.195007},{"code":"DBO","name":"Dubbo City Regional Airport","city":"Dubbo","country":"AU","lat":-32.2167015076,"lng":148.574996948},{"code":"SGO","name":"St George Airport","city":"","country":"AU","lat":-28.049699783325195,"lng":148.59500122070312},{"code":"MJK","name":"Shark Bay Airport","city":"Denham","country":"AU","lat":-25.897294,"lng":113.575802},{"code":"SBR","name":"Saibai Island Airport","city":"Saibai Island","country":"AU","lat":-9.37833,"lng":142.625},{"code":"SNB","name":"Snake Bay Airport","city":"Milikapiti","country":"AU","lat":-11.417674,"lng":130.648481},{"code":"NLK","name":"Norfolk Island International Airport","city":"Burnt Pine","country":"NF","lat":-29.041759,"lng":167.939474},{"code":"SYD","name":"Sydney Kingsford Smith International Airport","city":"Sydney (Mascot)","country":"AU","lat":-33.946098,"lng":151.177002},{"code":"TMW","name":"Tamworth Airport","city":"Tamworth","country":"AU","lat":-31.077898,"lng":150.845484},{"code":"WGA","name":"Wagga Wagga Airport","city":"Forest Hill","country":"AU","lat":-35.163484,"lng":147.46832},{"code":"XTG","name":"Thargomindah Airport","city":"Thargomindah","country":"AU","lat":-27.986368,"lng":143.812065},{"code":"THG","name":"Thangool Airport","city":"Biloela","country":"AU","lat":-24.494863,"lng":150.577967},{"code":"TCA","name":"Tennant Creek Airport","city":"Tennant Creek","country":"AU","lat":-19.6343994140625,"lng":134.18299865722656},{"code":"SYU","name":"Warraber Island Airport","city":"Sue Islet","country":"AU","lat":-10.20829963684082,"lng":142.8249969482422},{"code":"WNR","name":"Windorah Airport","city":"Windorah","country":"AU","lat":-25.410641,"lng":142.668428},{"code":"WYA","name":"Whyalla Airport","city":"Whyalla","country":"AU","lat":-33.05889892578125,"lng":137.51400756835938},{"code":"WGE","name":"Walgett Airport","city":"","country":"AU","lat":-30.032801,"lng":148.126007},{"code":"NTL","name":"Newcastle Airport","city":"Williamtown","country":"AU","lat":-32.796114,"lng":151.835025},{"code":"WUN","name":"Wiluna Airport","city":"","country":"AU","lat":-26.629199981689453,"lng":120.22100067138672},{"code":"WIN","name":"Winton Airport","city":"","country":"AU","lat":-22.36359977722168,"lng":143.08599853515625},{"code":"BWT","name":"Wynyard Airport","city":"Burnie","country":"AU","lat":-40.997039,"lng":145.726},{"code":"OKR","name":"Yorke Island Airport","city":"Yorke Island","country":"AU","lat":-9.752801,"lng":143.405673},{"code":"XMY","name":"Yam Island Airport","city":"Yam Island","country":"AU","lat":-9.899187,"lng":142.774265},{"code":"KKI","name":"Akiachak Airport","city":"Akiachak","country":"US","lat":60.913809,"lng":-161.493329},{"code":"KBC","name":"Birch Creek Airport","city":"Birch Creek","country":"US","lat":66.2740020752,"lng":-145.824005127},{"code":"PEK","name":"Beijing Capital International Airport","city":"Beijing","country":"CN","lat":40.077349,"lng":116.596702},{"code":"PKX","name":"Beijing Daxing International Airport","city":"Beijing","country":"CN","lat":39.501289,"lng":116.413967},{"code":"CDE","name":"Chengde Puning Airport","city":"Chengde","country":"CN","lat":41.1225,"lng":118.073889},{"code":"CIF","name":"Chifeng Yulong Airport","city":"Chifeng","country":"CN","lat":42.159723,"lng":118.840971},{"code":"CIH","name":"Changzhi Wangcun Airport","city":"Changzhi","country":"CN","lat":36.247501,"lng":113.125999},{"code":"BPE","name":"Qinhuangdao Beidaihe Airport","city":"Qinhuangdao (Changli)","country":"CN","lat":39.666384,"lng":119.061384},{"code":"DSN","name":"Ordos Ejin Horo International Airport","city":"Ordos","country":"CN","lat":39.493514,"lng":109.8599},{"code":"DAT","name":"Datong Yungang International Airport","city":"Datong","country":"CN","lat":40.06139,"lng":113.480509},{"code":"ERL","name":"Erenhot Saiwusu International Airport","city":"Erenhot","country":"CN","lat":43.424079,"lng":112.091081},{"code":"YIE","name":"Arxan Yi'ershi Airport","city":"Arxan","country":"CN","lat":47.3106,"lng":119.9117},{"code":"HDG","name":"Handan Airport","city":"Handan","country":"CN","lat":36.524824,"lng":114.424126},{"code":"HET","name":"Hohhot Baita International Airport","city":"Hohhot","country":"CN","lat":40.849658,"lng":111.824598},{"code":"HUO","name":"Holingol Huolinhe Airport","city":"Holingol","country":"CN","lat":45.487222,"lng":119.407222},{"code":"HLD","name":"Hulunbuir Hailar Airport","city":"Hailar","country":"CN","lat":49.208616,"lng":119.822301},{"code":"LFQ","name":"Linfen Yaodu Airport","city":"Linfen (Yaodu)","country":"CN","lat":36.132629,"lng":111.641236},{"code":"LLV","name":"Lüliang Dawu Airport","city":"Lüliang","country":"CN","lat":37.683333,"lng":111.142778},{"code":"NZH","name":"Manzhouli Xijiao Airport","city":"Manzhouli","country":"CN","lat":49.566667,"lng":117.33},{"code":"BAV","name":"Baotou Donghe International Airport","city":"Baotou","country":"CN","lat":40.560001,"lng":109.997002},{"code":"SZH","name":"Shuozhou Zirun Airport","city":"Shuozhou","country":"CN","lat":39.273241,"lng":112.691552},{"code":"SJW","name":"Shijiazhuang Zhengding International Airport","city":"Shijiazhuang","country":"CN","lat":38.280701,"lng":114.696999},{"code":"TVS","name":"Tangshan Sannühe Airport","city":"Tangshan (Fengrun)","country":"CN","lat":39.7178,"lng":118.002625},{"code":"TSN","name":"Tianjin Binhai International Airport","city":"Tianjin","country":"CN","lat":39.124401092499994,"lng":117.346000671},{"code":"TGO","name":"Tongliao Airport","city":"Tongliao","country":"CN","lat":43.556702,"lng":122.199997},{"code":"UCB","name":"Ulanqab Jining Airport","city":"Ulanqab","country":"CN","lat":41.130266,"lng":113.107274},{"code":"WUA","name":"Wuhai Airport","city":"Wuhai","country":"CN","lat":39.7934,"lng":106.7993},{"code":"HLH","name":"Ulanhot Yilelite Airport","city":"Ulanhot","country":"CN","lat":46.195333,"lng":122.008333},{"code":"XIL","name":"Xilinhot Airport","city":"Xilinhot","country":"CN","lat":43.91559982299805,"lng":115.96399688720703},{"code":"WUT","name":"Xinzhou Wutaishan Airport","city":"Xinzhou","country":"CN","lat":38.597456,"lng":112.969173},{"code":"YCU","name":"Yuncheng Yanhu International Airport","city":"Yuncheng (Yanhu)","country":"CN","lat":35.117823,"lng":111.034023},{"code":"TYN","name":"Taiyuan Wusu International Airport","city":"Taiyuan","country":"CN","lat":37.746899,"lng":112.627998},{"code":"RLK","name":"Bayannur Tianjitai Airport","city":"Bayannur","country":"CN","lat":40.926358,"lng":107.74093},{"code":"ZQZ","name":"Zhangjiakou Ningyuan Airport","city":"Zhangjiakou","country":"CN","lat":40.738664,"lng":114.933395},{"code":"NZL","name":"Zhalantun Genghis Khan Airport","city":"Zhalantun","country":"CN","lat":47.865942,"lng":122.768662},{"code":"BHY","name":"Beihai Fucheng Airport","city":"Beihai","country":"CN","lat":21.538659,"lng":109.293683},{"code":"AEB","name":"Baise (Bose) Bama Airport","city":"Baise (Tianyang)","country":"CN","lat":23.7206,"lng":106.959999},{"code":"CGD","name":"Changde Taohuayuan Airport","city":"Changde (Dingcheng)","country":"CN","lat":28.9189,"lng":111.639999},{"code":"HJJ","name":"Huaihua Zhijiang Airport","city":"Huaihua","country":"CN","lat":27.443087,"lng":109.704666},{"code":"HCZ","name":"Chenzhou Beihu Airport","city":"Chenzhou","country":"CN","lat":25.753419,"lng":112.845404},{"code":"DYG","name":"Zhangjiajie Hehua International Airport","city":"Zhangjiajie (Yongding)","country":"CN","lat":29.104749,"lng":110.442786},{"code":"FUO","name":"Foshan Shadi Airport","city":"Foshan (Nanhai)","country":"CN","lat":23.0825,"lng":113.070833},{"code":"CAN","name":"Guangzhou Baiyun International Airport","city":"Guangzhou (Huadu)","country":"CN","lat":23.392401,"lng":113.299004},{"code":"CSX","name":"Changsha Huanghua International Airport","city":"Changsha (Changsha)","country":"CN","lat":28.189199,"lng":113.220001},{"code":"HCJ","name":"Hechi Jinchengjiang Airport","city":"Hechi (Jinchengjiang)","country":"CN","lat":24.804344,"lng":107.710819},{"code":"HNY","name":"Hengyang Nanyue Airport","city":"Hengyang","country":"CN","lat":26.72208,"lng":112.617958},{"code":"HUZ","name":"Huizhou Pingtan Airport","city":"Huizhou (Pingtan)","country":"CN","lat":23.049999,"lng":114.599998},{"code":"KWL","name":"Guilin Liangjiang International Airport","city":"Guilin (Lingui)","country":"CN","lat":25.219828,"lng":110.039553},{"code":"LLF","name":"Yongzhou Lingling Airport","city":"Yongzhou","country":"CN","lat":26.338661,"lng":111.610043},{"code":"MXZ","name":"Meizhou Meixian Changgangji International Airport","city":"Meizhou (Meixian)","country":"CN","lat":24.263425,"lng":116.097857},{"code":"NNG","name":"Nanning Wuxu International Airport","city":"Nanning (Jiangnan)","country":"CN","lat":22.598071,"lng":108.181922},{"code":"SWA","name":"Jieyang Chaoshan International Airport","city":"Jieyang (Rongcheng)","country":"CN","lat":23.552,"lng":116.5033},{"code":"ZUH","name":"Zhuhai Jinwan Airport","city":"Zhuhai (Jinwan)","country":"CN","lat":22.006399,"lng":113.375999},{"code":"WGN","name":"Shaoyang Wugang Airport","city":"Shaoyang (Wugang)","country":"CN","lat":26.806123,"lng":110.641042},{"code":"SZX","name":"Shenzhen Bao'an International Airport","city":"Shenzhen","country":"CN","lat":22.639474,"lng":113.803262},{"code":"WUZ","name":"Wuzhou Xijiang Airport","city":"Tangbu","country":"CN","lat":23.40316,"lng":111.09331},{"code":"DXJ","name":"Xiangxi Biancheng Airport","city":"Xiangxi","country":"CN","lat":28.4972,"lng":109.5218},{"code":"YLX","name":"Yulin Fumian Airport","city":"Yulin","country":"CN","lat":22.433042,"lng":110.119996},{"code":"YYA","name":"Yueyang Sanhe Airport","city":"Yueyang (Yueyanglou)","country":"CN","lat":29.311699,"lng":113.281574},{"code":"LZH","name":"Liuzhou Bailian Airport / Bailian Air Base","city":"Liuzhou (Liujiang)","country":"CN","lat":24.2075,"lng":109.390999},{"code":"ZHA","name":"Zhanjiang Wuchuan International Airport","city":"Zhanjiang","country":"CN","lat":21.481667,"lng":110.590278},{"code":"CGO","name":"Zhengzhou Xinzheng International Airport","city":"Zhengzhou","country":"CN","lat":34.526497,"lng":113.849165},{"code":"EHU","name":"Ezhou Huahu International Airport","city":"Ezhou","country":"CN","lat":30.341178,"lng":115.03926},{"code":"ENH","name":"Enshi Xujiaping Airport","city":"Enshi (Enshi)","country":"CN","lat":30.320299,"lng":109.485001},{"code":"WUH","name":"Wuhan Tianhe International Airport","city":"Wuhan (Huangpi)","country":"CN","lat":30.774798,"lng":114.213723},{"code":"SHS","name":"Jingzhou Shashi Airport","city":"Jingzhou (Shashi)","country":"CN","lat":30.29281,"lng":112.44854},{"code":"LYA","name":"Luoyang Beijiao Airport","city":"Luoyang (Laocheng)","country":"CN","lat":34.7411,"lng":112.388},{"code":"NNY","name":"Nanyang Jiangying Airport","city":"Nanyang (Wancheng)","country":"CN","lat":32.982696,"lng":112.617467},{"code":"HQQ","name":"Anyang Hongqiqu Airport","city":"Anyang","country":"CN","lat":35.870833,"lng":114.461944},{"code":"HPG","name":"Shennongjia Hongping Airport","city":"Shennongjia (Hongping)","country":"CN","lat":31.626,"lng":110.34},{"code":"WDS","name":"Shiyan Wudangshan Airport","city":"Shiyan (Maojian)","country":"CN","lat":32.592889,"lng":110.906296},{"code":"XFN","name":"Xiangyang Liuji Airport","city":"Xiangyang (Xiangzhou)","country":"CN","lat":32.152222,"lng":112.291666},{"code":"XAI","name":"Xinyang Minggang Airport","city":"Xinyang","country":"CN","lat":32.540819,"lng":114.079141},{"code":"YIH","name":"Yichang Sanxia Airport","city":"Yichang (Xiaoting)","country":"CN","lat":30.554132,"lng":111.482563},{"code":"HAK","name":"Haikou Meilan International Airport","city":"Haikou (Meilan)","country":"CN","lat":19.9349,"lng":110.459},{"code":"BAR","name":"Qionghai Bo'ao Airport","city":"Qionghai (Basuo)","country":"CN","lat":19.140951,"lng":110.452766},{"code":"SYX","name":"Sanya Phoenix International Airport","city":"Sanya (Tianya)","country":"CN","lat":18.3029,"lng":109.412003},{"code":"RGO","name":"Orang (Chongjin) Airport","city":"Hoemun-ri","country":"KP","lat":41.428538,"lng":129.647555},{"code":"FNJ","name":"Pyongyang Sunan International Airport","city":"Pyongyang","country":"KP","lat":39.224098,"lng":125.669998},{"code":"DSO","name":"Sondok Airport","city":"Sŏndŏng-ni","country":"KP","lat":39.745201,"lng":127.473999},{"code":"WOS","name":"Wonsan Kalma Airport","city":"Wonsan","country":"KP","lat":39.165219,"lng":127.487931},{"code":"AKA","name":"Ankang Fuqiang Airport","city":"Ankang (Hanbin)","country":"CN","lat":32.75696,"lng":108.87338},{"code":"DNH","name":"Dunhuang Mogao International Airport","city":"Dunhuang","country":"CN","lat":40.161953,"lng":94.812827},{"code":"HXD","name":"Haixi Delingha Airport","city":"Delingha","country":"CN","lat":37.125286,"lng":97.268658},{"code":"GMQ","name":"Golog Maqên Airport","city":"Golog (Maqên)","country":"CN","lat":34.418066,"lng":100.301144},{"code":"GOQ","name":"Golmud Airport","city":"Golmud","country":"CN","lat":36.4006,"lng":94.786102},{"code":"GYU","name":"Guyuan Liupanshan Airport","city":"Guyuan (Yuanzhou)","country":"CN","lat":36.078889,"lng":106.216944},{"code":"HBQ","name":"Haibei Qilian Airport","city":"Haibei (Qilian)","country":"CN","lat":38.008068,"lng":100.645065},{"code":"HTT","name":"Huatugou Airport","city":"Mengnai","country":"CN","lat":38.201645,"lng":90.837843},{"code":"HZG","name":"Hanzhong Chenggu Airport","city":"Hanzhong (Chenggu)","country":"CN","lat":33.133527,"lng":107.203817},{"code":"INC","name":"Yinchuan Hedong International Airport","city":"Yinchuan","country":"CN","lat":38.322758,"lng":106.393214},{"code":"JIC","name":"Jinchang Jinchuan Airport","city":"Jinchang","country":"CN","lat":38.542222,"lng":102.348333},{"code":"JGN","name":"Jiayuguan International Airport","city":"Jiayuguan","country":"CN","lat":39.859052,"lng":98.339344},{"code":"LHW","name":"Lanzhou Zhongchuan International Airport","city":"Lanzhou (Yongdeng)","country":"CN","lat":36.515202,"lng":103.620003},{"code":"LNL","name":"Longnan Chengzhou Airport","city":"Longnan (Cheng)","country":"CN","lat":33.789918,"lng":105.790014},{"code":"IQN","name":"Qingyang Xifeng Airport","city":"Qingyang (Xifeng)","country":"CN","lat":35.802638,"lng":107.598896},{"code":"THQ","name":"Tianshui Maijishan Airport","city":"Tianshui (Maiji)","country":"CN","lat":34.5601,"lng":105.860343},{"code":"GXH","name":"Gannan Xiahe Airport","city":"Gannan (Xiahe)","country":"CN","lat":34.819014,"lng":102.622261},{"code":"XNN","name":"Xining Caojiabao International Airport","city":"Haidong (Huzhu Tu Autonomous County)","country":"CN","lat":36.52775,"lng":102.040215},{"code":"XIY","name":"Xi'an Xianyang International Airport","city":"Xi'an","country":"CN","lat":34.442207,"lng":108.762385},{"code":"ENY","name":"Yan'an Nanniwan Airport","city":"Yan'an (Baota)","country":"CN","lat":36.479413,"lng":109.464083},{"code":"UYN","name":"Yulin Yuyang Airport","city":"Yulin","country":"CN","lat":38.35971,"lng":109.590927},{"code":"YUS","name":"Yushu Batang Airport","city":"Yushu (Batang)","country":"CN","lat":32.836389,"lng":97.036389},{"code":"ZHY","name":"Zhongwei Shapotou Airport","city":"Zhongwei (Shapotou)","country":"CN","lat":37.573125,"lng":105.154454},{"code":"YZY","name":"Zhangye Ganzhou Airport","city":"Zhangye (Ganzhou)","country":"CN","lat":38.801899,"lng":100.675003},{"code":"AVK","name":"Arvaikheer Airport","city":"Arvaikheer","country":"MN","lat":46.250301361083984,"lng":102.802001953125},{"code":"LTI","name":"Altai Airport","city":"Altai","country":"MN","lat":46.376399993896484,"lng":96.22109985351562},{"code":"BYN","name":"Bayankhongor Airport","city":"Bayankhongor","country":"MN","lat":46.1633,"lng":100.704002},{"code":"UGA","name":"Bulgan Airport","city":"Bulgan","country":"MN","lat":48.85499954223633,"lng":103.47599792480469},{"code":"COQ","name":"Choibalsan Airport","city":"","country":"MN","lat":48.135381,"lng":114.647231},{"code":"UBN","name":"Ulaanbaatar Chinggis Khaan International Airport","city":"Ulaanbaatar (Sergelen)","country":"MN","lat":47.646916,"lng":106.819833},{"code":"ULZ","name":"Donoi Airport","city":"Uliastai","country":"MN","lat":47.7093,"lng":96.5258},{"code":"DLZ","name":"Dalanzadgad Airport","city":"Dalanzadgad","country":"MN","lat":43.608628,"lng":104.367734},{"code":"HVD","name":"Khovd Airport","city":"Khovd","country":"MN","lat":47.954102,"lng":91.628197},{"code":"MXW","name":"Mandalgobi Airport","city":"Mandalgobi","country":"MN","lat":45.7380981445,"lng":106.268997192},{"code":"MXV","name":"Mörön Airport","city":"Mörön","country":"MN","lat":49.663677,"lng":100.100028},{"code":"ULO","name":"Ulaangom Airport","city":"Ulaangom","country":"MN","lat":50.066588,"lng":91.938273},{"code":"ULG","name":"Ölgii Mongolei International Airport","city":"Ölgii","country":"MN","lat":48.993301,"lng":89.922501},{"code":"BSD","name":"Baoshan Yunrui Airport","city":"Baoshan (Longyang)","country":"CN","lat":25.053301,"lng":99.168297},{"code":"CWJ","name":"Cangyuan Washan Airport","city":"Lincang (Cangyuan)","country":"CN","lat":23.276331,"lng":99.373169},{"code":"DLU","name":"Dali Fengyi Airport","city":"Dali (Xiaguan)","country":"CN","lat":25.649401,"lng":100.319},{"code":"DIG","name":"Diqing Shangri-La Airport","city":"Diqing (Shangri-La)","country":"CN","lat":27.7936,"lng":99.6772},{"code":"JHG","name":"Xishuangbanna Gasa International Airport","city":"Jinghong (Gasa)","country":"CN","lat":21.974648,"lng":100.762224},{"code":"JMJ","name":"Lancang Jingmai Airport","city":"Pu'er (Lancang)","country":"CN","lat":22.417733,"lng":99.784012},{"code":"LNJ","name":"Lincang Boshang Airport","city":"Lincang","country":"CN","lat":23.7381,"lng":100.025002},{"code":"LJG","name":"Lijiang Sanyi International Airport","city":"Lijiang","country":"CN","lat":26.677483,"lng":100.244944},{"code":"LUM","name":"Dehong Mangshi International Airport","city":"Dehong (Mangshi)","country":"CN","lat":24.4011,"lng":98.5317},{"code":"KMG","name":"Kunming Changshui International Airport","city":"Kunming","country":"CN","lat":25.110313,"lng":102.936743},{"code":"SYM","name":"Pu'er Simao Airport","city":"Pu'er","country":"CN","lat":22.793301,"lng":100.959},{"code":"WNH","name":"Wenshan Puzhehei Airport","city":"Wenshan","country":"CN","lat":23.5583,"lng":104.3255},{"code":"XMN","name":"Xiamen Gaoqi International Airport","city":"Xiamen","country":"CN","lat":24.543889,"lng":118.127454},{"code":"AQG","name":"Anqing Tianzhushan Airport / Anqing North Air Base","city":"Anqing","country":"CN","lat":30.582199,"lng":117.050003},{"code":"CZX","name":"Changzhou Benniu International Airport","city":"Changzhou","country":"CN","lat":31.920485,"lng":119.77546},{"code":"KHN","name":"Nanchang Changbei International Airport","city":"Nanchang","country":"CN","lat":28.864815,"lng":115.90271},{"code":"DOY","name":"Dongying Shengli Airport","city":"Dongying (Kenli)","country":"CN","lat":37.50137,"lng":118.789863},{"code":"FUG","name":"Fuyang Xiguan Airport","city":"Yingzhou, Fuyang","country":"CN","lat":32.882157,"lng":115.734364},{"code":"FOC","name":"Fuzhou Changle International Airport","city":"Fuzhou (Changle)","country":"CN","lat":25.929254,"lng":119.672524},{"code":"KOW","name":"Ganzhou Huangjin Airport","city":"Ganzhou","country":"CN","lat":25.853333,"lng":114.778889},{"code":"HGH","name":"Hangzhou Xiaoshan International Airport","city":"Hangzhou","country":"CN","lat":30.23609,"lng":120.428865},{"code":"HZA","name":"Heze Mudan Airport","city":"Heze (Dingtao)","country":"CN","lat":35.212972,"lng":115.736748},{"code":"JDZ","name":"Jingdezhen Luojia Airport","city":"Jingdezhen","country":"CN","lat":29.3386,"lng":117.176003},{"code":"JNG","name":"Jining Da'an Airport","city":"Jining","country":"CN","lat":35.647358,"lng":116.743269},{"code":"JIU","name":"Jiujiang Lushan Airport","city":"Jiujiang","country":"CN","lat":29.476944,"lng":115.801111},{"code":"TNA","name":"Jinan Yaoqiang International Airport","city":"Jinan (Licheng)","country":"CN","lat":36.857201,"lng":117.216003},{"code":"JUZ","name":"Quzhou Airport","city":"Quzhou (Kezheng)","country":"CN","lat":28.96613,"lng":118.898793},{"code":"LYG","name":"Lianyungang Huaguoshan International Airport","city":"Lianyungang","country":"CN","lat":34.41406,"lng":119.17899},{"code":"LCX","name":"Liancheng Guanzhishan Airport","city":"Longyan (Liancheng)","country":"CN","lat":25.67592,"lng":116.745907},{"code":"HYN","name":"Taizhou Luqiao Airport","city":"Taizhou (Luqiao)","country":"CN","lat":28.562201,"lng":121.429001},{"code":"LYI","name":"Linyi Qiyang Airport","city":"Linyi (Hedong)","country":"CN","lat":35.052918,"lng":118.411828},{"code":"NGB","name":"Ningbo Lishe International Airport","city":"Ningbo","country":"CN","lat":29.82670021057129,"lng":121.46199798583984},{"code":"NKG","name":"Nanjing Lukou International Airport","city":"Nanjing","country":"CN","lat":31.735032,"lng":118.865949},{"code":"NTG","name":"Nantong Xingdong International Airport","city":"Nantong (Tongzhou)","country":"CN","lat":32.073566,"lng":120.980076},{"code":"HFE","name":"Hefei Xinqiao International Airport","city":"Hefei","country":"CN","lat":31.98779,"lng":116.9769},{"code":"PVG","name":"Shanghai Pudong International Airport","city":"Shanghai (Pudong)","country":"CN","lat":31.1434,"lng":121.805},{"code":"TAO","name":"Qingdao Jiaodong International Airport","city":"Qingdao (Jiaozhou)","country":"CN","lat":36.361953,"lng":120.088171},{"code":"JJN","name":"Quanzhou Jinjiang International Airport","city":"Quanzhou","country":"CN","lat":24.795855,"lng":118.588599},{"code":"RIZ","name":"Rizhao Shanzihe Airport","city":"Rizhao (Donggang)","country":"CN","lat":35.405033,"lng":119.324403},{"code":"HIA","name":"Huai'an Lianshui Airport","city":"Huai'an","country":"CN","lat":33.792712,"lng":119.126657},{"code":"SQJ","name":"Sanming Shaxian Airport","city":"Sanming (Sha)","country":"CN","lat":26.4263,"lng":117.8336},{"code":"SQD","name":"Shangrao Sanqingshan Airport","city":"Shangrao (Hengfeng)","country":"CN","lat":28.3797,"lng":117.9643},{"code":"SHA","name":"Shanghai Hongqiao International Airport","city":"Shanghai (Minhang)","country":"CN","lat":31.198104,"lng":121.33426},{"code":"TXN","name":"Huangshan Tunxi International Airport","city":"Huangshan","country":"CN","lat":29.733299,"lng":118.255997},{"code":"WHA","name":"Wuhu Xuanzhou Airport","city":"Wuhu","country":"CN","lat":31.1045,"lng":118.66687},{"code":"WEF","name":"Weifang Nanyuan Airport","city":"Weifang (Kuiwen)","country":"CN","lat":36.646702,"lng":119.119003},{"code":"WEH","name":"Weihai Dashuibo Airport","city":"Weihai","country":"CN","lat":37.187099,"lng":122.228996},{"code":"WUX","name":"Sunan Shuofang International Airport","city":"Wuxi","country":"CN","lat":31.496952,"lng":120.43038},{"code":"WUS","name":"Nanping Wuyishan Airport","city":"Wuyishan","country":"CN","lat":27.7019,"lng":118.000999},{"code":"WNZ","name":"Wenzhou Longwan International Airport","city":"Wenzhou (Longwan)","country":"CN","lat":27.910572,"lng":120.853465},{"code":"XUZ","name":"Xuzhou Guanyin International Airport","city":"Xuzhou","country":"CN","lat":34.059056,"lng":117.555278},{"code":"YTY","name":"Yangzhou Taizhou Airport","city":"Yangzhou","country":"CN","lat":32.5634,"lng":119.7198},{"code":"YIC","name":"Yichun Mingyueshan Airport","city":"Yichun","country":"CN","lat":27.8025,"lng":114.3062},{"code":"YNZ","name":"Yancheng Nanyang International Airport","city":"Yancheng (Tinghu)","country":"CN","lat":33.428317,"lng":120.20545},{"code":"YNT","name":"Yantai Penglai International Airport","city":"Yantai","country":"CN","lat":37.659724,"lng":120.978124},{"code":"YIW","name":"Yiwu Airport","city":"Yiwu/Jinhua","country":"CN","lat":29.342095,"lng":120.03116},{"code":"HSN","name":"Zhoushan Putuoshan International Airport","city":"Zhoushan","country":"CN","lat":29.933874,"lng":122.362307},{"code":"NGQ","name":"Ngari Gunsa Airport","city":"Shiquanhe","country":"CN","lat":32.09794,"lng":80.053971},{"code":"AVA","name":"Anshun Huangguoshu Airport","city":"Anshun (Xixiu)","country":"CN","lat":26.260556,"lng":105.873333},{"code":"BPX","name":"Qamdo Bangda Airport","city":"Bangda","country":"CN","lat":30.553600311279297,"lng":97.1082992553711},{"code":"BFJ","name":"Bijie Feixiong Airport","city":"Bijie","country":"CN","lat":27.267066,"lng":105.472097},{"code":"CKG","name":"Chongqing Jiangbei International Airport","city":"Chongqing","country":"CN","lat":29.712254,"lng":106.651895},{"code":"DZH","name":"Dazhou Jinya Airport","city":"Dazhou (Dachuan)","country":"CN","lat":31.048815,"lng":107.435646},{"code":"DCY","name":"Daocheng Yading Airport","city":"Garzê (Daocheng)","country":"CN","lat":29.31632,"lng":100.060317},{"code":"DAX","name":"Dachuan Airport","city":"Dazhou (Daxian)","country":"CN","lat":31.1302,"lng":107.4295},{"code":"GYS","name":"Guangyuan Panlong Airport","city":"Guangyuan (Lizhou)","country":"CN","lat":32.390254,"lng":105.694571},{"code":"KWE","name":"Guiyang Longdongbao International Airport","city":"Guiyang (Nanming)","country":"CN","lat":26.541805,"lng":106.80402},{"code":"GZG","name":"Garze Gesar Airport","city":"Garzê (Garzê)","country":"CN","lat":31.7575,"lng":99.554167},{"code":"JZH","name":"Jiuzhai Huanglong Airport","city":"Ngawa (Songpan)","country":"CN","lat":32.853333,"lng":103.682222},{"code":"KGT","name":"Kangding Airport","city":"Garzê (Kangding)","country":"CN","lat":30.142464,"lng":101.73872},{"code":"KJH","name":"Kaili Huangping Airport","city":"Kaili (Huangping)","country":"CN","lat":26.972,"lng":107.988},{"code":"LLB","name":"Libo Airport","city":"Qiannan (Libo)","country":"CN","lat":25.4525,"lng":107.961667},{"code":"LXA","name":"Lhasa Gonggar International Airport","city":"Shannan (Gonggar)","country":"CN","lat":29.298001,"lng":90.911951},{"code":"LZO","name":"Luzhou Yunlong Airport","city":"Luzhou (Yunlong)","country":"CN","lat":29.030357,"lng":105.468407},{"code":"WMT","name":"Zunyi Maotai Airport","city":"Zunyi","country":"CN","lat":27.961837,"lng":106.435416},{"code":"MIG","name":"Mianyang Nanjiao Airport","city":"Mianyang (Fucheng)","country":"CN","lat":31.428101,"lng":104.740997},{"code":"NAO","name":"Nanchong Gaoping Airport","city":"Nanchong (Gaoping)","country":"CN","lat":30.798104,"lng":106.16415},{"code":"HZH","name":"Liping Airport","city":"Liping","country":"CN","lat":26.32217,"lng":109.1499},{"code":"LZY","name":"Nyingchi Mainling Airport","city":"Nyingchi (Mainling)","country":"CN","lat":29.303301,"lng":94.335297},{"code":"LPF","name":"Liupanshui Yuezhao Airport","city":"Liupanshui (Zhongshan)","country":"CN","lat":26.609417,"lng":104.979},{"code":"JIQ","name":"Qianjiang Wulingshan Airport","city":"Qianjiang","country":"CN","lat":29.5133333333,"lng":108.831111111},{"code":"RKZ","name":"Xigaze Peace Airport / Shigatse Air Base","city":"Xigazê (Samzhubzê)","country":"CN","lat":29.350876,"lng":89.299157},{"code":"LGZ","name":"Shannan Longzi Airport","city":"Shannan","country":"CN","lat":28.422422,"lng":92.348082},{"code":"TCZ","name":"Tengchong Tuofeng Airport","city":"Baoshan (Tengchong)","country":"CN","lat":24.938056,"lng":98.485833},{"code":"TFU","name":"Chengdu Tianfu International Airport","city":"Chengdu (Jianyang)","country":"CN","lat":30.31252,"lng":104.441284},{"code":"TEN","name":"Tongren Fenghuang Airport","city":"Tongren (Daxing)","country":"CN","lat":27.883333,"lng":109.308889},{"code":"CTU","name":"Chengdu Shuangliu International Airport","city":"Chengdu (Shuangliu)","country":"CN","lat":30.558257,"lng":103.945966},{"code":"CQW","name":"Chongqing Xiannüshan Airport","city":"Wulong","country":"CN","lat":29.465658,"lng":107.693664},{"code":"WSK","name":"Chongqing Wushan Airport","city":"Wushan","country":"CN","lat":31.06896,"lng":109.708958},{"code":"WXN","name":"Wanzhou Wuqiao Airport","city":"Wanzhou","country":"CN","lat":30.8017,"lng":108.433},{"code":"XIC","name":"Xichang Qingshan Airport","city":"Liangshan (Xichang)","country":"CN","lat":27.9891,"lng":102.183998},{"code":"YBP","name":"Yibin Wuliangye Airport","city":"Yibin (Cuiping)","country":"CN","lat":28.858431,"lng":104.526157},{"code":"ACX","name":"Xingyi Wanfenglin Airport","city":"Xingyi","country":"CN","lat":25.083423,"lng":104.960804},{"code":"PZI","name":"Panzhihua Bao'anying Airport","city":"Panzhihua (Renhe)","country":"CN","lat":26.54,"lng":101.79852},{"code":"ZYI","name":"Zunyi Xinzhou Airport","city":"Zunyi","country":"CN","lat":27.810723,"lng":107.247189},{"code":"AKU","name":"Aksu Hongqipo Airport","city":"Aksu (Onsu)","country":"CN","lat":41.262501,"lng":80.291702},{"code":"ACF","name":"Aral Tarim Airport","city":"Aral","country":"CN","lat":40.434748,"lng":81.26206},{"code":"AAT","name":"Altay Xuedu Airport","city":"Altay","country":"CN","lat":47.749886,"lng":88.085808},{"code":"BPL","name":"Bole Alashankou Airport","city":"Bole","country":"CN","lat":44.895461,"lng":82.30007},{"code":"IQM","name":"Qiemo Yudu Airport","city":"Qiemo","country":"CN","lat":38.234516,"lng":85.465462},{"code":"FYN","name":"Fuyun Koktokay Airport","city":"Fuyun","country":"CN","lat":46.804169,"lng":89.512006},{"code":"HMI","name":"Hami Airport","city":"Hami","country":"CN","lat":42.8414,"lng":93.669197},{"code":"SHF","name":"Shihezi Huayuan Airport","city":"Shihezi","country":"CN","lat":44.2421,"lng":85.8905},{"code":"KCA","name":"Kuqa Qiuci Airport","city":"Kuqa","country":"CN","lat":41.677856,"lng":82.872917},{"code":"KRL","name":"Korla Licheng Airport","city":"Korla","country":"CN","lat":41.614979,"lng":86.140817},{"code":"KRY","name":"Karamay Airport","city":"Karamay","country":"CN","lat":45.46655,"lng":84.9527},{"code":"KJI","name":"Burqin Kanas Airport","city":"Burqin","country":"CN","lat":48.2223,"lng":86.9959},{"code":"NLT","name":"Xinyuan Nalati Airport","city":"Xinyuan","country":"CN","lat":43.4318,"lng":83.3786},{"code":"RQA","name":"Ruoqiang Loulan Airport","city":"Ruoqiang Town","country":"CN","lat":38.974722,"lng":88.008333},{"code":"QSZ","name":"Shache Airport","city":"Shache","country":"CN","lat":38.24542,"lng":77.056149},{"code":"KHG","name":"Kashgar Laining International Airport","city":"Kashgar","country":"CN","lat":39.542273,"lng":76.02023},{"code":"TCG","name":"Tacheng Qianquan Airport","city":"Tacheng","country":"CN","lat":46.672501,"lng":83.340797},{"code":"TLQ","name":"Turpan Jiaohe Airport","city":"Turpan","country":"CN","lat":43.0308,"lng":89.0987},{"code":"HTN","name":"Hotan Airport","city":"Hotan","country":"CN","lat":37.03850173950195,"lng":79.86489868164062},{"code":"URC","name":"Ürümqi Tianshan International Airport","city":"Ürümqi","country":"CN","lat":43.913584,"lng":87.479372},{"code":"YIN","name":"Ili Yining International Airport","city":"Ili (Yining / Ghulja)","country":"CN","lat":43.955799,"lng":81.330299},{"code":"YTW","name":"Yutian Wanfang Airport","city":"Hotan (Yutian)","country":"CN","lat":36.8085,"lng":81.7827},{"code":"AOG","name":"Anshan Teng'ao Airport / Anshan Air Base","city":"Anshan","country":"CN","lat":41.105301,"lng":122.853996},{"code":"DBC","name":"Baicheng Chang'an Airport","city":"Baicheng","country":"CN","lat":45.505278,"lng":123.019722},{"code":"NBS","name":"Changbaishan Airport","city":"Baishan","country":"CN","lat":42.066944,"lng":127.602222},{"code":"CGQ","name":"Changchun Longjia International Airport","city":"Changchun","country":"CN","lat":43.996201,"lng":125.684998},{"code":"CNI","name":"Changhai Dachangshandao Airport","city":"Dalian (Changhai)","country":"CN","lat":39.266441,"lng":122.66696},{"code":"CHG","name":"Chaoyang Airport","city":"Shuangta, Chaoyang","country":"CN","lat":41.538101,"lng":120.434998},{"code":"DDG","name":"Dandong Langtou International Airport","city":"Dandong (Zhenxing)","country":"CN","lat":40.025453,"lng":124.28669},{"code":"DQA","name":"Daqing Sartu Airport","city":"Daqing","country":"CN","lat":46.750883,"lng":125.138642},{"code":"DTU","name":"Wudalianchi Dedu Airport","city":"Heihe","country":"CN","lat":48.441037,"lng":126.128374},{"code":"FYJ","name":"Fuyuan Dongji Airport","city":"Fuyuan","country":"CN","lat":48.197219,"lng":134.36298},{"code":"HRB","name":"Harbin Taiping International Airport","city":"Harbin","country":"CN","lat":45.623402,"lng":126.25},{"code":"HEK","name":"Heihe Aihui Airport","city":"Heihe","country":"CN","lat":50.171621,"lng":127.308884},{"code":"JGD","name":"Daxing'anling Elunchun Airport","city":"Jiagedaqi","country":"CN","lat":50.371389,"lng":124.1175},{"code":"JMU","name":"Jiamusi Songjiang International Airport","city":"Jiamusi","country":"CN","lat":46.842793,"lng":130.46426},{"code":"JSJ","name":"Jiansanjiang Shidi Airport","city":"Jiansanjiang","country":"CN","lat":47.108248,"lng":132.65792},{"code":"JXA","name":"Jixi Xingkaihu Airport","city":"Jixi","country":"CN","lat":45.293,"lng":131.193},{"code":"JNZ","name":"Jinzhou Bay Airport","city":"Jinzhou (Linghai)","country":"CN","lat":40.936032,"lng":121.277114},{"code":"LDS","name":"Yichun Lindu Airport","city":"Yichun","country":"CN","lat":47.752056,"lng":129.019125},{"code":"MDG","name":"Mudanjiang Hailang International Airport","city":"Mudanjiang","country":"CN","lat":44.525172,"lng":129.568634},{"code":"OHE","name":"Mohe Gulian Airport","city":"Mohe","country":"CN","lat":52.916871,"lng":122.422759},{"code":"NDG","name":"Qiqihar Sanjiazi Airport","city":"Qiqihar","country":"CN","lat":47.229969,"lng":123.914179},{"code":"YSQ","name":"Songyuan Chaganhu Airport","city":"Qian Gorlos Mongol Autonomous County","country":"CN","lat":44.931143,"lng":124.552121},{"code":"DLC","name":"Dalian Zhoushuizi International Airport","city":"Dalian (Ganjingzi)","country":"CN","lat":38.965719,"lng":121.538477},{"code":"TNH","name":"Tonghua Sanyuanpu Airport","city":"Tonghua","country":"CN","lat":42.048435,"lng":125.733963},{"code":"SHE","name":"Shenyang Taoxian International Airport","city":"Shenyang","country":"CN","lat":41.6398,"lng":123.483668},{"code":"YNJ","name":"Yanji Chaoyangchuan Airport","city":"Yanji","country":"CN","lat":42.8828010559,"lng":129.451004028},{"code":"YKH","name":"Yingkou Lanqi Airport","city":"Yingkou (Laobian)","country":"CN","lat":40.542524,"lng":122.3586}] \ No newline at end of file diff --git a/src/server/db.js b/src/server/db.js new file mode 100644 index 0000000..9a3d502 --- /dev/null +++ b/src/server/db.js @@ -0,0 +1,88 @@ +import Database from 'better-sqlite3'; + +const SCHEMA = ` +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + token_hash TEXT UNIQUE NOT NULL, + display_name TEXT NOT NULL, + created_at TEXT DEFAULT current_timestamp +); + +CREATE TABLE IF NOT EXISTS trips ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + start_date TEXT NOT NULL, + end_date TEXT NOT NULL, + owner_id INTEGER NOT NULL REFERENCES users(id), + currency TEXT NOT NULL DEFAULT 'USD', + join_code TEXT UNIQUE NOT NULL, + created_at TEXT DEFAULT current_timestamp +); + +CREATE TABLE IF NOT EXISTS trip_members ( + trip_id INTEGER NOT NULL REFERENCES trips(id), + user_id INTEGER NOT NULL REFERENCES users(id), + role TEXT NOT NULL DEFAULT 'editor', + PRIMARY KEY (trip_id, user_id) +); + +CREATE TABLE IF NOT EXISTS entries ( + id INTEGER PRIMARY KEY, + trip_id INTEGER NOT NULL REFERENCES trips(id), + date TEXT NOT NULL, + type TEXT NOT NULL, + title TEXT NOT NULL, + details TEXT DEFAULT '', + start_time TEXT, + end_time TEXT, + location_name TEXT, + lat REAL, + lng REAL, + sort_order INTEGER NOT NULL DEFAULT 0, + price REAL, + paid_by INTEGER REFERENCES users(id), + split_mode TEXT NOT NULL DEFAULT 'equal', + segments TEXT, + rental TEXT, + created_at TEXT DEFAULT current_timestamp +); + +CREATE TABLE IF NOT EXISTS entry_participants ( + entry_id INTEGER NOT NULL REFERENCES entries(id), + user_id INTEGER NOT NULL REFERENCES users(id), + PRIMARY KEY (entry_id, user_id) +); + +CREATE INDEX IF NOT EXISTS idx_entries_trip ON entries(trip_id, date, sort_order, id); +CREATE INDEX IF NOT EXISTS idx_members_user ON trip_members(user_id); +CREATE INDEX IF NOT EXISTS idx_participants_entry ON entry_participants(entry_id); +`; + +// Columns added after the initial release: CREATE TABLE IF NOT EXISTS never +// alters existing tables, so databases created by an older version need them +// backfilled with ALTER TABLE. +const MIGRATIONS = [ + { table: 'trips', column: 'currency', ddl: "ALTER TABLE trips ADD COLUMN currency TEXT NOT NULL DEFAULT 'USD'" }, + { table: 'entries', column: 'price', ddl: 'ALTER TABLE entries ADD COLUMN price REAL' }, + { table: 'entries', column: 'paid_by', ddl: 'ALTER TABLE entries ADD COLUMN paid_by INTEGER REFERENCES users(id)' }, + { table: 'entries', column: 'split_mode', ddl: "ALTER TABLE entries ADD COLUMN split_mode TEXT NOT NULL DEFAULT 'equal'" }, + { table: 'entries', column: 'segments', ddl: 'ALTER TABLE entries ADD COLUMN segments TEXT' }, + { table: 'entries', column: 'rental', ddl: 'ALTER TABLE entries ADD COLUMN rental TEXT' }, +]; + +function applyMigrations(db) { + for (const { table, column, ddl } of MIGRATIONS) { + const columns = db.prepare(`PRAGMA table_info(${table})`).all(); + if (!columns.some((c) => c.name === column)) db.exec(ddl); + } +} + +// Open (or create) the SQLite database at dbPath and ensure the schema exists. +export function openDb(dbPath) { + const db = new Database(dbPath); + db.pragma('journal_mode = WAL'); + db.pragma('foreign_keys = ON'); + db.exec(SCHEMA); + applyMigrations(db); + return db; +} diff --git a/src/server/index.js b/src/server/index.js new file mode 100644 index 0000000..17190aa --- /dev/null +++ b/src/server/index.js @@ -0,0 +1,25 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { createApp } from './app.js'; + +const PORT = Number(process.env.PORT) || 3000; +const DATA_DIR = process.env.DATA_DIR || './data'; + +let sessionSecret = process.env.SESSION_SECRET; +if (!sessionSecret) { + sessionSecret = 'trip-plan-dev-secret-change-me'; + console.warn( + '[trip-plan] SESSION_SECRET is not set; using an insecure development default. ' + + 'Set SESSION_SECRET in production.' + ); +} + +fs.mkdirSync(DATA_DIR, { recursive: true }); +const dbPath = path.join(DATA_DIR, 'trip-plan.db'); + +const app = createApp({ dbPath, sessionSecret }); + +app.listen(PORT, () => { + console.log(`[trip-plan] listening on http://localhost:${PORT}`); + console.log(`[trip-plan] data directory: ${path.resolve(DATA_DIR)}`); +}); diff --git a/src/server/routes/airports.js b/src/server/routes/airports.js new file mode 100644 index 0000000..23baa15 --- /dev/null +++ b/src/server/routes/airports.js @@ -0,0 +1,46 @@ +import express from 'express'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DATA_PATH = path.join(__dirname, '..', 'data', 'airports.json'); +const MAX_RESULTS = 8; + +// Loaded once at startup (bundled, public-domain OurAirports subset). +const AIRPORTS = JSON.parse(fs.readFileSync(DATA_PATH, 'utf8')); + +export default function airportsRoutes() { + const router = express.Router(); + + // GET /api/airports?q= + router.get('/', (req, res) => { + const q = typeof req.query.q === 'string' ? req.query.q.trim() : ''; + if (!q) return res.status(200).json({ results: [] }); + const ql = q.toLowerCase(); + + // Rank: exact IATA, then IATA prefix, then name/city substring. + const exact = []; + const prefix = []; + const substring = []; + for (const a of AIRPORTS) { + const code = a.code.toLowerCase(); + if (code === ql) { + exact.push(a); + } else if (code.startsWith(ql)) { + prefix.push(a); + } else if ( + (a.name && a.name.toLowerCase().includes(ql)) || + (a.city && a.city.toLowerCase().includes(ql)) + ) { + substring.push(a); + } + if (exact.length >= MAX_RESULTS) break; + } + + const results = [...exact, ...prefix, ...substring].slice(0, MAX_RESULTS); + res.status(200).json({ results }); + }); + + return router; +} diff --git a/src/server/routes/auth.js b/src/server/routes/auth.js new file mode 100644 index 0000000..a3aa88e --- /dev/null +++ b/src/server/routes/auth.js @@ -0,0 +1,84 @@ +import express from 'express'; +import { requireAuth } from '../auth.js'; +import { + generateAccountToken, + formatToken, + hashToken, +} from '../util/token.js'; +import { generateDisplayName } from '../util/names.js'; + +export default function authRoutes(db) { + const router = express.Router(); + + const insertUser = db.prepare( + 'INSERT INTO users (token_hash, display_name) VALUES (?, ?)' + ); + const findByHash = db.prepare( + 'SELECT id, display_name FROM users WHERE token_hash = ?' + ); + const findById = db.prepare('SELECT id, display_name FROM users WHERE id = ?'); + const hashExists = db.prepare('SELECT 1 FROM users WHERE token_hash = ?'); + + // POST /api/auth/account — create an account, log in, return the raw token once. + router.post('/account', (req, res) => { + let raw; + let hash; + do { + raw = generateAccountToken(); + hash = hashToken(raw); + } while (hashExists.get(hash)); + + const displayName = generateDisplayName(); + const info = insertUser.run(hash, displayName); + const user = { id: Number(info.lastInsertRowid), display_name: displayName }; + req.session.userId = user.id; + res.status(201).json({ user, token: formatToken(raw) }); + }); + + // POST /api/auth/login — { token } + router.post('/login', (req, res) => { + const { token } = req.body || {}; + if (typeof token !== 'string' || token.trim() === '') { + return res.status(401).json({ error: 'invalid token' }); + } + const user = findByHash.get(hashToken(token)); + if (!user) return res.status(401).json({ error: 'invalid token' }); + req.session.userId = user.id; + res.status(200).json({ user }); + }); + + // POST /api/auth/logout + router.post('/logout', (req, res) => { + req.session = null; + res.status(204).end(); + }); + + // GET /api/auth/me + router.get('/me', requireAuth, (req, res) => { + const user = findById.get(req.session.userId); + if (!user) { + req.session = null; + return res.status(401).json({ error: 'unauthorized' }); + } + res.status(200).json({ user }); + }); + + // PATCH /api/auth/me — { display_name } + router.patch('/me', requireAuth, (req, res) => { + const { display_name } = req.body || {}; + if (typeof display_name !== 'string') { + return res.status(400).json({ error: 'display_name is required' }); + } + const trimmed = display_name.trim(); + if (trimmed.length < 1 || trimmed.length > 40) { + return res.status(400).json({ error: 'display_name must be 1-40 characters' }); + } + db.prepare('UPDATE users SET display_name = ? WHERE id = ?').run( + trimmed, + req.session.userId + ); + res.status(200).json({ user: findById.get(req.session.userId) }); + }); + + return router; +} diff --git a/src/server/routes/entries.js b/src/server/routes/entries.js new file mode 100644 index 0000000..09b2092 --- /dev/null +++ b/src/server/routes/entries.js @@ -0,0 +1,313 @@ +import express from 'express'; +import { isValidDateStr } from '../util/dates.js'; +import { membership } from '../util/access.js'; +import { ENTRY_COLUMNS, attachParticipants } from '../util/entrySerialize.js'; +import { validateSegments } from '../util/segments.js'; +import { validateRental } from '../util/rental.js'; + +const ENTRY_TYPES = new Set([ + 'flight', + 'immigration', + 'travel', + 'hotel', + 'activity', + 'rental', + 'note', +]); +const SPLIT_MODES = new Set(['equal', 'own', 'payer']); +const TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/; + +function validTime(v) { + return v === null || v === undefined || (typeof v === 'string' && TIME_RE.test(v)); +} + +function validCoord(v, min, max) { + return typeof v === 'number' && Number.isFinite(v) && v >= min && v <= max; +} + +// Validate an entry body. `partial` = true for PATCH (only provided keys checked). +// `existing` is the current row (PATCH merges for the payer/paid_by rule). +// `memberIds` is a Set of the trip's member user ids. +// Returns { error } or { fields, participants, hasParticipants } where +// participants is null (= all members) or an array of ids. +function validateEntry(body, { partial, existing, memberIds }) { + const fields = {}; + const has = (k) => k in body; + + if (!partial || has('type')) { + if (!ENTRY_TYPES.has(body.type)) return { error: 'invalid entry type' }; + fields.type = body.type; + } + if (!partial || has('date')) { + if (!isValidDateStr(body.date)) { + return { error: 'date must be a valid YYYY-MM-DD date' }; + } + fields.date = body.date; + } + if (!partial || has('title')) { + if (typeof body.title !== 'string' || body.title.trim() === '') { + return { error: 'title is required' }; + } + if (body.title.length > 200) { + return { error: 'title must be at most 200 characters' }; + } + fields.title = body.title.trim(); + } + if (has('details')) { + if (typeof body.details !== 'string') { + return { error: 'details must be a string' }; + } + fields.details = body.details; + } + for (const key of ['start_time', 'end_time']) { + if (has(key)) { + if (!validTime(body[key])) return { error: `${key} must be HH:MM` }; + fields[key] = body[key] ?? null; + } + } + if (has('location_name')) { + const v = body.location_name; + if (v !== null && typeof v !== 'string') { + return { error: 'location_name must be a string' }; + } + fields.location_name = v ?? null; + } + + // lat/lng: both present or both absent. + const hasLat = has('lat'); + const hasLng = has('lng'); + if (hasLat !== hasLng) { + return { error: 'lat and lng must both be present or both absent' }; + } + if (hasLat && hasLng) { + const bothNull = body.lat === null && body.lng === null; + if (!bothNull) { + if (!validCoord(body.lat, -90, 90)) return { error: 'lat must be in [-90, 90]' }; + if (!validCoord(body.lng, -180, 180)) { + return { error: 'lng must be in [-180, 180]' }; + } + } + fields.lat = bothNull ? null : body.lat; + fields.lng = bothNull ? null : body.lng; + } + + if (has('sort_order')) { + if (!Number.isInteger(body.sort_order)) { + return { error: 'sort_order must be an integer' }; + } + fields.sort_order = body.sort_order; + } + + // price: null or a number >= 0. + if (has('price')) { + const v = body.price; + if (v !== null && !(typeof v === 'number' && Number.isFinite(v) && v >= 0)) { + return { error: 'price must be null or a number >= 0' }; + } + fields.price = v; + } + + // paid_by: null or a trip-member user id. + if (has('paid_by')) { + const v = body.paid_by; + if (v !== null && !(Number.isInteger(v) && memberIds.has(v))) { + return { error: 'paid_by must be null or a trip member id' }; + } + fields.paid_by = v; + } + + // split_mode: enum. + if (has('split_mode')) { + if (!SPLIT_MODES.has(body.split_mode)) { + return { error: 'split_mode must be one of equal, own, payer' }; + } + fields.split_mode = body.split_mode; + } + + // 'payer' requires an effective paid_by (merging existing values on PATCH). + const effMode = 'split_mode' in fields ? fields.split_mode : existing?.split_mode ?? 'equal'; + const effPaidBy = 'paid_by' in fields ? fields.paid_by : existing?.paid_by ?? null; + if (effMode === 'payer' && (effPaidBy === null || effPaidBy === undefined)) { + return { error: "split_mode 'payer' requires paid_by" }; + } + + // participants: null/[] (= all members) or array of trip-member ids. + let participants; + if (has('participants')) { + const v = body.participants; + if (v === null || (Array.isArray(v) && v.length === 0)) { + participants = null; + } else if (Array.isArray(v)) { + for (const id of v) { + if (!Number.isInteger(id) || !memberIds.has(id)) { + return { error: 'participants must be trip member ids' }; + } + } + participants = [...new Set(v)]; + } else { + return { error: 'participants must be null or an array of member ids' }; + } + } + + // segments: flight-only structured itinerary (null clears them). + if (has('segments')) { + if (body.segments === null) { + fields.segments = null; + } else { + const effType = 'type' in fields ? fields.type : existing?.type; + if (effType !== 'flight') { + return { error: 'segments are only allowed on flight entries' }; + } + const checked = validateSegments(body.segments); + if (checked.error) return { error: checked.error }; + fields.segments = JSON.stringify(checked.value); + } + } + + // rental: rental-only structured details (null clears them). + if (has('rental')) { + if (body.rental === null) { + fields.rental = null; + } else { + const effType = 'type' in fields ? fields.type : existing?.type; + if (effType !== 'rental') { + return { error: 'rental details are only allowed on rental entries' }; + } + const checked = validateRental(body.rental); + if (checked.error) return { error: checked.error }; + fields.rental = JSON.stringify(checked.value); + } + } + + return { fields, participants, hasParticipants: has('participants') }; +} + +export default function entriesRoutes(db) { + const router = express.Router(); + + const getEntry = db.prepare(`SELECT ${ENTRY_COLUMNS} FROM entries WHERE id = ?`); + const getTripEntries = db.prepare( + `SELECT ${ENTRY_COLUMNS} FROM entries WHERE trip_id = ? ORDER BY date, sort_order, id` + ); + const getEntryRow = db.prepare( + 'SELECT trip_id, split_mode, paid_by, type FROM entries WHERE id = ?' + ); + const getMemberIds = db.prepare('SELECT user_id FROM trip_members WHERE trip_id = ?'); + const insertParticipant = db.prepare( + 'INSERT OR IGNORE INTO entry_participants (entry_id, user_id) VALUES (?, ?)' + ); + const clearParticipants = db.prepare('DELETE FROM entry_participants WHERE entry_id = ?'); + + const memberIdSet = (tripId) => new Set(getMemberIds.all(tripId).map((r) => r.user_id)); + + function writeParticipants(entryId, participants) { + clearParticipants.run(entryId); + if (Array.isArray(participants)) { + for (const uid of participants) insertParticipant.run(entryId, uid); + } + } + + // GET /api/trips/:id/entries + router.get('/trips/:id/entries', (req, res) => { + const tripId = Number(req.params.id); + if (!Number.isInteger(tripId) || !membership(db, tripId, req.session.userId)) { + return res.status(404).json({ error: 'not found' }); + } + const entries = getTripEntries.all(tripId).map((r) => attachParticipants(db, r)); + res.status(200).json({ entries }); + }); + + // POST /api/trips/:id/entries + router.post('/trips/:id/entries', (req, res) => { + const tripId = Number(req.params.id); + if (!Number.isInteger(tripId) || !membership(db, tripId, req.session.userId)) { + return res.status(404).json({ error: 'not found' }); + } + const check = validateEntry(req.body || {}, { + partial: false, + existing: null, + memberIds: memberIdSet(tripId), + }); + if (check.error) return res.status(400).json({ error: check.error }); + const f = check.fields; + + const entry = db.transaction(() => { + const info = db + .prepare( + `INSERT INTO entries + (trip_id, date, type, title, details, start_time, end_time, + location_name, lat, lng, sort_order, price, paid_by, split_mode, segments, rental) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) + .run( + tripId, + f.date, + f.type, + f.title, + f.details ?? '', + f.start_time ?? null, + f.end_time ?? null, + f.location_name ?? null, + f.lat ?? null, + f.lng ?? null, + f.sort_order ?? 0, + f.price ?? null, + f.paid_by ?? null, + f.split_mode ?? 'equal', + f.segments ?? null, + f.rental ?? null + ); + const id = Number(info.lastInsertRowid); + // participants provided as an array -> store rows; null/absent -> all members. + if (Array.isArray(check.participants)) writeParticipants(id, check.participants); + return attachParticipants(db, getEntry.get(id)); + })(); + + res.status(201).json({ entry }); + }); + + // PATCH /api/entries/:id + router.patch('/entries/:id', (req, res) => { + const entryId = Number(req.params.id); + const row = Number.isInteger(entryId) ? getEntryRow.get(entryId) : null; + if (!row || !membership(db, row.trip_id, req.session.userId)) { + return res.status(404).json({ error: 'not found' }); + } + const check = validateEntry(req.body || {}, { + partial: true, + existing: row, + memberIds: memberIdSet(row.trip_id), + }); + if (check.error) return res.status(400).json({ error: check.error }); + + const entry = db.transaction(() => { + const keys = Object.keys(check.fields); + if (keys.length > 0) { + const setClause = keys.map((k) => `${k} = ?`).join(', '); + const values = keys.map((k) => check.fields[k]); + db.prepare(`UPDATE entries SET ${setClause} WHERE id = ?`).run(...values, entryId); + } + // participants key present -> replace the whole set (null/[] = all members). + if (check.hasParticipants) writeParticipants(entryId, check.participants); + return attachParticipants(db, getEntry.get(entryId)); + })(); + + res.status(200).json({ entry }); + }); + + // DELETE /api/entries/:id + router.delete('/entries/:id', (req, res) => { + const entryId = Number(req.params.id); + const row = Number.isInteger(entryId) ? getEntryRow.get(entryId) : null; + if (!row || !membership(db, row.trip_id, req.session.userId)) { + return res.status(404).json({ error: 'not found' }); + } + db.transaction(() => { + clearParticipants.run(entryId); + db.prepare('DELETE FROM entries WHERE id = ?').run(entryId); + })(); + res.status(204).end(); + }); + + return router; +} diff --git a/src/server/routes/geocode.js b/src/server/routes/geocode.js new file mode 100644 index 0000000..df2cfe4 --- /dev/null +++ b/src/server/routes/geocode.js @@ -0,0 +1,48 @@ +import express from 'express'; + +const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search'; +const USER_AGENT = 'trip-plan-app/0.1 (self-hosted)'; +const CACHE_TTL_MS = 10 * 60 * 1000; +const MAX_RESULTS = 5; + +export default function geocodeRoutes() { + const router = express.Router(); + const cache = new Map(); // query -> { at, results } + + // GET /api/geocode?q=... + router.get('/', async (req, res) => { + const q = typeof req.query.q === 'string' ? req.query.q.trim() : ''; + if (!q) return res.status(400).json({ error: 'query parameter q is required' }); + + const cached = cache.get(q); + if (cached && Date.now() - cached.at < CACHE_TTL_MS) { + return res.status(200).json({ results: cached.results }); + } + + const url = `${NOMINATIM_URL}?format=jsonv2&limit=${MAX_RESULTS}&accept-language=en&q=${encodeURIComponent(q)}`; + let data; + try { + const upstream = await fetch(url, { + headers: { 'User-Agent': USER_AGENT, Accept: 'application/json' }, + }); + if (!upstream.ok) throw new Error(`upstream status ${upstream.status}`); + data = await upstream.json(); + } catch { + return res.status(502).json({ error: 'geocoding unavailable' }); + } + + const results = (Array.isArray(data) ? data : []) + .slice(0, MAX_RESULTS) + .map((r) => ({ + name: r.display_name, + lat: Number(r.lat), + lng: Number(r.lon), + })) + .filter((r) => Number.isFinite(r.lat) && Number.isFinite(r.lng)); + + cache.set(q, { at: Date.now(), results }); + res.status(200).json({ results }); + }); + + return router; +} diff --git a/src/server/routes/trips.js b/src/server/routes/trips.js new file mode 100644 index 0000000..4461599 --- /dev/null +++ b/src/server/routes/trips.js @@ -0,0 +1,376 @@ +import express from 'express'; +import { isValidDateStr, daysInclusive } from '../util/dates.js'; +import { haversineKm } from '../util/distance.js'; +import { membership } from '../util/access.js'; +import { computeCosts } from '../util/costs.js'; +import { + ENTRY_COLUMNS, + attachParticipantsAll, + parseSegments, + parseRental, +} from '../util/entrySerialize.js'; +import { generateJoinCode, formatJoinCode, normalizeCode } from '../util/token.js'; + +const MAX_RANGE_DAYS = 365; +const MIN_LEG_KM = 0.05; +const CURRENCY_RE = /^[A-Z]{3}$/; + +// Trip JSON with the join_code shown in grouped display form. +function tripJson(row) { + if (!row) return row; + return { ...row, join_code: formatJoinCode(row.join_code) }; +} + +// Airport stops derived from a flight entry's coordinate-bearing segments: +// `from` of the first segment then `to` of each, skipping coordless airports +// and collapsing consecutive duplicate coordinates. Returns null if none. +function airportStopsFor(entry) { + const segs = entry.segments; + if (!Array.isArray(segs) || segs.length === 0) return null; + const candidates = [segs[0].from, ...segs.map((s) => s.to)]; + const stops = []; + for (const a of candidates) { + if (!a || a.lat === null || a.lat === undefined || a.lng === null || a.lng === undefined) { + continue; + } + const prev = stops[stops.length - 1]; + if (prev && prev.lat === a.lat && prev.lng === a.lng) continue; + stops.push({ code: a.code, name: a.name ?? null, lat: a.lat, lng: a.lng }); + } + return stops.length ? stops : null; +} + +// Ordered map stops: flight entries with located segments expand into airport +// stops; every other located entry is a single stop. +function buildStops(entries) { + const stops = []; + for (const e of entries) { + const airports = e.type === 'flight' ? airportStopsFor(e) : null; + if (airports) { + for (const a of airports) { + stops.push({ + entryId: e.id, + date: e.date, + type: e.type, + title: e.title, + kind: 'airport', + code: a.code, + location_name: a.name, + lat: a.lat, + lng: a.lng, + }); + } + } else if (e.lat !== null && e.lng !== null) { + stops.push({ + entryId: e.id, + date: e.date, + type: e.type, + title: e.title, + location_name: e.location_name, + lat: e.lat, + lng: e.lng, + }); + } + } + return stops; +} + +// Validate a {name, start_date, end_date, currency} object, merging with +// existing values (for PATCH). Returns { error } or { values }. +function validateTripFields(body, existing) { + const name = 'name' in body ? body.name : existing?.name; + const start = 'start_date' in body ? body.start_date : existing?.start_date; + const end = 'end_date' in body ? body.end_date : existing?.end_date; + const currency = + 'currency' in body ? body.currency : existing?.currency ?? 'USD'; + + if (typeof name !== 'string' || name.trim() === '') { + return { error: 'name is required' }; + } + if (name.length > 120) { + return { error: 'name must be at most 120 characters' }; + } + if (!isValidDateStr(start)) { + return { error: 'start_date must be a valid YYYY-MM-DD date' }; + } + if (!isValidDateStr(end)) { + return { error: 'end_date must be a valid YYYY-MM-DD date' }; + } + if (end < start) { + return { error: 'end_date must be on or after start_date' }; + } + if (daysInclusive(start, end) > MAX_RANGE_DAYS) { + return { error: 'date range must be at most 365 days' }; + } + if (typeof currency !== 'string' || !CURRENCY_RE.test(currency)) { + return { error: 'currency must be a 3-letter uppercase code' }; + } + return { + values: { name: name.trim(), start_date: start, end_date: end, currency }, + }; +} + +export default function tripsRoutes(db) { + const router = express.Router(); + + const listForUser = db.prepare(` + SELECT t.id, t.name, t.start_date, t.end_date, t.owner_id, t.currency, tm.role, + (SELECT COUNT(*) FROM trip_members WHERE trip_id = t.id) AS member_count, + (SELECT COUNT(*) FROM entries WHERE trip_id = t.id) AS entry_count + FROM trips t + JOIN trip_members tm ON tm.trip_id = t.id AND tm.user_id = ? + ORDER BY t.created_at DESC, t.id DESC + `); + const insertTrip = db.prepare( + 'INSERT INTO trips (name, start_date, end_date, owner_id, currency, join_code) VALUES (?, ?, ?, ?, ?, ?)' + ); + const insertMember = db.prepare( + 'INSERT INTO trip_members (trip_id, user_id, role) VALUES (?, ?, ?)' + ); + const getTrip = db.prepare( + 'SELECT id, name, start_date, end_date, owner_id, currency, join_code FROM trips WHERE id = ?' + ); + const getMembers = db.prepare(` + SELECT u.id, u.display_name, tm.role + FROM trip_members tm JOIN users u ON u.id = tm.user_id + WHERE tm.trip_id = ? ORDER BY tm.role = 'owner' DESC, u.display_name + `); + const getEntries = db.prepare( + `SELECT ${ENTRY_COLUMNS} FROM entries WHERE trip_id = ? ORDER BY date, sort_order, id` + ); + const findTripByJoinCode = db.prepare('SELECT id FROM trips WHERE join_code = ?'); + const joinCodeExists = db.prepare('SELECT 1 FROM trips WHERE join_code = ?'); + + // Generate a join_code not already in use. + function uniqueJoinCode() { + let code; + do { + code = generateJoinCode(); + } while (joinCodeExists.get(code)); + return code; + } + + // Resolve :id as an integer and confirm membership. Sends 404 and returns + // null when the trip does not exist or the user is not a member. + function requireMember(req, res) { + const tripId = Number(req.params.id); + if (!Number.isInteger(tripId)) { + res.status(404).json({ error: 'not found' }); + return null; + } + const member = membership(db, tripId, req.session.userId); + if (!member) { + res.status(404).json({ error: 'not found' }); + return null; + } + return { tripId, role: member.role }; + } + + // GET /api/trips + router.get('/', (req, res) => { + const trips = listForUser.all(req.session.userId); + res.status(200).json({ trips }); + }); + + // POST /api/trips + router.post('/', (req, res) => { + const check = validateTripFields(req.body || {}, null); + if (check.error) return res.status(400).json({ error: check.error }); + const { name, start_date, end_date, currency } = check.values; + const userId = req.session.userId; + const trip = db.transaction(() => { + const info = insertTrip.run( + name, + start_date, + end_date, + userId, + currency, + uniqueJoinCode() + ); + const id = Number(info.lastInsertRowid); + insertMember.run(id, userId, 'owner'); + return getTrip.get(id); + })(); + res.status(201).json({ trip: tripJson(trip) }); + }); + + // POST /api/trips/join { code } — join by code as editor (idempotent). + router.post('/join', (req, res) => { + const code = normalizeCode((req.body || {}).code); + if (!code) return res.status(404).json({ error: 'not found' }); + const found = findTripByJoinCode.get(code); + if (!found) return res.status(404).json({ error: 'not found' }); + if (!membership(db, found.id, req.session.userId)) { + insertMember.run(found.id, req.session.userId, 'editor'); + } + res.status(200).json({ trip: tripJson(getTrip.get(found.id)) }); + }); + + // GET /api/trips/:id + router.get('/:id', (req, res) => { + const ctx = requireMember(req, res); + if (!ctx) return; + res.status(200).json({ + trip: tripJson(getTrip.get(ctx.tripId)), + members: getMembers.all(ctx.tripId), + entries: attachParticipantsAll(db, getEntries.all(ctx.tripId)), + }); + }); + + // PATCH /api/trips/:id + router.patch('/:id', (req, res) => { + const ctx = requireMember(req, res); + if (!ctx) return; + const existing = getTrip.get(ctx.tripId); + const check = validateTripFields(req.body || {}, existing); + if (check.error) return res.status(400).json({ error: check.error }); + const { name, start_date, end_date, currency } = check.values; + db.prepare( + 'UPDATE trips SET name = ?, start_date = ?, end_date = ?, currency = ? WHERE id = ?' + ).run(name, start_date, end_date, currency, ctx.tripId); + res.status(200).json({ trip: tripJson(getTrip.get(ctx.tripId)) }); + }); + + // DELETE /api/trips/:id (owner only) + router.delete('/:id', (req, res) => { + const ctx = requireMember(req, res); + if (!ctx) return; + if (ctx.role !== 'owner') { + return res.status(403).json({ error: 'only the owner can delete a trip' }); + } + db.transaction(() => { + db.prepare('DELETE FROM entries WHERE trip_id = ?').run(ctx.tripId); + db.prepare('DELETE FROM trip_members WHERE trip_id = ?').run(ctx.tripId); + db.prepare('DELETE FROM trips WHERE id = ?').run(ctx.tripId); + })(); + res.status(204).end(); + }); + + // POST /api/trips/:id/join-code (owner only) — regenerate the join code. + router.post('/:id/join-code', (req, res) => { + const ctx = requireMember(req, res); + if (!ctx) return; + if (ctx.role !== 'owner') { + return res.status(403).json({ error: 'only the owner can regenerate the join code' }); + } + db.prepare('UPDATE trips SET join_code = ? WHERE id = ?').run( + uniqueJoinCode(), + ctx.tripId + ); + res.status(200).json({ trip: tripJson(getTrip.get(ctx.tripId)) }); + }); + + // DELETE /api/trips/:id/members/:userId (owner only) + router.delete('/:id/members/:userId', (req, res) => { + const ctx = requireMember(req, res); + if (!ctx) return; + if (ctx.role !== 'owner') { + return res.status(403).json({ error: 'only the owner can remove members' }); + } + const userId = Number(req.params.userId); + if (userId === req.session.userId) { + return res.status(400).json({ error: 'owner cannot remove themselves' }); + } + db.prepare('DELETE FROM trip_members WHERE trip_id = ? AND user_id = ?').run( + ctx.tripId, + userId + ); + res.status(204).end(); + }); + + // GET /api/trips/:id/route (computed legs + summary) + router.get('/:id/route', (req, res) => { + const ctx = requireMember(req, res); + if (!ctx) return; + const trip = getTrip.get(ctx.tripId); + const allEntries = getEntries + .all(ctx.tripId) + .map((r) => ({ + ...r, + segments: parseSegments(r.segments), + rental: parseRental(r.rental), + })); + + const stops = buildStops(allEntries); + + const legs = []; + let totalKm = 0; + let kmAir = 0; + let kmDriven = 0; + for (let i = 1; i < stops.length; i++) { + const a = stops[i - 1]; + const b = stops[i]; + const km = haversineKm(a.lat, a.lng, b.lat, b.lng); + if (km < MIN_LEG_KM) continue; + // "air" only when both endpoints are airport stops from the same flight entry. + const isAir = + a.kind === 'airport' && b.kind === 'airport' && a.entryId === b.entryId; + const mode = isAir ? 'air' : 'ground'; + legs.push({ + fromEntryId: a.entryId, + toEntryId: b.entryId, + km: Math.round(km * 10) / 10, + mode, + }); + totalKm += km; + if (isAir) kmAir += km; + else kmDriven += km; + } + + const countType = (t) => allEntries.filter((e) => e.type === t).length; + const flightSegments = allEntries.reduce( + (n, e) => + n + (e.type === 'flight' && Array.isArray(e.segments) ? e.segments.length : 0), + 0 + ); + // Sum non-null included_km across rentals; null when none specify one. + let includedKm = null; + for (const e of allEntries) { + if (e.type === 'rental' && e.rental && typeof e.rental.included_km === 'number') { + includedKm = (includedKm ?? 0) + e.rental.included_km; + } + } + if (includedKm !== null) includedKm = Math.round(includedKm * 10) / 10; + const days = daysInclusive(trip.start_date, trip.end_date); + const locations = []; + for (const s of stops) { + if (s.location_name && !locations.includes(s.location_name)) { + locations.push(s.location_name); + } + } + + res.status(200).json({ + stops, + legs, + totalKm: Math.round(totalKm * 10) / 10, + summary: { + days, + nights: Math.max(0, days - 1), + flights: countType('flight'), + flightSegments, + hotels: countType('hotel'), + travelLegs: countType('travel'), + activities: countType('activity'), + rentals: countType('rental'), + kmAir: Math.round(kmAir * 10) / 10, + kmDriven: Math.round(kmDriven * 10) / 10, + includedKm, + locations, + }, + }); + }); + + // GET /api/trips/:id/costs (computed cost split + settlements) + router.get('/:id/costs', (req, res) => { + const ctx = requireMember(req, res); + if (!ctx) return; + const trip = getTrip.get(ctx.tripId); + const members = getMembers.all(ctx.tripId); + const entries = attachParticipantsAll(db, getEntries.all(ctx.tripId)); + res.status(200).json( + computeCosts({ currency: trip.currency, members, entries }) + ); + }); + + return router; +} diff --git a/src/server/util/access.js b/src/server/util/access.js new file mode 100644 index 0000000..010f3b4 --- /dev/null +++ b/src/server/util/access.js @@ -0,0 +1,7 @@ +// Shared trip-membership lookup used by trip and entry routes. + +export function membership(db, tripId, userId) { + return db + .prepare('SELECT role FROM trip_members WHERE trip_id = ? AND user_id = ?') + .get(tripId, userId); +} diff --git a/src/server/util/costs.js b/src/server/util/costs.js new file mode 100644 index 0000000..d1b5341 --- /dev/null +++ b/src/server/util/costs.js @@ -0,0 +1,119 @@ +// Pure cost/splitting computation for GET /api/trips/:id/costs. +// Kept side-effect free so it can be unit-tested directly. + +function round2(v) { + return Math.round((v + Number.EPSILON) * 100) / 100; +} + +// members: [{ id, display_name }] (current trip members, in display order) +// entries: [{ type, price, paid_by, split_mode, participants }] +// participants = array of user ids ([] = all members) +// currency: trip currency string +export function computeCosts({ currency, members, entries }) { + const memberIds = members.map((m) => m.id); + const memberSet = new Set(memberIds); + const share = new Map(memberIds.map((id) => [id, 0])); + const paid = new Map(memberIds.map((id) => [id, 0])); + const add = (map, id, amt) => map.set(id, (map.get(id) || 0) + amt); + + const byType = {}; + let totalCost = 0; + let unassigned = 0; + + for (const e of entries) { + if (e.price === null || e.price === undefined) continue; + + // Effective participants: the entry's rows, else all current members. + let eff = + Array.isArray(e.participants) && e.participants.length + ? e.participants.filter((id) => memberSet.has(id)) + : memberIds; + if (eff.length === 0) eff = memberIds; + + const mode = e.split_mode || 'equal'; + let effTotal; + + if (mode === 'own') { + // price is per person; each pays their own, no debt. + for (const id of eff) { + add(share, id, e.price); + add(paid, id, e.price); + } + effTotal = e.price * eff.length; + } else if (mode === 'payer') { + // personal expense: paid_by owes and pays the whole price alone. + add(share, e.paid_by, e.price); + add(paid, e.paid_by, e.price); + effTotal = e.price; + } else { + // equal: price is the total, split equally among participants. + const per = e.price / eff.length; + for (const id of eff) add(share, id, per); + if (e.paid_by === null || e.paid_by === undefined) { + unassigned += e.price; + } else { + add(paid, e.paid_by, e.price); + } + effTotal = e.price; + } + + totalCost += effTotal; + byType[e.type] = (byType[e.type] || 0) + effTotal; + } + + const perUser = members.map((m) => { + const s = share.get(m.id) || 0; + const p = paid.get(m.id) || 0; + return { + userId: m.id, + displayName: m.display_name, + share: round2(s), + paid: round2(p), + net: round2(p - s), + }; + }); + + const roundedByType = {}; + for (const [type, amt] of Object.entries(byType)) { + roundedByType[type] = round2(amt); + } + + return { + currency, + totalCost: round2(totalCost), + byType: roundedByType, + perUser, + settlements: settle(perUser), + unassigned: round2(unassigned), + }; +} + +// Greedy minimal-transfer settlement over the rounded net balances. +function settle(perUser) { + const debtors = perUser + .filter((u) => u.net < -0.005) + .map((u) => ({ id: u.userId, amt: -u.net })); + const creditors = perUser + .filter((u) => u.net > 0.005) + .map((u) => ({ id: u.userId, amt: u.net })); + + const settlements = []; + while (debtors.length && creditors.length) { + debtors.sort((a, b) => b.amt - a.amt); + creditors.sort((a, b) => b.amt - a.amt); + const d = debtors[0]; + const c = creditors[0]; + const pay = Math.min(d.amt, c.amt); + if (pay < 0.01) break; + settlements.push({ + fromUserId: d.id, + toUserId: c.id, + amount: round2(pay), + }); + d.amt = round2(d.amt - pay); + c.amt = round2(c.amt - pay); + if (d.amt < 0.01) debtors.shift(); + if (c.amt < 0.01) creditors.shift(); + } + return settlements; +} diff --git a/src/server/util/dates.js b/src/server/util/dates.js new file mode 100644 index 0000000..6917a02 --- /dev/null +++ b/src/server/util/dates.js @@ -0,0 +1,24 @@ +// Date-string helpers. All trip/entry dates are YYYY-MM-DD strings. + +const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; + +export function isValidDateStr(s) { + if (typeof s !== 'string' || !DATE_RE.test(s)) return false; + const [y, m, d] = s.split('-').map(Number); + const dt = new Date(Date.UTC(y, m - 1, d)); + return ( + dt.getUTCFullYear() === y && + dt.getUTCMonth() === m - 1 && + dt.getUTCDate() === d + ); +} + +function dateToUTC(s) { + const [y, m, d] = s.split('-').map(Number); + return Date.UTC(y, m - 1, d); +} + +// Inclusive day count between two valid date strings (start <= end). +export function daysInclusive(start, end) { + return Math.round((dateToUTC(end) - dateToUTC(start)) / 86400000) + 1; +} diff --git a/src/server/util/distance.js b/src/server/util/distance.js new file mode 100644 index 0000000..a4633b3 --- /dev/null +++ b/src/server/util/distance.js @@ -0,0 +1,17 @@ +// Great-circle distance between two lat/lng points, in kilometers. + +const EARTH_RADIUS_KM = 6371; + +function toRad(deg) { + return (deg * Math.PI) / 180; +} + +export function haversineKm(lat1, lng1, lat2, lng2) { + const dLat = toRad(lat2 - lat1); + const dLng = toRad(lng2 - lng1); + const a = + Math.sin(dLat / 2) ** 2 + + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return EARTH_RADIUS_KM * c; +} diff --git a/src/server/util/entrySerialize.js b/src/server/util/entrySerialize.js new file mode 100644 index 0000000..e5df95c --- /dev/null +++ b/src/server/util/entrySerialize.js @@ -0,0 +1,44 @@ +// Shared entry column list and JSON serialization. Every entry response is a +// full row plus a `participants` array (the entry_participants rows; [] means +// "all trip members participate") and a parsed `segments` array (or null). + +export const ENTRY_COLUMNS = + 'id, trip_id, date, type, title, details, start_time, end_time, ' + + 'location_name, lat, lng, sort_order, price, paid_by, split_mode, segments, rental'; + +// Parse the stored segments JSON text into an array, or null if absent/invalid. +export function parseSegments(value) { + if (!value) return null; + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : null; + } catch { + return null; + } +} + +// Parse the stored rental JSON text into an object, or null if absent/invalid. +export function parseRental(value) { + if (!value) return null; + try { + const parsed = JSON.parse(value); + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null; + } catch { + return null; + } +} + +export function attachParticipants(db, row) { + if (!row) return row; + const rows = db + .prepare('SELECT user_id FROM entry_participants WHERE entry_id = ? ORDER BY user_id') + .all(row.id); + row.participants = rows.map((r) => r.user_id); + row.segments = parseSegments(row.segments); + row.rental = parseRental(row.rental); + return row; +} + +export function attachParticipantsAll(db, rows) { + return rows.map((r) => attachParticipants(db, r)); +} diff --git a/src/server/util/names.js b/src/server/util/names.js new file mode 100644 index 0000000..a59c7f3 --- /dev/null +++ b/src/server/util/names.js @@ -0,0 +1,21 @@ +// Auto-generated, non-unique display names: adjective-animal (e.g. brave-otter). + +const ADJECTIVES = [ + 'brave', 'calm', 'clever', 'bold', 'gentle', 'swift', 'quiet', 'bright', + 'lucky', 'mellow', 'nimble', 'sunny', 'witty', 'eager', 'jolly', 'keen', + 'proud', 'wise', 'zesty', 'cosmic', +]; + +const ANIMALS = [ + 'otter', 'heron', 'lynx', 'panda', 'koala', 'falcon', 'marmot', 'gecko', + 'tapir', 'ibex', 'narwhal', 'quokka', 'badger', 'osprey', 'manta', 'puffin', + 'yak', 'wombat', 'civet', 'raven', +]; + +function pick(list) { + return list[Math.floor(Math.random() * list.length)]; +} + +export function generateDisplayName() { + return `${pick(ADJECTIVES)}-${pick(ANIMALS)}`; +} diff --git a/src/server/util/rental.js b/src/server/util/rental.js new file mode 100644 index 0000000..768da96 --- /dev/null +++ b/src/server/util/rental.js @@ -0,0 +1,84 @@ +// Validation + normalization for rental-car entry details (see docs/API.md). + +import { isValidDateStr } from './dates.js'; + +const TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/; + +function validCoord(v, min, max) { + return typeof v === 'number' && Number.isFinite(v) && v >= min && v <= max; +} + +// Optional string field with a max length. +function checkString(obj, key, max) { + const v = obj[key]; + if (v === undefined || v === null) return { skip: true }; + if (typeof v !== 'string' || v.length > max) { + return { error: `${key} must be a string <= ${max} chars` }; + } + return { value: v }; +} + +// Validate a pickup/dropoff endpoint: date required, others optional. +function normalizeStop(stop, side) { + if (typeof stop !== 'object' || stop === null || Array.isArray(stop)) { + return { error: `${side} must be an object` }; + } + if (!isValidDateStr(stop.date)) { + return { error: `${side}.date must be a valid YYYY-MM-DD date` }; + } + const out = { date: stop.date }; + if (stop.time !== undefined && stop.time !== null) { + if (typeof stop.time !== 'string' || !TIME_RE.test(stop.time)) { + return { error: `${side}.time must be HH:MM` }; + } + out.time = stop.time; + } + const name = checkString(stop, 'location_name', 120); + if (name.error) return { error: `${side}.${name.error}` }; + if (!name.skip) out.location_name = name.value; + + const hasLat = stop.lat !== undefined && stop.lat !== null; + const hasLng = stop.lng !== undefined && stop.lng !== null; + if (hasLat !== hasLng) { + return { error: `${side} lat/lng must both be present or both absent` }; + } + if (hasLat) { + if (!validCoord(stop.lat, -90, 90)) return { error: `${side}.lat out of range` }; + if (!validCoord(stop.lng, -180, 180)) return { error: `${side}.lng out of range` }; + out.lat = stop.lat; + out.lng = stop.lng; + } + return { value: out }; +} + +// Validate a rental object. Returns { error } or { value: normalized }. +export function validateRental(rental) { + if (typeof rental !== 'object' || rental === null || Array.isArray(rental)) { + return { error: 'rental must be an object' }; + } + const out = {}; + + for (const [key, max] of [['brand', 60], ['model', 60], ['car_type', 40], ['booking_ref', 60]]) { + const r = checkString(rental, key, max); + if (r.error) return { error: r.error }; + if (!r.skip) out[key] = r.value; + } + + if (rental.included_km !== undefined) { + const v = rental.included_km; + if (v !== null && !(typeof v === 'number' && Number.isFinite(v) && v >= 0)) { + return { error: 'included_km must be null or a number >= 0' }; + } + out.included_km = v; + } + + for (const side of ['pickup', 'dropoff']) { + if (rental[side] !== undefined && rental[side] !== null) { + const checked = normalizeStop(rental[side], side); + if (checked.error) return { error: checked.error }; + out[side] = checked.value; + } + } + + return { value: out }; +} diff --git a/src/server/util/segments.js b/src/server/util/segments.js new file mode 100644 index 0000000..aa42618 --- /dev/null +++ b/src/server/util/segments.js @@ -0,0 +1,81 @@ +// Validation + normalization for flight-entry segments (see docs/API.md). + +const CODE_RE = /^[A-Z0-9]{2,4}$/; +const TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/; +const MAX_SEGMENTS = 8; + +function validCoord(v, min, max) { + return typeof v === 'number' && Number.isFinite(v) && v >= min && v <= max; +} + +// Validate and normalize one airport endpoint ({code, name?, lat?, lng?}). +function normalizeAirport(a, side, i) { + if (!a || typeof a !== 'object' || Array.isArray(a)) { + return { error: `segment ${i} ${side} is required` }; + } + if (typeof a.code !== 'string') { + return { error: `segment ${i} ${side}.code is required` }; + } + const code = a.code.trim().toUpperCase(); + if (!CODE_RE.test(code)) { + return { error: `segment ${i} ${side}.code must be 2-4 alphanumerics` }; + } + const out = { code }; + if (a.name !== undefined && a.name !== null) { + if (typeof a.name !== 'string' || a.name.length > 80) { + return { error: `segment ${i} ${side}.name must be a string <= 80 chars` }; + } + out.name = a.name; + } + const hasLat = a.lat !== undefined && a.lat !== null; + const hasLng = a.lng !== undefined && a.lng !== null; + if (hasLat !== hasLng) { + return { error: `segment ${i} ${side} lat/lng must both be present or both absent` }; + } + if (hasLat) { + if (!validCoord(a.lat, -90, 90)) return { error: `segment ${i} ${side}.lat out of range` }; + if (!validCoord(a.lng, -180, 180)) return { error: `segment ${i} ${side}.lng out of range` }; + out.lat = a.lat; + out.lng = a.lng; + } + return { value: out }; +} + +// Validate a segments array. Returns { error } or { value: normalizedArray }. +export function validateSegments(segments) { + if (!Array.isArray(segments)) { + return { error: 'segments must be an array' }; + } + if (segments.length < 1 || segments.length > MAX_SEGMENTS) { + return { error: `segments must have 1-${MAX_SEGMENTS} entries` }; + } + const out = []; + for (let i = 0; i < segments.length; i++) { + const s = segments[i]; + if (!s || typeof s !== 'object' || Array.isArray(s)) { + return { error: `segment ${i} must be an object` }; + } + const from = normalizeAirport(s.from, 'from', i); + if (from.error) return from; + const to = normalizeAirport(s.to, 'to', i); + if (to.error) return to; + + const seg = { from: from.value, to: to.value }; + if (s.flight_no !== undefined && s.flight_no !== null) { + if (typeof s.flight_no !== 'string' || s.flight_no.length > 12) { + return { error: `segment ${i} flight_no must be a string <= 12 chars` }; + } + seg.flight_no = s.flight_no; + } + for (const key of ['dep_time', 'arr_time']) { + if (s[key] !== undefined && s[key] !== null) { + if (typeof s[key] !== 'string' || !TIME_RE.test(s[key])) { + return { error: `segment ${i} ${key} must be HH:MM` }; + } + seg[key] = s[key]; + } + } + out.push(seg); + } + return { value: out }; +} diff --git a/src/server/util/token.js b/src/server/util/token.js new file mode 100644 index 0000000..8ca42a9 --- /dev/null +++ b/src/server/util/token.js @@ -0,0 +1,56 @@ +import crypto from 'node:crypto'; + +// Unambiguous alphabet (no I/L/O/0/1) shared by account tokens and join codes. +const ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'; // 31 chars + +// Random string of `length` chars, uniformly sampled (rejection sampling to +// avoid modulo bias) from ALPHABET. +export function randomCode(length) { + const n = ALPHABET.length; + const maxUnbiased = Math.floor(256 / n) * n; // 248: reject bytes >= this + let out = ''; + while (out.length < length) { + const bytes = crypto.randomBytes(length - out.length); + for (const b of bytes) { + if (b >= maxUnbiased) continue; + out += ALPHABET[b % n]; + if (out.length === length) break; + } + } + return out; +} + +// Strip dashes/spaces and uppercase — applied to both stored codes and user input. +export function normalizeCode(input) { + return String(input ?? '').replace(/[\s-]/g, '').toUpperCase(); +} + +export function sha256Hex(s) { + return crypto.createHash('sha256').update(s).digest('hex'); +} + +// Group a raw code into dash-separated blocks of `size` for display. +function group(raw, size) { + return raw.match(new RegExp(`.{1,${size}}`, 'g')).join('-'); +} + +export function generateAccountToken() { + return randomCode(16); +} + +export function generateJoinCode() { + return randomCode(8); +} + +export function formatToken(raw) { + return group(raw, 4); // XXXX-XXXX-XXXX-XXXX +} + +export function formatJoinCode(raw) { + return group(raw, 4); // XXXX-XXXX +} + +// sha256 of the normalized token/code — the only thing stored for accounts. +export function hashToken(input) { + return sha256Hex(normalizeCode(input)); +} diff --git a/tests/api.test.js b/tests/api.test.js new file mode 100644 index 0000000..96a54bb --- /dev/null +++ b/tests/api.test.js @@ -0,0 +1,458 @@ +import { test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import request from 'supertest'; +import { createApp } from '../src/server/app.js'; + +let tmpDir; +let app; + +beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tripplan-test-')); + app = createApp({ dbPath: path.join(tmpDir, 'test.db'), sessionSecret: 'test-secret' }); +}); + +afterEach(() => { + try { + if (app.locals.db) app.locals.db.close(); + } catch { + /* ignore */ + } + fs.rmSync(tmpDir, { recursive: true, force: true }); +}); + +// Create an account (Mullvad-style) and return an agent with the session set, +// plus the user object {id, display_name} and the raw token. +async function createAccount() { + const agent = request.agent(app); + const res = await agent.post('/api/auth/account').send({}); + assert.equal(res.status, 201); + return { agent, user: res.body.user, token: res.body.token }; +} + +// --------------------------------------------------------------------------- +// Auth (account tokens) +// --------------------------------------------------------------------------- + +test('account creation returns a grouped token and a generated display name', async () => { + const { user, token } = await createAccount(); + assert.deepEqual(Object.keys(user).sort(), ['display_name', 'id']); + assert.match(token, /^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}(-[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}){3}$/); + assert.match(user.display_name, /^[a-z]+-[a-z]+$/); +}); + +test('account / me / logout lifecycle', async () => { + const { agent, user } = await createAccount(); + + const me = await agent.get('/api/auth/me'); + assert.equal(me.status, 200); + assert.deepEqual(me.body.user, user); + + const out = await agent.post('/api/auth/logout'); + assert.equal(out.status, 204); + + const meAfter = await agent.get('/api/auth/me'); + assert.equal(meAfter.status, 401); + assert.deepEqual(meAfter.body, { error: 'unauthorized' }); +}); + +test('me requires auth', async () => { + const res = await request(app).get('/api/auth/me'); + assert.equal(res.status, 401); + assert.equal(res.body.error, 'unauthorized'); +}); + +test('login with token (normalized) succeeds; bad token 401', async () => { + const { user, token } = await createAccount(); + + // Fresh agent logs in with the token, dashes/lowercase/spaces tolerated. + const agent = request.agent(app); + const messy = ` ${token.toLowerCase().replace(/-/g, '')} `; + const ok = await agent.post('/api/auth/login').send({ token: messy }); + assert.equal(ok.status, 200); + assert.deepEqual(ok.body.user, user); + + const me = await agent.get('/api/auth/me'); + assert.equal(me.body.user.id, user.id); + + const bad = await request(app).post('/api/auth/login').send({ token: 'ZZZZ-ZZZZ-ZZZZ-ZZZZ' }); + assert.equal(bad.status, 401); + + const empty = await request(app).post('/api/auth/login').send({}); + assert.equal(empty.status, 401); +}); + +test('patch display_name validates 1-40 chars after trim', async () => { + const { agent, user } = await createAccount(); + + const ok = await agent.patch('/api/auth/me').send({ display_name: ' Anna the Explorer ' }); + assert.equal(ok.status, 200); + assert.equal(ok.body.user.display_name, 'Anna the Explorer'); + assert.equal(ok.body.user.id, user.id); + + const empty = await agent.patch('/api/auth/me').send({ display_name: ' ' }); + assert.equal(empty.status, 400); + + const tooLong = await agent.patch('/api/auth/me').send({ display_name: 'x'.repeat(41) }); + assert.equal(tooLong.status, 400); + + const noAuth = await request(app).patch('/api/auth/me').send({ display_name: 'nope' }); + assert.equal(noAuth.status, 401); +}); + +// --------------------------------------------------------------------------- +// Trips: CRUD + validation +// --------------------------------------------------------------------------- + +test('create trip and list it', async () => { + const { agent, user } = await createAccount(); + const create = await agent + .post('/api/trips') + .send({ name: 'Thailand', start_date: '2026-08-01', end_date: '2026-08-10' }); + assert.equal(create.status, 201); + assert.equal(create.body.trip.name, 'Thailand'); + assert.equal(create.body.trip.owner_id, user.id); + // join_code is present on the created trip, in grouped display form. + assert.match(create.body.trip.join_code, /^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}-[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}$/); + + const list = await agent.get('/api/trips'); + assert.equal(list.status, 200); + assert.equal(list.body.trips.length, 1); + assert.equal(list.body.trips[0].role, 'owner'); + assert.equal(list.body.trips[0].member_count, 1); + assert.equal(list.body.trips[0].entry_count, 0); +}); + +test('trip validation: bad dates and oversized range', async () => { + const { agent } = await createAccount(); + + const emptyName = await agent.post('/api/trips').send({ name: '', start_date: '2026-08-01', end_date: '2026-08-02' }); + assert.equal(emptyName.status, 400); + + const badDate = await agent.post('/api/trips').send({ name: 'X', start_date: '2026-13-40', end_date: '2026-08-02' }); + assert.equal(badDate.status, 400); + + const reversed = await agent.post('/api/trips').send({ name: 'X', start_date: '2026-08-10', end_date: '2026-08-01' }); + assert.equal(reversed.status, 400); + + const tooLong = await agent.post('/api/trips').send({ name: 'X', start_date: '2026-01-01', end_date: '2027-06-01' }); + assert.equal(tooLong.status, 400); +}); + +test('get / patch / delete trip', async () => { + const { agent } = await createAccount(); + const created = (await agent.post('/api/trips').send({ name: 'Trip', start_date: '2026-08-01', end_date: '2026-08-05' })).body.trip; + + const got = await agent.get(`/api/trips/${created.id}`); + assert.equal(got.status, 200); + assert.equal(got.body.trip.name, 'Trip'); + assert.ok(got.body.trip.join_code); + assert.equal(got.body.members.length, 1); + assert.deepEqual(Object.keys(got.body.members[0]).sort(), ['display_name', 'id', 'role']); + assert.deepEqual(got.body.entries, []); + + const patched = await agent.patch(`/api/trips/${created.id}`).send({ name: 'Renamed', end_date: '2026-08-08' }); + assert.equal(patched.status, 200); + assert.equal(patched.body.trip.name, 'Renamed'); + assert.equal(patched.body.trip.end_date, '2026-08-08'); + + const del = await agent.delete(`/api/trips/${created.id}`); + assert.equal(del.status, 204); + + const gone = await agent.get(`/api/trips/${created.id}`); + assert.equal(gone.status, 404); +}); + +test('unknown trip returns 404', async () => { + const { agent } = await createAccount(); + const res = await agent.get('/api/trips/9999'); + assert.equal(res.status, 404); + assert.equal(res.body.error, 'not found'); +}); + +// --------------------------------------------------------------------------- +// Membership via join code +// --------------------------------------------------------------------------- + +test('join by code: non-member 404, join as editor, idempotent, unknown 404', async () => { + const owner = await createAccount(); + const guest = await createAccount(); + + const trip = (await owner.agent.post('/api/trips').send({ name: 'Shared', start_date: '2026-08-01', end_date: '2026-08-03' })).body.trip; + const code = trip.join_code; + + // Non-member cannot see the trip. + assert.equal((await guest.agent.get(`/api/trips/${trip.id}`)).status, 404); + + // Unknown code -> 404. + assert.equal((await guest.agent.post('/api/trips/join').send({ code: 'ZZZZ-ZZZZ' })).status, 404); + + // Join with the code (lowercase/spacing tolerated) -> 200, joined as editor. + const joined = await guest.agent.post('/api/trips/join').send({ code: ` ${code.toLowerCase()} ` }); + assert.equal(joined.status, 200); + assert.equal(joined.body.trip.id, trip.id); + + const view = await guest.agent.get(`/api/trips/${trip.id}`); + assert.equal(view.status, 200); + const guestMember = view.body.members.find((m) => m.id === guest.user.id); + assert.equal(guestMember.role, 'editor'); + + // Idempotent: joining again still 200, membership count unchanged. + const again = await guest.agent.post('/api/trips/join').send({ code }); + assert.equal(again.status, 200); + const memberCount = (await owner.agent.get('/api/trips')).body.trips[0].member_count; + assert.equal(memberCount, 2); +}); + +test('join-code regeneration is owner-only and invalidates the old code', async () => { + const owner = await createAccount(); + const guest = await createAccount(); + const trip = (await owner.agent.post('/api/trips').send({ name: 'T', start_date: '2026-08-01', end_date: '2026-08-03' })).body.trip; + const oldCode = trip.join_code; + + // Non-member cannot regenerate (they can't even see the trip) -> 404. + assert.equal((await guest.agent.post(`/api/trips/${trip.id}/join-code`)).status, 404); + + const regen = await owner.agent.post(`/api/trips/${trip.id}/join-code`); + assert.equal(regen.status, 200); + assert.notEqual(regen.body.trip.join_code, oldCode); + + // Old code no longer works; new one does. + assert.equal((await guest.agent.post('/api/trips/join').send({ code: oldCode })).status, 404); + assert.equal((await guest.agent.post('/api/trips/join').send({ code: regen.body.trip.join_code })).status, 200); + + // A member who is not the owner cannot regenerate -> 403. + const nonOwnerRegen = await guest.agent.post(`/api/trips/${trip.id}/join-code`); + assert.equal(nonOwnerRegen.status, 403); +}); + +test('owner-only member removal; owner cannot remove self', async () => { + const owner = await createAccount(); + const guest = await createAccount(); + const trip = (await owner.agent.post('/api/trips').send({ name: 'T', start_date: '2026-08-01', end_date: '2026-08-03' })).body.trip; + await guest.agent.post('/api/trips/join').send({ code: trip.join_code }); + + // Non-owner cannot delete the trip. + assert.equal((await guest.agent.delete(`/api/trips/${trip.id}`)).status, 403); + + // Owner removes the guest. + assert.equal((await owner.agent.delete(`/api/trips/${trip.id}/members/${guest.user.id}`)).status, 204); + assert.equal((await guest.agent.get(`/api/trips/${trip.id}`)).status, 404); + + // Owner cannot remove self. + assert.equal((await owner.agent.delete(`/api/trips/${trip.id}/members/${owner.user.id}`)).status, 400); +}); + +// --------------------------------------------------------------------------- +// Entries: CRUD + validation +// --------------------------------------------------------------------------- + +async function makeTrip(agent) { + return (await agent.post('/api/trips').send({ name: 'E', start_date: '2026-08-01', end_date: '2026-08-10' })).body.trip; +} + +test('entry CRUD and full-row shape', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + + const create = await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', + type: 'flight', + title: 'BKK -> CNX', + start_time: '09:30', + location_name: 'Chiang Mai', + lat: 18.79, + lng: 98.98, + }); + assert.equal(create.status, 201); + const entry = create.body.entry; + assert.deepEqual( + Object.keys(entry).sort(), + ['date', 'details', 'end_time', 'id', 'lat', 'lng', 'location_name', 'paid_by', 'participants', 'price', 'rental', 'segments', 'sort_order', 'split_mode', 'start_time', 'title', 'trip_id', 'type'].sort() + ); + assert.equal(entry.details, ''); + assert.equal(entry.lat, 18.79); + assert.equal(entry.price, null); + assert.equal(entry.paid_by, null); + assert.equal(entry.split_mode, 'equal'); + assert.deepEqual(entry.participants, []); + assert.equal(entry.segments, null); + assert.equal(entry.rental, null); + + const patch = await agent.patch(`/api/entries/${entry.id}`).send({ title: 'BKK to CNX', details: 'window seat' }); + assert.equal(patch.status, 200); + assert.equal(patch.body.entry.title, 'BKK to CNX'); + assert.equal(patch.body.entry.details, 'window seat'); + + const list = await agent.get(`/api/trips/${trip.id}/entries`); + assert.equal(list.body.entries.length, 1); + + const del = await agent.delete(`/api/entries/${entry.id}`); + assert.equal(del.status, 204); + + const listAfter = await agent.get(`/api/trips/${trip.id}/entries`); + assert.equal(listAfter.body.entries.length, 0); +}); + +test('entry validation: type, title, lat/lng pairing and ranges', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + const base = `/api/trips/${trip.id}/entries`; + + const badType = await agent.post(base).send({ date: '2026-08-01', type: 'teleport', title: 'x' }); + assert.equal(badType.status, 400); + + const badDate = await agent.post(base).send({ date: '08/01/2026', type: 'note', title: 'x' }); + assert.equal(badDate.status, 400); + + const noTitle = await agent.post(base).send({ date: '2026-08-01', type: 'note', title: '' }); + assert.equal(noTitle.status, 400); + + const latOnly = await agent.post(base).send({ date: '2026-08-01', type: 'note', title: 'x', lat: 10 }); + assert.equal(latOnly.status, 400); + + const badLat = await agent.post(base).send({ date: '2026-08-01', type: 'note', title: 'x', lat: 99, lng: 10 }); + assert.equal(badLat.status, 400); + + const ok = await agent.post(base).send({ date: '2026-08-01', type: 'note', title: 'x', lat: 10, lng: 20 }); + assert.equal(ok.status, 201); +}); + +test('non-member cannot add or view entries', async () => { + const owner = await createAccount(); + const guest = await createAccount(); + const trip = await makeTrip(owner.agent); + + const post = await guest.agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-01', type: 'note', title: 'x' }); + assert.equal(post.status, 404); + + const list = await guest.agent.get(`/api/trips/${trip.id}/entries`); + assert.equal(list.status, 404); +}); + +// --------------------------------------------------------------------------- +// Route + summary math +// --------------------------------------------------------------------------- + +test('route computes legs, totalKm and summary counts', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); // 2026-08-01 .. 2026-08-10 => 10 days + + // Bangkok + await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'flight', title: 'Depart', sort_order: 0, + location_name: 'Bangkok', lat: 13.7563, lng: 100.5018, + }); + // Chiang Mai + await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'hotel', title: 'Hotel CNX', sort_order: 1, + location_name: 'Chiang Mai', lat: 18.7883, lng: 98.9853, + }); + // A second flight + hotel + activities + a travel leg (no coords) to exercise counts + await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-02', type: 'flight', title: 'F2' }); + await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-02', type: 'hotel', title: 'H2' }); + await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-02', type: 'travel', title: 'Drive' }); + await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-03', type: 'activity', title: 'A1' }); + await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-03', type: 'activity', title: 'A2' }); + + const res = await agent.get(`/api/trips/${trip.id}/route`); + assert.equal(res.status, 200); + + // Two located stops -> one leg. BKK<->CNX great-circle ~ 585 km. + assert.equal(res.body.stops.length, 2); + assert.equal(res.body.legs.length, 1); + assert.ok(res.body.legs[0].km > 570 && res.body.legs[0].km < 600, `unexpected km ${res.body.legs[0].km}`); + assert.equal(res.body.totalKm, res.body.legs[0].km); + + const s = res.body.summary; + assert.equal(s.days, 10); + assert.equal(s.nights, 9); + assert.equal(s.flights, 2); + assert.equal(s.hotels, 2); + assert.equal(s.travelLegs, 1); + assert.equal(s.activities, 2); + assert.deepEqual(s.locations, ['Bangkok', 'Chiang Mai']); +}); + +test('route skips zero-distance legs but keeps the stop', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + + await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-01', type: 'activity', title: 'A', sort_order: 0, location_name: 'Same', lat: 10, lng: 10 }); + await agent.post(`/api/trips/${trip.id}/entries`).send({ date: '2026-08-01', type: 'activity', title: 'B', sort_order: 1, location_name: 'Same', lat: 10, lng: 10 }); + + const res = await agent.get(`/api/trips/${trip.id}/route`); + assert.equal(res.body.stops.length, 2); + assert.equal(res.body.legs.length, 0); + assert.equal(res.body.totalKm, 0); + assert.deepEqual(res.body.summary.locations, ['Same']); +}); + +// --------------------------------------------------------------------------- +// Geocode (mocked upstream — never hits the real Nominatim) +// --------------------------------------------------------------------------- + +test('geocode proxies and caches results (mocked fetch)', async () => { + const { agent } = await createAccount(); + const original = global.fetch; + let calls = 0; + global.fetch = async () => { + calls += 1; + return { + ok: true, + json: async () => [ + { display_name: 'Chiang Mai, Thailand', lat: '18.7883', lon: '98.9853' }, + ], + }; + }; + try { + const first = await agent.get('/api/geocode?q=Chiang%20Mai'); + assert.equal(first.status, 200); + assert.equal(first.body.results.length, 1); + assert.deepEqual(first.body.results[0], { name: 'Chiang Mai, Thailand', lat: 18.7883, lng: 98.9853 }); + + // Second identical query is served from cache -> fetch not called again. + const second = await agent.get('/api/geocode?q=Chiang%20Mai'); + assert.equal(second.status, 200); + assert.equal(calls, 1); + } finally { + global.fetch = original; + } +}); + +test('geocode returns 502 on upstream failure (mocked fetch)', async () => { + const { agent } = await createAccount(); + const original = global.fetch; + global.fetch = async () => { + throw new Error('network down'); + }; + try { + const res = await agent.get('/api/geocode?q=Nowhere'); + assert.equal(res.status, 502); + assert.deepEqual(res.body, { error: 'geocoding unavailable' }); + } finally { + global.fetch = original; + } +}); + +test('geocode requires auth and a query', async () => { + const noAuth = await request(app).get('/api/geocode?q=x'); + assert.equal(noAuth.status, 401); + + const { agent } = await createAccount(); + const noQuery = await agent.get('/api/geocode'); + assert.equal(noQuery.status, 400); +}); + +// --------------------------------------------------------------------------- +// Unknown /api route -> JSON 404 +// --------------------------------------------------------------------------- + +test('unknown api path returns JSON 404', async () => { + const { agent } = await createAccount(); + const res = await agent.get('/api/does-not-exist'); + assert.equal(res.status, 404); + assert.deepEqual(res.body, { error: 'not found' }); +}); diff --git a/tests/costs.test.js b/tests/costs.test.js new file mode 100644 index 0000000..92ad4df --- /dev/null +++ b/tests/costs.test.js @@ -0,0 +1,220 @@ +import { test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import request from 'supertest'; +import { createApp } from '../src/server/app.js'; +import { computeCosts } from '../src/server/util/costs.js'; + +let tmpDir; +let app; + +beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tripplan-costs-')); + app = createApp({ dbPath: path.join(tmpDir, 'test.db'), sessionSecret: 'test-secret' }); +}); + +afterEach(() => { + try { + if (app.locals.db) app.locals.db.close(); + } catch { + /* ignore */ + } + fs.rmSync(tmpDir, { recursive: true, force: true }); +}); + +async function createAccount() { + const agent = request.agent(app); + const res = await agent.post('/api/auth/account').send({}); + assert.equal(res.status, 201); + return { agent, user: res.body.user, token: res.body.token }; +} + +// Build a trip owned by `owner`; each member agent joins via the join code. +async function costTrip(owner, memberAccounts = []) { + const trip = (await owner.agent.post('/api/trips').send({ + name: 'Costs', start_date: '2026-08-01', end_date: '2026-08-05', currency: 'USD', + })).body.trip; + for (const m of memberAccounts) { + const res = await m.agent.post('/api/trips/join').send({ code: trip.join_code }); + assert.equal(res.status, 200); + } + return trip; +} + +function findUser(costs, userId) { + return costs.perUser.find((u) => u.userId === userId); +} + +// --------------------------------------------------------------------------- +// Currency +// --------------------------------------------------------------------------- + +test('currency: defaults to USD, validates format, patchable', async () => { + const { agent } = await createAccount(); + + const def = await agent.post('/api/trips').send({ name: 'T', start_date: '2026-08-01', end_date: '2026-08-05' }); + assert.equal(def.status, 201); + assert.equal(def.body.trip.currency, 'USD'); + + const bad = await agent.post('/api/trips').send({ name: 'T', start_date: '2026-08-01', end_date: '2026-08-05', currency: 'us' }); + assert.equal(bad.status, 400); + + const eur = await agent.post('/api/trips').send({ name: 'T', start_date: '2026-08-01', end_date: '2026-08-05', currency: 'EUR' }); + assert.equal(eur.status, 201); + assert.equal(eur.body.trip.currency, 'EUR'); + + const patched = await agent.patch(`/api/trips/${eur.body.trip.id}`).send({ currency: 'THB' }); + assert.equal(patched.status, 200); + assert.equal(patched.body.trip.currency, 'THB'); + + const listed = await agent.get('/api/trips'); + assert.ok(listed.body.trips.every((t) => typeof t.currency === 'string')); +}); + +// --------------------------------------------------------------------------- +// Costs & splitting (endpoint) +// --------------------------------------------------------------------------- + +test('costs: equal split with payer produces net balances and a settlement', async () => { + const anna = await createAccount(); + const ben = await createAccount(); + const trip = await costTrip(anna, [ben]); + + // anna pays 950 (hotel), ben pays 500 (travel); both split equally between the two. + await anna.agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'hotel', title: 'Hotel', price: 950, paid_by: anna.user.id, + }); + await anna.agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-02', type: 'travel', title: 'Van', price: 500, paid_by: ben.user.id, + }); + + const res = await anna.agent.get(`/api/trips/${trip.id}/costs`); + assert.equal(res.status, 200); + const c = res.body; + assert.equal(c.currency, 'USD'); + assert.equal(c.totalCost, 1450); + assert.deepEqual(c.byType, { hotel: 950, travel: 500 }); + assert.equal(c.unassigned, 0); + + const a = findUser(c, anna.user.id); + const b = findUser(c, ben.user.id); + assert.equal(typeof a.displayName, 'string'); + assert.deepEqual([a.share, a.paid, a.net], [725, 950, 225]); + assert.deepEqual([b.share, b.paid, b.net], [725, 500, -225]); + + assert.equal(c.settlements.length, 1); + assert.deepEqual(c.settlements[0], { fromUserId: ben.user.id, toUserId: anna.user.id, amount: 225 }); +}); + +test('costs: own mode creates no debt and totals price x participants', async () => { + const anna = await createAccount(); + const ben = await createAccount(); + const trip = await costTrip(anna, [ben]); + + await anna.agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'flight', title: 'Flights', price: 300, split_mode: 'own', + }); + + const c = (await anna.agent.get(`/api/trips/${trip.id}/costs`)).body; + assert.equal(c.totalCost, 600); // 300 per person x 2 + assert.deepEqual(c.byType, { flight: 600 }); + for (const u of c.perUser) { + assert.deepEqual([u.share, u.paid, u.net], [300, 300, 0]); + } + assert.deepEqual(c.settlements, []); + assert.equal(c.unassigned, 0); +}); + +test('costs: payer mode is a personal expense (requires paid_by)', async () => { + const anna = await createAccount(); + const ben = await createAccount(); + const trip = await costTrip(anna, [ben]); + + // payer without paid_by -> validation error + const bad = await anna.agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'activity', title: 'Solo tour', price: 100, split_mode: 'payer', + }); + assert.equal(bad.status, 400); + + await anna.agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'activity', title: 'Solo tour', price: 100, split_mode: 'payer', paid_by: ben.user.id, + }); + + const c = (await anna.agent.get(`/api/trips/${trip.id}/costs`)).body; + assert.equal(c.totalCost, 100); + const a = findUser(c, anna.user.id); + const b = findUser(c, ben.user.id); + assert.deepEqual([a.share, a.paid, a.net], [0, 0, 0]); + assert.deepEqual([b.share, b.paid, b.net], [100, 100, 0]); + assert.deepEqual(c.settlements, []); +}); + +test('costs: participants subset only splits among the chosen members', async () => { + const anna = await createAccount(); + const ben = await createAccount(); + const carol = await createAccount(); + const trip = await costTrip(anna, [ben, carol]); + + // 90 split equally between anna & ben only (carol excluded), anna pays. + await anna.agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'travel', title: 'Taxi', price: 90, paid_by: anna.user.id, + participants: [anna.user.id, ben.user.id], + }); + + const c = (await anna.agent.get(`/api/trips/${trip.id}/costs`)).body; + assert.equal(c.totalCost, 90); + const a = findUser(c, anna.user.id); + assert.deepEqual([a.share, a.paid, a.net], [45, 90, 45]); + assert.deepEqual([findUser(c, ben.user.id).share, findUser(c, ben.user.id).net], [45, -45]); + assert.deepEqual([findUser(c, carol.user.id).share, findUser(c, carol.user.id).net], [0, 0]); + assert.equal(c.settlements.length, 1); + assert.deepEqual(c.settlements[0], { fromUserId: ben.user.id, toUserId: anna.user.id, amount: 45 }); +}); + +test('costs: equal with no payer accumulates into unassigned', async () => { + const anna = await createAccount(); + const ben = await createAccount(); + const trip = await costTrip(anna, [ben]); + + await anna.agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'hotel', title: 'Hotel', price: 200, // paid_by omitted (null) + }); + + const c = (await anna.agent.get(`/api/trips/${trip.id}/costs`)).body; + assert.equal(c.totalCost, 200); + assert.equal(c.unassigned, 200); + assert.deepEqual(c.byType, { hotel: 200 }); + for (const u of c.perUser) { + assert.deepEqual([u.share, u.paid, u.net], [100, 0, -100]); + } + // Nobody paid, so there is no creditor to settle toward. + assert.deepEqual(c.settlements, []); +}); + +// --------------------------------------------------------------------------- +// Costs computation (pure unit test — greedy minimal-transfer) +// --------------------------------------------------------------------------- + +test('computeCosts: greedy settlement matches largest debtor with largest creditor', () => { + const members = [ + { id: 1, display_name: 'brave-otter' }, + { id: 2, display_name: 'calm-heron' }, + { id: 3, display_name: 'wise-lynx' }, + ]; + // id 1 pays 300 for a 3-way equal split (100 each); id 1 is owed 200 total. + const c = computeCosts({ + currency: 'USD', + members, + entries: [{ type: 'hotel', price: 300, paid_by: 1, split_mode: 'equal', participants: [] }], + }); + assert.equal(c.totalCost, 300); + assert.equal(findUser(c, 1).displayName, 'brave-otter'); + assert.equal(findUser(c, 1).net, 200); + assert.equal(findUser(c, 2).net, -100); + assert.equal(findUser(c, 3).net, -100); + // Two transfers of 100 into the single creditor (id 1). + assert.equal(c.settlements.length, 2); + assert.ok(c.settlements.every((s) => s.toUserId === 1 && s.amount === 100)); +}); diff --git a/tests/flights.test.js b/tests/flights.test.js new file mode 100644 index 0000000..c7fb892 --- /dev/null +++ b/tests/flights.test.js @@ -0,0 +1,227 @@ +import { test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import request from 'supertest'; +import { createApp } from '../src/server/app.js'; + +let tmpDir; +let app; + +beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tripplan-flights-')); + app = createApp({ dbPath: path.join(tmpDir, 'test.db'), sessionSecret: 'test-secret' }); +}); + +afterEach(() => { + try { + if (app.locals.db) app.locals.db.close(); + } catch { + /* ignore */ + } + fs.rmSync(tmpDir, { recursive: true, force: true }); +}); + +async function createAccount() { + const agent = request.agent(app); + const res = await agent.post('/api/auth/account').send({}); + assert.equal(res.status, 201); + return { agent, user: res.body.user }; +} + +async function makeTrip(agent) { + return (await agent.post('/api/trips').send({ name: 'F', start_date: '2026-08-01', end_date: '2026-08-10' })).body.trip; +} + +const CNX = { code: 'CNX', name: 'Chiang Mai Intl', lat: 18.77, lng: 98.96 }; +const BKK = { code: 'BKK', name: 'Suvarnabhumi', lat: 13.68, lng: 100.75 }; +const DXB = { code: 'DXB', name: 'Dubai Intl', lat: 25.25, lng: 55.36 }; + +// --------------------------------------------------------------------------- +// Segments validation +// --------------------------------------------------------------------------- + +test('segments: accepted on flight, parsed back, codes normalized to uppercase', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + + const res = await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'flight', title: 'CNX-BKK-DXB', + segments: [ + { flight_no: 'TG103', dep_time: '10:30', arr_time: '11:45', from: { code: 'cnx', ...{ name: CNX.name, lat: CNX.lat, lng: CNX.lng } }, to: BKK }, + { flight_no: 'EK385', from: BKK, to: DXB }, + ], + }); + assert.equal(res.status, 201); + assert.equal(res.body.entry.segments.length, 2); + assert.equal(res.body.entry.segments[0].from.code, 'CNX'); // normalized + assert.equal(res.body.entry.segments[0].flight_no, 'TG103'); + assert.equal(res.body.entry.segments[1].to.code, 'DXB'); +}); + +test('segments: rejected on non-flight entries', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + const res = await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'note', title: 'Nope', + segments: [{ from: CNX, to: BKK }], + }); + assert.equal(res.status, 400); +}); + +test('segments: bad code and out-of-range count rejected', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + const base = `/api/trips/${trip.id}/entries`; + + const badCode = await agent.post(base).send({ + date: '2026-08-01', type: 'flight', title: 'x', + segments: [{ from: { code: 'TOOLONG' }, to: BKK }], + }); + assert.equal(badCode.status, 400); + + const empty = await agent.post(base).send({ + date: '2026-08-01', type: 'flight', title: 'x', segments: [], + }); + assert.equal(empty.status, 400); + + const tooMany = await agent.post(base).send({ + date: '2026-08-01', type: 'flight', title: 'x', + segments: Array.from({ length: 9 }, () => ({ from: CNX, to: BKK })), + }); + assert.equal(tooMany.status, 400); +}); + +test('segments: PATCH with null clears them', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + const entry = (await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'flight', title: 'x', segments: [{ from: CNX, to: BKK }], + })).body.entry; + assert.equal(entry.segments.length, 1); + + const cleared = await agent.patch(`/api/entries/${entry.id}`).send({ segments: null }); + assert.equal(cleared.status, 200); + assert.equal(cleared.body.entry.segments, null); +}); + +// --------------------------------------------------------------------------- +// Route expansion +// --------------------------------------------------------------------------- + +test('route expands a CNX-BKK-DXB flight into airport stops and legs', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + + await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'flight', title: 'CNX-BKK-DXB', + segments: [ + { flight_no: 'TG103', from: CNX, to: BKK }, + { flight_no: 'EK385', from: BKK, to: DXB }, + ], + }); + + const res = await agent.get(`/api/trips/${trip.id}/route`); + assert.equal(res.status, 200); + + // from(CNX) + to(BKK) + to(DXB) = 3 stops, consecutive dup (BKK) collapsed. + assert.equal(res.body.stops.length, 3); + assert.deepEqual(res.body.stops.map((s) => s.code), ['CNX', 'BKK', 'DXB']); + assert.ok(res.body.stops.every((s) => s.kind === 'airport')); + + assert.equal(res.body.legs.length, 2); + assert.ok(res.body.legs.every((l) => l.km > 0)); + // Both legs connect airport stops from the same flight entry -> air. + assert.ok(res.body.legs.every((l) => l.mode === 'air')); + assert.ok(res.body.totalKm > 0); + + assert.equal(res.body.summary.flights, 1); + assert.equal(res.body.summary.flightSegments, 2); + assert.equal(res.body.summary.kmAir, res.body.totalKm); + assert.equal(res.body.summary.kmDriven, 0); +}); + +test('route: mixed ground transfers + air segments split kmAir / kmDriven', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + + // Hotel near CNX (ground), then the CNX-BKK-DXB flight, then a hotel near DXB. + await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'hotel', title: 'CNX Hotel', sort_order: 0, + location_name: 'Chiang Mai', lat: 18.79, lng: 98.99, + }); + await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'flight', title: 'CNX-BKK-DXB', sort_order: 1, + segments: [ + { flight_no: 'TG103', from: CNX, to: BKK }, + { flight_no: 'EK385', from: BKK, to: DXB }, + ], + }); + await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-02', type: 'hotel', title: 'DXB Hotel', sort_order: 2, + location_name: 'Dubai', lat: 25.2, lng: 55.27, + }); + + const res = await agent.get(`/api/trips/${trip.id}/route`); + assert.equal(res.status, 200); + + // Stops: CNX-hotel, CNX, BKK, DXB, DXB-hotel = 5. + assert.equal(res.body.stops.length, 5); + assert.deepEqual(res.body.legs.map((l) => l.mode), ['ground', 'air', 'air', 'ground']); + + const s = res.body.summary; + assert.ok(s.kmAir > 0 && s.kmDriven > 0); + // Air = the two inter-airport legs; driven = the two hotel<->airport transfers. + assert.ok(Math.abs(s.kmAir - (res.body.legs[1].km + res.body.legs[2].km)) < 0.11); + assert.ok(Math.abs(s.kmDriven - (res.body.legs[0].km + res.body.legs[3].km)) < 0.11); + assert.ok(Math.abs(s.kmAir + s.kmDriven - res.body.totalKm) < 0.2); +}); + +test('route: segmentless flight falls back to entry lat/lng', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'flight', title: 'plain', location_name: 'Bangkok', lat: 13.68, lng: 100.75, + }); + const res = await agent.get(`/api/trips/${trip.id}/route`); + assert.equal(res.body.stops.length, 1); + assert.equal(res.body.stops[0].kind, undefined); + assert.equal(res.body.summary.flightSegments, 0); + // No legs -> both per-mode sums are 0. + assert.equal(res.body.legs.length, 0); + assert.equal(res.body.summary.kmAir, 0); + assert.equal(res.body.summary.kmDriven, 0); +}); + +// --------------------------------------------------------------------------- +// Airports lookup +// --------------------------------------------------------------------------- + +test('airports: exact IATA match ranks first; requires auth', async () => { + const noAuth = await request(app).get('/api/airports?q=CNX'); + assert.equal(noAuth.status, 401); + + const { agent } = await createAccount(); + const res = await agent.get('/api/airports?q=CNX'); + assert.equal(res.status, 200); + assert.ok(res.body.results.length >= 1); + assert.ok(res.body.results.length <= 8); + assert.equal(res.body.results[0].code, 'CNX'); + assert.match(res.body.results[0].name, /Chiang Mai/i); + assert.deepEqual( + Object.keys(res.body.results[0]).sort(), + ['city', 'code', 'country', 'lat', 'lng', 'name'].sort() + ); +}); + +test('airports: case-insensitive and empty query returns empty list', async () => { + const { agent } = await createAccount(); + + const lower = await agent.get('/api/airports?q=cnx'); + assert.equal(lower.body.results[0].code, 'CNX'); + + const empty = await agent.get('/api/airports'); + assert.equal(empty.status, 200); + assert.deepEqual(empty.body.results, []); +}); diff --git a/tests/index.js b/tests/index.js new file mode 100644 index 0000000..e2a1b02 --- /dev/null +++ b/tests/index.js @@ -0,0 +1,12 @@ +// Test barrel. The package.json `test` script runs `node --test tests/`, which +// on this Node build resolves the directory to `tests/index.js` and runs it as +// a single test file rather than scanning the directory. Importing each test +// module here registers its tests with the runner. Add new test files below. +// +// These run in one process, so each test file scopes its fixtures with +// top-level beforeEach/afterEach that touch only its own module-level +// app/tmpDir — keeping the files independent. +import './api.test.js'; +import './costs.test.js'; +import './flights.test.js'; +import './rental.test.js'; diff --git a/tests/rental.test.js b/tests/rental.test.js new file mode 100644 index 0000000..9387ea2 --- /dev/null +++ b/tests/rental.test.js @@ -0,0 +1,153 @@ +import { test, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import request from 'supertest'; +import { createApp } from '../src/server/app.js'; + +let tmpDir; +let app; + +beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tripplan-rental-')); + app = createApp({ dbPath: path.join(tmpDir, 'test.db'), sessionSecret: 'test-secret' }); +}); + +afterEach(() => { + try { + if (app.locals.db) app.locals.db.close(); + } catch { + /* ignore */ + } + fs.rmSync(tmpDir, { recursive: true, force: true }); +}); + +async function createAccount() { + const agent = request.agent(app); + const res = await agent.post('/api/auth/account').send({}); + assert.equal(res.status, 201); + return { agent, user: res.body.user }; +} + +async function makeTrip(agent) { + return (await agent.post('/api/trips').send({ name: 'R', start_date: '2026-08-01', end_date: '2026-08-10' })).body.trip; +} + +const RENTAL = { + brand: 'Toyota', model: 'Yaris Cross', car_type: 'SUV', + booking_ref: 'RC-889231', included_km: 1500, + pickup: { date: '2026-08-01', time: '09:00', location_name: 'CNX Airport', lat: 18.77, lng: 98.96 }, + dropoff: { date: '2026-08-07', time: '18:00', location_name: 'Old Town', lat: 18.79, lng: 98.98 }, +}; + +// --------------------------------------------------------------------------- +// Rental validation +// --------------------------------------------------------------------------- + +test('rental: accepted on rental entry, parsed back, cleared via null', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + + const res = await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'rental', title: 'Car', rental: RENTAL, + }); + assert.equal(res.status, 201); + const r = res.body.entry.rental; + assert.equal(r.brand, 'Toyota'); + assert.equal(r.included_km, 1500); + assert.equal(r.pickup.location_name, 'CNX Airport'); + assert.equal(r.dropoff.date, '2026-08-07'); + + const cleared = await agent.patch(`/api/entries/${res.body.entry.id}`).send({ rental: null }); + assert.equal(cleared.status, 200); + assert.equal(cleared.body.entry.rental, null); +}); + +test('rental: rejected on non-rental entries', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + const res = await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'hotel', title: 'Nope', rental: RENTAL, + }); + assert.equal(res.status, 400); +}); + +test('rental: bad shapes rejected', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + const base = `/api/trips/${trip.id}/entries`; + + const longBrand = await agent.post(base).send({ + date: '2026-08-01', type: 'rental', title: 'x', rental: { brand: 'z'.repeat(61) }, + }); + assert.equal(longBrand.status, 400); + + const negKm = await agent.post(base).send({ + date: '2026-08-01', type: 'rental', title: 'x', rental: { included_km: -5 }, + }); + assert.equal(negKm.status, 400); + + const noDate = await agent.post(base).send({ + date: '2026-08-01', type: 'rental', title: 'x', rental: { pickup: { time: '09:00' } }, + }); + assert.equal(noDate.status, 400); + + const halfCoord = await agent.post(base).send({ + date: '2026-08-01', type: 'rental', title: 'x', rental: { pickup: { date: '2026-08-01', lat: 18.77 } }, + }); + assert.equal(halfCoord.status, 400); +}); + +test('rental: minimal object with only included_km null is accepted', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + const res = await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'rental', title: 'Car', rental: { brand: 'Kia', included_km: null }, + }); + assert.equal(res.status, 201); + assert.equal(res.body.entry.rental.included_km, null); +}); + +// --------------------------------------------------------------------------- +// Summary + costs +// --------------------------------------------------------------------------- + +test('route summary: rentals count and includedKm sum', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + + await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'rental', title: 'Car A', rental: { included_km: 1500 }, + }); + await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-05', type: 'rental', title: 'Car B', rental: { included_km: 300 }, + }); + + const res = await agent.get(`/api/trips/${trip.id}/route`); + assert.equal(res.status, 200); + assert.equal(res.body.summary.rentals, 2); + assert.equal(res.body.summary.includedKm, 1800); +}); + +test('route summary: includedKm is null when no rental specifies one', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'rental', title: 'Car', rental: { brand: 'Kia' }, + }); + const res = await agent.get(`/api/trips/${trip.id}/route`); + assert.equal(res.body.summary.rentals, 1); + assert.equal(res.body.summary.includedKm, null); +}); + +test('costs byType includes rental', async () => { + const { agent } = await createAccount(); + const trip = await makeTrip(agent); + await agent.post(`/api/trips/${trip.id}/entries`).send({ + date: '2026-08-01', type: 'rental', title: 'Car', price: 240, rental: { brand: 'Kia' }, + }); + const c = (await agent.get(`/api/trips/${trip.id}/costs`)).body; + assert.equal(c.totalCost, 240); + assert.equal(c.byType.rental, 240); +});