@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
@@ -0,0 +1,257 @@
1
+ import { z } from 'zod';
2
+ import { UkSourceApiError, UkSourceParseError } from './errors.js';
3
+ import { readFixtureJson } from './fixtures.js';
4
+ /**
5
+ * Half-hourly carbon intensity for Great Britain's electricity grid, from the
6
+ * National Energy System Operator's Carbon Intensity API. Every call answers
7
+ * without a key, and the API's own terms place the data under the Creative
8
+ * Commons Attribution 4.0 licence: <https://carbonintensity.org.uk/>
9
+ */
10
+ export const CARBON_INTENSITY_SOURCE_ID = 'carbon-intensity';
11
+ /** Root every call here is built from. */
12
+ export const CARBON_INTENSITY_API_BASE_URL = 'https://api.carbonintensity.org.uk';
13
+ /** Half-hourly readings between two instants. */
14
+ export const CARBON_INTENSITY_RANGE_URL = `${CARBON_INTENSITY_API_BASE_URL}/intensity`;
15
+ /**
16
+ * The API refuses a range longer than 31 days ("The date range you have
17
+ * specified is greater than 31 days"). The window stays a day inside that.
18
+ */
19
+ export const CARBON_INTENSITY_MAX_WINDOW_DAYS = 30;
20
+ /** How many complete days the default window covers. */
21
+ export const DEFAULT_CARBON_INTENSITY_WINDOW_DAYS = 30;
22
+ /** How long one reading covers. */
23
+ export const CARBON_INTENSITY_PERIOD_MINUTES = 30;
24
+ /** Milliseconds in a day, for turning a window length into an instant. */
25
+ const MILLISECONDS_PER_DAY = 86_400_000;
26
+ /** Milliseconds in the half hour one reading covers. */
27
+ const MILLISECONDS_PER_PERIOD = CARBON_INTENSITY_PERIOD_MINUTES * 60 * 1000;
28
+ /** Half-hour readings in a whole day. */
29
+ const PERIODS_PER_DAY = (24 * 60) / CARBON_INTENSITY_PERIOD_MINUTES;
30
+ /**
31
+ * The grades the API puts each half hour in, lowest intensity first. They are
32
+ * the operator's own words: "The index is a measure of the Carbon Intensity
33
+ * represented on a scale between 'very low', 'low', 'moderate', 'high', 'very
34
+ * high'."
35
+ */
36
+ export const CARBON_INTENSITY_INDEX_BANDS = [
37
+ 'very low',
38
+ 'low',
39
+ 'moderate',
40
+ 'high',
41
+ 'very high',
42
+ ];
43
+ const CARBON_INTENSITY_RESPONSE_SCHEMA = z.object({
44
+ data: z.array(z.object({
45
+ from: z.string(),
46
+ to: z.string(),
47
+ intensity: z.object({
48
+ forecast: z.number().nullable(),
49
+ actual: z.number().nullable(),
50
+ index: z.string(),
51
+ }),
52
+ })),
53
+ });
54
+ const CARBON_INTENSITY_INSTANT_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z$/;
55
+ /** Milliseconds for an ISO instant, or NaN when the string is not one. */
56
+ function instantMilliseconds(instant) {
57
+ return CARBON_INTENSITY_INSTANT_PATTERN.test(instant) ? Date.parse(instant) : Number.NaN;
58
+ }
59
+ /**
60
+ * Writes a Date as the ISO instant the API takes in its path, e.g.
61
+ * `2026-08-30T00:00Z`.
62
+ *
63
+ * @param date - the instant to write
64
+ * @returns the instant as `YYYY-MM-DDThh:mmZ`
65
+ */
66
+ export function formatCarbonIntensityInstant(date) {
67
+ return `${date.toISOString().slice(0, 16)}Z`;
68
+ }
69
+ /**
70
+ * Works out the window a build reads: whole UTC days, ending at the start of
71
+ * today, so every reading in it is settled rather than a forecast.
72
+ *
73
+ * @param options - how many days, and the clock to measure back from
74
+ * @returns the window's start and end as ISO instants
75
+ */
76
+ export function resolveCarbonIntensityWindow(options = {}) {
77
+ const windowDays = options.windowDays ?? DEFAULT_CARBON_INTENSITY_WINDOW_DAYS;
78
+ if (!Number.isInteger(windowDays) || windowDays < 1) {
79
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, `a window of ${String(windowDays)} days is not a whole number of days`);
80
+ }
81
+ if (windowDays > CARBON_INTENSITY_MAX_WINDOW_DAYS) {
82
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, `the API refuses a range longer than ${String(CARBON_INTENSITY_MAX_WINDOW_DAYS)} days`);
83
+ }
84
+ const now = options.now ?? new Date();
85
+ const to = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
86
+ const from = new Date(to.getTime() - windowDays * MILLISECONDS_PER_DAY);
87
+ return { from: formatCarbonIntensityInstant(from), to: formatCarbonIntensityInstant(to) };
88
+ }
89
+ /**
90
+ * Checks a window a caller passed in rather than one the adapter worked out.
91
+ *
92
+ * The API refuses a range longer than 31 days and answers in half hours, so a
93
+ * window that is not a whole number of half hours, or that runs past the
94
+ * limit, throws here rather than at the API.
95
+ *
96
+ * @param window - the start and end a caller asked for
97
+ * @returns the same window, once it has been checked
98
+ */
99
+ export function validateCarbonIntensityWindow(window) {
100
+ const { from, to } = window;
101
+ if (from === undefined || to === undefined) {
102
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, 'a window needs both a start and an end');
103
+ }
104
+ const fromMilliseconds = instantMilliseconds(from);
105
+ const toMilliseconds = instantMilliseconds(to);
106
+ if (Number.isNaN(fromMilliseconds) || Number.isNaN(toMilliseconds)) {
107
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, `expected instants as YYYY-MM-DDThh:mmZ, got ${from} and ${to}`);
108
+ }
109
+ const span = toMilliseconds - fromMilliseconds;
110
+ if (span <= 0) {
111
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, `the window ends at ${to}, before it starts at ${from}`);
112
+ }
113
+ if (span % MILLISECONDS_PER_PERIOD !== 0) {
114
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, `the window ${from} to ${to} is not a whole number of half hours`);
115
+ }
116
+ if (span > CARBON_INTENSITY_MAX_WINDOW_DAYS * MILLISECONDS_PER_DAY) {
117
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, `the API refuses a range longer than ${String(CARBON_INTENSITY_MAX_WINDOW_DAYS)} days`);
118
+ }
119
+ return { from, to };
120
+ }
121
+ /**
122
+ * Builds the URL for one window of half-hourly readings.
123
+ *
124
+ * @param window - the start and end of the window, as ISO instants
125
+ * @returns the full range URL
126
+ */
127
+ export function buildCarbonIntensityWindowUrl(window) {
128
+ return `${CARBON_INTENSITY_RANGE_URL}/${window.from}/${window.to}`;
129
+ }
130
+ /** Counts the half hours the window holds at each grade, lowest grade first. */
131
+ function countByBand(periods) {
132
+ return CARBON_INTENSITY_INDEX_BANDS.map((index) => ({
133
+ index,
134
+ periodCount: periods.filter((period) => period.index === index).length,
135
+ }));
136
+ }
137
+ /**
138
+ * Parses a range response into the readings the page draws.
139
+ *
140
+ * Every check here is one the page would otherwise show as a wrong number: a
141
+ * half hour with no settled reading, a period that does not run for half an
142
+ * hour, an instant that does not move forwards, or a grade the API's own band
143
+ * list does not carry all throw rather than quietly changing the counts.
144
+ *
145
+ * @param payload - the range response, or the committed fixture of one
146
+ * @returns every reading in the window, oldest first, with its totals
147
+ */
148
+ export function parseCarbonIntensityWindow(payload) {
149
+ const parsed = CARBON_INTENSITY_RESPONSE_SCHEMA.safeParse(payload);
150
+ if (!parsed.success) {
151
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, parsed.error.message);
152
+ }
153
+ const responses = parsed.data.data;
154
+ if (responses.length === 0) {
155
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, 'the window returned no readings');
156
+ }
157
+ const bands = new Set(CARBON_INTENSITY_INDEX_BANDS);
158
+ let previousFrom = Number.NEGATIVE_INFINITY;
159
+ const periods = responses.map((response) => {
160
+ const from = instantMilliseconds(response.from);
161
+ const to = instantMilliseconds(response.to);
162
+ if (Number.isNaN(from) || Number.isNaN(to)) {
163
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, `expected an instant as YYYY-MM-DDThh:mmZ, got ${JSON.stringify(response.from)}`);
164
+ }
165
+ if (to - from !== MILLISECONDS_PER_PERIOD) {
166
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, `the half hour starting ${response.from} runs for ${String((to - from) / 60_000)} minutes`);
167
+ }
168
+ if (from <= previousFrom) {
169
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, `the readings do not move forwards at ${response.from}`);
170
+ }
171
+ previousFrom = from;
172
+ if (response.intensity.actual === null) {
173
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, `the half hour starting ${response.from} carries no settled reading`);
174
+ }
175
+ if (!bands.has(response.intensity.index)) {
176
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, `unrecognised intensity grade ${JSON.stringify(response.intensity.index)}`);
177
+ }
178
+ return {
179
+ from: response.from,
180
+ to: response.to,
181
+ intensity: response.intensity.actual,
182
+ index: response.intensity.index,
183
+ };
184
+ });
185
+ const total = periods.reduce((sum, period) => sum + period.intensity, 0);
186
+ const lowestPeriod = periods.reduce((lowest, period) => period.intensity < lowest.intensity ? period : lowest);
187
+ const highestPeriod = periods.reduce((highest, period) => period.intensity > highest.intensity ? period : highest);
188
+ const firstPeriod = periods[0];
189
+ const lastPeriod = periods.at(-1);
190
+ if (firstPeriod === undefined || lastPeriod === undefined) {
191
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, 'the window holds no readings');
192
+ }
193
+ return {
194
+ periodCount: periods.length,
195
+ dayCount: Math.floor(periods.length / PERIODS_PER_DAY),
196
+ firstPeriodFrom: firstPeriod.from,
197
+ lastPeriodTo: lastPeriod.to,
198
+ averageIntensity: Math.round(total / periods.length),
199
+ lowestPeriod,
200
+ highestPeriod,
201
+ bandCounts: countByBand(periods),
202
+ periods,
203
+ };
204
+ }
205
+ /** One window of half-hourly readings, or a throw naming what failed. */
206
+ async function fetchCarbonIntensityPayload(options) {
207
+ const url = buildCarbonIntensityWindowUrl({ from: options.from, to: options.to });
208
+ const response = await options.fetchImpl(url, { headers: { accept: 'application/json' } });
209
+ if (!response.ok) {
210
+ throw new UkSourceApiError(CARBON_INTENSITY_SOURCE_ID, `HTTP ${response.status} from ${url}`);
211
+ }
212
+ return response.json();
213
+ }
214
+ /**
215
+ * Reads a window of half-hourly carbon intensity.
216
+ *
217
+ * The range endpoint also returns the half hour that ends at the window's
218
+ * start, so the first response is dropped: without that, the window is a half
219
+ * hour longer than the whole days it claims to cover.
220
+ *
221
+ * A caller can also pass the window's own start and end, which is how a page
222
+ * aligns the window to whole days in its own time zone rather than to UTC.
223
+ *
224
+ * @param options - how many days, or an explicit window, and an optional fetch implementation
225
+ * @returns every reading in the window, oldest first, with its totals
226
+ */
227
+ export async function fetchCarbonIntensityWindow(options = {}) {
228
+ const { from, to } = options.from === undefined && options.to === undefined
229
+ ? resolveCarbonIntensityWindow(options)
230
+ : validateCarbonIntensityWindow({ from: options.from, to: options.to });
231
+ const payload = await fetchCarbonIntensityPayload({
232
+ from,
233
+ to,
234
+ fetchImpl: options.fetchImpl ?? globalThis.fetch,
235
+ });
236
+ const parsed = CARBON_INTENSITY_RESPONSE_SCHEMA.safeParse(payload);
237
+ if (!parsed.success) {
238
+ throw new UkSourceParseError(CARBON_INTENSITY_SOURCE_ID, parsed.error.message);
239
+ }
240
+ const windowStart = Date.parse(from);
241
+ return parseCarbonIntensityWindow({
242
+ data: parsed.data.data.filter((response) => instantMilliseconds(response.from) >= windowStart),
243
+ });
244
+ }
245
+ /** National Energy System Operator half-hourly carbon intensity, keyless. */
246
+ export const carbonIntensityAdapter = {
247
+ id: CARBON_INTENSITY_SOURCE_ID,
248
+ name: 'National Energy System Operator carbon intensity',
249
+ auth: 'none',
250
+ description: 'Half-hourly carbon intensity for Great Britain, with the cleanest and dirtiest half hours in the window.',
251
+ fetchLive: (options) => fetchCarbonIntensityWindow({
252
+ ...(options?.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
253
+ }),
254
+ parse: parseCarbonIntensityWindow,
255
+ loadFixture: () => parseCarbonIntensityWindow(readFixtureJson('carbon-intensity.json')),
256
+ };
257
+ //# sourceMappingURL=carbonIntensity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"carbonIntensity.js","sourceRoot":"","sources":["../src/carbonIntensity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /** Base error for any UK data source client in this package. */
2
+ export declare class UkSourceError extends Error {
3
+ constructor(message: string, options?: ErrorOptions);
4
+ }
5
+ /** The remote API rejected the request (HTTP error or bad payload). */
6
+ export declare class UkSourceApiError extends UkSourceError {
7
+ readonly source: string;
8
+ constructor(source: string, message: string, options?: ErrorOptions);
9
+ }
10
+ /** The remote payload did not match the expected shape. */
11
+ export declare class UkSourceParseError extends UkSourceError {
12
+ readonly source: string;
13
+ constructor(source: string, message: string, options?: ErrorOptions);
14
+ }
15
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAIpD;AAED,uEAAuE;AACvE,qBAAa,gBAAiB,SAAQ,aAAa;aAE/B,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EAC9B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY;CAKzB;AAED,2DAA2D;AAC3D,qBAAa,kBAAmB,SAAQ,aAAa;aAEjC,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EAC9B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY;CAKzB"}
package/dist/errors.js ADDED
@@ -0,0 +1,26 @@
1
+ /** Base error for any UK data source client in this package. */
2
+ export class UkSourceError extends Error {
3
+ constructor(message, options) {
4
+ super(message, options);
5
+ this.name = 'UkSourceError';
6
+ }
7
+ }
8
+ /** The remote API rejected the request (HTTP error or bad payload). */
9
+ export class UkSourceApiError extends UkSourceError {
10
+ source;
11
+ constructor(source, message, options) {
12
+ super(`${source}: ${message}`, options);
13
+ this.source = source;
14
+ this.name = 'UkSourceApiError';
15
+ }
16
+ }
17
+ /** The remote payload did not match the expected shape. */
18
+ export class UkSourceParseError extends UkSourceError {
19
+ source;
20
+ constructor(source, message, options) {
21
+ super(`${source}: ${message}`, options);
22
+ this.source = source;
23
+ this.name = 'UkSourceParseError';
24
+ }
25
+ }
26
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,MAAM,OAAO,aAAc,SAAQ,KAAK;IACtC,YAAY,OAAe,EAAE,OAAsB;QACjD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAED,uEAAuE;AACvE,MAAM,OAAO,gBAAiB,SAAQ,aAAa;IAE/B;IADlB,YACkB,MAAc,EAC9B,OAAe,EACf,OAAsB;QAEtB,KAAK,CAAC,GAAG,MAAM,KAAK,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;QAJxB,WAAM,GAAN,MAAM,CAAQ;QAK9B,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAED,2DAA2D;AAC3D,MAAM,OAAO,kBAAmB,SAAQ,aAAa;IAEjC;IADlB,YACkB,MAAc,EAC9B,OAAe,EACf,OAAsB;QAEtB,KAAK,CAAC,GAAG,MAAM,KAAK,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;QAJxB,WAAM,GAAN,MAAM,CAAQ;QAK9B,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF"}
@@ -0,0 +1,5 @@
1
+ /** Reads a committed fixture from src/fixtures as parsed JSON. */
2
+ export declare function readFixtureJson(filename: string): unknown;
3
+ /** Reads a committed fixture from src/fixtures as raw text. */
4
+ export declare function readFixtureText(filename: string): string;
5
+ //# sourceMappingURL=fixtures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../src/fixtures.ts"],"names":[],"mappings":"AAGA,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,+DAA+D;AAC/D,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExD"}
@@ -0,0 +1,11 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ /** Reads a committed fixture from src/fixtures as parsed JSON. */
4
+ export function readFixtureJson(filename) {
5
+ return JSON.parse(readFileSync(path.join(process.cwd(), 'src/fixtures', filename), 'utf8'));
6
+ }
7
+ /** Reads a committed fixture from src/fixtures as raw text. */
8
+ export function readFixtureText(filename) {
9
+ return readFileSync(path.join(process.cwd(), 'src/fixtures', filename), 'utf8');
10
+ }
11
+ //# sourceMappingURL=fixtures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixtures.js","sourceRoot":"","sources":["../src/fixtures.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,kEAAkE;AAClE,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAC9F,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;AAClF,CAAC"}
@@ -0,0 +1,70 @@
1
+ import type { UkDataAdapter } from './types.js';
2
+ /** One measure a station publishes, such as water level at 15 minute steps. */
3
+ export interface FloodMeasure {
4
+ id: string;
5
+ parameter: string;
6
+ parameterName: string;
7
+ qualifier: string;
8
+ unitName: string;
9
+ periodSeconds: number;
10
+ }
11
+ /** One monitoring station in the Environment Agency network. */
12
+ export interface FloodStation {
13
+ id: string;
14
+ notation: string;
15
+ label: string;
16
+ riverName: string;
17
+ catchmentName: string;
18
+ /** Null for the handful of groundwater stations that publish no coordinates. */
19
+ latitude: number | null;
20
+ /** Null for the handful of groundwater stations that publish no coordinates. */
21
+ longitude: number | null;
22
+ measures: FloodMeasure[];
23
+ }
24
+ /** One reading from a station measure. */
25
+ export interface FloodReading {
26
+ measureId: string;
27
+ dateTime: string;
28
+ value: number;
29
+ }
30
+ /** Rolled-up facts about a run of readings. */
31
+ export interface FloodReadingSummary {
32
+ count: number;
33
+ latest: FloodReading | undefined;
34
+ earliest: FloodReading | undefined;
35
+ minValue: number | undefined;
36
+ maxValue: number | undefined;
37
+ trend: 'rising' | 'falling' | 'steady' | 'unknown';
38
+ }
39
+ /**
40
+ * Station the readings adapter probes by default: Bourton Dickler on the
41
+ * River Dikler, which publishes level readings every 15 minutes.
42
+ */
43
+ export declare const DEFAULT_FLOOD_STATION_REFERENCE = "1029TH";
44
+ /** Parses an Environment Agency /id/stations payload into stations. */
45
+ export declare function parseFloodStations(payload: unknown): FloodStation[];
46
+ /** Parses an Environment Agency /readings payload into readings. */
47
+ export declare function parseFloodReadings(payload: unknown): FloodReading[];
48
+ /** Summarizes a run of readings: count, ends, range, and direction. */
49
+ export declare function summarizeFloodReadings(readings: FloodReading[]): FloodReadingSummary;
50
+ /**
51
+ * Lists monitoring stations from the Environment Agency flood-monitoring
52
+ * API. Keyless, Open Government Licence v3.
53
+ */
54
+ export declare function fetchFloodStations(options?: {
55
+ limit?: number;
56
+ fetchImpl?: typeof globalThis.fetch;
57
+ }): Promise<FloodStation[]>;
58
+ /**
59
+ * Reads the most recent readings for one station. `_sorted` returns the
60
+ * newest first, so the first item is the current level.
61
+ */
62
+ export declare function fetchFloodStationReadings(stationReference?: string, options?: {
63
+ limit?: number;
64
+ fetchImpl?: typeof globalThis.fetch;
65
+ }): Promise<FloodReading[]>;
66
+ /** Flood-monitoring station list adapter, keyless. */
67
+ export declare const floodStationsAdapter: UkDataAdapter<FloodStation[]>;
68
+ /** Flood-monitoring readings adapter for the default station, keyless. */
69
+ export declare const floodReadingsAdapter: UkDataAdapter<FloodReading[]>;
70
+ //# sourceMappingURL=floodMonitoring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floodMonitoring.d.ts","sourceRoot":"","sources":["../src/floodMonitoring.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,gEAAgE;AAChE,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,gFAAgF;IAChF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED,0CAA0C;AAC1C,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,+CAA+C;AAC/C,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,YAAY,GAAG,SAAS,CAAC;IACjC,QAAQ,EAAE,YAAY,GAAG,SAAS,CAAC;IACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;CACpD;AAED;;;GAGG;AACH,eAAO,MAAM,+BAA+B,WAAW,CAAC;AAoDxD,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,EAAE,CAsBnE;AAED,oEAAoE;AACpE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,EAAE,CAUnE;AAED,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,mBAAmB,CA8BpF;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CAAO,GACpE,OAAO,CAAC,YAAY,EAAE,CAAC,CASzB;AAED;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,gBAAgB,GAAE,MAAwC,EAC1D,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CAAO,GACpE,OAAO,CAAC,YAAY,EAAE,CAAC,CAUzB;AAED,sDAAsD;AACtD,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,YAAY,EAAE,CAS9D,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,YAAY,EAAE,CAY9D,CAAC"}
@@ -0,0 +1,164 @@
1
+ import { z } from 'zod';
2
+ import { UkSourceApiError, UkSourceParseError } from './errors.js';
3
+ import { readFixtureJson } from './fixtures.js';
4
+ /**
5
+ * Station the readings adapter probes by default: Bourton Dickler on the
6
+ * River Dikler, which publishes level readings every 15 minutes.
7
+ */
8
+ export const DEFAULT_FLOOD_STATION_REFERENCE = '1029TH';
9
+ /** How close two readings must be to count as steady, in metres. */
10
+ const STEADY_LEVEL_TOLERANCE_METRES = 0.01;
11
+ const FLOOD_MEASURE_SCHEMA = z.object({
12
+ '@id': z.string(),
13
+ parameter: z.string(),
14
+ parameterName: z.string(),
15
+ qualifier: z.string(),
16
+ unitName: z.string(),
17
+ period: z.number().optional(),
18
+ });
19
+ // The agency returns a bare string for most fields but wraps a few in an
20
+ // array, so both shapes have to be accepted.
21
+ const FLOOD_TEXT_SCHEMA = z.union([z.string(), z.array(z.string())]);
22
+ const FLOOD_STATION_SCHEMA = z.object({
23
+ '@id': z.string(),
24
+ notation: z.string(),
25
+ label: FLOOD_TEXT_SCHEMA,
26
+ riverName: FLOOD_TEXT_SCHEMA.optional(),
27
+ catchmentName: FLOOD_TEXT_SCHEMA.optional(),
28
+ lat: z.number().optional(),
29
+ long: z.number().optional(),
30
+ measures: z.array(FLOOD_MEASURE_SCHEMA).optional(),
31
+ });
32
+ /** Flattens a field that the agency publishes as a string or an array. */
33
+ function readFloodText(value) {
34
+ if (value === undefined) {
35
+ return '';
36
+ }
37
+ return Array.isArray(value) ? value.join(' / ') : value;
38
+ }
39
+ const FLOOD_STATIONS_RESPONSE_SCHEMA = z.object({
40
+ items: z.array(FLOOD_STATION_SCHEMA),
41
+ });
42
+ const FLOOD_READING_SCHEMA = z.object({
43
+ '@id': z.string(),
44
+ dateTime: z.string(),
45
+ measure: z.string(),
46
+ value: z.number(),
47
+ });
48
+ const FLOOD_READINGS_RESPONSE_SCHEMA = z.object({
49
+ items: z.array(FLOOD_READING_SCHEMA),
50
+ });
51
+ /** Parses an Environment Agency /id/stations payload into stations. */
52
+ export function parseFloodStations(payload) {
53
+ const parsed = FLOOD_STATIONS_RESPONSE_SCHEMA.safeParse(payload);
54
+ if (!parsed.success) {
55
+ throw new UkSourceParseError('flood-monitoring', parsed.error.message);
56
+ }
57
+ return parsed.data.items.map((station) => ({
58
+ id: station['@id'],
59
+ notation: station.notation,
60
+ label: readFloodText(station.label),
61
+ riverName: readFloodText(station.riverName),
62
+ catchmentName: readFloodText(station.catchmentName),
63
+ latitude: station.lat ?? null,
64
+ longitude: station.long ?? null,
65
+ measures: (station.measures ?? []).map((measure) => ({
66
+ id: measure['@id'],
67
+ parameter: measure.parameter,
68
+ parameterName: measure.parameterName,
69
+ qualifier: measure.qualifier,
70
+ unitName: measure.unitName,
71
+ periodSeconds: measure.period ?? 0,
72
+ })),
73
+ }));
74
+ }
75
+ /** Parses an Environment Agency /readings payload into readings. */
76
+ export function parseFloodReadings(payload) {
77
+ const parsed = FLOOD_READINGS_RESPONSE_SCHEMA.safeParse(payload);
78
+ if (!parsed.success) {
79
+ throw new UkSourceParseError('flood-monitoring', parsed.error.message);
80
+ }
81
+ return parsed.data.items.map((reading) => ({
82
+ measureId: reading.measure,
83
+ dateTime: reading.dateTime,
84
+ value: reading.value,
85
+ }));
86
+ }
87
+ /** Summarizes a run of readings: count, ends, range, and direction. */
88
+ export function summarizeFloodReadings(readings) {
89
+ if (readings.length === 0) {
90
+ return {
91
+ count: 0,
92
+ latest: undefined,
93
+ earliest: undefined,
94
+ minValue: undefined,
95
+ maxValue: undefined,
96
+ trend: 'unknown',
97
+ };
98
+ }
99
+ const ordered = [...readings].sort((left, right) => left.dateTime.localeCompare(right.dateTime));
100
+ const first = ordered[0];
101
+ const last = ordered[ordered.length - 1];
102
+ const values = ordered.map((reading) => reading.value);
103
+ const movement = last !== undefined && first !== undefined ? last.value - first.value : 0;
104
+ const trend = Math.abs(movement) <= STEADY_LEVEL_TOLERANCE_METRES
105
+ ? 'steady'
106
+ : movement > 0
107
+ ? 'rising'
108
+ : 'falling';
109
+ return {
110
+ count: ordered.length,
111
+ latest: last,
112
+ earliest: first,
113
+ minValue: Math.min(...values),
114
+ maxValue: Math.max(...values),
115
+ trend,
116
+ };
117
+ }
118
+ /**
119
+ * Lists monitoring stations from the Environment Agency flood-monitoring
120
+ * API. Keyless, Open Government Licence v3.
121
+ */
122
+ export async function fetchFloodStations(options = {}) {
123
+ const { limit = 25, fetchImpl = globalThis.fetch } = options;
124
+ const response = await fetchImpl(`https://environment.data.gov.uk/flood-monitoring/id/stations?_limit=${limit}`);
125
+ if (!response.ok) {
126
+ throw new UkSourceApiError('flood-monitoring', `HTTP ${response.status} listing stations`);
127
+ }
128
+ return parseFloodStations(await response.json());
129
+ }
130
+ /**
131
+ * Reads the most recent readings for one station. `_sorted` returns the
132
+ * newest first, so the first item is the current level.
133
+ */
134
+ export async function fetchFloodStationReadings(stationReference = DEFAULT_FLOOD_STATION_REFERENCE, options = {}) {
135
+ const { limit = 96, fetchImpl = globalThis.fetch } = options;
136
+ const url = `https://environment.data.gov.uk/flood-monitoring/id/stations/${stationReference}` +
137
+ `/readings?_limit=${limit}&_sorted`;
138
+ const response = await fetchImpl(url);
139
+ if (!response.ok) {
140
+ throw new UkSourceApiError('flood-monitoring', `HTTP ${response.status} reading station`);
141
+ }
142
+ return parseFloodReadings(await response.json());
143
+ }
144
+ /** Flood-monitoring station list adapter, keyless. */
145
+ export const floodStationsAdapter = {
146
+ id: 'flood-stations',
147
+ name: 'Environment Agency flood-monitoring stations',
148
+ auth: 'none',
149
+ description: 'Monitoring stations with their river, catchment, and published measures.',
150
+ fetchLive: (options) => fetchFloodStations(options?.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
151
+ parse: parseFloodStations,
152
+ loadFixture: () => parseFloodStations(readFixtureJson('flood-stations.json')),
153
+ };
154
+ /** Flood-monitoring readings adapter for the default station, keyless. */
155
+ export const floodReadingsAdapter = {
156
+ id: 'flood-readings',
157
+ name: 'Environment Agency flood-monitoring readings',
158
+ auth: 'none',
159
+ description: 'Recent water level readings for one station, newest first.',
160
+ fetchLive: (options) => fetchFloodStationReadings(DEFAULT_FLOOD_STATION_REFERENCE, options?.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
161
+ parse: parseFloodReadings,
162
+ loadFixture: () => parseFloodReadings(readFixtureJson('flood-station-readings.json')),
163
+ };
164
+ //# sourceMappingURL=floodMonitoring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floodMonitoring.js","sourceRoot":"","sources":["../src/floodMonitoring.ts"],"names":[],"mappings":"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"}