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.
- package/README.md +41 -242
- package/dist/index.js +256 -35
- 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
|
|
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
|
-
|
|
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
|
+
[](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
|
|
12
|
-
- **What is this aerodrome's zone?** `get_aerodrome_zone` by name or ICAO code
|
|
13
|
-
- **Is there a NOTAM in force?** `check_notams`
|
|
14
|
-
- **What does my route cross?** `check_route` for
|
|
15
|
-
- **Can I take off here?** `check_takeoff_site
|
|
16
|
-
- **Where can I park?** `find_parking`
|
|
17
|
-
- **Can I fly here, now?** `preflight_briefing
|
|
18
|
-
- **What is the ground doing?** `check_terrain`
|
|
19
|
-
- **Where could I take off?** `find_takeoff_spots` scores
|
|
20
|
-
- **What can my drone do?** `check_drone_rules
|
|
21
|
-
- **Is the weather flyable?** `check_weather
|
|
22
|
-
- **Show me.**
|
|
23
|
-
- Plus `geocode`
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
77
|
+
## The map
|
|
154
78
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
## Documentation
|
|
207
88
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
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
|
|
75548
|
-
|
|
75549
|
-
|
|
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 -
|
|
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: -
|
|
78107
|
-
#info, #info.closed { position: static; width: auto; max-width:
|
|
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
|
-
|
|
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('')
|
|
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
|
-
|
|
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 (
|
|
78914
|
-
if (req.silent) areaLoadDone();
|
|
78915
|
-
if (
|
|
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.
|
|
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",
|