@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.
Files changed (62) hide show
  1. package/README.md +206 -0
  2. package/dist/ancientWoodland.d.ts +102 -0
  3. package/dist/ancientWoodland.d.ts.map +1 -0
  4. package/dist/ancientWoodland.js +245 -0
  5. package/dist/ancientWoodland.js.map +1 -0
  6. package/dist/bankRate.d.ts +107 -0
  7. package/dist/bankRate.d.ts.map +1 -0
  8. package/dist/bankRate.js +251 -0
  9. package/dist/bankRate.js.map +1 -0
  10. package/dist/carbonIntensity.d.ts +151 -0
  11. package/dist/carbonIntensity.d.ts.map +1 -0
  12. package/dist/carbonIntensity.js +257 -0
  13. package/dist/carbonIntensity.js.map +1 -0
  14. package/dist/errors.d.ts +15 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/dist/errors.js +26 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/fixtures.d.ts +5 -0
  19. package/dist/fixtures.d.ts.map +1 -0
  20. package/dist/fixtures.js +11 -0
  21. package/dist/fixtures.js.map +1 -0
  22. package/dist/floodMonitoring.d.ts +70 -0
  23. package/dist/floodMonitoring.d.ts.map +1 -0
  24. package/dist/floodMonitoring.js +164 -0
  25. package/dist/floodMonitoring.js.map +1 -0
  26. package/dist/foodHygiene.d.ts +68 -0
  27. package/dist/foodHygiene.d.ts.map +1 -0
  28. package/dist/foodHygiene.js +103 -0
  29. package/dist/foodHygiene.js.map +1 -0
  30. package/dist/index.d.ts +47 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +25 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/onsDatasets.d.ts +67 -0
  35. package/dist/onsDatasets.d.ts.map +1 -0
  36. package/dist/onsDatasets.js +111 -0
  37. package/dist/onsDatasets.js.map +1 -0
  38. package/dist/parliamentSeats.d.ts +115 -0
  39. package/dist/parliamentSeats.d.ts.map +1 -0
  40. package/dist/parliamentSeats.js +202 -0
  41. package/dist/parliamentSeats.js.map +1 -0
  42. package/dist/planningDatasets.d.ts +65 -0
  43. package/dist/planningDatasets.d.ts.map +1 -0
  44. package/dist/planningDatasets.js +100 -0
  45. package/dist/planningDatasets.js.map +1 -0
  46. package/dist/policeCrimes.d.ts +152 -0
  47. package/dist/policeCrimes.d.ts.map +1 -0
  48. package/dist/policeCrimes.js +262 -0
  49. package/dist/policeCrimes.js.map +1 -0
  50. package/dist/registry.d.ts +17 -0
  51. package/dist/registry.d.ts.map +1 -0
  52. package/dist/registry.js +67 -0
  53. package/dist/registry.js.map +1 -0
  54. package/dist/tflBikePoints.d.ts +65 -0
  55. package/dist/tflBikePoints.d.ts.map +1 -0
  56. package/dist/tflBikePoints.js +149 -0
  57. package/dist/tflBikePoints.js.map +1 -0
  58. package/dist/types.d.ts +30 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. 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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"}