fpv-airspace 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +41 -242
  2. package/dist/index.js +256 -35
  3. package/package.json +11 -1
package/README.md CHANGED
@@ -1,26 +1,32 @@
1
- # FPV Airspace
1
+ # <img src="assets/icon.svg" width="40" alt="" valign="middle"> FPV Airspace
2
2
 
3
- An MCP server that answers the question existing airspace tools do not: **can I legally take off and fly a drone here, in the UK?**
3
+ An MCP server and map that answer the question existing airspace tools do not: **can I legally take off and fly a drone here, in the UK?**
4
4
 
5
- Airspace restriction data is the easy half. A UK pilot actually has to clear three layers: permanent airspace restrictions (aerodrome flight restriction zones, prohibited, restricted and danger areas), temporary restrictions (NOTAMs), and landowner rules (National Trust byelaws, council park byelaws). The practical workaround pilots use is launching from a public right of way, where no landowner permission is needed. This server puts all of that behind twelve tools that take a place name, a postcode or coordinates.
5
+ <p align="center"><img src="docs/images/hero.webp" alt="The FPV Airspace map at Durdle Door: satellite imagery, footpaths, parking, hazards, the weather timeline and a drone marker" width="900"></p>
6
+
7
+ Airspace restriction data is the easy half. A UK pilot actually has to clear three layers: permanent airspace restrictions (aerodrome flight restriction zones, prohibited, restricted and danger areas, the 400 m zone around every prison), temporary restrictions (NOTAMs), and landowner rules (National Trust byelaws, Forestry England permits, council park byelaws). The practical workaround pilots use is launching from a public right of way, where no landowner permission is needed. This server puts all of that behind thirteen tools that take a place name, a postcode or coordinates, covers England, Wales, Scotland and Northern Ireland, and draws it on a map that Claude can show inline or that you can open on a phone.
6
8
 
7
9
  Informational only. It is not a substitute for a NATS pre-flight briefing, the CAA Drone Code, or permission from the landowner and any relevant aerodrome.
8
10
 
