@uk-open-data-connectors/uk-sources 0.1.0
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 +206 -0
- package/dist/ancientWoodland.d.ts +102 -0
- package/dist/ancientWoodland.d.ts.map +1 -0
- package/dist/ancientWoodland.js +245 -0
- package/dist/ancientWoodland.js.map +1 -0
- package/dist/bankRate.d.ts +107 -0
- package/dist/bankRate.d.ts.map +1 -0
- package/dist/bankRate.js +251 -0
- package/dist/bankRate.js.map +1 -0
- package/dist/carbonIntensity.d.ts +151 -0
- package/dist/carbonIntensity.d.ts.map +1 -0
- package/dist/carbonIntensity.js +257 -0
- package/dist/carbonIntensity.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -0
- package/dist/fixtures.d.ts +5 -0
- package/dist/fixtures.d.ts.map +1 -0
- package/dist/fixtures.js +11 -0
- package/dist/fixtures.js.map +1 -0
- package/dist/floodMonitoring.d.ts +70 -0
- package/dist/floodMonitoring.d.ts.map +1 -0
- package/dist/floodMonitoring.js +164 -0
- package/dist/floodMonitoring.js.map +1 -0
- package/dist/foodHygiene.d.ts +68 -0
- package/dist/foodHygiene.d.ts.map +1 -0
- package/dist/foodHygiene.js +103 -0
- package/dist/foodHygiene.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/onsDatasets.d.ts +67 -0
- package/dist/onsDatasets.d.ts.map +1 -0
- package/dist/onsDatasets.js +111 -0
- package/dist/onsDatasets.js.map +1 -0
- package/dist/parliamentSeats.d.ts +115 -0
- package/dist/parliamentSeats.d.ts.map +1 -0
- package/dist/parliamentSeats.js +202 -0
- package/dist/parliamentSeats.js.map +1 -0
- package/dist/planningDatasets.d.ts +65 -0
- package/dist/planningDatasets.d.ts.map +1 -0
- package/dist/planningDatasets.js +100 -0
- package/dist/planningDatasets.js.map +1 -0
- package/dist/policeCrimes.d.ts +152 -0
- package/dist/policeCrimes.d.ts.map +1 -0
- package/dist/policeCrimes.js +262 -0
- package/dist/policeCrimes.js.map +1 -0
- package/dist/registry.d.ts +17 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +67 -0
- package/dist/registry.js.map +1 -0
- package/dist/tflBikePoints.d.ts +65 -0
- package/dist/tflBikePoints.d.ts.map +1 -0
- package/dist/tflBikePoints.js +149 -0
- package/dist/tflBikePoints.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# @uk-open-data-connectors/uk-sources
|
|
2
|
+
|
|
3
|
+
Uniform TypeScript adapters for UK public data sources. Every adapter has the
|
|
4
|
+
same shape: a live fetch, a strict parse, and a committed fixture fallback so
|
|
5
|
+
builds work offline.
|
|
6
|
+
|
|
7
|
+
## Adapters
|
|
8
|
+
|
|
9
|
+
| id | Source | Auth | What it does |
|
|
10
|
+
| -------------------------- | ----------------------------------- | ---- | ------------------------------------------------------------ |
|
|
11
|
+
| `flood-stations` | Environment Agency flood-monitoring | none | Monitoring stations with river and catchment |
|
|
12
|
+
| `flood-readings` | Environment Agency flood-monitoring | none | Recent water levels for one station, newest first |
|
|
13
|
+
| `ons-datasets` | ONS beta API dataset catalogue | none | Every dataset the ONS lists, with its state and stamp |
|
|
14
|
+
| `food-hygiene-authorities` | Food Standards Agency food hygiene | none | Every local authority register, with its establishment count |
|
|
15
|
+
| `tfl-bike-points` | Transport for London cycle hire | none | Every Santander Cycles docking station, with docking points and docked bikes |
|
|
16
|
+
| `planning-datasets` | Planning Data platform (MHCLG) | none | Every planning dataset, with the records published behind it |
|
|
17
|
+
| `ancient-woodland` | Natural England ancient woodland | none | Ancient woodland polygons for England, counted by type and size |
|
|
18
|
+
| `bank-rate` | Bank of England Bank Rate | none | The daily official Bank Rate, one reading per business day since 1975 |
|
|
19
|
+
| `police-crimes` | Home Office police.uk | none | Street-level crime within a mile of a point, counted by crime type and outcome |
|
|
20
|
+
| `carbon-intensity` | National Energy System Operator | none | Half-hourly carbon intensity for Great Britain, with the window's cleanest and dirtiest half hours |
|
|
21
|
+
| `parliament-seats` | UK Parliament Members API | none | Seats each party holds in the Commons, with the members counted behind them |
|
|
22
|
+
|
|
23
|
+
The flood-monitoring adapters use `environment.data.gov.uk` under the Open
|
|
24
|
+
Government Licence v3:
|
|
25
|
+
<https://environment.data.gov.uk/flood-monitoring/doc/reference>
|
|
26
|
+
|
|
27
|
+
The ONS adapter uses the beta API behind the ONS website rebuild,
|
|
28
|
+
<https://api.beta.ons.gov.uk/v1/datasets>, also keyless and under the Open
|
|
29
|
+
Government Licence v3.
|
|
30
|
+
|
|
31
|
+
The food hygiene adapter uses the FSA Food Hygiene Rating Scheme API,
|
|
32
|
+
<https://api.ratings.food.gov.uk/Authorities/basic>, also keyless and under the
|
|
33
|
+
Open Government Licence v3. It answers only with the version header the FSA
|
|
34
|
+
asks for, `x-api-version: 2`; without it the endpoint returns HTTP 404.
|
|
35
|
+
|
|
36
|
+
The Planning Data platform adapter uses the dataset catalogue behind
|
|
37
|
+
<https://www.planning.data.gov.uk/dataset>, <https://www.planning.data.gov.uk/dataset.json>,
|
|
38
|
+
which answers without a key under the Open Government Licence v3. The file
|
|
39
|
+
lists datasets alongside the platform's pipeline configuration and provenance
|
|
40
|
+
tables, so the adapter keeps the entries whose `realm` is `dataset`.
|
|
41
|
+
|
|
42
|
+
The ancient woodland adapter uses Natural England's Ancient Woodland (England)
|
|
43
|
+
layer on the Defra ArcGIS estate,
|
|
44
|
+
<https://services.arcgis.com/JJzESW51TqeY9uat/arcgis/rest/services/Ancient_Woodland_England/FeatureServer/0>,
|
|
45
|
+
which answers without a key under the Open Government Licence v3. The layer
|
|
46
|
+
carries more than fifty thousand polygons, past ArcGIS's page size, so the
|
|
47
|
+
adapter reads the counts from the service's own statistics queries instead of
|
|
48
|
+
downloading the features.
|
|
49
|
+
|
|
50
|
+
The Bank Rate adapter uses the Bank of England's Interactive Statistical
|
|
51
|
+
Database (IADB),
|
|
52
|
+
<https://www.bankofengland.co.uk/boeapps/database/_iadb-fromshowcolumns.asp>,
|
|
53
|
+
which answers without a key. The daily series, `IUDBEDR`, starts on 2 January
|
|
54
|
+
1975; asking for an earlier start returns the database's error page instead of
|
|
55
|
+
a CSV, so the adapter starts there. The Bank's published terms place
|
|
56
|
+
reproduction of Database data under the Open Government Licence v3:
|
|
57
|
+
<https://www.bankofengland.co.uk/legal>.
|
|
58
|
+
|
|
59
|
+
The recorded crime adapter uses the Home Office police.uk API,
|
|
60
|
+
<https://data.police.uk/api/crimes-street/all-crime>, which answers without a
|
|
61
|
+
key under the Open Government Licence v3. It holds the last 36 months of
|
|
62
|
+
street-level crime from the 44 forces of England, Wales and Northern Ireland,
|
|
63
|
+
so nothing here covers Scotland. A call takes a point and returns everything
|
|
64
|
+
within a mile of it, which is why the adapter counts around one point rather
|
|
65
|
+
than a boundary, and it asks for one month at a time to stay inside the API's
|
|
66
|
+
limit of 15 requests a second.
|
|
67
|
+
|
|
68
|
+
The docking station adapter uses TfL's Unified API,
|
|
69
|
+
<https://api.tfl.gov.uk/BikePoint>, which answers without a key. TfL asks for
|
|
70
|
+
an app key above the free rate limit, so `fetchTflBikePoints` takes an optional
|
|
71
|
+
key and sends it as the `app_key` query parameter. The data is published as TfL
|
|
72
|
+
Open Data: <https://tfl.gov.uk/info-for/open-data-users/>.
|
|
73
|
+
|
|
74
|
+
The carbon intensity adapter uses the National Energy System Operator's Carbon
|
|
75
|
+
Intensity API, <https://api.carbonintensity.org.uk/intensity>, which answers
|
|
76
|
+
without a key. It is the official half-hourly series for Great Britain, with a
|
|
77
|
+
reading and a grade for every half hour, and the API's own terms place the data
|
|
78
|
+
under the Creative Commons Attribution 4.0 licence:
|
|
79
|
+
<https://terms.carbonintensity.org.uk/>. The API refuses a range longer than 31
|
|
80
|
+
days, so the adapter reads whole days and asks for a window that ends at the
|
|
81
|
+
start of today, which keeps every reading in it a settled one.
|
|
82
|
+
|
|
83
|
+
The Parliament seat adapter uses the UK Parliament Members API,
|
|
84
|
+
<https://members-api.parliament.uk/api/Parties/StateOfTheParties>, which
|
|
85
|
+
answers without a key. It is the current state of the parties in each house,
|
|
86
|
+
with the seats a party holds and the members counted behind them, published
|
|
87
|
+
under the Open Parliament Licence v3.0:
|
|
88
|
+
<https://www.parliament.uk/site-information/copyright-parliament/open-parliament-licence/>.
|
|
89
|
+
The call needs a house and a date, so the adapter asks for the caller's own
|
|
90
|
+
date rather than a fixed one.
|
|
91
|
+
|
|
92
|
+
Note on sources that look obvious but are not usable: `api.ons.gov.uk` was
|
|
93
|
+
retired on 2024-11-25 and now answers every request with a decommission notice.
|
|
94
|
+
The beta API at `api.beta.ons.gov.uk/v1` replaced it for dataset metadata,
|
|
95
|
+
editions, versions, and observations.
|
|
96
|
+
|
|
97
|
+
## Usage
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { fetchFloodStationReadings, summarizeFloodReadings } from '@uk-open-data-connectors/uk-sources';
|
|
101
|
+
import { fetchOnsDatasets, summarizeOnsDatasets } from '@uk-open-data-connectors/uk-sources';
|
|
102
|
+
import { fetchFoodHygieneAuthorities, summarizeFoodHygieneAuthorities } from '@uk-open-data-connectors/uk-sources';
|
|
103
|
+
import { fetchTflBikePoints, summarizeTflBikePoints } from '@uk-open-data-connectors/uk-sources';
|
|
104
|
+
import { fetchPlanningDatasets, summarizePlanningDatasets } from '@uk-open-data-connectors/uk-sources';
|
|
105
|
+
import { fetchAncientWoodlandProfile } from '@uk-open-data-connectors/uk-sources';
|
|
106
|
+
import { fetchBankRateObservations, summarizeBankRateSeries } from '@uk-open-data-connectors/uk-sources';
|
|
107
|
+
import { fetchPoliceCrimeSummary } from '@uk-open-data-connectors/uk-sources';
|
|
108
|
+
import { fetchCarbonIntensityWindow } from '@uk-open-data-connectors/uk-sources';
|
|
109
|
+
import { fetchParliamentSeats } from '@uk-open-data-connectors/uk-sources';
|
|
110
|
+
|
|
111
|
+
const readings = await fetchFloodStationReadings('1029TH', { limit: 96 });
|
|
112
|
+
const summary = summarizeFloodReadings(readings);
|
|
113
|
+
console.log(summary.latest?.value, summary.trend);
|
|
114
|
+
|
|
115
|
+
const catalogue = summarizeOnsDatasets(await fetchOnsDatasets());
|
|
116
|
+
console.log(catalogue.datasetCount, catalogue.yearCounts);
|
|
117
|
+
|
|
118
|
+
const registers = summarizeFoodHygieneAuthorities(await fetchFoodHygieneAuthorities());
|
|
119
|
+
console.log(registers.authorityCount, registers.establishmentCount);
|
|
120
|
+
|
|
121
|
+
const docks = summarizeTflBikePoints(await fetchTflBikePoints());
|
|
122
|
+
console.log(docks.stationCount, docks.dockCount, docks.largestStations[0]?.name);
|
|
123
|
+
|
|
124
|
+
const planning = summarizePlanningDatasets(await fetchPlanningDatasets());
|
|
125
|
+
console.log(planning.datasetCount, planning.entityCount, planning.emptyDatasetCount);
|
|
126
|
+
|
|
127
|
+
const woodland = await fetchAncientWoodlandProfile();
|
|
128
|
+
console.log(woodland.recordCount, woodland.totalHectares, woodland.sizeBands[0]?.recordCount);
|
|
129
|
+
|
|
130
|
+
const intensity = await fetchCarbonIntensityWindow();
|
|
131
|
+
console.log(intensity.periodCount, intensity.averageIntensity, intensity.lowestPeriod.intensity);
|
|
132
|
+
|
|
133
|
+
const seats = await fetchParliamentSeats();
|
|
134
|
+
console.log(seats.seatCount, seats.partyCount, seats.largestParty.party.name);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## UK Parliament seats
|
|
138
|
+
|
|
139
|
+
`fetchParliamentSeats` reads the state of the parties in the House of Commons
|
|
140
|
+
and returns the seat counts a page can print.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { fetchParliamentSeats } from '@uk-open-data-connectors/uk-sources';
|
|
144
|
+
|
|
145
|
+
const seats = await fetchParliamentSeats();
|
|
146
|
+
// { seatCount: 650, partyCount: 18, majorityThreshold: 326, ... }
|
|
147
|
+
console.log(seats.largestParty.party.name, seats.largestParty.seatCount);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The payload is validated with this schema before anything is counted:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const PARLIAMENT_PARTY_SCHEMA = z.object({
|
|
154
|
+
id: z.number().int(),
|
|
155
|
+
name: z.string().nullable(),
|
|
156
|
+
abbreviation: z.string().nullable(),
|
|
157
|
+
backgroundColour: z.string().nullable(),
|
|
158
|
+
foregroundColour: z.string().nullable(),
|
|
159
|
+
isIndependentParty: z.boolean(),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const PARLIAMENT_SEAT_COUNT_SCHEMA = z.object({
|
|
163
|
+
male: z.number().int().nonnegative().nullable(),
|
|
164
|
+
female: z.number().int().nonnegative().nullable(),
|
|
165
|
+
nonBinary: z.number().int().nonnegative().nullable(),
|
|
166
|
+
total: z.number().int(),
|
|
167
|
+
party: PARLIAMENT_PARTY_SCHEMA.nullable(),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const PARLIAMENT_RESPONSE_SCHEMA = z.object({
|
|
171
|
+
items: z.array(z.object({ value: PARLIAMENT_SEAT_COUNT_SCHEMA.nullable() })).nullable(),
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The public surface also carries `buildParliamentSeatsUrl(house, forDate)` for
|
|
176
|
+
a specific house and date, `formatParliamentQueryDate(date)` for the
|
|
177
|
+
`YYYY-MM-DD` the path takes, and `parseParliamentSeats(payload)` /
|
|
178
|
+
`summarizeParliamentPartySeats(seats)` for parsing a payload or a list of
|
|
179
|
+
parties on its own. `PARLIAMENT_COMMONS_HOUSE` is 1 and
|
|
180
|
+
`PARLIAMENT_LORDS_HOUSE` is 2.
|
|
181
|
+
|
|
182
|
+
Quirks worth knowing before you build on this:
|
|
183
|
+
|
|
184
|
+
- A vacant seat appears as its own party, named `Vacant`, with one seat and
|
|
185
|
+
no members behind it. The summary keeps it in `seats` and reports the gap
|
|
186
|
+
per row as `unallocatedSeatCount`, so `male + female + nonBinary` can sit
|
|
187
|
+
below `seatCount`.
|
|
188
|
+
- `backgroundColour` and `foregroundColour` are null for a few parties,
|
|
189
|
+
including the Speaker, so a chart needs its own fallback colour rather than
|
|
190
|
+
reading the field straight.
|
|
191
|
+
- The house is a path segment, not a query parameter: 1 is the Commons and 2
|
|
192
|
+
is the Lords, and the same call returns peers for house 2.
|
|
193
|
+
- The call can be slow. One request here timed out at 25 seconds and the next
|
|
194
|
+
answered in two, so give it a generous timeout and keep a committed fixture
|
|
195
|
+
fallback.
|
|
196
|
+
- The date is part of the call, so a page that wants "today" has to send its
|
|
197
|
+
own date; the response does not echo the date or the house back.
|
|
198
|
+
|
|
199
|
+
## Tests
|
|
200
|
+
|
|
201
|
+
Unit tests run against the committed fixtures in `src/fixtures`, offline.
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npm run test --workspace @uk-open-data-connectors/uk-sources
|
|
205
|
+
npm run test:smoke --workspace @uk-open-data-connectors/uk-sources # hits the live APIs
|
|
206
|
+
```
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { UkDataAdapter } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Natural England's Ancient Woodland (England) polygons, served from the Defra
|
|
4
|
+
* ArcGIS estate. The layer behind the Ancient Woodland Inventory: land that
|
|
5
|
+
* has carried continuous woodland cover since at least 1600. Keyless, and
|
|
6
|
+
* published under the Open Government Licence v3.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ANCIENT_WOODLAND_QUERY_URL = "https://services.arcgis.com/JJzESW51TqeY9uat/arcgis/rest/services/Ancient_Woodland_England/FeatureServer/0/query";
|
|
9
|
+
/** The dataset's page on the Natural England open data hub. */
|
|
10
|
+
export declare const ANCIENT_WOODLAND_DATASET_URL = "https://naturalengland-defra.opendata.arcgis.com/datasets/ancient-woodland-england";
|
|
11
|
+
/** The source id these errors and probes carry. */
|
|
12
|
+
export declare const ANCIENT_WOODLAND_SOURCE_ID = "ancient-woodland";
|
|
13
|
+
/** The three woodland types the layer's STATUS field uses. */
|
|
14
|
+
export type AncientWoodlandCategoryId = 'asnw' | 'paws' | 'awp';
|
|
15
|
+
/** One woodland type in the layer, with the name Natural England gives it. */
|
|
16
|
+
export interface AncientWoodlandCategory {
|
|
17
|
+
id: AncientWoodlandCategoryId;
|
|
18
|
+
/** The value the layer writes in its STATUS field. */
|
|
19
|
+
status: string;
|
|
20
|
+
label: string;
|
|
21
|
+
description: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The layer's own abbreviations, spelled out: Ancient Semi-Natural Woodland,
|
|
25
|
+
* Plantation on an Ancient Woodland Site, and Ancient Wood Pasture.
|
|
26
|
+
*/
|
|
27
|
+
export declare const ANCIENT_WOODLAND_CATEGORIES: readonly AncientWoodlandCategory[];
|
|
28
|
+
/** One size band the profile counts woodland records into. */
|
|
29
|
+
export interface AncientWoodlandSizeBandDefinition {
|
|
30
|
+
label: string;
|
|
31
|
+
/** Lower bound in hectares, inclusive. */
|
|
32
|
+
minHectares: number;
|
|
33
|
+
/** Upper bound in hectares, exclusive; absent on the open top band. */
|
|
34
|
+
maxHectares?: number;
|
|
35
|
+
/** The where clause this band asks the layer for. */
|
|
36
|
+
where: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The size bands the profile counts records into, smallest first. The first
|
|
40
|
+
* band is open at the bottom because a handful of records carry areas below
|
|
41
|
+
* the 0.25 hectare mapping threshold.
|
|
42
|
+
*/
|
|
43
|
+
export declare const ANCIENT_WOODLAND_SIZE_BANDS: readonly AncientWoodlandSizeBandDefinition[];
|
|
44
|
+
/** One size band with the records the layer returned for it. */
|
|
45
|
+
export interface AncientWoodlandSizeBand {
|
|
46
|
+
label: string;
|
|
47
|
+
minHectares: number;
|
|
48
|
+
maxHectares?: number;
|
|
49
|
+
recordCount: number;
|
|
50
|
+
/** Records in this band, counted by woodland type. */
|
|
51
|
+
categoryCounts: Record<AncientWoodlandCategoryId, number>;
|
|
52
|
+
}
|
|
53
|
+
/** Everything the ancient woodland story draws, counted by the layer itself. */
|
|
54
|
+
export interface AncientWoodlandProfile {
|
|
55
|
+
recordCount: number;
|
|
56
|
+
totalHectares: number;
|
|
57
|
+
largestRecordHectares: number;
|
|
58
|
+
averageRecordHectares: number;
|
|
59
|
+
/** Records per woodland type across the whole layer. */
|
|
60
|
+
categoryCounts: Record<AncientWoodlandCategoryId, number>;
|
|
61
|
+
/** Hectares per woodland type across the whole layer. */
|
|
62
|
+
categoryHectares: Record<AncientWoodlandCategoryId, number>;
|
|
63
|
+
/** One entry per size band, smallest first. */
|
|
64
|
+
sizeBands: AncientWoodlandSizeBand[];
|
|
65
|
+
}
|
|
66
|
+
/** The raw layer responses one profile is parsed from. */
|
|
67
|
+
export interface AncientWoodlandResponses {
|
|
68
|
+
/** Count, sum, max, and average over the whole layer. */
|
|
69
|
+
totals: unknown;
|
|
70
|
+
/** Count and area per woodland type over the whole layer. */
|
|
71
|
+
categoryTotals: unknown;
|
|
72
|
+
/** One grouped count response per entry in {@link ANCIENT_WOODLAND_SIZE_BANDS}. */
|
|
73
|
+
sizeBandCounts: unknown[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Adds up the layer's counts and woodland types from its own statistics.
|
|
77
|
+
*
|
|
78
|
+
* The size bands are asked for one at a time, so the parser checks that they
|
|
79
|
+
* add up to the layer's own total before trusting them: a query that silently
|
|
80
|
+
* drops records would otherwise show as a short bar rather than an error.
|
|
81
|
+
*
|
|
82
|
+
* @param payload - the raw responses, or the committed fixture of them
|
|
83
|
+
* @returns record, area, and size-band counts for the layer
|
|
84
|
+
*/
|
|
85
|
+
export declare function parseAncientWoodlandProfile(payload: unknown): AncientWoodlandProfile;
|
|
86
|
+
/**
|
|
87
|
+
* Counts England's ancient woodland from Natural England's own layer.
|
|
88
|
+
*
|
|
89
|
+
* The layer holds more than fifty thousand polygons, well past ArcGIS's page
|
|
90
|
+
* size, so the counts come from the service's statistics rather than from a
|
|
91
|
+
* download: one query for the layer totals, one for the woodland types, and
|
|
92
|
+
* one per size band.
|
|
93
|
+
*
|
|
94
|
+
* @param options - an optional fetch implementation
|
|
95
|
+
* @returns record, area, and size-band counts for the layer
|
|
96
|
+
*/
|
|
97
|
+
export declare function fetchAncientWoodlandProfile(options?: {
|
|
98
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
99
|
+
}): Promise<AncientWoodlandProfile>;
|
|
100
|
+
/** Natural England's ancient woodland layer for England, keyless. */
|
|
101
|
+
export declare const ancientWoodlandAdapter: UkDataAdapter<AncientWoodlandProfile>;
|
|
102
|
+
//# sourceMappingURL=ancientWoodland.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ancientWoodland.d.ts","sourceRoot":"","sources":["../src/ancientWoodland.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,qHAC6E,CAAC;AAErH,+DAA+D;AAC/D,eAAO,MAAM,4BAA4B,uFAC6C,CAAC;AAEvF,mDAAmD;AACnD,eAAO,MAAM,0BAA0B,qBAAqB,CAAC;AAE7D,8DAA8D;AAC9D,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;AAEhE,8EAA8E;AAC9E,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,yBAAyB,CAAC;IAC9B,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,eAAO,MAAM,2BAA2B,EAAE,SAAS,uBAAuB,EAoBzE,CAAC;AAEF,8DAA8D;AAC9D,MAAM,WAAW,iCAAiC;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,EAAE,SAAS,iCAAiC,EA6BnF,CAAC;AAEF,gEAAgE;AAChE,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,cAAc,EAAE,MAAM,CAAC,yBAAyB,EAAE,MAAM,CAAC,CAAC;CAC3D;AAED,gFAAgF;AAChF,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,wDAAwD;IACxD,cAAc,EAAE,MAAM,CAAC,yBAAyB,EAAE,MAAM,CAAC,CAAC;IAC1D,yDAAyD;IACzD,gBAAgB,EAAE,MAAM,CAAC,yBAAyB,EAAE,MAAM,CAAC,CAAC;IAC5D,+CAA+C;IAC/C,SAAS,EAAE,uBAAuB,EAAE,CAAC;CACtC;AAED,0DAA0D;AAC1D,MAAM,WAAW,wBAAwB;IACvC,yDAAyD;IACzD,MAAM,EAAE,OAAO,CAAC;IAChB,6DAA6D;IAC7D,cAAc,EAAE,OAAO,CAAC;IACxB,mFAAmF;IACnF,cAAc,EAAE,OAAO,EAAE,CAAC;CAC3B;AAqDD;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,sBAAsB,CAuEpF;AAsDD;;;;;;;;;;GAUG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CAAO,GACpD,OAAO,CAAC,sBAAsB,CAAC,CAkBjC;AAED,qEAAqE;AACrE,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,sBAAsB,CAWxE,CAAC"}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { UkSourceApiError, UkSourceParseError } from './errors.js';
|
|
3
|
+
import { readFixtureJson } from './fixtures.js';
|
|
4
|
+
/**
|
|
5
|
+
* Natural England's Ancient Woodland (England) polygons, served from the Defra
|
|
6
|
+
* ArcGIS estate. The layer behind the Ancient Woodland Inventory: land that
|
|
7
|
+
* has carried continuous woodland cover since at least 1600. Keyless, and
|
|
8
|
+
* published under the Open Government Licence v3.
|
|
9
|
+
*/
|
|
10
|
+
export const ANCIENT_WOODLAND_QUERY_URL = 'https://services.arcgis.com/JJzESW51TqeY9uat/arcgis/rest/services/Ancient_Woodland_England/FeatureServer/0/query';
|
|
11
|
+
/** The dataset's page on the Natural England open data hub. */
|
|
12
|
+
export const ANCIENT_WOODLAND_DATASET_URL = 'https://naturalengland-defra.opendata.arcgis.com/datasets/ancient-woodland-england';
|
|
13
|
+
/** The source id these errors and probes carry. */
|
|
14
|
+
export const ANCIENT_WOODLAND_SOURCE_ID = 'ancient-woodland';
|
|
15
|
+
/**
|
|
16
|
+
* The layer's own abbreviations, spelled out: Ancient Semi-Natural Woodland,
|
|
17
|
+
* Plantation on an Ancient Woodland Site, and Ancient Wood Pasture.
|
|
18
|
+
*/
|
|
19
|
+
export const ANCIENT_WOODLAND_CATEGORIES = [
|
|
20
|
+
{
|
|
21
|
+
id: 'asnw',
|
|
22
|
+
status: 'ASNW',
|
|
23
|
+
label: 'Ancient semi-natural woodland',
|
|
24
|
+
description: 'Native tree and shrub cover that has never been cleared and replanted.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'paws',
|
|
28
|
+
status: 'PAWS',
|
|
29
|
+
label: 'Plantation on ancient woodland',
|
|
30
|
+
description: 'Woodland that was felled and replanted, often with conifers, on ground that has been wooded since 1600.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'awp',
|
|
34
|
+
status: 'AWP',
|
|
35
|
+
label: 'Ancient wood pasture',
|
|
36
|
+
description: 'Wooded ground grazed by livestock or deer, usually with veteran trees.',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* The size bands the profile counts records into, smallest first. The first
|
|
41
|
+
* band is open at the bottom because a handful of records carry areas below
|
|
42
|
+
* the 0.25 hectare mapping threshold.
|
|
43
|
+
*/
|
|
44
|
+
export const ANCIENT_WOODLAND_SIZE_BANDS = [
|
|
45
|
+
{ label: 'Under 1 hectare', minHectares: 0, maxHectares: 1, where: 'AREA < 1' },
|
|
46
|
+
{ label: '1 to 2 hectares', minHectares: 1, maxHectares: 2, where: 'AREA >= 1 AND AREA < 2' },
|
|
47
|
+
{ label: '2 to 5 hectares', minHectares: 2, maxHectares: 5, where: 'AREA >= 2 AND AREA < 5' },
|
|
48
|
+
{
|
|
49
|
+
label: '5 to 10 hectares',
|
|
50
|
+
minHectares: 5,
|
|
51
|
+
maxHectares: 10,
|
|
52
|
+
where: 'AREA >= 5 AND AREA < 10',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
label: '10 to 20 hectares',
|
|
56
|
+
minHectares: 10,
|
|
57
|
+
maxHectares: 20,
|
|
58
|
+
where: 'AREA >= 10 AND AREA < 20',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
label: '20 to 50 hectares',
|
|
62
|
+
minHectares: 20,
|
|
63
|
+
maxHectares: 50,
|
|
64
|
+
where: 'AREA >= 20 AND AREA < 50',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: '50 to 100 hectares',
|
|
68
|
+
minHectares: 50,
|
|
69
|
+
maxHectares: 100,
|
|
70
|
+
where: 'AREA >= 50 AND AREA < 100',
|
|
71
|
+
},
|
|
72
|
+
{ label: '100 hectares or more', minHectares: 100, where: 'AREA >= 100' },
|
|
73
|
+
];
|
|
74
|
+
const ARCGIS_STATISTICS_RESPONSE_SCHEMA = z.object({
|
|
75
|
+
features: z.array(z.object({ attributes: z.record(z.unknown()) })),
|
|
76
|
+
});
|
|
77
|
+
const ANCIENT_WOODLAND_RESPONSES_SCHEMA = z.object({
|
|
78
|
+
totals: ARCGIS_STATISTICS_RESPONSE_SCHEMA,
|
|
79
|
+
categoryTotals: ARCGIS_STATISTICS_RESPONSE_SCHEMA,
|
|
80
|
+
sizeBandCounts: z.array(ARCGIS_STATISTICS_RESPONSE_SCHEMA),
|
|
81
|
+
});
|
|
82
|
+
/** The ArcGIS error body, which arrives with HTTP 200 rather than a 4xx. */
|
|
83
|
+
const ARCGIS_ERROR_SCHEMA = z.object({
|
|
84
|
+
error: z.object({ message: z.string().optional() }),
|
|
85
|
+
});
|
|
86
|
+
/** Every category starts at zero, so a missing type reads as none rather than undefined. */
|
|
87
|
+
function emptyCategoryRecord() {
|
|
88
|
+
return { asnw: 0, paws: 0, awp: 0 };
|
|
89
|
+
}
|
|
90
|
+
/** Looks up the woodland type behind one STATUS value, or throws when it is new. */
|
|
91
|
+
function categoryIdForStatus(status) {
|
|
92
|
+
const match = ANCIENT_WOODLAND_CATEGORIES.find((category) => category.status === status);
|
|
93
|
+
if (match === undefined) {
|
|
94
|
+
throw new UkSourceParseError(ANCIENT_WOODLAND_SOURCE_ID, `unrecognised woodland status ${JSON.stringify(status)}`);
|
|
95
|
+
}
|
|
96
|
+
return match.id;
|
|
97
|
+
}
|
|
98
|
+
/** Reads one numeric attribute, naming it when the layer left it out. */
|
|
99
|
+
function readNumberAttribute(attributes, name) {
|
|
100
|
+
const raw = attributes[name];
|
|
101
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
|
102
|
+
throw new UkSourceParseError(ANCIENT_WOODLAND_SOURCE_ID, `expected a number in "${name}", got ${JSON.stringify(raw)}`);
|
|
103
|
+
}
|
|
104
|
+
return raw;
|
|
105
|
+
}
|
|
106
|
+
/** Reads the single row of a whole-layer statistics response. */
|
|
107
|
+
function readAttributeRows(response) {
|
|
108
|
+
return response.features.map((feature) => feature.attributes);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Adds up the layer's counts and woodland types from its own statistics.
|
|
112
|
+
*
|
|
113
|
+
* The size bands are asked for one at a time, so the parser checks that they
|
|
114
|
+
* add up to the layer's own total before trusting them: a query that silently
|
|
115
|
+
* drops records would otherwise show as a short bar rather than an error.
|
|
116
|
+
*
|
|
117
|
+
* @param payload - the raw responses, or the committed fixture of them
|
|
118
|
+
* @returns record, area, and size-band counts for the layer
|
|
119
|
+
*/
|
|
120
|
+
export function parseAncientWoodlandProfile(payload) {
|
|
121
|
+
const parsed = ANCIENT_WOODLAND_RESPONSES_SCHEMA.safeParse(payload);
|
|
122
|
+
if (!parsed.success) {
|
|
123
|
+
throw new UkSourceParseError(ANCIENT_WOODLAND_SOURCE_ID, parsed.error.message);
|
|
124
|
+
}
|
|
125
|
+
const { totals, categoryTotals, sizeBandCounts } = parsed.data;
|
|
126
|
+
if (sizeBandCounts.length !== ANCIENT_WOODLAND_SIZE_BANDS.length) {
|
|
127
|
+
throw new UkSourceParseError(ANCIENT_WOODLAND_SOURCE_ID, `expected ${ANCIENT_WOODLAND_SIZE_BANDS.length} size band responses, got ${sizeBandCounts.length}`);
|
|
128
|
+
}
|
|
129
|
+
const totalsRow = readAttributeRows(totals)[0];
|
|
130
|
+
if (totalsRow === undefined) {
|
|
131
|
+
throw new UkSourceParseError(ANCIENT_WOODLAND_SOURCE_ID, 'the totals query returned no rows');
|
|
132
|
+
}
|
|
133
|
+
const recordCount = readNumberAttribute(totalsRow, 'recordCount');
|
|
134
|
+
const categoryCounts = emptyCategoryRecord();
|
|
135
|
+
const categoryHectares = emptyCategoryRecord();
|
|
136
|
+
for (const row of readAttributeRows(categoryTotals)) {
|
|
137
|
+
const categoryId = categoryIdForStatus(row.STATUS);
|
|
138
|
+
categoryCounts[categoryId] = readNumberAttribute(row, 'recordCount');
|
|
139
|
+
categoryHectares[categoryId] = readNumberAttribute(row, 'hectares');
|
|
140
|
+
}
|
|
141
|
+
const sizeBands = ANCIENT_WOODLAND_SIZE_BANDS.map((definition, index) => {
|
|
142
|
+
const response = sizeBandCounts[index];
|
|
143
|
+
if (response === undefined) {
|
|
144
|
+
throw new UkSourceParseError(ANCIENT_WOODLAND_SOURCE_ID, `no response for the "${definition.label}" band`);
|
|
145
|
+
}
|
|
146
|
+
const bandCounts = emptyCategoryRecord();
|
|
147
|
+
for (const row of readAttributeRows(response)) {
|
|
148
|
+
bandCounts[categoryIdForStatus(row.STATUS)] = readNumberAttribute(row, 'recordCount');
|
|
149
|
+
}
|
|
150
|
+
const bandTotal = ANCIENT_WOODLAND_CATEGORIES.reduce((total, category) => total + bandCounts[category.id], 0);
|
|
151
|
+
return {
|
|
152
|
+
label: definition.label,
|
|
153
|
+
minHectares: definition.minHectares,
|
|
154
|
+
...(definition.maxHectares === undefined ? {} : { maxHectares: definition.maxHectares }),
|
|
155
|
+
recordCount: bandTotal,
|
|
156
|
+
categoryCounts: bandCounts,
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
const bandRecordCount = sizeBands.reduce((total, band) => total + band.recordCount, 0);
|
|
160
|
+
if (bandRecordCount !== recordCount) {
|
|
161
|
+
throw new UkSourceParseError(ANCIENT_WOODLAND_SOURCE_ID, `the size bands hold ${bandRecordCount} records against a layer total of ${recordCount}`);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
recordCount,
|
|
165
|
+
totalHectares: readNumberAttribute(totalsRow, 'hectares'),
|
|
166
|
+
largestRecordHectares: readNumberAttribute(totalsRow, 'largestHectares'),
|
|
167
|
+
averageRecordHectares: readNumberAttribute(totalsRow, 'averageHectares'),
|
|
168
|
+
categoryCounts,
|
|
169
|
+
categoryHectares,
|
|
170
|
+
sizeBands,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** Count and area per group, the shape every query here asks for. */
|
|
174
|
+
const COUNT_AND_AREA_STATISTICS = [
|
|
175
|
+
{ statisticType: 'count', onStatisticField: 'OBJECTID', outStatisticFieldName: 'recordCount' },
|
|
176
|
+
{ statisticType: 'sum', onStatisticField: 'AREA', outStatisticFieldName: 'hectares' },
|
|
177
|
+
];
|
|
178
|
+
/** Count, area, largest, and average over the whole layer. */
|
|
179
|
+
const LAYER_TOTALS_STATISTICS = [
|
|
180
|
+
...COUNT_AND_AREA_STATISTICS,
|
|
181
|
+
{ statisticType: 'max', onStatisticField: 'AREA', outStatisticFieldName: 'largestHectares' },
|
|
182
|
+
{ statisticType: 'avg', onStatisticField: 'AREA', outStatisticFieldName: 'averageHectares' },
|
|
183
|
+
];
|
|
184
|
+
/** Runs one statistics query, turning ArcGIS's 200-with-an-error into a throw. */
|
|
185
|
+
async function queryArcgisStatistics(fetchImpl, query) {
|
|
186
|
+
const url = new URL(ANCIENT_WOODLAND_QUERY_URL);
|
|
187
|
+
url.searchParams.set('where', query.where);
|
|
188
|
+
url.searchParams.set('outStatistics', JSON.stringify(query.statistics));
|
|
189
|
+
if (query.groupByField !== undefined) {
|
|
190
|
+
url.searchParams.set('groupByFieldsForStatistics', query.groupByField);
|
|
191
|
+
}
|
|
192
|
+
url.searchParams.set('returnGeometry', 'false');
|
|
193
|
+
url.searchParams.set('f', 'json');
|
|
194
|
+
const response = await fetchImpl(url);
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
throw new UkSourceApiError(ANCIENT_WOODLAND_SOURCE_ID, `HTTP ${response.status} querying the ancient woodland layer`);
|
|
197
|
+
}
|
|
198
|
+
const payload = await response.json();
|
|
199
|
+
const arcgisError = ARCGIS_ERROR_SCHEMA.safeParse(payload);
|
|
200
|
+
if (arcgisError.success) {
|
|
201
|
+
throw new UkSourceApiError(ANCIENT_WOODLAND_SOURCE_ID, arcgisError.data.error.message ?? 'the layer rejected a statistics query');
|
|
202
|
+
}
|
|
203
|
+
return payload;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Counts England's ancient woodland from Natural England's own layer.
|
|
207
|
+
*
|
|
208
|
+
* The layer holds more than fifty thousand polygons, well past ArcGIS's page
|
|
209
|
+
* size, so the counts come from the service's statistics rather than from a
|
|
210
|
+
* download: one query for the layer totals, one for the woodland types, and
|
|
211
|
+
* one per size band.
|
|
212
|
+
*
|
|
213
|
+
* @param options - an optional fetch implementation
|
|
214
|
+
* @returns record, area, and size-band counts for the layer
|
|
215
|
+
*/
|
|
216
|
+
export async function fetchAncientWoodlandProfile(options = {}) {
|
|
217
|
+
const { fetchImpl = globalThis.fetch } = options;
|
|
218
|
+
const [totals, categoryTotals, ...sizeBandCounts] = await Promise.all([
|
|
219
|
+
queryArcgisStatistics(fetchImpl, { where: '1=1', statistics: LAYER_TOTALS_STATISTICS }),
|
|
220
|
+
queryArcgisStatistics(fetchImpl, {
|
|
221
|
+
where: '1=1',
|
|
222
|
+
statistics: COUNT_AND_AREA_STATISTICS,
|
|
223
|
+
groupByField: 'STATUS',
|
|
224
|
+
}),
|
|
225
|
+
...ANCIENT_WOODLAND_SIZE_BANDS.map((band) => queryArcgisStatistics(fetchImpl, {
|
|
226
|
+
where: band.where,
|
|
227
|
+
statistics: COUNT_AND_AREA_STATISTICS,
|
|
228
|
+
groupByField: 'STATUS',
|
|
229
|
+
})),
|
|
230
|
+
]);
|
|
231
|
+
return parseAncientWoodlandProfile({ totals, categoryTotals, sizeBandCounts });
|
|
232
|
+
}
|
|
233
|
+
/** Natural England's ancient woodland layer for England, keyless. */
|
|
234
|
+
export const ancientWoodlandAdapter = {
|
|
235
|
+
id: ANCIENT_WOODLAND_SOURCE_ID,
|
|
236
|
+
name: 'Natural England ancient woodland (England)',
|
|
237
|
+
auth: 'none',
|
|
238
|
+
description: 'Ancient woodland polygons for England, counted by woodland type and by size.',
|
|
239
|
+
fetchLive: (options) => fetchAncientWoodlandProfile({
|
|
240
|
+
...(options?.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
|
|
241
|
+
}),
|
|
242
|
+
parse: parseAncientWoodlandProfile,
|
|
243
|
+
loadFixture: () => parseAncientWoodlandProfile(readFixtureJson('ancient-woodland.json')),
|
|
244
|
+
};
|
|
245
|
+
//# sourceMappingURL=ancientWoodland.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ancientWoodland.js","sourceRoot":"","sources":["../src/ancientWoodland.ts"],"names":[],"mappings":"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"}
|