11
+ [![Sponsor](https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/noodlemctwoodle)
12
+
13
+ FPV Airspace is free and stays free: the data licences it relies on are for non-commercial use, so nothing is ever paywalled. If it saves you a wasted trip, a [one-off donation](https://donate.stripe.com/3cI28sesu6T0f4n7IP2Nq00), a [monthly donation](https://donate.stripe.com/8x2bJ23NQcdke0j3sz2Nq01) or [sponsoring on GitHub](https://github.com/sponsors/noodlemctwoodle) helps cover the hosting and the weather API.
14
+
9
15
  ## What it answers
10
16
 
11
- - **Is this point inside a restriction?** `check_location` returns a one-line verdict and every zone containing the point, with vertical limits, activation notes and who to ask. Prison restricted areas (the 400 m zones around every closed prison and young offender institution in England and Wales, an offence to enter without HMPPS permission) are recognised as their own zone type rather than as aerodromes.
12
- - **What is this aerodrome's zone?** `get_aerodrome_zone` by name or ICAO code, including runway protection zones.
13
- - **Is there a NOTAM in force?** `check_notams` reads the live NATS UK bulletin, filters by point, radius and date, and never silently drops NOTAMs it cannot place.
14
- - **What does my route cross?** `check_route` for a list of waypoints or an area, with the distance along the route at which each zone is entered.
15
- - **Can I take off here?** `check_takeoff_site` lists the nearest public rights of way with distances and the responsible council, the nearest public parking, ground hazards within 1 km (railways, major roads, power lines and pylons, substations, helipads, masts, military land) and places people gather (schools, hospitals, parks), plus National Trust land and known council byelaws at the point.
16
- - **Where can I park?** `find_parking` lists car parks, laybys and rest areas from OpenStreetMap, nearest first, with fee and access notes.
17
- - **Can I fly here, now?** `preflight_briefing` combines everything into one GO, CAUTION or NO-GO answer with reasons: airspace verdict, live NOTAMs, the weather window and geomagnetic activity, rights of way, parking and, when you name your drone, its rules. A live source that fails is reported as an outage, never assumed clear.
18
- - **What is the ground doing?** `check_terrain` profiles ground elevation along a route or around a point against the 120 m rule, which is measured from the surface below the aircraft, and warns when rising ground eats the clearance or falling ground puts a fixed height above the limit.
19
- - **Where could I take off?** `find_takeoff_spots` scores points on public rights of way, next to parking and on open access land within a radius, excludes anything inside prohibited, restricted, prison or aerodrome zones or on banned land, and returns the best few with reasons; the map shows them numbered.
20
- - **What can my drone do?** `check_drone_rules` takes a model name or a weight and class mark and answers which open subcategory applies (A1, A2 or A3), the separation from people, whether Flyer and Operator IDs are needed and when Remote ID is required, under the CAA class mark rules in force from 2026. `check_takeoff_site` accepts a `drone` too and adds the same summary to the site report. Both tools also list open access land (context for take-off, never permission) and SSSI or National Park designations (advisory) at the point, and treat Forestry England land as a take-off ban without a permit. Take-off and location reports also name the local authority at the point and any council-wide drone policy recorded for it in the seed list.
21
- - **Is the weather flyable?** `check_weather` gives an hourly forecast from Open-Meteo with wind and gusts at 10 m, wind at 120 m, rain, visibility, cloud, temperature and daylight, each hour rated good, caution or poor against typical small-drone limits.
22
- - **Show me.** On the hosted server every location answer carries a map link, and clients that support MCP Apps (Claude web, desktop and mobile) render the map inline: zones coloured by severity, NOTAM circles, footpaths, landowner land and parking.
23
- - Plus `geocode` to disambiguate place names and `get_data_status` for data provenance and attribution.
17
+ - **Is this point inside a restriction?** `check_location`: a one-line verdict and every zone containing the point, prisons included.
18
+ - **What is this aerodrome's zone?** `get_aerodrome_zone` by name or ICAO code.
19
+ - **Is there a NOTAM in force?** `check_notams` from the live NATS bulletin; unplaceable NOTAMs are listed, never dropped.
20
+ - **What does my route cross?** `check_route` for waypoints or an area, with the distance at which each zone is entered.
21
+ - **Can I take off here?** `check_takeoff_site`: nearest rights of way, parking, ground hazards, landowner rules, access land, designations and the council at the point.
22
+ - **Where can I park?** `find_parking` from OpenStreetMap, nearest first.
23
+ - **Can I fly here, now?** `preflight_briefing`: one GO, CAUTION or NO-GO with reasons from airspace, NOTAMs, weather, geomagnetic activity, paths, parking and your drone's rules.
24
+ - **What is the ground doing?** `check_terrain` against the 120 m rule along a route or around a point.
25
+ - **Where could I take off?** `find_takeoff_spots` scores and ranks candidate spots, and the map numbers them.
26
+ - **What can my drone do?** `check_drone_rules`: subcategory, separation, registration and Remote ID under the CAA class mark rules.
27
+ - **Is the weather flyable?** `check_weather`: hourly ratings against small-drone limits, plus the K-index.
28
+ - **Show me.** Every location answer on the hosted server carries a map that Claude renders inline.
29
+ - Plus `geocode` and `get_data_status`. Arguments and examples for each are in [docs/tools.md](docs/tools.md).
24
30
 
25
31
  ## Quick start
26
32
 
@@ -50,7 +56,7 @@ Add to `claude_desktop_config.json` (Settings > Developer > Edit Config):
50
56
 
51
57
  `OS_NAMES_API_KEY` is optional. Without it, geocoding uses OpenStreetMap's Nominatim. With a free key from the [OS Data Hub](https://osdatahub.os.uk/), Ordnance Survey place names are tried first.
52
58
 
53
- On first use the server downloads the current airspace data pack (about 49 MB compressed, 115 MB on disk: 1,050 restriction zones, 520,391 rights of way, 1,694 landowner polygons) into `~/.cache/fpv-airspace` and reports progress on stderr. NOTAM and geocoding tools work while it downloads. The pack is refreshed automatically when a new AIRAC cycle is published.
59
+ On first use the server downloads the current airspace data pack (about 195 MB compressed, 527 MB on disk: 1,069 restriction zones, 520,000 rights of way, 94,000 landowner and designation polygons, 586,000 parking spots and 816,000 ground hazards) into `~/.cache/fpv-airspace` and reports progress on stderr. NOTAM and geocoding tools work while it downloads. The pack is refreshed automatically when a new AIRAC cycle is published.
54
60
 
55
61
  ### Claude Desktop extension (.mcpb)
56
62
 
@@ -66,243 +72,36 @@ claude mcp add fpv-airspace -- npx -y fpv-airspace
66
72
 
67
73
  The same server runs as a Cloudflare Worker at **`https://fpv-airspace.fetchlabs.co.uk/mcp`**, which is what Claude's mobile app and voice mode can reach: they cannot run local servers, only remote connectors. Add it on claude.ai under Settings > Connectors > Add custom connector with that URL, and it becomes available on every surface, including voice conversations. Every tool accepts `format: "brief"`, which returns two or three spoken-friendly sentences instead of the full report.
68
74
 
69
- Hosting your own copy:
70
-
71
- ```bash
72
- npx wrangler login
73
- ```
74
-
75
- ```bash
76
- npm run worker:setup
77
- ```
78
-
79
- The setup script creates the D1 database and KV namespace, writes their ids into `wrangler.toml`, and loads the latest national pack into D1. Then `npm run worker:deploy` prints the URL. With the repository variable `CLOUDFLARE_DEPLOY=true` and the `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` secrets set, GitHub Actions deploys on every push to `main` and reloads D1 whenever a new data pack is built.
80
-
81
- ### Hosted (streamable HTTP on your own server)
82
-
83
- ```bash
84
- npx -y fpv-airspace --transport http --port 8080
85
- # or
86
- docker build -t fpv-airspace . && docker run -p 8080:8080 -v drone-data:/data fpv-airspace
87
- ```
88
-
89
- `POST /mcp` speaks the MCP streamable HTTP transport (stateless), `GET /healthz` reports pack and NOTAM cache state.
90
-
91
- ## Tools
92
-
93
- Every tool accepts `format: "text"` (default, a plain-text report), `format: "json"` (the same data as structured JSON) or `format: "brief"` (two or three sentences written to be read aloud, for voice). Tools that take a location accept either `place` (name, postcode or landmark) or `lat` and `lon`. When a place name matches several places, the tool returns the candidates instead of guessing.
94
-
95
- ### `check_location`
96
-
97
- Permanent airspace restrictions and landowner rules at a point.
98
-
99
- | Argument | Type | Notes |
100
- |---|---|---|
101
- | `place` / `lat`+`lon` | string / numbers | One form only |
102
- | `include_above_120m` | boolean | Also list zones whose lower limit is above 400 ft |
103
-
104
- Example prompt: *"Can I fly my drone at Tyndale Monument?"*
105
-
106
- ```
107
- No permanent airspace restriction at this point.
108
- Note: "the layby below Tyndale Monument" was not found; results are for "Tyndale Monument".
109
-
110
- Location: Tyndale Monument, North Nibley, Gloucestershire (51.68390, -2.40540) via nominatim
111
- ...
112
- Attribution: Airspace restrictions: UK AIP ENR 5.1 UAS Flight Restrictions dataset © NATS Limited ...; Geocoding © OpenStreetMap contributors (ODbL), via Nominatim
113
- ```
114
-
115
- ### `get_aerodrome_zone`
116
-
117
- | Argument | Type | Notes |
118
- |---|---|---|
119
- | `aerodrome` | string | Name or ICAO code, e.g. `Bristol` or `EGGD` |
120
- | `include_geojson` | boolean | Include zone geometry |
121
-
122
- Example prompt: *"Show me the Gatwick FRZ."*
123
-
124
- ### `check_notams`
125
-
126
- | Argument | Type | Notes |
127
- |---|---|---|
128
- | `place` / `lat`+`lon` | | |
129
- | `radius_km` | 0–50 | Also list NOTAMs whose circle comes within this distance. 0 = covering only |
130
- | `date` | ISO 8601 | Defaults to now; the bulletin covers the next 7 days |
131
- | `max_results` | 1–50 | |
132
-
133
- Example prompt: *"Any NOTAMs over Durdle Door this Saturday?"*
134
-
135
- ### `check_route`
136
-
137
- | Argument | Type | Notes |
138
- |---|---|---|
139
- | `waypoints` | array of `[lon, lat]` or place names | 2–50 points; routes over 500 km are rejected |
140
- | `area` | `{ bbox: [w, s, e, n] }` or GeoJSON Polygon | Instead of waypoints |
141
- | `include_above_120m` | boolean | |
142
-
143
- Example prompt: *"Check a route from Clevedon to Portishead along the coast."*
144
-
145
- ### `check_takeoff_site`
146
-
147
- | Argument | Type | Notes |
148
- |---|---|---|
149
- | `place` / `lat`+`lon` | | |
150
- | `max_paths` | 1–20 | Rights of way to list (default 5) |
151
- | `search_radius_m` | 50–5000 | Default 1000 |
75
+ Running your own copy, on Cloudflare or as plain streamable HTTP or Docker, is covered in [docs/hosting.md](docs/hosting.md).
152
76
 
153
- Example prompt: *"Where's the nearest footpath I could take off from near Corfe Castle?"*
77
+ ## The map
154
78
 
155
- ```
156
- Take-off restricted by landowner rule.
157
- No permanent airspace restriction at this point.
158
- Landowner rule: Corfe Castle (National Trust (always open land)) - National Trust byelaws prohibit taking off or landing unmanned aircraft on Trust land without the Trust's permission.
159
- Nearest public right of way: 120 m away (footpath, Dorset).
160
- ...
161
- ```
162
-
163
- ### `find_parking`
164
-
165
- | Argument | Type | Notes |
166
- |---|---|---|
167
- | `place` / `lat`+`lon` | | |
168
- | `max_results` | 1–20 | Default 5 |
169
- | `search_radius_m` | 100–10000 | Default 2000 |
170
- | `include_private` | boolean | Also list private, customers-only and permit parking |
171
-
172
- Example prompt: *"Where can I park near Durdle Door?"*
173
-
174
- ### `check_weather`
175
-
176
- | Argument | Type | Notes |
177
- |---|---|---|
178
- | `place` / `lat`+`lon` | | |
179
- | `date` | ISO 8601 | A bare date reports daylight hours; a date-time starts there. Defaults to now |
180
- | `hours` | 1–24 | Hours to report from the start (default 6) |
181
-
182
- Example prompt: *"Is it flyable at Ilkley Moor on Saturday afternoon?"*
183
-
184
- Ratings are advisory: caution from 8 m/s, poor from 10.7 m/s sustained or 12 m/s gusts, any rain, visibility under 1.5 km, and a caution for freezing temperatures, low cloud or strong wind at 120 m.
185
-
186
- The report also carries the NOAA planetary K-index (geomagnetic activity), which affects GPS accuracy and compass behaviour: quiet, unsettled, active (Kp 4) or storm (Kp 5 and above). It does not change the weather rating; the briefing tool treats a storm as caution.
187
-
188
- ### `preflight_briefing`
189
-
190
- One report for a take-off point and time. `place` / `lat`+`lon`, `date` (default now), `hours` (window length, default 3), optional `drone` and `a2_certificate`, `frz_permission` when an aerodrome has already agreed the flight, `notam_radius_km` (default 10), `max_paths`. The status is deterministic: prohibited, prison or restricted airspace, an FRZ without permission, or a landowner ban is NO-GO; an FRZ with permission, a covering NOTAM, a danger area, poor weather, a geomagnetic storm, a physical ground hazard within 200 m, or any live source that could not be read is CAUTION; otherwise GO. Notes (marginal weather, nearby or unlocated NOTAMs, a school, hospital or park within 150 m, no right of way nearby, A3 separation) never change the status. The JSON carries each sub-result, the reasons and an `outages` list.
191
-
192
- ### `check_terrain`
193
-
194
- Ground elevation against the 120 m rule. Give `waypoints` (2 to 50) for a profile, or a point with `radius_m` (default 500) for the ground around it; `flight_height_m` (default 120) is the planned height above take-off and `step_m` the sample spacing. Reports the highest and lowest ground relative to the take-off point and warns when the ground rises within 30 m of the flight height (caution) or above it (poor), or falls far enough that the flight would be more than 120 m above the surface. Elevations come from Copernicus GLO-90 via Open-Meteo at about 90 m resolution, so cliffs and buildings are not resolved.
195
-
196
- ### `find_takeoff_spots`
197
-
198
- Candidate spots come from the nearest rights of way (the closest point of each plus samples every 250 m), public parking and open access land within `search_radius_m` (default 3 km), de-duplicated on a 50 m grid and capped at 60. Each is checked against the airspace and landowner layers: prohibited, restricted, prison and aerodrome zones and take-off bans exclude it; a covering NOTAM, a danger area, a landowner rule or a ground hazard within 200 m lower the score; being on a right of way, having parking within 300 m and open access land raise it; distance from the centre costs a little. Scores are integers with a reason per term, so the ordering is deterministic. `max_results` (default 3), optional `drone` and `a2_certificate` (a school, hospital, park or similar within 150 m costs a little, and a lot for A3 pilots). The JSON lists the spots and the excluded candidates grouped by reason. A right of way is a right to pass, not to stop and fly.
199
-
200
- ### `check_drone_rules`
201
-
202
- Which UK open category rules apply to a consumer drone. Give `model` (looked up in the curated catalogue in `src/services/drones/catalogue.ts`: DJI, Autel, Potensic, HoverAir and Parrot models with take-off weight, EU C-class and UK class marks) or `weight_g` with an optional `class_mark` (C0 to C4, UK0 to UK4, or none). Set `a2_certificate` if the pilot holds an A2 CofC and `date` to see the rules on a future date. The answer gives the subcategory, overflight and separation rules, registration (Flyer ID and Operator ID, 100 g threshold from 2026), Remote ID dates (UK1 to UK3 from 2026, camera aircraft of 100 g or more otherwise from 2028) and the transition under which EU C-class labels count as UK classes until the end of 2027. Rules and dates live in `src/services/drones/rules.ts` with the CAA pages they were taken from; the catalogue is community-maintained like the byelaw list, and an unconfirmed class mark is left null so the aircraft is treated as legacy.
79
+ <table align="center"><tr>
80
+ <td><img src="docs/images/phone-empty.webp" alt="The map on a phone before a location is chosen" width="230"></td>
81
+ <td><img src="docs/images/phone.webp" alt="Durdle Door on a phone with the location sheet collapsed" width="230"></td>
82
+ <td><img src="docs/images/phone-sheet.webp" alt="The location sheet expanded: counts, drone picker, conditions and the weather timeline" width="230"></td>
83
+ </tr></table>
203
84
 
204
- ### Maps
85
+ Search a place, tap the map or use your position; every zone, NOTAM, path, landowner rule, parking spot and ground hazard in view is drawn with its own icon, a long press lists what is under a point, the card shows the coming week's flyability hour by hour, and the overlays load as you pan. Claude renders the same map inline with an answer. The full guide, with the layers key and more screenshots, is in [docs/map.md](docs/map.md).
205
86
 
206
- The hosted server serves `GET /map?lat=&lon=[&radius=][&route=lon,lat;lon,lat]` as a standalone Leaflet map, `GET /api/view` as the JSON behind it, and `GET /api/wind?bbox=w,s,e,n&z=` for the wind field (one Open-Meteo request per view, snapped to a fixed lattice of at most 64 points and cached per point). It also publishes an MCP App resource (`ui://fpv-airspace/map`) attached to `check_location`, `check_takeoff_site`, `check_route` and `find_parking`, so hosts that support MCP Apps show the map inline with the answer; `preflight_briefing`, `check_terrain` and `find_takeoff_spots` carry it too, the last with its spots numbered on the map. Claude web, desktop and mobile render it; Claude Code shows the text only.
87
+ ## Documentation
207
88
 
208
- The layers button (bottom left) opens a panel with Map or Satellite base layers and a checkbox for every overlay, grouped into Airspace (each zone class and NOTAMs), On the ground (rights of way, landowner land, parking, route), Ground hazards (one row per kind with its icon and count, and All or None shortcuts) and Weather. Prohibited and restricted areas and aerodrome FRZs are always drawn and cannot be switched off. Satellite is Esri World Imagery with a place-name overlay; add `basemap=satellite` to the `/map` URL to open in that view. Weather has three toggles: Conditions now (a badge with the Open-Meteo flyability rating for the coming hour, including the wind at 120 m), Wind flow (animated streamlines over the visible map, as on a forecast chart, coloured by the advisory thresholds; a still frame when the browser prefers reduced motion) and Rain radar (the latest RainViewer frame, coarse at about 600 m per pixel on the free tier). `weather=0` on `/api/view` skips the forecast. Base layer, overlay and panel choices are remembered per browser. On a phone the search bar spans the top, the location card is a collapsible sheet at the bottom, the layers panel opens as a sheet from the same button, and pinch replaces the zoom buttons; a page opened without a location invites a search or offers to use the phone's location.
209
-
210
- The search box (top left) takes a place name, a postcode or `lat, lon` and calls `GET /api/geocode?q=`, which runs the same resolver as the tools: a confident match moves the map, an ambiguous name shows the candidates to pick from, and nothing is ever guessed. The page URL updates as you search, so it can be shared. Tapping or clicking anywhere on the map checks that point, and the locate button uses the browser's location.
211
-
212
- The location card has a Your drone picker fed by `GET /api/drones` (the catalogue with each model's rules summary and a drawn silhouette). Choosing a model makes it the location marker and the key swatch, and shows its subcategory and overflight rule in the card. Add `drone=<catalogue id>` to the `/map` URL to preselect one; `check_takeoff_site` called with a `drone` does this for the MCP App. The silhouettes are original drawings, one per family (palm, mini, air, mavic, fpv, phantom), because manufacturer photographs are copyrighted.
213
-
214
- Map tiles © OpenStreetMap contributors; imagery © Esri, Maxar, Earthstar Geographics, and the GIS User Community; rain radar © RainViewer; weather © Open-Meteo.com (CC BY 4.0).
215
-
216
- ### `geocode`
217
-
218
- | Argument | Type | Notes |
219
- |---|---|---|
220
- | `query` | string | |
221
- | `limit` | 1–10 | |
222
-
223
- ### `get_data_status`
224
-
225
- No arguments. Reports pack tag, AIRAC effective dates, per-source fetch dates and licences, NOTAM cache age, geocoding providers and every attribution string.
226
-
227
- ## Data sources and attribution
228
-
229
- | Layer | Source | Refresh | Licence |
230
- |---|---|---|---|
231
- | Flight restrictions | [NATS UK AIP ENR 5.1 UAS Flight Restrictions dataset](https://nats-uk.ead-it.com/cms-nats/opencms/en/uas-restriction-zones/) (AIXM 5.1, cross-checked against the NATS KML) | Every AIRAC cycle (28 days) | © NATS Limited; redistribution terms unconfirmed, see [licences/NATS.md](licences/NATS.md) |
232
- | NOTAMs | [NATS AIS contingency PIB](https://www.nats.aero/do-it-online/pre-flight-information-bulletins/) (all UK NOTAMs in force or within 7 days) | Hourly upstream, 30 min cache | Informational; obtain an official briefing |
233
- | Rights of way | Council open data aggregated by [rowmaps.com](https://www.rowmaps.com/) (143 authorities, England and Wales) | Weekly | Open Government Licence v3 per council, OS attribution, see [licences/rowmaps.md](licences/rowmaps.md) |
234
- | National Trust land | [National Trust Open Data](https://open-data-national-trust.hub.arcgis.com/) Always Open and Limited Access | When edited | OGL v3 / CC-BY |
235
- | Council byelaws | [data/byelaws/seed.yaml](data/byelaws/seed.yaml) in this repository | Manual | MIT; incomplete by nature |
236
- | Elevation | [Open-Meteo Elevation API](https://open-meteo.com/en/docs/elevation-api), Copernicus GLO-90 DEM | Live, cached 30 days per 100 m cell | CC BY 4.0 (Open-Meteo), Copernicus data licence |
237
- | Geomagnetic activity | [NOAA SWPC planetary K-index](https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json) | Live, cached 15 min | US Government, public domain |
238
- | Drone rules | [CAA class marks](https://www.caa.co.uk/drones/getting-started-with-drones-and-model-aircraft/class-marks/) and the [Drone Code](https://register-drones.caa.co.uk/drone-code), summarised in `src/services/drones/rules.ts`; drone catalogue curated from manufacturer specifications | With the code | Crown copyright, OGL v3; the CAA pages are authoritative |
239
- | Open access land | [Natural England CRoW Access Layer](https://naturalengland-defra.opendata.arcgis.com/datasets/Defra::crow-act-2000-access-layer/about), [NRW open country and common land](https://datamap.gov.wales/) | With the pack | OGL v3, see [licences/natural-england.md](licences/natural-england.md) and [licences/natural-resources-wales.md](licences/natural-resources-wales.md) |
240
- | SSSI and National Parks | Natural England and Natural Resources Wales designation boundaries | With the pack | OGL v3, advisory only |
241
- | Northern Ireland designations | [ASSI](https://www.opendatani.gov.uk/dataset/areas-of-special-scientific-interest), [AONB](https://www.opendatani.gov.uk/dataset/areas-of-outstanding-natural-beauty) and [National Nature Reserves](https://www.opendatani.gov.uk/dataset/national-nature-reserves) from the Northern Ireland Environment Agency on OpenDataNI | With the pack | OGL v3, advisory only, see [licences/northern-ireland.md](licences/northern-ireland.md) |
242
- | Forestry England land | [Forestry England Legal Boundary](https://data-forestry.opendata.arcgis.com/) | With the pack | OGL v3 with acknowledgement, see [licences/forestry-england.md](licences/forestry-england.md); byelaws need a permit for drones |
243
- | Northern Ireland rights of way | Asserted public rights of way published by councils, so far [Mid Ulster](https://www.opendatani.gov.uk/dataset/mid-ulster-council-public-rights-of-way); there is no definitive map | Weekly | OGL v3 per council, see [licences/northern-ireland.md](licences/northern-ireland.md) |
244
- | Scottish core paths | [Core Paths - Scotland, Improvement Service Spatial Hub](https://data.spatialhub.scot/dataset/core_paths-is) (needs a free account key, `SPATIALHUB_AUTHKEY`) | Weekly | OGL v3 per council, see [licences/improvement-service-core-paths.md](licences/improvement-service-core-paths.md) |
245
- | Local authorities | [ONS Local Authority Districts (May 2026) BSC](https://geoportal.statistics.gov.uk/) | With the pack | OGL v3, see [licences/ONS.md](licences/ONS.md) |
246
- | Ground hazards | OpenStreetMap via the Geofabrik Great Britain and Ireland extracts (the latter clipped to Northern Ireland): railways, motorways and trunk roads, bridges, power lines and minor lines, pylons, substations, generators, helipads, masts, military land, schools, nurseries, hospitals, fire and fuel stations, parks, cemeteries | Weekly | ODbL, see [licences/openstreetmap-ODbL.md](licences/openstreetmap-ODbL.md); advisory only |
247
- | Parking and laybys | OpenStreetMap via the [Geofabrik Great Britain extract](https://download.geofabrik.de/europe/great-britain.html) plus the Ireland extract clipped to Northern Ireland (`amenity=parking`, `highway=rest_area`) | Weekly | ODbL |
248
- | Country boundaries | ONS Countries (December 2024) BUC | Yearly | OGL v3 |
249
- | Geocoding | postcodes.io, OS Names API (optional), Nominatim | Live, cached 30 days | OGL v3; ODbL |
250
- | Weather | [Open-Meteo](https://open-meteo.com/) forecast API | Live, cached 15 min | CC BY 4.0 |
251
-
252
- Every response ends with an `Attribution:` line listing only the sources actually used, including the per-council attribution that the OGL requires for rights-of-way data.
253
-
254
- The permanent layers are assembled into a data pack by [`.github/workflows/build-pack.yml`](.github/workflows/build-pack.yml), published as a GitHub Release tagged `pack-<AIRAC date>-<run>`, and verified by sha256 on download. See [CLAUDE.md](CLAUDE.md) for the pipeline layout.
255
-
256
- ## Configuration
257
-
258
- | Variable | Default | Purpose |
259
- |---|---|---|
260
- | `OS_NAMES_API_KEY` | unset | Enables the OS Names geocoder |
261
- | `OS_NAMES_URL` | `https://api.os.uk/search/names/v1/find` | |
262
- | `OPEN_METEO_ELEVATION_URL` | `https://api.open-meteo.com/v1/elevation` | |
263
- | `ELEVATION_CACHE_TTL_SECONDS` | `2592000` | Terrain does not change |
264
- | `NOAA_KP_URL` | `https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json` | |
265
- | `SPACE_WEATHER_CACHE_TTL_SECONDS` | `900` | |
266
- | `OSM_PBF_URL`, `OSM_PBF_URL_IRELAND`, `NE_CROW_URL`, `NE_SSSI_URL`, `NE_NATIONAL_PARKS_URL`, `NRW_WFS_URL`, `FE_LEGAL_BOUNDARY_URL`, `ONS_LAD_URL`, `SPATIALHUB_WFS_URL` | (upstream defaults) | Build only: override a source endpoint when a publisher moves it; the `NRW_*_TYPENAME` and `SPATIALHUB_TYPENAME` variables do the same for WFS layer names |
267
- | `SPATIALHUB_AUTHKEY` | (unset) | Build only: Spatial Hub account key for Scottish core paths; without it the pack has none |
268
- | `NOMINATIM_URL` | `https://nominatim.openstreetmap.org/search` | Self-host to lift the 1 req/s limit |
269
- | `POSTCODES_IO_URL` | `https://api.postcodes.io` | |
270
- | `NOTAM_PIB_URL` | `https://pibs.nats.co.uk/operational/pibs/PIB.xml` | |
271
- | `NOTAM_CACHE_TTL_SECONDS` | `1800` | |
272
- | `OPEN_METEO_URL` | `https://api.open-meteo.com/v1/forecast` | |
273
- | `WEATHER_CACHE_TTL_SECONDS` | `900` | |
274
- | `GEOCODE_CACHE_TTL_SECONDS` | `2592000` | 30 days |
275
- | `HTTP_TIMEOUT_MS` | `8000` | Live calls |
276
- | `FPV_AIRSPACE_CACHE_DIR` | `~/.cache/fpv-airspace` | Pack and caches (`DRONE_AIRSPACE_CACHE_DIR` and an existing `~/.cache/uk-drone-airspace-mcp` are still honoured) |
277
- | `PACK_MANIFEST_URL` | latest GitHub release manifest | Point at a mirror or `file://` |
278
- | `PACK_PATH` | unset | Use a local pack and skip downloads |
279
- | `PACK_UPDATE_CHECK` | `true` | Background check for a newer pack |
280
- | `PACK_STALE_HOURS` | `24` | How often to check |
281
- | `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error`, `silent` |
282
- | `MCP_TRANSPORT` / `--transport` | `stdio` | `stdio` or `http` |
283
- | `PORT` / `--port` | `8080` | HTTP transport |
89
+ - [Tools](docs/tools.md): every tool, its arguments and example prompts.
90
+ - [The map](docs/map.md): search, layers, hazards, weather, the phone layout and URL parameters.
91
+ - [Data sources and attribution](docs/data-sources.md): where each layer comes from, how often it refreshes and under what licence.
92
+ - [Configuration](docs/configuration.md): environment variables for the server and the build.
93
+ - [Hosting](docs/hosting.md): Cloudflare Worker, streamable HTTP and Docker.
94
+ - [Development](docs/development.md): tests, packs and releases.
284
95
 
285
96
  ## Caveats
286
97
 
287
- - Rights-of-way data is an interpretation of each council's Definitive Map, not the Definitive Map itself, and covers England and Wales only. Scotland has no definitive map (access rights apply instead) and Northern Ireland has very few recorded rights of way; the tools say so.
288
- - The landowner rule layer holds National Trust land and a hand-curated list of council byelaws. Absence of a rule never means take-off is permitted.
98
+ - Rights-of-way data is an interpretation of each council's Definitive Map, not the Definitive Map itself, and covers England and Wales. Scotland has no definitive map (access rights apply instead, and core paths need a Spatial Hub key) and Northern Ireland has no definitive map either: only councils that publish their asserted paths appear, so absence there means unknown; the tools say so.
99
+ - The landowner rule layer holds National Trust and Forestry England land and a hand-curated list of council byelaws and policies. The National Trust's open data omits its Northern Ireland properties. Absence of a rule never means take-off is permitted.
100
+ - Ground hazards and parking come from OpenStreetMap and are as complete as the map is; they are advisory and never change a verdict.
289
101
  - Only zones reaching below 400 ft (120 m) count towards a verdict; higher zones are listed on request.
290
102
  - NOTAMs are read from the NATS contingency bulletin, which may lag the live system by up to an hour.
291
103
  - ICAO codes for aerodromes come from a curated table covering major and well-known UK aerodromes; lookups by name always work.
292
104
 
293
- ## Development
294
-
295
- ```bash
296
- npm install
297
- npm test # offline vitest suite
298
- npm run lint && npm run typecheck
299
- npm run build && npx @modelcontextprotocol/inspector node dist/index.js
300
- npm run pack:build -- --region south-west --tag pack-dev # build a small real pack (network)
301
- PACK_PATH=build/pack/pack-dev.sqlite node dist/index.js
302
- ```
303
-
304
- Releases: tag `vX.Y.Z` to publish to npm and attach the `.mcpb` bundle. The data pack has its own release cadence driven by `build-pack.yml`.
305
-
306
105
  ## Licence
307
106
 
308
107
  MIT. Data sources carry their own licences; see the [licences](licences/) folder.
package/dist/index.js CHANGED
@@ -62045,7 +62045,13 @@ var envSchema = external_exports.object({
62045
62045
  MCP_TRANSPORT: external_exports.enum(["stdio", "http"]).default("stdio"),
62046
62046
  PORT: intFromEnv(8080),
62047
62047
  GITHUB_TOKEN: external_exports.string().optional(),
62048
- PUBLIC_URL: external_exports.string().optional().transform((v2) => v2 && /^https?:\/\//.test(v2) ? v2.replace(/\/+$/, "") : void 0)
62048
+ PUBLIC_URL: external_exports.string().optional().transform((v2) => v2 && /^https?:\/\//.test(v2) ? v2.replace(/\/+$/, "") : void 0),
62049
+ SUPPORT_URL: external_exports.string().optional().transform((v2) => v2 && /^https:\/\//.test(v2) ? v2 : void 0),
62050
+ SUPPORT_MONTHLY_URL: external_exports.string().optional().transform((v2) => v2 && /^https:\/\//.test(v2) ? v2 : void 0),
62051
+ STRIPE_SECRET_KEY: external_exports.string().optional(),
62052
+ STRIPE_PUBLISHABLE_KEY: external_exports.string().optional(),
62053
+ STRIPE_PRICE_ONCE: external_exports.string().optional(),
62054
+ STRIPE_PRICE_MONTHLY: external_exports.string().optional()
62049
62055
  });
62050
62056
  function parseCliArgs(argv) {
62051
62057
  const { values } = parseArgs({
@@ -62111,7 +62117,10 @@ function loadConfig(env2 = process.env, cli = {}) {
62111
62117
  transport: cli.transport ?? e.MCP_TRANSPORT,
62112
62118
  port: cli.port ?? e.PORT,
62113
62119
  githubToken: e.GITHUB_TOKEN && e.GITHUB_TOKEN.trim() !== "" ? e.GITHUB_TOKEN : void 0,
62114
- publicUrl: e.PUBLIC_URL
62120
+ publicUrl: e.PUBLIC_URL,
62121
+ supportUrl: e.SUPPORT_URL,
62122
+ supportMonthlyUrl: e.SUPPORT_MONTHLY_URL,
62123
+ stripe: { secretKey: e.STRIPE_SECRET_KEY || void 0, publishableKey: e.STRIPE_PUBLISHABLE_KEY || void 0, priceOnce: e.STRIPE_PRICE_ONCE || void 0, priceMonthly: e.STRIPE_PRICE_MONTHLY || void 0 }
62115
62124
  });
62116
62125
  }
62117
62126
 
@@ -64799,12 +64808,14 @@ var QueryPackRepository = class {
64799
64808
  }
64800
64809
  return out;
64801
64810
  }
64802
- async hazardsInBbox(bbox, limit = 300, kinds) {
64811
+ async hazardsInBbox(bbox, limit = 300, kinds, nearestTo) {
64803
64812
  const kindFilter = kinds && kinds.length > 0 ? ` AND h.kind IN (${kinds.map(() => "?").join(",")})` : "";
64813
+ const order = nearestTo ? " ORDER BY (h.lon - ?) * (h.lon - ?) + (h.lat - ?) * (h.lat - ?) * 2.6" : "";
64814
+ const orderParams = nearestTo ? [nearestTo[0], nearestTo[0], nearestTo[1], nearestTo[1]] : [];
64804
64815
  const rows = await this.q.all(
64805
- `SELECT h.* FROM hazards_rtree r JOIN hazards h ON h.id = r.id
64806
- WHERE r.min_lon <= ? AND r.max_lon >= ? AND r.min_lat <= ? AND r.max_lat >= ?${kindFilter} LIMIT ?`,
64807
- [bbox[2], bbox[0], bbox[3], bbox[1], ...kinds && kinds.length > 0 ? kinds : [], limit]
64816
+ `SELECT h.* FROM hazards_rtree r CROSS JOIN hazards h ON h.id = r.id
64817
+ WHERE r.min_lon <= ? AND r.max_lon >= ? AND r.min_lat <= ? AND r.max_lat >= ?${kindFilter}${order} LIMIT ?`,
64818
+ [bbox[2], bbox[0], bbox[3], bbox[1], ...kinds && kinds.length > 0 ? kinds : [], ...orderParams, limit]
64808
64819
  );
64809
64820
  const out = [];
64810
64821
  for (const row of rows) {
@@ -75544,9 +75555,10 @@ var OTHER_HAZARD_KINDS = HAZARD_KINDS.filter((k) => !LINE_HAZARD_KINDS.includes(
75544
75555
  var LINE_HAZARD_LIMIT = 1500;
75545
75556
  var POINT_HAZARD_LIMIT = 600;
75546
75557
  async function viewHazards(pack, bbox, lon, lat) {
75547
- const lines = await pack.hazardsInBbox(bbox, LINE_HAZARD_LIMIT, LINE_HAZARD_KINDS);
75548
- const others = await pack.hazardsInBbox(bbox, 4e3, OTHER_HAZARD_KINDS);
75549
- const nearest = others.map((h) => ({ h, d: (h.lon - lon) ** 2 + ((h.lat - lat) * 1.6) ** 2 })).sort((a, b) => a.d - b.d).slice(0, POINT_HAZARD_LIMIT).map((x) => x.h);
75558
+ const [lines, nearest] = await Promise.all([
75559
+ pack.hazardsInBbox(bbox, LINE_HAZARD_LIMIT, LINE_HAZARD_KINDS),
75560
+ pack.hazardsInBbox(bbox, POINT_HAZARD_LIMIT, OTHER_HAZARD_KINDS, [lon, lat])
75561
+ ]);
75550
75562
  return [...lines, ...nearest];
75551
75563
  }
75552
75564
  function weatherTimeline(hourly, fromKey) {
@@ -77823,6 +77835,12 @@ var toolDefinitions = [
77823
77835
  }
77824
77836
  ];
77825
77837
 
77838
+ // src/map/icon.ts
77839
+ var ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">\n <defs>\n <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">\n <stop offset="0%" stop-color="#1e293b"/>\n <stop offset="100%" stop-color="#0f172a"/>\n </linearGradient>\n <linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="100%">\n <stop offset="0%" stop-color="#38bdf8"/>\n <stop offset="100%" stop-color="#3b82f6"/>\n </linearGradient>\n </defs>\n \n <!-- App Icon Background -->\n <rect width="512" height="512" rx="112" fill="url(#bg)"/>\n \n <!-- Airspace Radar Rings -->\n <circle cx="256" cy="256" r="160" fill="none" stroke="#334155" stroke-width="8" stroke-dasharray="24 12"/>\n <circle cx="256" cy="256" r="220" fill="none" stroke="#1e293b" stroke-width="12"/>\n \n <!-- Geometric Quadcopter -->\n <g stroke="url(#accent)" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" fill="none">\n <!-- Drone Arms -->\n <line x1="160" y1="160" x2="352" y2="352" />\n <line x1="352" y1="160" x2="160" y2="352" />\n \n <!-- Center Chassis -->\n <rect x="216" y="216" width="80" height="80" rx="20" fill="#0f172a" stroke="url(#accent)" stroke-width="16" />\n \n <!-- Propellers -->\n <circle cx="160" cy="160" r="44" stroke-width="16"/>\n <circle cx="352" cy="160" r="44" stroke-width="16"/>\n <circle cx="160" cy="352" r="44" stroke-width="16"/>\n <circle cx="352" cy="352" r="44" stroke-width="16"/>\n </g>\n \n <!-- Center Location/Signal Dot -->\n <circle cx="256" cy="256" r="14" fill="#38bdf8"/>\n</svg>\n';
77840
+ var ICON_PNG_180_BASE64 = "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";
77841
+ var ICON_PNG_180 = Uint8Array.from(atob(ICON_PNG_180_BASE64), (c) => c.charCodeAt(0));
77842
+ var WEB_MANIFEST = (base) => JSON.stringify({ name: "FPV Airspace", short_name: "FPV Airspace", start_url: `${base}/map`, display: "standalone", background_color: "#0f172a", theme_color: "#0f172a", icons: [{ src: `${base}/icon.svg`, sizes: "any", type: "image/svg+xml" }, { src: `${base}/icon-180.png`, sizes: "180x180", type: "image/png" }] });
77843
+
77826
77844
  // src/map/html.ts
77827
77845
  var MAP_CSP = {
77828
77846
  resourceDomains: [
@@ -77901,7 +77919,13 @@ var BASE_OVERLAYS = [
77901
77919
  ];
77902
77920
  var OVERLAYS = [...BASE_OVERLAYS, ...HAZARD_OVERLAYS];
77903
77921
  function mapHtml(opts) {
77922
+ const iconSvgBase64 = Buffer.from(ICON_SVG).toString("base64");
77923
+ const iconLinks = opts.apiBase ? `<link rel="apple-touch-icon" href="${opts.apiBase}/icon-180.png">
77924
+ <link rel="manifest" href="${opts.apiBase}/manifest.webmanifest">` : "";
77904
77925
  const apiBase = JSON.stringify(opts.apiBase ?? "");
77926
+ const supportUrl = JSON.stringify(opts.supportUrl && /^https:\/\//.test(opts.supportUrl) ? opts.supportUrl : "");
77927
+ const supportMonthlyUrl = JSON.stringify(opts.supportMonthlyUrl && /^https:\/\//.test(opts.supportMonthlyUrl) ? opts.supportMonthlyUrl : "");
77928
+ const stripePk = JSON.stringify(opts.mode === "page" && opts.stripePublishableKey && /^pk_(live|test)_/.test(opts.stripePublishableKey) ? opts.stripePublishableKey : "");
77905
77929
  const mode = JSON.stringify(opts.mode);
77906
77930
  const basemaps = JSON.stringify(BASEMAPS);
77907
77931
  const radar = JSON.stringify(RADAR);
@@ -77913,6 +77937,9 @@ function mapHtml(opts) {
77913
77937
  <meta charset="utf-8">
77914
77938
  <meta name="viewport" content="width=device-width, initial-scale=1">
77915
77939
  <title>FPV Airspace map</title>
77940
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,${iconSvgBase64}">
77941
+ ${iconLinks}
77942
+ <meta name="theme-color" content="#0f172a">
77916
77943
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
77917
77944
  <style>
77918
77945
  :root {
@@ -78021,10 +78048,31 @@ function mapHtml(opts) {
78021
78048
  #sources.open ul { display: block; }
78022
78049
  #sources li { padding: 5px 0; border-top: 1px solid var(--line); line-height: 1.35; }
78023
78050
  #sources li:first-child { border-top: 0; }
78051
+ #sources li.support a { color: var(--accent); font-weight: 600; text-decoration: none; }
78024
78052
  .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
78053
+ #donate { position: absolute; left: 64px; bottom: 10px; z-index: 1001; height: 44px; background: #e0245e; box-shadow: var(--shadow); }
78054
+ #donate[hidden] { display: none; }
78055
+ #donate button { all: unset; display: flex; align-items: center; gap: 5px; height: 44px; padding: 0 11px 0 9px; cursor: pointer; color: #fff; font-weight: 700; font-size: 13.5px; }
78056
+ #donate button svg { width: 17px; height: 17px; }
78057
+ #donate.open { background: #b71c46; }
78058
+ #donate .pop { display: none; position: absolute; left: 0; bottom: 54px; box-sizing: border-box; width: min(300px, calc(100vw - 20px)); padding: 12px 14px; background: var(--panel); color: var(--ink); border-radius: 12px; box-shadow: var(--shadow); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); font-size: 13px; }
78059
+ #donate.open .pop { display: block; }
78060
+ #donate .pop b { display: block; font-size: 14px; margin-bottom: 4px; }
78061
+ #donate .pop p { margin: 0 0 10px; color: var(--muted); line-height: 1.4; }
78062
+ #donate .row { display: flex; gap: 8px; }
78063
+ #donate .row a { flex: 1; text-align: center; padding: 8px 10px; border-radius: 8px; background: var(--accent); color: #fff; font-weight: 600; text-decoration: none; font-size: 13px; }
78064
+ #donate .row a.alt { background: rgba(128,140,152,.16); color: var(--ink); }
78065
+ #checkout-modal { position: absolute; inset: 0; z-index: 2000; background: rgba(0,0,0,.45); display: flex; align-items: center; justify-content: center; padding: 12px; }
78066
+ #checkout-modal[hidden] { display: none; }
78067
+ #checkout-modal .sheet { width: min(520px, 100%); max-height: 100%; display: flex; flex-direction: column; overflow: hidden; background: #fff; color: #16202a; }
78068
+ #checkout-modal .head { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; border-bottom: 1px solid rgba(16,24,32,.09); font-size: 15px; }
78069
+ #checkout-modal .head button { all: unset; cursor: pointer; font-size: 22px; line-height: 1; padding: 0 4px; color: #5f6b77; }
78070
+ #checkout { flex: 1; overflow: auto; min-height: 320px; }
78071
+ #checkout-modal .note { padding: 10px 14px; font-size: 13px; color: #5f6b77; }
78072
+ #checkout-modal .note:empty { display: none; }
78025
78073
 
78026
78074
  /* Layers panel */
78027
- .layers { display: none; position: absolute; left: 10px; bottom: 64px; z-index: 1000; width: 256px; max-height: calc(100vh - 90px); overflow: auto; font-size: 13px; }
78075
+ .layers { display: none; position: absolute; left: 10px; bottom: 64px; z-index: 1000; width: 256px; max-height: calc(100vh - 134px); overflow: auto; font-size: 13px; }
78028
78076
  body.layers-open .layers { display: block; }
78029
78077
  .layers h2 { padding: 9px 12px 9px 14px; font-size: 13.5px; display: flex; align-items: center; justify-content: space-between; cursor: pointer; user-select: none; }
78030
78078
  .layers h2 .chev { width: 14px; height: 14px; transition: transform .15s; color: var(--muted); }
@@ -78087,6 +78135,8 @@ function mapHtml(opts) {
78087
78135
  .wind-canvas { position: absolute; left: 0; top: 0; pointer-events: none; }
78088
78136
  .leaflet-zoom-anim .wind-canvas { visibility: hidden; }
78089
78137
  .leaflet-popup-content hr.sep { border: 0; border-top: 1px solid var(--line); margin: 7px 0; }
78138
+ .leaflet-popup-content a.dir { display: inline-flex; align-items: center; gap: 5px; margin-top: 7px; padding: 5px 9px; border-radius: 7px; background: var(--accent); color: #fff; font-weight: 600; font-size: 12px; text-decoration: none; }
78139
+ .leaflet-popup-content a.dir svg { width: 13px; height: 13px; }
78090
78140
  #bottom { display: contents; }
78091
78141
  #layers-fab { position: absolute; left: 10px; bottom: 10px; z-index: 1001; display: flex; align-items: center; justify-content: center; width: 44px; height: 44px; border: 0; padding: 0; color: var(--ink); cursor: pointer; }
78092
78142
  #layers-fab svg { width: 24px; height: 24px; }
@@ -78101,10 +78151,14 @@ function mapHtml(opts) {
78101
78151
  #search input { font-size: 16px; }
78102
78152
  #bottom { display: flex; flex-direction: column; gap: 8px; position: absolute; left: 10px; right: 10px; bottom: 10px; z-index: 1000; max-height: calc(100vh - 80px); }
78103
78153
  #bottom .fabrow { display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; }
78104
- #sources { position: relative; right: auto; bottom: auto; flex: none; }
78154
+ #sources { position: relative; right: auto; bottom: auto; flex: none; margin-left: auto; }
78155
+ #donate { position: relative; left: auto; bottom: auto; flex: none; order: -1; margin-left: 8px; }
78156
+ #donate .pop { left: 0; }
78105
78157
  #sources ul { right: 0; left: auto; width: calc(100vw - 20px); }
78106
- #layers-fab { position: static; flex: none; order: -1; }
78107
- #info, #info.closed { position: static; width: auto; max-width: none; overflow: auto; min-height: 0; }
78158
+ #layers-fab { position: static; flex: none; order: -2; }
78159
+ #info, #info.closed { position: static; width: auto; max-width: 100%; min-width: 0; overflow: auto; min-height: 0; }
78160
+ .drone-row select { min-width: 0; max-width: 100%; }
78161
+ .wx-days { min-width: 0; }
78108
78162
  #info.closed .place { padding-bottom: 10px; }
78109
78163
  .drone-summary { font-size: 12.5px; }
78110
78164
  .layers { position: static; width: auto; max-height: none; min-height: 0; }
@@ -78123,12 +78177,27 @@ function mapHtml(opts) {
78123
78177
  </div>
78124
78178
  <div class="results" id="search-results"></div>
78125
78179
  </form>
78180
+ <div id="checkout-modal" hidden>
78181
+ <div class="sheet card">
78182
+ <div class="head"><b id="checkout-title">Support FPV Airspace</b><button type="button" id="checkout-close" aria-label="Close">\xD7</button></div>
78183
+ <div id="checkout"></div>
78184
+ <div id="checkout-note" class="note"></div>
78185
+ </div>
78186
+ </div>
78126
78187
  <div id="bottom">
78127
78188
  <div class="fabrow">
78128
78189
  <div id="sources" class="card">
78129
78190
  <button type="button" id="sources-btn" aria-label="Map credits and data sources"><svg viewBox="0 0 20 20"><circle cx="10" cy="10" r="8" fill="none" stroke="currentColor" stroke-width="1.7"/><circle cx="10" cy="6.4" r="1.1" fill="currentColor"/><path d="M8.6 9h1.9v5.2M8.6 14.2h3" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg><span id="sources-label" class="sr-only">Sources</span></button>
78130
78191
  <ul id="sources-list"></ul>
78131
78192
  </div>
78193
+ <div id="donate" class="card" hidden>
78194
+ <button type="button" id="donate-btn" title="Support this project"><svg viewBox="0 0 20 20"><path d="M10 17s-6.5-4.1-6.5-8.6A3.6 3.6 0 0 1 10 6.4a3.6 3.6 0 0 1 6.5 2c0 4.5-6.5 8.6-6.5 8.6z" fill="currentColor"/></svg><span>Donate</span></button>
78195
+ <div class="pop">
78196
+ <b>Support FPV Airspace</b>
78197
+ <p>Free to use and free of adverts. Donations cover the hosting and the weather data. Nothing is unlocked.</p>
78198
+ <div class="row" id="donate-links"></div>
78199
+ </div>
78200
+ </div>
78132
78201
  <button type="button" id="layers-fab" class="card" aria-label="Layers"><svg viewBox="0 0 20 20"><path d="M10 3l7 4-7 4-7-4z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M3 11l7 4 7-4M3 14.5l7 4 7-4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" opacity=".65"/></svg></button>
78133
78202
  </div>
78134
78203
  <div id="info" class="card">
@@ -78148,6 +78217,9 @@ function mapHtml(opts) {
78148
78217
  <script>
78149
78218
  (function () {
78150
78219
  var API_BASE = ${apiBase};
78220
+ var SUPPORT_URL = ${supportUrl};
78221
+ var SUPPORT_MONTHLY_URL = ${supportMonthlyUrl};
78222
+ var STRIPE_PK = ${stripePk};
78151
78223
  var MODE = ${mode};
78152
78224
  var BASEMAPS = ${basemaps};
78153
78225
  var RADAR = ${radar};
@@ -78157,7 +78229,61 @@ function mapHtml(opts) {
78157
78229
  var sourcesEl = document.getElementById('sources'), sourcesLabel = document.getElementById('sources-label'), sourcesList = document.getElementById('sources-list');
78158
78230
  var loaded = false, dataSources = [];
78159
78231
  function setStatus(text) { placeEl.textContent = text; }
78160
- document.getElementById('sources-btn').onclick = function () { var open = !sourcesEl.classList.contains('open'); sourcesEl.classList.toggle('open', open); if (open) setLayersSheet(false); };
78232
+ var donateEl = document.getElementById('donate');
78233
+ if (SUPPORT_URL || SUPPORT_MONTHLY_URL) {
78234
+ donateEl.hidden = false;
78235
+ document.getElementById('donate-links').innerHTML =
78236
+ (SUPPORT_URL ? '<a href="' + esc(SUPPORT_URL) + '" data-kind="once" target="_blank" rel="noopener">One-off</a>' : '') +
78237
+ (SUPPORT_MONTHLY_URL ? '<a class="alt" href="' + esc(SUPPORT_MONTHLY_URL) + '" data-kind="monthly" target="_blank" rel="noopener">Monthly</a>' : '');
78238
+ // Embedded checkout keeps the donor on the map; the links stay as the fallback when Stripe.js or the session fails.
78239
+ var modal = document.getElementById('checkout-modal'), checkoutEl = document.getElementById('checkout'), checkoutNote = document.getElementById('checkout-note');
78240
+ var embedded = null, stripeJs = null;
78241
+ function loadStripe() {
78242
+ if (stripeJs) return stripeJs;
78243
+ stripeJs = new Promise(function (resolve, reject) {
78244
+ if (window.Stripe) return resolve(window.Stripe);
78245
+ var sc = document.createElement('script'); sc.src = 'https://js.stripe.com/v3/'; sc.async = true;
78246
+ sc.onload = function () { resolve(window.Stripe); }; sc.onerror = function () { reject(new Error('Stripe.js failed to load')); };
78247
+ document.head.appendChild(sc);
78248
+ });
78249
+ return stripeJs;
78250
+ }
78251
+ function closeCheckout() { modal.hidden = true; if (embedded) { try { embedded.destroy(); } catch (err) { /* already gone */ } embedded = null; } checkoutEl.innerHTML = ''; checkoutNote.textContent = ''; }
78252
+ document.getElementById('checkout-close').onclick = closeCheckout;
78253
+ var openedAt = 0;
78254
+ // Ignore a backdrop click in the first moments after opening: a tap on the link can be followed by a synthetic click at the same spot.
78255
+ modal.addEventListener('click', function (e) { if (e.target === modal && Date.now() - openedAt > 600) closeCheckout(); });
78256
+ function openCheckout(kind) {
78257
+ donateEl.classList.remove('open');
78258
+ modal.hidden = false; openedAt = Date.now();
78259
+ document.getElementById('checkout-title').textContent = kind === 'monthly' ? 'Monthly donation' : 'One-off donation';
78260
+ checkoutNote.textContent = 'Loading secure checkout\u2026';
78261
+ Promise.all([loadStripe(), fetch(API_BASE + '/api/donate', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ kind: kind }) }).then(function (r) { return r.json().then(function (j) { if (!r.ok) throw new Error(j.error || 'HTTP ' + r.status); return j; }); })])
78262
+ .then(function (res) {
78263
+ var Stripe = res[0], session = res[1];
78264
+ checkoutNote.textContent = '';
78265
+ return Stripe(STRIPE_PK).initEmbeddedCheckout({ clientSecret: session.clientSecret, onComplete: function () { checkoutEl.innerHTML = ''; checkoutNote.textContent = 'Thank you. Your support keeps the map free for everyone.'; } })
78266
+ .then(function (c) { embedded = c; c.mount('#checkout'); });
78267
+ })
78268
+ .catch(function (err) {
78269
+ // Fall back to the hosted page so the donor is never stuck.
78270
+ var link = kind === 'monthly' ? SUPPORT_MONTHLY_URL : SUPPORT_URL;
78271
+ checkoutNote.textContent = 'Checkout could not be embedded here (' + err.message + '). Opening it in a new tab.';
78272
+ if (link) { if (openExternal) openExternal(link); else window.open(link, '_blank', 'noopener'); }
78273
+ });
78274
+ }
78275
+ if (STRIPE_PK && API_BASE) {
78276
+ document.getElementById('donate-links').addEventListener('click', function (e) {
78277
+ var a = e.target.closest('a[data-kind]');
78278
+ if (!a) return;
78279
+ e.preventDefault();
78280
+ openCheckout(a.dataset.kind);
78281
+ });
78282
+ }
78283
+ document.getElementById('donate-btn').onclick = function () { var open = !donateEl.classList.contains('open'); donateEl.classList.toggle('open', open); if (open) { sourcesEl.classList.remove('open'); setLayersSheet(false); } };
78284
+ document.addEventListener('click', function (e) { if (!donateEl.contains(e.target)) donateEl.classList.remove('open'); });
78285
+ }
78286
+ document.getElementById('sources-btn').onclick = function () { var open = !sourcesEl.classList.contains('open'); sourcesEl.classList.toggle('open', open); if (open) { setLayersSheet(false); donateEl.classList.remove('open'); } };
78161
78287
  var infoEl = document.getElementById('info'), infoPill = document.getElementById('info-pill');
78162
78288
  var SMALL = window.innerWidth <= 720;
78163
78289
  if (!recall('infoOpen', !SMALL)) infoEl.classList.add('closed');
@@ -78189,7 +78315,11 @@ function mapHtml(opts) {
78189
78315
  var items = [sat ? BASEMAPS.satellite.attribution : 'Map tiles ' + BASEMAPS.map.attribution].concat(dataSources);
78190
78316
  if (map.hasLayer(groups.radar)) items.push(RADAR.attribution);
78191
78317
  items.push('Built with Leaflet');
78192
- sourcesList.innerHTML = items.map(function (s) { return '<li>' + esc(s) + '</li>'; }).join('');
78318
+ sourcesList.innerHTML = items.map(function (s) { return '<li>' + esc(s) + '</li>'; }).join('') +
78319
+ (SUPPORT_URL || SUPPORT_MONTHLY_URL ? '<li class="support">Free to use, funded by donations. ' +
78320
+ (SUPPORT_URL ? '<a href="' + esc(SUPPORT_URL) + '" target="_blank" rel="noopener">One-off donation</a>' : '') +
78321
+ (SUPPORT_URL && SUPPORT_MONTHLY_URL ? ' \xB7 ' : '') +
78322
+ (SUPPORT_MONTHLY_URL ? '<a href="' + esc(SUPPORT_MONTHLY_URL) + '" target="_blank" rel="noopener">Monthly</a>' : '') + '</li>' : '');
78193
78323
  }
78194
78324
 
78195
78325
  // One layer group per overlay so each can be switched off; hidden ones are remembered.
@@ -78349,7 +78479,7 @@ function mapHtml(opts) {
78349
78479
  }
78350
78480
  function setLayersSheet(open) {
78351
78481
  document.body.classList.toggle('layers-open', open);
78352
- if (open) sourcesEl.classList.remove('open');
78482
+ if (open) { sourcesEl.classList.remove('open'); donateEl.classList.remove('open'); }
78353
78483
  if (open && SMALL) infoEl.classList.add('closed');
78354
78484
  if (!SMALL) remember('layersOpen', open);
78355
78485
  }
@@ -78662,6 +78792,27 @@ function mapHtml(opts) {
78662
78792
  } else {
78663
78793
  document.getElementById('drone-row').style.display = 'none';
78664
78794
  }
78795
+ // Directions in the device's own maps app: Apple Maps on iOS, the maps chooser on Android, Google Maps in a tab elsewhere.
78796
+ var UA = navigator.userAgent || '';
78797
+ var IS_IOS = /iPhone|iPad|iPod/.test(UA) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
78798
+ var IS_ANDROID = /Android/.test(UA);
78799
+ function directionsHref(lat, lon, label) {
78800
+ var ll = lat.toFixed(5) + ',' + lon.toFixed(5);
78801
+ if (IS_IOS) return 'maps://?daddr=' + ll + '&dirflg=d';
78802
+ if (IS_ANDROID) return 'geo:' + ll + '?q=' + ll + '(' + encodeURIComponent(label || 'Destination') + ')';
78803
+ return 'https://www.google.com/maps/dir/?api=1&destination=' + ll;
78804
+ }
78805
+ var openExternal = null; // set in MCP App mode: asks the host to open a link outside the sandbox
78806
+ document.addEventListener('click', function (e) {
78807
+ var a = e.target && e.target.closest ? e.target.closest('a.dir') : null;
78808
+ if (!a) return;
78809
+ if (openExternal) { e.preventDefault(); openExternal(a.href); }
78810
+ else if (!/^https?:/.test(a.href)) { e.preventDefault(); window.location.href = a.href; } // geo: and maps: schemes hand off to the app
78811
+ });
78812
+ function directionsLink(lat, lon, label) {
78813
+ return '<a class="dir" href="' + directionsHref(lat, lon, label) + '" target="_blank" rel="noopener">' +
78814
+ '<svg viewBox="0 0 16 16"><path d="M8 1.5l6.5 6.5L8 14.5 1.5 8z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M6 9.5V7h3.5M8 5.5L9.5 7 8 8.5" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>Directions</a>';
78815
+ }
78665
78816
  // Parking: a UK-style sign, blue square with a white P.
78666
78817
  var parkingIcon = L.divIcon({ className: 'p-sign', iconSize: [24, 24], iconAnchor: [12, 12], popupAnchor: [0, -12], html:
78667
78818
  '<svg viewBox="0 0 24 24"><rect x="1" y="1" width="22" height="22" rx="4" fill="' + COLOUR.parking + '" stroke="#fff" stroke-width="1.5"/>' +
@@ -78695,14 +78846,10 @@ function mapHtml(opts) {
78695
78846
  L.geoJSON(f, { style: { color: COLOUR[key], weight: 1, fillOpacity: key === 'land' ? 0.18 : 0.1 } }).addTo(groups[key]);
78696
78847
  hitTargets.push({ key: key, geometry: f.geometry, html: '<b>' + esc(f.properties.name) + '</b><br>' + esc(f.properties.owner) + (f.properties.takeoffBanned ? '<br>Take-off not permitted' : key === 'access' ? '<br>Open access land: not a take-off permission' : key === 'designation' ? '<br>Advisory designation' : '') });
78697
78848
  });
78698
- // Over a wide view the dense point kinds would bury the map; they are counted in the key but drawn only closer in.
78699
- var wide = map.distance([b[1], b[0]], [b[1], b[2]]) > 6000;
78700
- var DENSE = { substation: 1, pylon: 1, minor_power_line: 1, school: 1, kindergarten: 1, cemetery: 1, park: 1, fuel_station: 1, fire_station: 1, bridge: 1, tower: 1, power_generator: 1 };
78701
78849
  (view.hazards || []).forEach(function (f) {
78702
78850
  var kind = f.properties.kind, key = 'hz_' + kind, colour = COLOUR[key] || COLOUR.hz_school;
78703
78851
  if (!groups[key]) return; // a kind this page does not know yet
78704
78852
  counts[key]++;
78705
- if (wide && DENSE[kind]) return;
78706
78853
  var label = '<b>' + esc(HAZARD_LABEL[kind] || kind) + '</b>' + (f.properties.name ? '<br>' + esc(f.properties.name) : '') + (f.properties.operator ? '<br>' + esc(f.properties.operator) : '');
78707
78854
  if (f.geometry.type === 'Point') L.marker([f.geometry.coordinates[1], f.geometry.coordinates[0]], { icon: hazardIcon(kind) }).bindPopup(label).addTo(groups[key]);
78708
78855
  else if (f.geometry.type === 'LineString' || f.geometry.type === 'MultiLineString') {
@@ -78730,7 +78877,7 @@ function mapHtml(opts) {
78730
78877
  hitTargets.push({ key: 'prow', geometry: f.geometry, html: '<b>Public ' + esc(f.properties.pathType.replace('_', ' ')) + (f.properties.routeNo ? ' ' + esc(f.properties.routeNo) : '') + '</b><br>' + esc(f.properties.authority) + '<br>' + f.properties.distanceM + ' m from the point' });
78731
78878
  });
78732
78879
  view.parking.forEach(function (p) {
78733
- L.marker([p.lat, p.lon], { icon: parkingIcon, keyboard: false }).bindPopup('<b>' + esc(p.name || (p.kind === 'layby' ? 'Layby' : 'Car park')) + '</b><br>' + p.distanceM + ' m away' + (p.fee === 'yes' ? '<br>Pay to park' : p.fee === 'no' ? '<br>Free' : '')).addTo(groups.parking);
78880
+ L.marker([p.lat, p.lon], { icon: parkingIcon, keyboard: false }).bindPopup('<b>' + esc(p.name || (p.kind === 'layby' ? 'Layby' : 'Car park')) + '</b><br>' + p.distanceM + ' m away' + (p.fee === 'yes' ? '<br>Pay to park' : p.fee === 'no' ? '<br>Free' : '') + directionsLink(p.lat, p.lon, p.name || 'Parking')).addTo(groups.parking);
78734
78881
  });
78735
78882
  if (view.route && view.route.length > 1) {
78736
78883
  L.polyline(view.route.map(function (p) { return [p[1], p[0]]; }), { color: COLOUR.route, weight: 3, dashArray: '8 6' }).addTo(groups.route);
@@ -78738,9 +78885,9 @@ function mapHtml(opts) {
78738
78885
  if (opts.silent && currentCentre) {
78739
78886
  // An area load: the chosen point, its route and the spots stay where they were.
78740
78887
  if (currentRoute) L.polyline(currentRoute.map(function (p) { return [p[1], p[0]]; }), { color: COLOUR.route, weight: 3, dashArray: '8 6' }).addTo(groups.route);
78741
- centreMarker = L.marker(currentCentre, { icon: currentPinIcon() }).bindPopup(esc(currentName || 'Your location')).addTo(groups.route);
78888
+ centreMarker = L.marker(currentCentre, { icon: currentPinIcon() }).bindPopup(esc(currentName || 'Your location') + directionsLink(currentCentre[0], currentCentre[1], currentName || 'Your location')).addTo(groups.route);
78742
78889
  } else {
78743
- centreMarker = L.marker([view.centre.lat, view.centre.lon], { icon: currentPinIcon() }).bindPopup(esc(view.centre.name || 'Your location')).addTo(groups.route);
78890
+ centreMarker = L.marker([view.centre.lat, view.centre.lon], { icon: currentPinIcon() }).bindPopup(esc(view.centre.name || 'Your location') + directionsLink(view.centre.lat, view.centre.lon, view.centre.name || 'Your location')).addTo(groups.route);
78744
78891
  currentWeather = view.weather || null;
78745
78892
  selectedHour = null;
78746
78893
  currentCentre = [view.centre.lat, view.centre.lon];
@@ -78748,7 +78895,7 @@ function mapHtml(opts) {
78748
78895
  currentRoute = view.route && view.route.length > 1 ? view.route : null;
78749
78896
  setStatus(view.centre.name || (view.route ? 'Your route' : 'Your location'));
78750
78897
  }
78751
- pendingSpots.forEach(function (s) { L.marker([s.lat, s.lon], { icon: spotIcon(s.rank) }).bindPopup('<b>Spot ' + esc(s.rank) + '</b><br>' + esc(s.label)).addTo(groups.spots); });
78898
+ pendingSpots.forEach(function (s) { L.marker([s.lat, s.lon], { icon: spotIcon(s.rank) }).bindPopup('<b>Spot ' + esc(s.rank) + '</b><br>' + esc(s.label) + directionsLink(s.lat, s.lon, 'Take-off spot ' + s.rank)).addTo(groups.spots); });
78752
78899
  counts.spots = pendingSpots.length;
78753
78900
  loaded = true;
78754
78901
  placeLineIcons();
@@ -78756,8 +78903,8 @@ function mapHtml(opts) {
78756
78903
  setCounts(counts);
78757
78904
  var relevant = view.zones.filter(function (f) { return f.properties.relevant; }).length;
78758
78905
  chipsEl.innerHTML = [
78759
- [relevant, 'zone', 'zones', 'below 400 ft'], [view.notams.length, 'NOTAM', 'NOTAMs'], [view.rightsOfWay.length, 'path', 'paths'], [view.parking.length, 'parking spot', 'parking spots'], [counts.land, 'landowner rule', 'landowner rules'], [counts.access, 'access area', 'access areas'], [OVERLAYS.reduce(function (n, o) { return n + (o.section === 'hazards' ? counts[o.key] : 0); }, 0), 'hazard', 'hazards']
78760
- ].map(function (c) { return '<span class="chip' + (c[0] ? '' : ' zero') + '"><b>' + c[0] + '</b> ' + (c[0] === 1 ? c[1] : c[2]) + (c[3] ? ' ' + c[3] : '') + '</span>'; }).join('') + (wide ? '<span class="chip zero">zoom in for every hazard icon</span>' : '');
78906
+ [relevant, 'zone', 'zones', 'below 400 ft'], [view.notams.length, 'NOTAM', 'NOTAMs'], [view.rightsOfWay.length, 'path', 'paths'], [view.parking.length, 'parking spot', 'parking spots'], [counts.land, 'landowner rule', 'landowner rules'], [counts.access, 'access area', 'access areas'], [counts.designation, 'designation', 'designations'], [OVERLAYS.reduce(function (n, o) { return n + (o.section === 'hazards' ? counts[o.key] : 0); }, 0), 'hazard', 'hazards']
78907
+ ].map(function (c) { return '<span class="chip' + (c[0] ? '' : ' zero') + '"><b>' + c[0] + '</b> ' + (c[0] === 1 ? c[1] : c[2]) + (c[3] ? ' ' + c[3] : '') + '</span>'; }).join('');
78761
78908
  dataSources = view.attribution || [];
78762
78909
  updateSources();
78763
78910
  showWeather();
@@ -78905,14 +79052,17 @@ function mapHtml(opts) {
78905
79052
  if (req.waypoints && req.waypoints.length > 1) q.set('waypoints', req.waypoints.map(function (w) { return Array.isArray(w) ? w[0] + ',' + w[1] : w; }).join(';'));
78906
79053
  if (!req.silent) setStatus('Loading\u2026');
78907
79054
  var url = API_BASE + '/api/view?' + q.toString();
78908
- var seq = ++loadSeq;
79055
+ // Tap and search loads supersede each other; quiet area loads have their own sequence and never cancel them.
79056
+ var seq = req.silent ? ++areaSeq : ++loadSeq;
79057
+ if (!req.silent) mainLoading = true;
79058
+ var current = function () { return req.silent ? seq === areaSeq : seq === loadSeq; };
78909
79059
  var attempt = function (n) {
78910
79060
  return fetch(url).then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
78911
79061
  .catch(function (e) { if (n > 0 && !/^HTTP/.test(e.message)) return new Promise(function (res) { setTimeout(res, 800); }).then(function () { return attempt(n - 1); }); throw e; });
78912
79062
  };
78913
- attempt(1).then(function (view) { if (seq === loadSeq) render(view, { silent: !!req.silent }); if (req.silent) areaLoadDone(); }).catch(function (e) {
78914
- if (req.silent) areaLoadDone();
78915
- if (seq !== loadSeq) return;
79063
+ attempt(1).then(function (view) { if (current()) render(view, { silent: !!req.silent }); if (req.silent) areaLoadDone(); else mainLoadDone(); }).catch(function (e) {
79064
+ if (req.silent) areaLoadDone(); else mainLoadDone();
79065
+ if (!current()) return;
78916
79066
  if (req.silent) return; // a quiet area load that failed just leaves the previous overlays
78917
79067
  setStatus('Could not load map data (' + e.message + ')');
78918
79068
  infoEl.classList.remove('closed');
@@ -78925,7 +79075,7 @@ function mapHtml(opts) {
78925
79075
 
78926
79076
  // Pan or zoom away from the loaded area and the overlays for the new view load on their own,
78927
79077
  // quietly: the chosen point, its weather and the framing stay put. Zoomed out beyond 10 km it waits.
78928
- var loadSeq = 0, areaLoading = false, areaDirty = false, areaTimer = null;
79078
+ var loadSeq = 0, areaSeq = 0, mainLoading = false, areaLoading = false, areaDirty = false, areaTimer = null;
78929
79079
  function setAreaHint(text) {
78930
79080
  var el = document.getElementById('area-hint');
78931
79081
  if (!text) { if (el) el.remove(); return; }
@@ -78934,7 +79084,7 @@ function mapHtml(opts) {
78934
79084
  }
78935
79085
  function loadVisibleArea() {
78936
79086
  if (!loaded || !API_BASE || !currentCentre) return;
78937
- if (areaLoading) { areaDirty = true; return; } // a load is in flight: run again when it lands
79087
+ if (areaLoading || mainLoading) { areaDirty = true; return; } // a load is in flight: run again when it lands
78938
79088
  var vb = map.getBounds();
78939
79089
  if (loadedBounds && loadedBounds.contains(vb)) return;
78940
79090
  var c = map.getCenter();
@@ -78945,6 +79095,10 @@ function mapHtml(opts) {
78945
79095
  areaLoading = true;
78946
79096
  load({ lat: Math.round(c.lat * 1e5) / 1e5, lon: Math.round(c.lng * 1e5) / 1e5, radiusM: Math.max(300, radius), silent: true });
78947
79097
  }
79098
+ function mainLoadDone() {
79099
+ mainLoading = false;
79100
+ if (areaDirty) { areaDirty = false; setTimeout(loadVisibleArea, 50); }
79101
+ }
78948
79102
  function areaLoadDone() {
78949
79103
  areaLoading = false;
78950
79104
  setAreaHint('');
@@ -78958,6 +79112,10 @@ function mapHtml(opts) {
78958
79112
  var route = (qs.get('route') || '').split(';').filter(Boolean).map(function (s) { return s.split(',').map(parseFloat); });
78959
79113
  var wps = (qs.get('waypoints') || '').split(';').filter(Boolean);
78960
79114
  if (qs.get('drone')) selectDrone(qs.get('drone'), true);
79115
+ // ui=layers opens the layers panel, ui=info the location card: handy for screenshots and shared links.
79116
+ var ui = (qs.get('ui') || '').split(',');
79117
+ if (ui.indexOf('layers') >= 0) setLayersSheet(true);
79118
+ if (ui.indexOf('info') >= 0) infoEl.classList.remove('closed');
78961
79119
  var spots = (qs.get('spots') || '').split(';').filter(Boolean).map(function (s, i) { var p = s.split(',').map(parseFloat); return { lat: p[0], lon: p[1], rank: i + 1, label: 'Spot ' + (i + 1) }; });
78962
79120
  if (isFinite(lat) && isFinite(lon)) load({ lat: lat, lon: lon, name: qs.get('name') || undefined, radiusM: parseFloat(qs.get('radius')) || undefined, route: route.length > 1 ? route : undefined, spots: spots });
78963
79121
  else if (qs.get('place')) load({ place: qs.get('place'), radiusM: parseFloat(qs.get('radius')) || undefined });
@@ -78967,6 +79125,10 @@ function mapHtml(opts) {
78967
79125
  // MCP App: JSON-RPC over postMessage with the host (ui/initialize, then ui/notifications/tool-result).
78968
79126
  var nextId = 1;
78969
79127
  function send(msg) { window.parent.postMessage(msg, '*'); }
79128
+ openExternal = function (url) {
79129
+ send({ jsonrpc: '2.0', id: nextId++, method: 'ui/open-link', params: { url: url } });
79130
+ try { window.open(url, '_blank', 'noopener'); } catch (err) { /* the host request above is the real path */ }
79131
+ };
78970
79132
  window.addEventListener('message', function (ev) {
78971
79133
  var m = ev.data;
78972
79134
  if (!m || m.jsonrpc !== '2.0') return;
@@ -86115,6 +86277,15 @@ var StreamableHttpTransport = class {
86115
86277
  });
86116
86278
  if (this.extra.mapHtml) {
86117
86279
  const html = this.extra.mapHtml;
86280
+ app.get("/icon.svg", (_req, res) => {
86281
+ res.type("image/svg+xml").set("cache-control", "public, max-age=604800").send(ICON_SVG);
86282
+ });
86283
+ app.get("/icon-180.png", (_req, res) => {
86284
+ res.type("image/png").set("cache-control", "public, max-age=604800").send(Buffer.from(ICON_PNG_180));
86285
+ });
86286
+ app.get("/manifest.webmanifest", (req, res) => {
86287
+ res.type("application/manifest+json").send(WEB_MANIFEST(`${req.protocol}://${req.get("host")}`));
86288
+ });
86118
86289
  app.get("/map", (req, res) => {
86119
86290
  res.type("html").send(html(`${req.protocol}://${req.get("host")}`));
86120
86291
  });
@@ -86159,6 +86330,16 @@ var StreamableHttpTransport = class {
86159
86330
  }
86160
86331
  });
86161
86332
  }
86333
+ if (this.extra.donate) {
86334
+ const donate = this.extra.donate;
86335
+ app.post("/api/donate", import_express.default.json(), async (req, res) => {
86336
+ res.set("access-control-allow-origin", "*");
86337
+ const body = req.body ?? {};
86338
+ const r = await donate(body.kind === "monthly" ? "monthly" : "once", typeof body.quantity === "number" ? body.quantity : void 0);
86339
+ if ("error" in r) res.status(r.status).json({ error: r.error });
86340
+ else res.set("cache-control", "no-store").json(r);
86341
+ });
86342
+ }
86162
86343
  if (this.extra.droneIndex) {
86163
86344
  const drones = this.extra.droneIndex;
86164
86345
  app.get("/api/drones", (_req, res) => {
@@ -86331,6 +86512,45 @@ var StdioTransport = class {
86331
86512
  }
86332
86513
  };
86333
86514
 
86515
+ // src/map/donate.ts
86516
+ function donationEnabled(cfg) {
86517
+ return !!cfg.secretKey && (!!cfg.priceOnce || !!cfg.priceMonthly);
86518
+ }
86519
+ function donationSessionParams(cfg, kind, quantity = 3) {
86520
+ const price = kind === "monthly" ? cfg.priceMonthly : cfg.priceOnce;
86521
+ if (!price) return null;
86522
+ const q = Math.min(50, Math.max(1, Math.round(quantity)));
86523
+ const p = new URLSearchParams();
86524
+ p.set("ui_mode", "embedded_page");
86525
+ p.set("redirect_on_completion", "never");
86526
+ p.set("mode", kind === "monthly" ? "subscription" : "payment");
86527
+ p.set("line_items[0][price]", price);
86528
+ p.set("line_items[0][quantity]", kind === "monthly" ? String(q) : "1");
86529
+ if (kind === "monthly") {
86530
+ p.set("line_items[0][adjustable_quantity][enabled]", "true");
86531
+ p.set("line_items[0][adjustable_quantity][minimum]", "1");
86532
+ p.set("line_items[0][adjustable_quantity][maximum]", "50");
86533
+ } else {
86534
+ p.set("submit_type", "donate");
86535
+ }
86536
+ p.set("metadata[project]", "fpv-airspace");
86537
+ p.set("metadata[purpose]", kind === "monthly" ? "donation-monthly" : "donation");
86538
+ return p;
86539
+ }
86540
+ async function createDonationSession(cfg, kind, quantity, fetchImpl = fetch) {
86541
+ if (!cfg.secretKey) return { error: "donations are not configured", status: 404 };
86542
+ const params = donationSessionParams(cfg, kind, quantity);
86543
+ if (!params) return { error: `no ${kind} price configured`, status: 404 };
86544
+ const res = await fetchImpl("https://api.stripe.com/v1/checkout/sessions", {
86545
+ method: "POST",
86546
+ headers: { Authorization: `Bearer ${cfg.secretKey}`, "Content-Type": "application/x-www-form-urlencoded" },
86547
+ body: params.toString()
86548
+ });
86549
+ const json3 = await res.json().catch(() => ({}));
86550
+ if (!res.ok || !json3.client_secret) return { error: json3.error?.message ?? `Stripe answered ${res.status}`, status: 502 };
86551
+ return { clientSecret: json3.client_secret };
86552
+ }
86553
+
86334
86554
  // src/bootstrap.ts
86335
86555
  var HELP = `${NAME} ${VERSION}
86336
86556
 
@@ -86377,7 +86597,7 @@ function buildDeps(config2, logger, overrides = {}) {
86377
86597
  nearestParking: (lon, lat, m, n, priv) => proxy.get().nearestParking(lon, lat, m, n, priv),
86378
86598
  landRestrictionsInBbox: (b, l) => proxy.get().landRestrictionsInBbox(b, l),
86379
86599
  hazardsNear: (lon, lat, m, n) => proxy.get().hazardsNear(lon, lat, m, n),
86380
- hazardsInBbox: (b, l, k) => proxy.get().hazardsInBbox(b, l, k),
86600
+ hazardsInBbox: (b, l, k, n) => proxy.get().hazardsInBbox(b, l, k, n),
86381
86601
  adminAreaAt: (lon, lat) => proxy.get().adminAreaAt(lon, lat),
86382
86602
  close: () => void 0
86383
86603
  };
@@ -86416,7 +86636,7 @@ async function bootstrap(argv) {
86416
86636
  () => ({ version: VERSION, pack: packManager.status(), notamCacheAgeSeconds: deps.notams.status().ageSeconds }),
86417
86637
  "0.0.0.0",
86418
86638
  {
86419
- mapHtml: (origin) => mapHtml({ mode: "page", apiBase: config2.publicUrl ?? origin }),
86639
+ mapHtml: (origin) => mapHtml({ mode: "page", apiBase: config2.publicUrl ?? origin, supportUrl: config2.supportUrl ?? null, supportMonthlyUrl: config2.supportMonthlyUrl ?? null, stripePublishableKey: donationEnabled(config2.stripe) ? config2.stripe.publishableKey ?? null : null }),
86420
86640
  viewData: async (params) => {
86421
86641
  const req = await resolveViewQuery(params, deps);
86422
86642
  return req ? buildViewData(deps, req) : void 0;
@@ -86426,7 +86646,8 @@ async function bootstrap(argv) {
86426
86646
  return q ? buildWindField(deps, q) : void 0;
86427
86647
  },
86428
86648
  droneIndex: () => buildDroneIndex(deps.now()),
86429
- geocode: (params) => geocodeQuery(params, deps)
86649
+ geocode: (params) => geocodeQuery(params, deps),
86650
+ donate: donationEnabled(config2.stripe) ? (kind, quantity) => createDonationSession(config2.stripe, kind, quantity) : void 0
86430
86651
  }
86431
86652
  ) : new StdioTransport(() => createServer(deps), logger);
86432
86653
  await transport.start();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fpv-airspace",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "MCP server answering where a drone can legally take off and fly in the UK: NATS flight restriction zones, live NOTAMs, public rights of way and landowner rules",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,6 +50,16 @@
50
50
  "type": "git",
51
51
  "url": "git+https://github.com/noodlemctwoodle/fpv-airspace.git"
52
52
  },
53
+ "funding": [
54
+ {
55
+ "type": "github",
56
+ "url": "https://github.com/sponsors/noodlemctwoodle"
57
+ },
58
+ {
59
+ "type": "individual",
60
+ "url": "https://donate.stripe.com/3cI28sesu6T0f4n7IP2Nq00"
61
+ }
62
+ ],
53
63
  "keywords": [
54
64
  "mcp",
55
65
  "model-context-protocol",