river-data-widget
Advanced tools
+597
| /*! RiverDataWidget v1.2.0 2023-06-17 01:06:58 | ||
| *! https://github.com/pb-uk/river-data-widget#readme | ||
| *! Copyright (C) 2023 pbuk (https://github.com/pb-uk). | ||
| *! License MIT. | ||
| */ | ||
| 'use strict'; | ||
| const FONT_STACK = '-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif'; | ||
| const round3 = (value) => value < 100 ? value.toPrecision(3) : Math.round(value).toString(); | ||
| class FloodMonitoringApiError extends Error { | ||
| constructor(msg, info = {}) { | ||
| super(msg); | ||
| this.name = 'FloodMonitoringApiError'; | ||
| this.info = info; | ||
| } | ||
| } | ||
| const parseMeasureId = (measureId) => { | ||
| // ............base/ stat-paramet-qualifi- type -interva-unit | ||
| const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/; | ||
| const matches = measureId.match(regExp); | ||
| if (matches === null) { | ||
| throw new FloodMonitoringApiError('Cannot parse measure id', { measureId }); | ||
| } | ||
| const [unit, interval, type, qualifier, parameter, stationId] = matches.reverse(); | ||
| const qualifiedParameter = qualifier.length | ||
| ? `${parameter}-${qualifier}` | ||
| : parameter; | ||
| return { | ||
| stationId, | ||
| parameter, | ||
| qualifier, | ||
| type, | ||
| interval, | ||
| unit, | ||
| qualifiedParameter, | ||
| }; | ||
| }; | ||
| const measureTranslations = { | ||
| unit: { | ||
| m3_s: 'm³/s', | ||
| mAOD: 'm', | ||
| mASD: 'm', | ||
| }, | ||
| qualifiedParameter: { | ||
| 'level-stage': 'level', | ||
| 'level-downstage': 'downstream level', | ||
| }, | ||
| }; | ||
| const translateMeasureProperties = (measure) => { | ||
| const translated = {}; | ||
| for (const prop in measure) { | ||
| const value = measure[prop]; | ||
| if (measureTranslations[prop] && measureTranslations[prop][value]) { | ||
| translated[prop] = measureTranslations[prop][value]; | ||
| } | ||
| else { | ||
| translated[prop] = value; | ||
| } | ||
| } | ||
| return translated; | ||
| }; | ||
| /** | ||
| * RiverDataWidget https://github.com/pb-uk/river-data-widget. | ||
| * | ||
| * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk. | ||
| * @license AGPL-3.0-or-later see LICENSE.md. | ||
| */ | ||
| const setAttributes = (el, attributes) => { | ||
| for (const [key, value] of Object.entries(attributes)) { | ||
| el.setAttribute(key, `${value}`); | ||
| } | ||
| return el; | ||
| }; | ||
| const setStyles = (el, styles) => { | ||
| for (const [key, value] of Object.entries(styles)) { | ||
| // Workaround (el.style.setProperty uses kebab-case keys). | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| el.style[key] = value; | ||
| } | ||
| return el; | ||
| }; | ||
| const createSvgElement = (name = 'svg', attributes = {}, styles = {}, innerHTML = false) => { | ||
| const el = document.createElementNS('http://www.w3.org/2000/svg', name); | ||
| if (innerHTML !== false) { | ||
| el.innerHTML = innerHTML; | ||
| } | ||
| return setStyles(setAttributes(el, attributes), styles); | ||
| }; | ||
| const MINUTE_MS = 60000; | ||
| // const HOUR_MS = 3600000; | ||
| const DAY_MS = 86400000; | ||
| /** | ||
| * Get the Date at the start of a day in UTC or local time. | ||
| * | ||
| * @param offset | ||
| * @param timeZone The time zone offset in minutes, or set to `true` to use the | ||
| * local time zone (`false`, the default, uses UTC). | ||
| * @returns The reqested date. | ||
| */ | ||
| const startOfDay = (date = null, offset = 0, timeZone = false) => { | ||
| if (timeZone === false) { | ||
| // Use UTC. | ||
| const base = date === null ? Date.now() : date.valueOf(); | ||
| return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS); | ||
| } | ||
| const now = new Date(); | ||
| const tz = timeZone === true ? now.getTimezoneOffset() : timeZone; | ||
| const local = now.valueOf() + tz * MINUTE_MS; | ||
| return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS); | ||
| }; | ||
| const timeFormatter = new Intl.DateTimeFormat('en-GB', { | ||
| hour: '2-digit', | ||
| minute: '2-digit', | ||
| hour12: false, | ||
| timeZoneName: 'short', | ||
| }); | ||
| const dddFormatter = new Intl.DateTimeFormat('en-GB', { | ||
| weekday: 'short', | ||
| }); | ||
| const dMmmFormatter = new Intl.DateTimeFormat('en-GB', { | ||
| day: 'numeric', | ||
| month: 'short', | ||
| }); | ||
| class Chart { | ||
| constructor(el, series, options = {}) { | ||
| var _a; | ||
| this.strokeWidth = 2; | ||
| this.fontSizePx = 14; | ||
| this.width = 480; // 400; | ||
| this.height = 270; // 225; | ||
| this.plotHeight = this.height - this.fontSizePx * 4.5; | ||
| this.plotWidth = this.width - this.strokeWidth; | ||
| this.plotColor = '#77C'; | ||
| this.labelBg = 'rgba(255,255,255,0.5)'; | ||
| this.labelBgWidth = '0.5em'; | ||
| this.attribution = 'Uses Environment Agency data from the real-time API (Beta)'; | ||
| // CSS settings. | ||
| // Just readable at 320x180. | ||
| // Good from 400x225. | ||
| // Perfect at 480x270 (font is 12px); | ||
| this.styles = { | ||
| 'font-family': FONT_STACK, | ||
| 'font-size': `${this.fontSizePx}px`, | ||
| display: 'block', | ||
| margin: 'auto', | ||
| 'max-width': '150vh', | ||
| }; | ||
| this.series = series; | ||
| this.options = options; | ||
| const viewBox = `0 0 ${this.width} ${this.height}`; | ||
| this.attribution = (_a = options.attribution) !== null && _a !== void 0 ? _a : this.attribution; | ||
| this.el = createSvgElement('svg', { viewBox }, this.styles); | ||
| el.append(this.el); | ||
| } | ||
| getLimits() { | ||
| if (this.limits == null) { | ||
| throw new FloodMonitoringApiError('Chart axis limits have not been set'); | ||
| } | ||
| return this.limits; | ||
| } | ||
| getHorizontalGridlines() { | ||
| const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } = this.getLimits(); | ||
| const xOffset = this.strokeWidth / 2; | ||
| const yOffset = this.plotHeight; | ||
| const x1 = xOffset; | ||
| const x2 = xOffset + (maxTime - minTime) * timeScale; | ||
| // Horizontal grid lines. | ||
| const stroke = '#ddd'; | ||
| const lines = createSvgElement('g', { stroke }); | ||
| const labels = createSvgElement('g'); | ||
| const valueRange = maxValue - minValue; | ||
| // Horizontal grid interval. | ||
| const [interval, exponent] = getInterval(valueRange, 9); | ||
| const factor = 10 ** -exponent; | ||
| const base = Math.ceil((minValue * factor) / interval + 1) * interval; | ||
| let i = 0; | ||
| let current = base / factor; | ||
| while (current < maxValue) { | ||
| const y1 = yOffset - (current - minValue) * valueScale; | ||
| lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 })); | ||
| labels.append(createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`)); | ||
| ++i; | ||
| current = (base + i * interval) / factor; | ||
| } | ||
| const timeAxisLine = createSvgElement('line', { x1, y1: yOffset, x2, y2: yOffset }, { stroke: '#777' }); | ||
| return [lines, labels, timeAxisLine]; | ||
| } | ||
| getTimeScale() { | ||
| const { minTime, maxTime, timeScale } = this.getLimits(); | ||
| const xOffset = this.strokeWidth / 2; | ||
| const yOffset = this.plotHeight + this.strokeWidth / 2; | ||
| const y1 = yOffset + this.fontSizePx * 3; | ||
| const y2 = yOffset - this.plotHeight; | ||
| // Vertical grid lines. | ||
| const stroke = '#ddd'; | ||
| const lines = createSvgElement('g', { stroke }); | ||
| const labels = createSvgElement('g'); | ||
| // Vertical grid interval. | ||
| const base = minTime; | ||
| const interval = 86400; | ||
| let i = 0; | ||
| let current = base; | ||
| const labelOffset = 43200 * timeScale; | ||
| const fill = '#444'; | ||
| while (current <= maxTime) { | ||
| const x1 = xOffset + (current - minTime) * timeScale; | ||
| const d = new Date(current * 1000); | ||
| // lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 })); | ||
| lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 })); | ||
| labels.append(createSvgElement('text', { | ||
| x: x1 + labelOffset, | ||
| y: y1 - this.fontSizePx * 1.8, | ||
| 'text-anchor': 'middle', | ||
| }, { fill }, `${dddFormatter.format(d)}`), createSvgElement('text', { | ||
| x: x1 + labelOffset, | ||
| y: y1 - this.fontSizePx * 0.5, | ||
| 'text-anchor': 'middle', | ||
| }, { fill }, `${dMmmFormatter.format(d)}`)); | ||
| ++i; | ||
| current = base + i * interval; | ||
| } | ||
| return [lines, labels]; | ||
| } | ||
| render() { | ||
| var _a, _b, _c, _d; | ||
| // Calculate axis scales. | ||
| const limits = getLimits(this.series[0].data); | ||
| limits.minValue = (_a = this.series[0].min) !== null && _a !== void 0 ? _a : limits.minValue; | ||
| limits.maxValue = (_b = this.series[0].max) !== null && _b !== void 0 ? _b : limits.maxValue; | ||
| limits.minTime = (_c = this.options.minTime) !== null && _c !== void 0 ? _c : limits.minTime; | ||
| limits.maxTime = (_d = this.options.maxTime) !== null && _d !== void 0 ? _d : limits.maxTime; | ||
| this.limits = Object.assign(Object.assign({}, limits), { valueScale: (this.plotHeight - this.strokeWidth) / | ||
| (limits.maxValue - limits.minValue), timeScale: (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime) }); | ||
| // Time axis. | ||
| const [timeLines, timeLabels] = this.getTimeScale(); | ||
| this.el.append(timeLines); | ||
| // Value axis. | ||
| const [valueLines, valueLabels, timeAxisLine] = this.getHorizontalGridlines(); | ||
| this.el.append(valueLines); | ||
| this.el.append(timeAxisLine); | ||
| this.plotData(); | ||
| // Plot labels on top of the line. | ||
| this.el.append(timeLabels); | ||
| this.el.append(valueLabels); | ||
| this.el.append(createSvgElement('text', { | ||
| x: this.width / 2, | ||
| 'text-anchor': 'middle', | ||
| y: this.height - this.fontSizePx * 0.5, | ||
| }, { fill: '#595959' }, this.attribution)); | ||
| this.plotLastValue(); | ||
| } | ||
| plotLastValue() { | ||
| const { data, unit, formatter } = this.series[0]; | ||
| // If there is no data show a message. | ||
| if (data.length === 0) { | ||
| const x = this.plotWidth / 2; | ||
| const y = this.plotHeight / 2; | ||
| this.el.append(...this.createLargeLabel(x, y, 'No data', 'middle')); | ||
| return; | ||
| } | ||
| const [time, value] = data[data.length - 1]; | ||
| const { minTime, timeScale, maxValue, minValue } = this.getLimits(); | ||
| const v = formatter == null ? value : formatter(value); | ||
| const xOffset = this.strokeWidth / 2; | ||
| // const yOffset = this.plotHeight - this.strokeWidth / 2; | ||
| const x = xOffset + (time - minTime) * timeScale; | ||
| const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5; | ||
| const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2; | ||
| this.el.append( | ||
| // Value label. | ||
| ...this.createLargeLabel(x, y, `${v} ${unit}`), | ||
| // Time label. | ||
| ...this.createLabel(x, y, `${timeFormatter.format(new Date(time * 1000))}`)); | ||
| } | ||
| plotData() { | ||
| const { data } = this.series[0]; | ||
| // Don't do anything we don't have to! | ||
| if (data.length === 0) | ||
| return; | ||
| const xOffset = this.strokeWidth / 2; | ||
| const yOffset = this.plotHeight - this.strokeWidth / 2; | ||
| const { minTime, timeScale, minValue, valueScale } = this.getLimits(); | ||
| // First data point. | ||
| const x = xOffset + (data[0][0] - minTime) * timeScale; | ||
| const y = yOffset - (data[0][1] - minValue) * valueScale; | ||
| const points = [`M${x},${y}`]; | ||
| // Remaining data points. | ||
| for (let i = 1; i < data.length; ++i) { | ||
| const x = xOffset + (data[i][0] - minTime) * timeScale; | ||
| const y = yOffset - (data[i][1] - minValue) * valueScale; | ||
| points.push(`L${x},${y}`); | ||
| } | ||
| // Plot the data. | ||
| const path = createSvgElement('path', { | ||
| d: points.join(''), | ||
| stroke: this.plotColor, | ||
| 'stroke-width': this.strokeWidth, | ||
| fill: 'none', | ||
| }); | ||
| this.el.append(path); | ||
| } | ||
| createLabel(x, y, text, anchor = 'end') { | ||
| return [ | ||
| // Background for time label. | ||
| createSvgElement('text', { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor }, { | ||
| stroke: this.labelBg, | ||
| 'stroke-width': this.labelBgWidth, | ||
| }, text), | ||
| // Time label. | ||
| createSvgElement('text', { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor }, { fill: this.plotColor }, text), | ||
| ]; | ||
| } | ||
| createLargeLabel(x, y, text, anchor = 'end') { | ||
| return [ | ||
| // Background for value label. | ||
| createSvgElement('text', { x, y, 'text-anchor': anchor }, { | ||
| 'font-size': '1.5em', | ||
| 'font-weight': 'bold', | ||
| stroke: this.labelBg, | ||
| 'stroke-width': this.labelBgWidth, | ||
| }, text), | ||
| // Value label. | ||
| createSvgElement('text', { x, y, 'text-anchor': anchor }, { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' }, text), | ||
| ]; | ||
| } | ||
| } | ||
| const getLimits = (data) => { | ||
| if (data.length < 1) { | ||
| return { minTime: 0, maxTime: 1, minValue: 0, maxValue: 0 }; | ||
| } | ||
| const minTime = data[0][0]; | ||
| const maxTime = data[data.length - 1][0]; | ||
| let minValue = Infinity; | ||
| let maxValue = -minValue; | ||
| for (const [, value] of data) { | ||
| minValue = Math.min(minValue, value); | ||
| maxValue = Math.max(maxValue, value); | ||
| } | ||
| return { minTime, maxTime, minValue, maxValue }; | ||
| }; | ||
| const getInterval = (range, maxDivisions) => { | ||
| const exponent = Math.floor(Math.log10(range)) - 1; | ||
| const k = range / (maxDivisions * 10 ** exponent); | ||
| const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10; | ||
| return [mantissa, exponent]; | ||
| }; | ||
| // There is no need to be secure about this! | ||
| const baseUrl = 'http://environment.data.gov.uk/flood-monitoring'; | ||
| const apiFetch = async (path, query = {}) => { | ||
| const queryString = new URLSearchParams(query).toString(); | ||
| const uri = queryString | ||
| ? `${baseUrl}${path}?${queryString}` | ||
| : `${baseUrl}${path}`; | ||
| const response = await fetch(uri); | ||
| return { data: await response.json(), response }; | ||
| }; | ||
| /** | ||
| * Convert a Date to a format recognized by the EA API for a query parameter. | ||
| * | ||
| * @param date Convert from. | ||
| * @returns A string in the EA API query parameter format. | ||
| */ | ||
| const toTimeParameter = (date) => { | ||
| return date.toISOString().substring(0, 19) + 'Z'; | ||
| }; | ||
| /* | ||
| Useful response headers | ||
| Date: 'Sat, 13 May 2023 09:14:07 GMT', | ||
| last-modified: Sat, 13 May 2023 09:03:13 GMT, | ||
| Response meta: | ||
| publisher: 'Environment Agency', | ||
| license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/', | ||
| documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference', | ||
| version: '0.9', | ||
| comment: 'Status: Beta service', | ||
| hasFormat: [ | ||
| "http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z", | ||
| "http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z", | ||
| "http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z", | ||
| "http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z" | ||
| ], | ||
| */ | ||
| const prefix = 'riverDataWidget'; | ||
| const addPrefix = (key) => `${prefix}|${key}`; | ||
| let instance; | ||
| class Store { | ||
| clear(destroy = false) { | ||
| for (const key of this.keys()) { | ||
| localStorage.removeItem(addPrefix(key)); | ||
| } | ||
| if (destroy) { | ||
| localStorage.removeItem(prefix); | ||
| return; | ||
| } | ||
| localStorage.setItem(prefix, JSON.stringify([])); | ||
| } | ||
| get(key) { | ||
| const value = localStorage.getItem(addPrefix(key)); | ||
| return value === null ? null : JSON.parse(value); | ||
| } | ||
| has(key) { | ||
| return this.keys().includes(key); | ||
| } | ||
| /** | ||
| * Detect active localStorage. | ||
| * | ||
| * @returns true iff localStorage for the widget is active. | ||
| */ | ||
| isActive() { | ||
| return localStorage.getItem(prefix) !== null; | ||
| } | ||
| keys() { | ||
| const storedKeys = localStorage.getItem(prefix); | ||
| return storedKeys === null ? [] : JSON.parse(storedKeys); | ||
| } | ||
| set(key, value) { | ||
| const json = JSON.stringify(value); | ||
| const storedKeys = localStorage.getItem(prefix); | ||
| const keys = storedKeys === null ? [] : JSON.parse(storedKeys); | ||
| if (!keys.includes(key)) { | ||
| keys.push(key); | ||
| localStorage.setItem(prefix, JSON.stringify(keys)); | ||
| } | ||
| localStorage.setItem(addPrefix(key), json); | ||
| } | ||
| unset(key) { | ||
| // Remove it before we do anything else. | ||
| localStorage.removeItem(addPrefix(key)); | ||
| // Then remove it from the list of keys. | ||
| const storedKeys = localStorage.getItem(prefix); | ||
| const keys = storedKeys === null ? [] : JSON.parse(storedKeys); | ||
| const index = keys.indexOf(key); | ||
| // If it doesn't exist we don't have to remove it. | ||
| if (index === -1) | ||
| return false; | ||
| keys.splice(index, 1); | ||
| localStorage.setItem(prefix, JSON.stringify(keys)); | ||
| return true; | ||
| } | ||
| } | ||
| const useStore = () => { | ||
| if (!instance) { | ||
| instance = new Store(); | ||
| } | ||
| return instance; | ||
| }; | ||
| // Throttle requests to five minutes. | ||
| const THROTTLE_MS = 5 * MINUTE_MS; | ||
| /** | ||
| * Fetch the readings for a measure. | ||
| * | ||
| * @param id The EA measure id. | ||
| * @returns A promise for an array of readings for the measure. | ||
| */ | ||
| const fetchMeasureReadings = async (id, options = {}) => { | ||
| // Set the parameters for the request. | ||
| const params = { _sorted: '' }; | ||
| if (options.since) { | ||
| params.since = toTimeParameter(options.since); | ||
| } | ||
| // Get the response, casting the items to ReadingDTOs. | ||
| const response = (await apiFetch(`/id/measures/${id}/readings`, params)); | ||
| return [parseReadings(response.data.items)[id] || [], response]; | ||
| }; | ||
| const filterSince = (data, since) => { | ||
| const position = data.findIndex((reading) => reading[0] >= since); | ||
| return position < 0 ? [] : data.slice(position); | ||
| }; | ||
| /** | ||
| * Get the readings for a measure. | ||
| * | ||
| * @todo Caching and throttling. | ||
| * | ||
| * @param id The EA measure id. | ||
| * @returns A promise for an array of readings for the measure. | ||
| */ | ||
| const getMeasureReadings = async (id, options = {}) => { | ||
| // Get the saved readings. | ||
| const key = `readings|${id}`; | ||
| const store = useStore(); | ||
| const stored = store.get(key) || { | ||
| data: [], | ||
| lastCheck: 0, | ||
| storedSince: Infinity, | ||
| }; | ||
| const { data, lastCheck } = stored; | ||
| let { storedSince } = stored; | ||
| const discardBefore = startOfDay(null, -8, true).valueOf() / 1000; | ||
| // Discard any older than 30 days. | ||
| while (data.length && data[0][0] < discardBefore) { | ||
| [storedSince] = data[0]; | ||
| data.shift(); | ||
| } | ||
| // If we have data early enough apply throttle. | ||
| const lastStored = data.length ? data[data.length - 1][0] : 0; | ||
| const requestedSince = (options.since && options.since.valueOf() / 1000) || 0; | ||
| if (storedSince <= requestedSince && | ||
| Date.now() < lastCheck * 1000 + THROTTLE_MS) { | ||
| // Throttled. | ||
| return filterSince(data, requestedSince); | ||
| } | ||
| const fetchOptions = Object.assign(Object.assign({}, options), { since: new Date(Math.max(requestedSince, lastStored) * 1000) }); | ||
| const [newData] = await fetchMeasureReadings(id, fetchOptions); | ||
| mergeReadings(data, newData); | ||
| storedSince = Math.min(requestedSince, storedSince); | ||
| store.set(key, { lastCheck: Date.now() / 1000, data, storedSince }); | ||
| return filterSince(data, requestedSince); | ||
| }; | ||
| const mergeReadings = (first, second) => { | ||
| if (!second.length) | ||
| return; | ||
| let firstPos = first.length - 1; | ||
| while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) { | ||
| --firstPos; | ||
| } | ||
| first.splice(firstPos + 1, Infinity, ...second); | ||
| }; | ||
| const parseReadings = (items) => { | ||
| const ranges = {}; | ||
| for (const { measure, dateTime, value } of items) { | ||
| if (ranges[measure] == null) { | ||
| ranges[measure] = []; | ||
| } | ||
| ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]); | ||
| } | ||
| const rangesById = {}; | ||
| for (const [key, range] of Object.entries(ranges)) { | ||
| rangesById[key.substring(key.lastIndexOf('/') + 1)] = range; | ||
| } | ||
| return rangesById; | ||
| }; | ||
| const drawMeasureWidget = async (parentEl, measureId, options = {}) => { | ||
| var _a; | ||
| // Get readings for the last 7 days in local time. | ||
| const since = startOfDay(null, -7, true); | ||
| let data = []; | ||
| // Get the right API. | ||
| const parts = measureId.split('/'); | ||
| const id = (_a = parts.pop()) !== null && _a !== void 0 ? _a : ''; | ||
| const api = parts.length === 0 ? 'flood' : parts[0]; | ||
| switch (api) { | ||
| case 'flood': | ||
| data = await getMeasureReadings(id, { since }); | ||
| } | ||
| // Clear the GUI deck. | ||
| parentEl.replaceChildren(); | ||
| const measure = parseMeasureId(measureId); | ||
| const { unit } = translateMeasureProperties(measure); | ||
| const series1 = { data, unit, formatter: round3 }; | ||
| // Set max/min options for plot from widget options. | ||
| if (options.riverDataWidgetMaxValue != null) { | ||
| series1.max = parseFloat(options.riverDataWidgetMaxValue); | ||
| } | ||
| if (options.riverDataWidgetMinValue != null) { | ||
| series1.min = parseFloat(options.riverDataWidgetMinValue); | ||
| } | ||
| // Deal with no data. | ||
| if (data.length === 0) { | ||
| const minTime = since.valueOf() / 1000; | ||
| const maxTime = minTime + 86400 * 7; | ||
| const chartOptions = { minTime, maxTime }; | ||
| const chart = new Chart(parentEl, [series1], chartOptions); | ||
| chart.render(); | ||
| return; | ||
| } | ||
| const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000; | ||
| const maxTime = startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000; | ||
| const chartOptions = { | ||
| minTime, | ||
| maxTime, | ||
| // attribution: `www.riverdata.co.uk/station/${measure.stationId}`, | ||
| }; | ||
| const chart = new Chart(parentEl, [series1], chartOptions); | ||
| chart.render(); | ||
| }; | ||
| /** | ||
| * RiverDataWidget https://github.com/pb-uk/river-data-widget. | ||
| * | ||
| * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk. | ||
| * @license AGPL-3.0-or-later see LICENSE.md. | ||
| */ | ||
| const version = '1.2.0'; | ||
| exports.drawMeasureWidget = drawMeasureWidget; | ||
| exports.version = version; | ||
| //# sourceMappingURL=index.cjs.map |
| {"version":3,"file":"index.cjs","sources":["src/helpers/format.ts","src/flood-monitoring-api/error.ts","src/flood-monitoring-api/measure.ts","src/helpers/dom.ts","src/helpers/time.ts","src/widget/chart.ts","src/flood-monitoring-api/api.ts","src/flood-monitoring-api/store.ts","src/flood-monitoring-api/reading.ts","src/widget/render.ts","src/index.ts"],"sourcesContent":["export const FONT_STACK =\n '-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Roboto\",\"Helvetica Neue\",Arial,sans-serif';\n\nexport const round3 = (value: number) =>\n value < 100 ? value.toPrecision(3) : Math.round(value).toString();\n","export class FloodMonitoringApiError extends Error {\n public info: Record<string, unknown>;\n\n constructor(msg: string, info: Record<string, unknown> = {}) {\n super(msg);\n this.name = 'FloodMonitoringApiError';\n this.info = info;\n }\n}\n","import { FloodMonitoringApiError } from './error';\n\nexport { parseMeasureId };\n\nconst parseMeasureId = (measureId: string) => {\n // ............base/ stat-paramet-qualifi- type -interva-unit\n const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/;\n const matches = measureId.match(regExp);\n if (matches === null) {\n throw new FloodMonitoringApiError('Cannot parse measure id', { measureId });\n }\n const [unit, interval, type, qualifier, parameter, stationId] =\n matches.reverse();\n const qualifiedParameter = qualifier.length\n ? `${parameter}-${qualifier}`\n : parameter;\n return {\n stationId,\n parameter,\n qualifier,\n type,\n interval,\n unit,\n qualifiedParameter,\n };\n};\n\nconst measureTranslations: Record<string, Record<string, string>> = {\n unit: {\n m3_s: 'm³/s',\n mAOD: 'm',\n mASD: 'm',\n },\n qualifiedParameter: {\n 'level-stage': 'level',\n 'level-downstage': 'downstream level',\n },\n};\n\nexport const translateMeasureProperties = (measure: Record<string, string>) => {\n const translated: Record<string, string> = {};\n for (const prop in measure) {\n const value = measure[prop];\n if (measureTranslations[prop] && measureTranslations[prop][value]) {\n translated[prop] = measureTranslations[prop][value];\n } else {\n translated[prop] = value;\n }\n }\n return translated;\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { createElement, createSvgElement, setAttributes, setStyles };\n\ntype AttributeList = Record<string, string | number>;\n\nconst setAttributes = <T extends HTMLElement | SVGElement>(\n el: T,\n attributes: AttributeList\n): T => {\n for (const [key, value] of Object.entries(attributes)) {\n el.setAttribute(key, `${value}`);\n }\n return el;\n};\n\nconst setStyles = <T extends HTMLElement | SVGElement>(\n el: T,\n styles: AttributeList\n): T => {\n for (const [key, value] of Object.entries(styles)) {\n // Workaround (el.style.setProperty uses kebab-case keys).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (<any>el.style)[key] = value;\n }\n return el;\n};\n\nconst createElement = (\n name = 'div',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n): HTMLElement => {\n const el = document.createElement(name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n\nconst createSvgElement = (\n name = 'svg',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n) => {\n const el = document.createElementNS('http://www.w3.org/2000/svg', name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n","export const MINUTE_MS = 60000;\n// const HOUR_MS = 3600000;\nexport const DAY_MS = 86400000;\n\n/**\n * Get the Date at the start of a day in UTC or local time.\n *\n * @param offset\n * @param timeZone The time zone offset in minutes, or set to `true` to use the\n * local time zone (`false`, the default, uses UTC).\n * @returns The reqested date.\n */\nexport const startOfDay = (\n date: Date | null = null,\n offset = 0,\n timeZone: boolean | number = false\n): Date => {\n if (timeZone === false) {\n // Use UTC.\n const base = date === null ? Date.now() : date.valueOf();\n return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS);\n }\n\n const now = new Date();\n const tz = timeZone === true ? now.getTimezoneOffset() : timeZone;\n const local = now.valueOf() + tz * MINUTE_MS;\n return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS);\n};\n\n/**\n * | | long |short|narrow|numeric|2-digit|\n * |:-------:|:-----------:|:---:|:----:|:-----:|:-----:|\n * | weekday | Monday | Mon | M | | |\n * | era | Anno Domini | AD | A | | |\n * | year | | | | 2012 | 12 |\n * | month | March | Mar | M | 3 | 03 |\n * | day | | | | 1 | 01 |\n * | hour | | | | 1 | 01 |\n * | minute | | | | 1 | 01 |\n * | second | | | | 1 | 01 |\n *\n * * fractionalSecondDigits: 1, 2 or 3 for number of digits.\n * * timeZoneName: long (Pacific Standard Time), short (PST),\n * longOffset (GMT-0800), shortOffset (GMT-8), longGeneric (Pacific Time),\n * shortGeneric (PT).\n */\n\nexport const dateFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n // year: 'numeric',\n});\n\nexport const timeFormatter = new Intl.DateTimeFormat('en-GB', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n timeZoneName: 'short',\n});\n\nexport const dddFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'short',\n});\n\nexport const dMmmFormatter = new Intl.DateTimeFormat('en-GB', {\n day: 'numeric',\n month: 'short',\n});\n","import { createSvgElement } from '../helpers/dom';\nimport { timeFormatter, dddFormatter, dMmmFormatter } from '../helpers/time';\nimport { FloodMonitoringApiError } from '../flood-monitoring-api/error';\nimport { FONT_STACK } from '../helpers/format';\n\nexport interface ChartOptions {\n minTime?: number;\n maxTime?: number;\n attribution?: string;\n}\n\nexport interface ChartScaleLimits {\n minTime: number;\n maxTime: number;\n timeScale: number;\n minValue: number;\n maxValue: number;\n valueScale: number;\n}\n\nexport interface ChartSeries {\n data: TimeSeriesValue[];\n min?: number;\n max?: number;\n unit?: string;\n formatter?: (value: number) => string;\n}\n\nexport type TimeSeriesValue = [\n ts: number, // Unix time stamp (seconds).\n v: number // Value.\n];\n\nexport class Chart {\n protected strokeWidth = 2;\n protected fontSizePx = 14;\n\n protected el: SVGElement;\n protected series: ChartSeries[];\n protected options: ChartOptions;\n\n protected width = 480; // 400;\n protected height = 270; // 225;\n protected plotHeight = this.height - this.fontSizePx * 4.5;\n protected plotWidth = this.width - this.strokeWidth;\n\n protected limits?: ChartScaleLimits;\n\n protected plotColor = '#77C';\n protected labelBg = 'rgba(255,255,255,0.5)';\n protected labelBgWidth = '0.5em';\n\n protected attribution =\n 'Uses Environment Agency data from the real-time API (Beta)';\n\n // CSS settings.\n // Just readable at 320x180.\n // Good from 400x225.\n // Perfect at 480x270 (font is 12px);\n protected styles = {\n 'font-family': FONT_STACK,\n 'font-size': `${this.fontSizePx}px`,\n display: 'block',\n margin: 'auto',\n 'max-width': '150vh',\n };\n\n constructor(\n el: HTMLElement,\n series: ChartSeries[],\n options: ChartOptions = {}\n ) {\n this.series = series;\n this.options = options;\n const viewBox = `0 0 ${this.width} ${this.height}`;\n this.attribution = options.attribution ?? this.attribution;\n this.el = createSvgElement('svg', { viewBox }, this.styles);\n el.append(this.el);\n }\n\n getLimits(): ChartScaleLimits {\n if (this.limits == null) {\n throw new FloodMonitoringApiError('Chart axis limits have not been set');\n }\n return this.limits;\n }\n\n getHorizontalGridlines(): SVGElement[] {\n const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } =\n this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight;\n const x1 = xOffset;\n const x2 = xOffset + (maxTime - minTime) * timeScale;\n // Horizontal grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n const valueRange = maxValue - minValue;\n // Horizontal grid interval.\n const [interval, exponent] = getInterval(valueRange, 9);\n const factor = 10 ** -exponent;\n const base = Math.ceil((minValue * factor) / interval + 1) * interval;\n let i = 0;\n let current = base / factor;\n while (current < maxValue) {\n const y1 = yOffset - (current - minValue) * valueScale;\n lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n labels.append(\n createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`)\n );\n ++i;\n current = (base + i * interval) / factor;\n }\n const timeAxisLine = createSvgElement(\n 'line',\n { x1, y1: yOffset, x2, y2: yOffset },\n { stroke: '#777' }\n );\n\n return [lines, labels, timeAxisLine];\n }\n\n getTimeScale(): SVGElement[] {\n const { minTime, maxTime, timeScale } = this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight + this.strokeWidth / 2;\n const y1 = yOffset + this.fontSizePx * 3;\n const y2 = yOffset - this.plotHeight;\n // Vertical grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n // Vertical grid interval.\n const base = minTime;\n const interval = 86400;\n let i = 0;\n let current = base;\n const labelOffset = 43200 * timeScale;\n const fill = '#444';\n while (current <= maxTime) {\n const x1 = xOffset + (current - minTime) * timeScale;\n const d = new Date(current * 1000);\n // lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 }));\n labels.append(\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 1.8,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dddFormatter.format(d)}`\n ),\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 0.5,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dMmmFormatter.format(d)}`\n )\n );\n ++i;\n current = base + i * interval;\n }\n return [lines, labels];\n }\n\n render() {\n // Calculate axis scales.\n const limits = getLimits(this.series[0].data);\n limits.minValue = this.series[0].min ?? limits.minValue;\n limits.maxValue = this.series[0].max ?? limits.maxValue;\n limits.minTime = this.options.minTime ?? limits.minTime;\n limits.maxTime = this.options.maxTime ?? limits.maxTime;\n\n this.limits = {\n ...limits,\n valueScale:\n (this.plotHeight - this.strokeWidth) /\n (limits.maxValue - limits.minValue),\n timeScale:\n (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime),\n };\n\n // Time axis.\n const [timeLines, timeLabels] = this.getTimeScale();\n this.el.append(timeLines);\n\n // Value axis.\n const [valueLines, valueLabels, timeAxisLine] =\n this.getHorizontalGridlines();\n this.el.append(valueLines);\n this.el.append(timeAxisLine);\n\n this.plotData();\n\n // Plot labels on top of the line.\n this.el.append(timeLabels);\n this.el.append(valueLabels);\n\n this.el.append(\n createSvgElement(\n 'text',\n {\n x: this.width / 2,\n 'text-anchor': 'middle',\n y: this.height - this.fontSizePx * 0.5,\n },\n { fill: '#595959' },\n this.attribution\n )\n );\n\n this.plotLastValue();\n }\n\n plotLastValue() {\n const { data, unit, formatter } = this.series[0];\n\n // If there is no data show a message.\n if (data.length === 0) {\n const x = this.plotWidth / 2;\n const y = this.plotHeight / 2;\n this.el.append(...this.createLargeLabel(x, y, 'No data', 'middle'));\n return;\n }\n\n const [time, value] = data[data.length - 1];\n const { minTime, timeScale, maxValue, minValue } = this.getLimits();\n\n const v = formatter == null ? value : formatter(value);\n const xOffset = this.strokeWidth / 2;\n // const yOffset = this.plotHeight - this.strokeWidth / 2;\n const x = xOffset + (time - minTime) * timeScale;\n const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5;\n const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2;\n\n this.el.append(\n // Value label.\n ...this.createLargeLabel(x, y, `${v} ${unit}`),\n // Time label.\n ...this.createLabel(\n x,\n y,\n `${timeFormatter.format(new Date(time * 1000))}`\n )\n );\n }\n\n plotData() {\n const { data } = this.series[0];\n // Don't do anything we don't have to!\n if (data.length === 0) return;\n\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight - this.strokeWidth / 2;\n const { minTime, timeScale, minValue, valueScale } = this.getLimits();\n // First data point.\n const x = xOffset + (data[0][0] - minTime) * timeScale;\n const y = yOffset - (data[0][1] - minValue) * valueScale;\n const points = [`M${x},${y}`];\n // Remaining data points.\n for (let i = 1; i < data.length; ++i) {\n const x = xOffset + (data[i][0] - minTime) * timeScale;\n const y = yOffset - (data[i][1] - minValue) * valueScale;\n points.push(`L${x},${y}`);\n }\n // Plot the data.\n const path = createSvgElement('path', {\n d: points.join(''),\n stroke: this.plotColor,\n 'stroke-width': this.strokeWidth,\n fill: 'none',\n });\n this.el.append(path);\n }\n\n protected createLabel(x: number, y: number, text: string, anchor = 'end') {\n return [\n // Background for time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor },\n {\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n text\n ),\n // Time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor },\n { fill: this.plotColor },\n text\n ),\n ];\n }\n\n protected createLargeLabel(\n x: number,\n y: number,\n text: string,\n anchor = 'end'\n ) {\n return [\n // Background for value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': anchor },\n {\n 'font-size': '1.5em',\n 'font-weight': 'bold',\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n text\n ),\n // Value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': anchor },\n { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' },\n text\n ),\n ];\n }\n}\n\nexport const getLimits = (data: TimeSeriesValue[]) => {\n if (data.length < 1) {\n return { minTime: 0, maxTime: 1, minValue: 0, maxValue: 0 };\n }\n const minTime = data[0][0];\n const maxTime = data[data.length - 1][0];\n let minValue = Infinity;\n let maxValue = -minValue;\n for (const [, value] of data) {\n minValue = Math.min(minValue, value);\n maxValue = Math.max(maxValue, value);\n }\n return { minTime, maxTime, minValue, maxValue };\n};\n\nexport const getInterval = (range: number, maxDivisions: number) => {\n const exponent = Math.floor(Math.log10(range)) - 1;\n const k = range / (maxDivisions * 10 ** exponent);\n const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10;\n return [mantissa, exponent];\n};\n","// There is no need to be secure about this!\nconst baseUrl = 'http://environment.data.gov.uk/flood-monitoring';\n\nexport interface ApiResponse<T> {\n data: {\n items: T;\n };\n response: Response;\n}\n\nexport interface ApiParameters {\n since?: string; // Time from.\n _sorted?: ''; // Flag for sorting.\n}\n\nexport const apiFetch = async (\n path: string,\n query = {}\n): Promise<ApiResponse<unknown>> => {\n const queryString = new URLSearchParams(query).toString();\n const uri = queryString\n ? `${baseUrl}${path}?${queryString}`\n : `${baseUrl}${path}`;\n const response = await fetch(uri);\n return { data: await response.json(), response };\n};\n\n/**\n * Convert a Date to a format recognized by the EA API for a query parameter.\n *\n * @param date Convert from.\n * @returns A string in the EA API query parameter format.\n */\nexport const toTimeParameter = (date: Date): string => {\n return date.toISOString().substring(0, 19) + 'Z';\n};\n\n/*\nUseful response headers\n Date: 'Sat, 13 May 2023 09:14:07 GMT',\n last-modified: Sat, 13 May 2023 09:03:13 GMT,\nResponse meta:\n publisher: 'Environment Agency',\n license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/',\n documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference',\n version: '0.9',\n comment: 'Status: Beta service',\n hasFormat: [\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z\"\n ],\n*/\n","const prefix = 'riverDataWidget';\n\nconst addPrefix = (key: string): string => `${prefix}|${key}`;\n\nlet instance: Store;\n\nclass Store {\n clear(destroy = false) {\n for (const key of this.keys()) {\n localStorage.removeItem(addPrefix(key));\n }\n if (destroy) {\n localStorage.removeItem(prefix);\n return;\n }\n localStorage.setItem(prefix, JSON.stringify([]));\n }\n\n get(key: string) {\n const value = localStorage.getItem(addPrefix(key));\n return value === null ? null : JSON.parse(value);\n }\n\n has(key: string): boolean {\n return this.keys().includes(key);\n }\n\n /**\n * Detect active localStorage.\n *\n * @returns true iff localStorage for the widget is active.\n */\n isActive() {\n return localStorage.getItem(prefix) !== null;\n }\n\n keys(): string[] {\n const storedKeys = localStorage.getItem(prefix);\n return storedKeys === null ? [] : JSON.parse(storedKeys);\n }\n\n set(key: string, value: unknown) {\n const json = JSON.stringify(value);\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n if (!keys.includes(key)) {\n keys.push(key);\n localStorage.setItem(prefix, JSON.stringify(keys));\n }\n localStorage.setItem(addPrefix(key), json);\n }\n\n unset(key: string): boolean {\n // Remove it before we do anything else.\n localStorage.removeItem(addPrefix(key));\n\n // Then remove it from the list of keys.\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n const index = keys.indexOf(key);\n\n // If it doesn't exist we don't have to remove it.\n if (index === -1) return false;\n\n keys.splice(index, 1);\n localStorage.setItem(prefix, JSON.stringify(keys));\n return true;\n }\n}\n\nexport const useStore = (): Store => {\n if (!instance) {\n instance = new Store();\n }\n return instance;\n};\n","import { apiFetch, toTimeParameter } from './api';\nimport { useStore } from './store';\nimport { MINUTE_MS, startOfDay } from '../helpers/time';\n\nimport type { ApiParameters, ApiResponse } from './api';\n\n// Throttle requests to five minutes.\nconst THROTTLE_MS = 5 * MINUTE_MS;\n\n/**\n * Internal format for readings.\n */\nexport type Reading = [\n timestamp: number, // Unix epoch timestamp (seconds).\n value: number // Value.\n];\n\n/**\n * Internal format for readings.\n */\nexport interface ReadingOptions {\n since?: Date; // Time from.\n}\n\n/**\n * Internal format for readings.\n */\ntype ReadingResponse = [a: Reading[], b: ApiResponse<ReadingDTO[]>];\n\n/**\n * Data transfer object for readings provided by the API.\n */\ninterface ReadingDTO {\n '@id': string; // The URL of this reading.\n dateTime: string; // e.g. '2023-05-13T09:00:00Z'.\n measure: string; // The URL of the measure.\n value: number; // The value in the appropriate units.\n}\n\ninterface StoredReadings {\n storedSince: number;\n lastCheck: number;\n data: Reading[];\n}\n\n/**\n * Fetch the readings for a measure.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nconst fetchMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<ReadingResponse> => {\n // Set the parameters for the request.\n const params: ApiParameters = { _sorted: '' };\n if (options.since) {\n params.since = toTimeParameter(options.since);\n }\n // Get the response, casting the items to ReadingDTOs.\n const response = <ApiResponse<ReadingDTO[]>>(\n await apiFetch(`/id/measures/${id}/readings`, params)\n );\n return [parseReadings(response.data.items)[id] || [], response];\n};\n\nexport const filterSince = (data: Reading[], since: number) => {\n const position = data.findIndex((reading) => reading[0] >= since);\n return position < 0 ? [] : data.slice(position);\n};\n\n/**\n * Get the readings for a measure.\n *\n * @todo Caching and throttling.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nexport const getMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<Reading[]> => {\n // Get the saved readings.\n const key = `readings|${id}`;\n const store = useStore();\n\n const stored: StoredReadings = store.get(key) || {\n data: [],\n lastCheck: 0,\n storedSince: Infinity,\n };\n const { data, lastCheck } = stored;\n let { storedSince } = stored;\n\n const discardBefore = startOfDay(null, -8, true).valueOf() / 1000;\n\n // Discard any older than 30 days.\n while (data.length && data[0][0] < discardBefore) {\n [storedSince] = data[0];\n data.shift();\n }\n\n // If we have data early enough apply throttle.\n const lastStored = data.length ? data[data.length - 1][0] : 0;\n const requestedSince = (options.since && options.since.valueOf() / 1000) || 0;\n if (\n storedSince <= requestedSince &&\n Date.now() < lastCheck * 1000 + THROTTLE_MS\n ) {\n // Throttled.\n return filterSince(data, requestedSince);\n }\n\n const fetchOptions: ReadingOptions = {\n ...options,\n since: new Date(Math.max(requestedSince, lastStored) * 1000),\n };\n\n const [newData] = await fetchMeasureReadings(id, fetchOptions);\n mergeReadings(data, newData);\n storedSince = Math.min(requestedSince, storedSince);\n store.set(key, { lastCheck: Date.now() / 1000, data, storedSince });\n return filterSince(data, requestedSince);\n};\n\nexport const mergeReadings = (first: Reading[], second: Reading[]): void => {\n if (!second.length) return;\n\n let firstPos = first.length - 1;\n while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) {\n --firstPos;\n }\n first.splice(firstPos + 1, Infinity, ...second);\n};\n\nconst parseReadings = (items: ReadingDTO[]): Record<string, Reading[]> => {\n const ranges: Record<string, Reading[]> = {};\n for (const { measure, dateTime, value } of items) {\n if (ranges[measure] == null) {\n ranges[measure] = [];\n }\n ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]);\n }\n\n const rangesById: Record<string, Reading[]> = {};\n for (const [key, range] of Object.entries(ranges)) {\n rangesById[key.substring(key.lastIndexOf('/') + 1)] = range;\n }\n\n return rangesById;\n};\n","import { RiverDataWidgetError } from '../error';\nimport { round3 } from '../helpers/format';\nimport {\n parseMeasureId,\n translateMeasureProperties,\n} from '../flood-monitoring-api/measure';\nimport { Chart } from './chart';\nimport { getMeasureReadings as getFloodMeasureReadings } from '../flood-monitoring-api';\nimport { startOfDay } from '../helpers/time';\n\nimport type { ChartSeries } from './chart';\nimport type { Reading } from '../flood-monitoring-api/reading';\n\nexport const drawMeasureWidget = async (\n parentEl: HTMLElement,\n measureId: string,\n options: Record<string, unknown> = {}\n) => {\n // Get readings for the last 7 days in local time.\n const since = startOfDay(null, -7, true);\n let data: Reading[] = [];\n\n // Get the right API.\n const parts = measureId.split('/');\n const id = parts.pop() ?? '';\n const api = parts.length === 0 ? 'flood' : parts[0];\n switch (api) {\n case 'flood':\n data = await getFloodMeasureReadings(id, { since });\n }\n\n // Clear the GUI deck.\n parentEl.replaceChildren();\n\n const measure = parseMeasureId(measureId);\n const { unit } = translateMeasureProperties(measure);\n\n const series1: ChartSeries = { data, unit, formatter: round3 };\n // Set max/min options for plot from widget options.\n if (options.riverDataWidgetMaxValue != null) {\n series1.max = parseFloat(<string>options.riverDataWidgetMaxValue);\n }\n if (options.riverDataWidgetMinValue != null) {\n series1.min = parseFloat(<string>options.riverDataWidgetMinValue);\n }\n\n // Deal with no data.\n if (data.length === 0) {\n const minTime = since.valueOf() / 1000;\n const maxTime = minTime + 86400 * 7;\n const chartOptions = { minTime, maxTime };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n return;\n }\n\n const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000;\n const maxTime =\n startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000;\n const chartOptions = {\n minTime,\n maxTime,\n // attribution: `www.riverdata.co.uk/station/${measure.stationId}`,\n };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n};\n\n/**\n * Load a widget specified by a DOM element.\n */\nexport const loadWidget = (el: HTMLElement | string) => {\n // Get the target element from a query selector if necessary and check it\n // exists.\n const targetEl =\n typeof el === 'string' ? <HTMLElement>document.querySelector(el) : el;\n if (targetEl === null) {\n throw new Error('Target element not found');\n }\n\n // Parse element for widget type and options.\n const widgetIdParts = targetEl.dataset.riverDataWidget?.split(':') ?? [];\n const type = widgetIdParts.shift();\n const id = widgetIdParts.join(':');\n const options = targetEl.dataset;\n\n switch (type) {\n case 'measure':\n drawMeasureWidget(targetEl, id, options);\n break;\n default:\n throw new RiverDataWidgetError('Unknown widget definition', { type, id });\n }\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { drawMeasureWidget } from './widget/render';\n\nexport const version = '1.2.0';\n"],"names":["getFloodMeasureReadings"],"mappings":"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"} |
| /*! RiverDataWidget v1.2.0 2023-06-17 01:06:58 | ||
| *! https://github.com/pb-uk/river-data-widget#readme | ||
| *! Copyright (C) 2023 pbuk (https://github.com/pb-uk). | ||
| *! License MIT. | ||
| */ | ||
| var RiverDataWidget=function(t){"use strict";class e extends Error{constructor(t,e={}){super(t),this.name="RiverDataWidgetError",this.info=e}}const i=t=>t<100?t.toPrecision(3):Math.round(t).toString();class n extends Error{constructor(t,e={}){super(t),this.name="FloodMonitoringApiError",this.info=e}}const s={unit:{m3_s:"m³/s",mAOD:"m",mASD:"m"},qualifiedParameter:{"level-stage":"level","level-downstage":"downstream level"}},a=(t="svg",e={},i={},n=!1)=>{const s=document.createElementNS("http://www.w3.org/2000/svg",t);return!1!==n&&(s.innerHTML=n),((t,e)=>{for(const[i,n]of Object.entries(e))t.style[i]=n;return t})(((t,e)=>{for(const[i,n]of Object.entries(e))t.setAttribute(i,`${n}`);return t})(s,e),i)},o=864e5,r=(t=null,e=0,i=!1)=>{if(!1===i){const i=null===t?Date.now():t.valueOf();return new Date(Math.floor(i/o+e)*o)}const n=new Date,s=!0===i?n.getTimezoneOffset():i,a=n.valueOf()+6e4*s;return new Date(Math.floor(a/o+e)*o)},l=new Intl.DateTimeFormat("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1,timeZoneName:"short"}),h=new Intl.DateTimeFormat("en-GB",{weekday:"short"}),m=new Intl.DateTimeFormat("en-GB",{day:"numeric",month:"short"});class c{constructor(t,e,i={}){var n;this.strokeWidth=2,this.fontSizePx=14,this.width=480,this.height=270,this.plotHeight=this.height-4.5*this.fontSizePx,this.plotWidth=this.width-this.strokeWidth,this.plotColor="#77C",this.labelBg="rgba(255,255,255,0.5)",this.labelBgWidth="0.5em",this.attribution="Uses Environment Agency data from the real-time API (Beta)",this.styles={"font-family":'-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif',"font-size":`${this.fontSizePx}px`,display:"block",margin:"auto","max-width":"150vh"},this.series=e,this.options=i;const s=`0 0 ${this.width} ${this.height}`;this.attribution=null!==(n=i.attribution)&&void 0!==n?n:this.attribution,this.el=a("svg",{viewBox:s},this.styles),t.append(this.el)}getLimits(){if(null==this.limits)throw new n("Chart axis limits have not been set");return this.limits}getHorizontalGridlines(){const{minTime:t,maxTime:e,timeScale:i,minValue:n,maxValue:s,valueScale:o}=this.getLimits(),r=this.strokeWidth/2,l=this.plotHeight,h=r,m=r+(e-t)*i,c=a("g",{stroke:"#ddd"}),d=a("g"),g=s-n,[f,p]=u(g,9),x=10**-p,v=Math.ceil(n*x/f+1)*f;let S=0,w=v/x;for(;w<s;){const t=l-(w-n)*o;c.append(a("line",{x1:h,y1:t,x2:m,y2:t})),d.append(a("text",{x:h+4,y:t+4},{},`${w}`)),++S,w=(v+S*f)/x}return[c,d,a("line",{x1:h,y1:l,x2:m,y2:l},{stroke:"#777"})]}getTimeScale(){const{minTime:t,maxTime:e,timeScale:i}=this.getLimits(),n=this.strokeWidth/2,s=this.plotHeight+this.strokeWidth/2,o=s+3*this.fontSizePx,r=s-this.plotHeight,l=a("g",{stroke:"#ddd"}),c=a("g"),d=t;let u=0,g=d;const f=43200*i,p="#444";for(;g<=e;){const e=n+(g-t)*i,s=new Date(1e3*g);l.append(a("line",{x1:e,y1:o,x2:e,y2:r})),c.append(a("text",{x:e+f,y:o-1.8*this.fontSizePx,"text-anchor":"middle"},{fill:p},`${h.format(s)}`),a("text",{x:e+f,y:o-.5*this.fontSizePx,"text-anchor":"middle"},{fill:p},`${m.format(s)}`)),++u,g=d+86400*u}return[l,c]}render(){var t,e,i,n;const s=d(this.series[0].data);s.minValue=null!==(t=this.series[0].min)&&void 0!==t?t:s.minValue,s.maxValue=null!==(e=this.series[0].max)&&void 0!==e?e:s.maxValue,s.minTime=null!==(i=this.options.minTime)&&void 0!==i?i:s.minTime,s.maxTime=null!==(n=this.options.maxTime)&&void 0!==n?n:s.maxTime,this.limits=Object.assign(Object.assign({},s),{valueScale:(this.plotHeight-this.strokeWidth)/(s.maxValue-s.minValue),timeScale:(this.width-this.strokeWidth)/(s.maxTime-s.minTime)});const[o,r]=this.getTimeScale();this.el.append(o);const[l,h,m]=this.getHorizontalGridlines();this.el.append(l),this.el.append(m),this.plotData(),this.el.append(r),this.el.append(h),this.el.append(a("text",{x:this.width/2,"text-anchor":"middle",y:this.height-.5*this.fontSizePx},{fill:"#595959"},this.attribution)),this.plotLastValue()}plotLastValue(){const{data:t,unit:e,formatter:i}=this.series[0];if(0===t.length){const t=this.plotWidth/2,e=this.plotHeight/2;return void this.el.append(...this.createLargeLabel(t,e,"No data","middle"))}const[n,s]=t[t.length-1],{minTime:a,timeScale:o,maxValue:r,minValue:h}=this.getLimits(),m=null==i?s:i(s),c=this.strokeWidth/2+(n-a)*o,d=(s-h)/(r-h)<.5,u=this.plotHeight*(d?0:.5)+2*this.fontSizePx;this.el.append(...this.createLargeLabel(c,u,`${m} ${e}`),...this.createLabel(c,u,`${l.format(new Date(1e3*n))}`))}plotData(){const{data:t}=this.series[0];if(0===t.length)return;const e=this.strokeWidth/2,i=this.plotHeight-this.strokeWidth/2,{minTime:n,timeScale:s,minValue:o,valueScale:r}=this.getLimits(),l=[`M${e+(t[0][0]-n)*s},${i-(t[0][1]-o)*r}`];for(let a=1;a<t.length;++a){const h=e+(t[a][0]-n)*s,m=i-(t[a][1]-o)*r;l.push(`L${h},${m}`)}const h=a("path",{d:l.join(""),stroke:this.plotColor,"stroke-width":this.strokeWidth,fill:"none"});this.el.append(h)}createLabel(t,e,i,n="end"){return[a("text",{x:t,y:e+1.5*this.fontSizePx,"text-anchor":n},{stroke:this.labelBg,"stroke-width":this.labelBgWidth},i),a("text",{x:t,y:e+1.5*this.fontSizePx,"text-anchor":n},{fill:this.plotColor},i)]}createLargeLabel(t,e,i,n="end"){return[a("text",{x:t,y:e,"text-anchor":n},{"font-size":"1.5em","font-weight":"bold",stroke:this.labelBg,"stroke-width":this.labelBgWidth},i),a("text",{x:t,y:e,"text-anchor":n},{fill:this.plotColor,"font-size":"1.5em","font-weight":"bold"},i)]}}const d=t=>{if(t.length<1)return{minTime:0,maxTime:1,minValue:0,maxValue:0};const e=t[0][0],i=t[t.length-1][0];let n=1/0,s=-n;for(const[,e]of t)n=Math.min(n,e),s=Math.max(s,e);return{minTime:e,maxTime:i,minValue:n,maxValue:s}},u=(t,e)=>{const i=Math.floor(Math.log10(t))-1,n=t/(e*10**i);return[n<=2?2:n<=5?5:10,i]},g="http://environment.data.gov.uk/flood-monitoring",f="riverDataWidget",p=t=>`${f}|${t}`;let x;class v{clear(t=!1){for(const t of this.keys())localStorage.removeItem(p(t));t?localStorage.removeItem(f):localStorage.setItem(f,JSON.stringify([]))}get(t){const e=localStorage.getItem(p(t));return null===e?null:JSON.parse(e)}has(t){return this.keys().includes(t)}isActive(){return null!==localStorage.getItem(f)}keys(){const t=localStorage.getItem(f);return null===t?[]:JSON.parse(t)}set(t,e){const i=JSON.stringify(e),n=localStorage.getItem(f),s=null===n?[]:JSON.parse(n);s.includes(t)||(s.push(t),localStorage.setItem(f,JSON.stringify(s))),localStorage.setItem(p(t),i)}unset(t){localStorage.removeItem(p(t));const e=localStorage.getItem(f),i=null===e?[]:JSON.parse(e),n=i.indexOf(t);return-1!==n&&(i.splice(n,1),localStorage.setItem(f,JSON.stringify(i)),!0)}}const S=async(t,e={})=>{const i={_sorted:""};e.since&&(i.since=e.since.toISOString().substring(0,19)+"Z");const n=await(async(t,e={})=>{const i=new URLSearchParams(e).toString(),n=i?`${g}${t}?${i}`:`${g}${t}`,s=await fetch(n);return{data:await s.json(),response:s}})(`/id/measures/${t}/readings`,i);return[k(n.data.items)[t]||[],n]},w=(t,e)=>{const i=t.findIndex((t=>t[0]>=e));return i<0?[]:t.slice(i)},y=async(t,e={})=>{const i=`readings|${t}`,n=(x||(x=new v),x),s=n.get(i)||{data:[],lastCheck:0,storedSince:1/0},{data:a,lastCheck:o}=s;let{storedSince:l}=s;const h=r(null,-8,!0).valueOf()/1e3;for(;a.length&&a[0][0]<h;)[l]=a[0],a.shift();const m=a.length?a[a.length-1][0]:0,c=e.since&&e.since.valueOf()/1e3||0;if(l<=c&&Date.now()<1e3*o+3e5)return w(a,c);const d=Object.assign(Object.assign({},e),{since:new Date(1e3*Math.max(c,m))}),[u]=await S(t,d);return b(a,u),l=Math.min(c,l),n.set(i,{lastCheck:Date.now()/1e3,data:a,storedSince:l}),w(a,c)},b=(t,e)=>{if(!e.length)return;let i=t.length-1;for(;i>=0&&t[i][0]>=e[0][0];)--i;t.splice(i+1,1/0,...e)},k=t=>{const e={};for(const{measure:i,dateTime:n,value:s}of t)null==e[i]&&(e[i]=[]),e[i].unshift([new Date(n).valueOf()/1e3,s]);const i={};for(const[t,n]of Object.entries(e))i[t.substring(t.lastIndexOf("/")+1)]=n;return i},T=async(t,e,a={})=>{var o;const l=r(null,-7,!0);let h=[];const m=e.split("/"),d=null!==(o=m.pop())&&void 0!==o?o:"";if("flood"===(0===m.length?"flood":m[0]))h=await y(d,{since:l});t.replaceChildren();const u=(t=>{const e=t.match(/(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/);if(null===e)throw new n("Cannot parse measure id",{measureId:t});const[i,s,a,o,r,l]=e.reverse();return{stationId:l,parameter:r,qualifier:o,type:a,interval:s,unit:i,qualifiedParameter:o.length?`${r}-${o}`:r}})(e),{unit:g}=(t=>{const e={};for(const i in t){const n=t[i];s[i]&&s[i][n]?e[i]=s[i][n]:e[i]=n}return e})(u),f={data:h,unit:g,formatter:i};if(null!=a.riverDataWidgetMaxValue&&(f.max=parseFloat(a.riverDataWidgetMaxValue)),null!=a.riverDataWidgetMinValue&&(f.min=parseFloat(a.riverDataWidgetMinValue)),0===h.length){const e=l.valueOf()/1e3;return void new c(t,[f],{minTime:e,maxTime:e+604800}).render()}const p=r(new Date(1e3*h[0][0])).valueOf()/1e3,x=r(new Date(1e3*h[h.length-1][0]),1).valueOf()/1e3;new c(t,[f],{minTime:p,maxTime:x}).render()},O=t=>{var i,n;const s="string"==typeof t?document.querySelector(t):t;if(null===s)throw new Error("Target element not found");const a=null!==(n=null===(i=s.dataset.riverDataWidget)||void 0===i?void 0:i.split(":"))&&void 0!==n?n:[],o=a.shift(),r=a.join(":"),l=s.dataset;if("measure"!==o)throw new e("Unknown widget definition",{type:o,id:r});T(s,r,l)},D=async()=>{for(const t of document.querySelectorAll("[data-river-data-widget]"))try{O(t)}catch(t){console.error(t,{error:t})}};return"loading"===document.readyState?document.addEventListener("DOMContentLoaded",D):D(),t.version="1.2.0",t}({}); | ||
| //# sourceMappingURL=index.min.js.map |
| {"version":3,"file":"index.min.js","sources":["src/error.ts","src/helpers/format.ts","src/flood-monitoring-api/error.ts","src/flood-monitoring-api/measure.ts","src/helpers/dom.ts","src/helpers/time.ts","src/widget/chart.ts","src/flood-monitoring-api/api.ts","src/flood-monitoring-api/store.ts","src/flood-monitoring-api/reading.ts","src/widget/render.ts","src/autoload.ts","src/index.ts"],"sourcesContent":["export type RiverDataWidgetErrorInfo = Record<string, unknown>;\n\nexport class RiverDataWidgetError extends Error {\n public info: RiverDataWidgetErrorInfo;\n\n constructor(msg: string, info: RiverDataWidgetErrorInfo = {}) {\n super(msg);\n this.name = 'RiverDataWidgetError';\n this.info = info;\n }\n}\n","export const FONT_STACK =\n '-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Roboto\",\"Helvetica Neue\",Arial,sans-serif';\n\nexport const round3 = (value: number) =>\n value < 100 ? value.toPrecision(3) : Math.round(value).toString();\n","export class FloodMonitoringApiError extends Error {\n public info: Record<string, unknown>;\n\n constructor(msg: string, info: Record<string, unknown> = {}) {\n super(msg);\n this.name = 'FloodMonitoringApiError';\n this.info = info;\n }\n}\n","import { FloodMonitoringApiError } from './error';\n\nexport { parseMeasureId };\n\nconst parseMeasureId = (measureId: string) => {\n // ............base/ stat-paramet-qualifi- type -interva-unit\n const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/;\n const matches = measureId.match(regExp);\n if (matches === null) {\n throw new FloodMonitoringApiError('Cannot parse measure id', { measureId });\n }\n const [unit, interval, type, qualifier, parameter, stationId] =\n matches.reverse();\n const qualifiedParameter = qualifier.length\n ? `${parameter}-${qualifier}`\n : parameter;\n return {\n stationId,\n parameter,\n qualifier,\n type,\n interval,\n unit,\n qualifiedParameter,\n };\n};\n\nconst measureTranslations: Record<string, Record<string, string>> = {\n unit: {\n m3_s: 'm³/s',\n mAOD: 'm',\n mASD: 'm',\n },\n qualifiedParameter: {\n 'level-stage': 'level',\n 'level-downstage': 'downstream level',\n },\n};\n\nexport const translateMeasureProperties = (measure: Record<string, string>) => {\n const translated: Record<string, string> = {};\n for (const prop in measure) {\n const value = measure[prop];\n if (measureTranslations[prop] && measureTranslations[prop][value]) {\n translated[prop] = measureTranslations[prop][value];\n } else {\n translated[prop] = value;\n }\n }\n return translated;\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { createElement, createSvgElement, setAttributes, setStyles };\n\ntype AttributeList = Record<string, string | number>;\n\nconst setAttributes = <T extends HTMLElement | SVGElement>(\n el: T,\n attributes: AttributeList\n): T => {\n for (const [key, value] of Object.entries(attributes)) {\n el.setAttribute(key, `${value}`);\n }\n return el;\n};\n\nconst setStyles = <T extends HTMLElement | SVGElement>(\n el: T,\n styles: AttributeList\n): T => {\n for (const [key, value] of Object.entries(styles)) {\n // Workaround (el.style.setProperty uses kebab-case keys).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (<any>el.style)[key] = value;\n }\n return el;\n};\n\nconst createElement = (\n name = 'div',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n): HTMLElement => {\n const el = document.createElement(name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n\nconst createSvgElement = (\n name = 'svg',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n) => {\n const el = document.createElementNS('http://www.w3.org/2000/svg', name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n","export const MINUTE_MS = 60000;\n// const HOUR_MS = 3600000;\nexport const DAY_MS = 86400000;\n\n/**\n * Get the Date at the start of a day in UTC or local time.\n *\n * @param offset\n * @param timeZone The time zone offset in minutes, or set to `true` to use the\n * local time zone (`false`, the default, uses UTC).\n * @returns The reqested date.\n */\nexport const startOfDay = (\n date: Date | null = null,\n offset = 0,\n timeZone: boolean | number = false\n): Date => {\n if (timeZone === false) {\n // Use UTC.\n const base = date === null ? Date.now() : date.valueOf();\n return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS);\n }\n\n const now = new Date();\n const tz = timeZone === true ? now.getTimezoneOffset() : timeZone;\n const local = now.valueOf() + tz * MINUTE_MS;\n return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS);\n};\n\n/**\n * | | long |short|narrow|numeric|2-digit|\n * |:-------:|:-----------:|:---:|:----:|:-----:|:-----:|\n * | weekday | Monday | Mon | M | | |\n * | era | Anno Domini | AD | A | | |\n * | year | | | | 2012 | 12 |\n * | month | March | Mar | M | 3 | 03 |\n * | day | | | | 1 | 01 |\n * | hour | | | | 1 | 01 |\n * | minute | | | | 1 | 01 |\n * | second | | | | 1 | 01 |\n *\n * * fractionalSecondDigits: 1, 2 or 3 for number of digits.\n * * timeZoneName: long (Pacific Standard Time), short (PST),\n * longOffset (GMT-0800), shortOffset (GMT-8), longGeneric (Pacific Time),\n * shortGeneric (PT).\n */\n\nexport const dateFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n // year: 'numeric',\n});\n\nexport const timeFormatter = new Intl.DateTimeFormat('en-GB', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n timeZoneName: 'short',\n});\n\nexport const dddFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'short',\n});\n\nexport const dMmmFormatter = new Intl.DateTimeFormat('en-GB', {\n day: 'numeric',\n month: 'short',\n});\n","import { createSvgElement } from '../helpers/dom';\nimport { timeFormatter, dddFormatter, dMmmFormatter } from '../helpers/time';\nimport { FloodMonitoringApiError } from '../flood-monitoring-api/error';\nimport { FONT_STACK } from '../helpers/format';\n\nexport interface ChartOptions {\n minTime?: number;\n maxTime?: number;\n attribution?: string;\n}\n\nexport interface ChartScaleLimits {\n minTime: number;\n maxTime: number;\n timeScale: number;\n minValue: number;\n maxValue: number;\n valueScale: number;\n}\n\nexport interface ChartSeries {\n data: TimeSeriesValue[];\n min?: number;\n max?: number;\n unit?: string;\n formatter?: (value: number) => string;\n}\n\nexport type TimeSeriesValue = [\n ts: number, // Unix time stamp (seconds).\n v: number // Value.\n];\n\nexport class Chart {\n protected strokeWidth = 2;\n protected fontSizePx = 14;\n\n protected el: SVGElement;\n protected series: ChartSeries[];\n protected options: ChartOptions;\n\n protected width = 480; // 400;\n protected height = 270; // 225;\n protected plotHeight = this.height - this.fontSizePx * 4.5;\n protected plotWidth = this.width - this.strokeWidth;\n\n protected limits?: ChartScaleLimits;\n\n protected plotColor = '#77C';\n protected labelBg = 'rgba(255,255,255,0.5)';\n protected labelBgWidth = '0.5em';\n\n protected attribution =\n 'Uses Environment Agency data from the real-time API (Beta)';\n\n // CSS settings.\n // Just readable at 320x180.\n // Good from 400x225.\n // Perfect at 480x270 (font is 12px);\n protected styles = {\n 'font-family': FONT_STACK,\n 'font-size': `${this.fontSizePx}px`,\n display: 'block',\n margin: 'auto',\n 'max-width': '150vh',\n };\n\n constructor(\n el: HTMLElement,\n series: ChartSeries[],\n options: ChartOptions = {}\n ) {\n this.series = series;\n this.options = options;\n const viewBox = `0 0 ${this.width} ${this.height}`;\n this.attribution = options.attribution ?? this.attribution;\n this.el = createSvgElement('svg', { viewBox }, this.styles);\n el.append(this.el);\n }\n\n getLimits(): ChartScaleLimits {\n if (this.limits == null) {\n throw new FloodMonitoringApiError('Chart axis limits have not been set');\n }\n return this.limits;\n }\n\n getHorizontalGridlines(): SVGElement[] {\n const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } =\n this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight;\n const x1 = xOffset;\n const x2 = xOffset + (maxTime - minTime) * timeScale;\n // Horizontal grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n const valueRange = maxValue - minValue;\n // Horizontal grid interval.\n const [interval, exponent] = getInterval(valueRange, 9);\n const factor = 10 ** -exponent;\n const base = Math.ceil((minValue * factor) / interval + 1) * interval;\n let i = 0;\n let current = base / factor;\n while (current < maxValue) {\n const y1 = yOffset - (current - minValue) * valueScale;\n lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n labels.append(\n createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`)\n );\n ++i;\n current = (base + i * interval) / factor;\n }\n const timeAxisLine = createSvgElement(\n 'line',\n { x1, y1: yOffset, x2, y2: yOffset },\n { stroke: '#777' }\n );\n\n return [lines, labels, timeAxisLine];\n }\n\n getTimeScale(): SVGElement[] {\n const { minTime, maxTime, timeScale } = this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight + this.strokeWidth / 2;\n const y1 = yOffset + this.fontSizePx * 3;\n const y2 = yOffset - this.plotHeight;\n // Vertical grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n // Vertical grid interval.\n const base = minTime;\n const interval = 86400;\n let i = 0;\n let current = base;\n const labelOffset = 43200 * timeScale;\n const fill = '#444';\n while (current <= maxTime) {\n const x1 = xOffset + (current - minTime) * timeScale;\n const d = new Date(current * 1000);\n // lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 }));\n labels.append(\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 1.8,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dddFormatter.format(d)}`\n ),\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 0.5,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dMmmFormatter.format(d)}`\n )\n );\n ++i;\n current = base + i * interval;\n }\n return [lines, labels];\n }\n\n render() {\n // Calculate axis scales.\n const limits = getLimits(this.series[0].data);\n limits.minValue = this.series[0].min ?? limits.minValue;\n limits.maxValue = this.series[0].max ?? limits.maxValue;\n limits.minTime = this.options.minTime ?? limits.minTime;\n limits.maxTime = this.options.maxTime ?? limits.maxTime;\n\n this.limits = {\n ...limits,\n valueScale:\n (this.plotHeight - this.strokeWidth) /\n (limits.maxValue - limits.minValue),\n timeScale:\n (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime),\n };\n\n // Time axis.\n const [timeLines, timeLabels] = this.getTimeScale();\n this.el.append(timeLines);\n\n // Value axis.\n const [valueLines, valueLabels, timeAxisLine] =\n this.getHorizontalGridlines();\n this.el.append(valueLines);\n this.el.append(timeAxisLine);\n\n this.plotData();\n\n // Plot labels on top of the line.\n this.el.append(timeLabels);\n this.el.append(valueLabels);\n\n this.el.append(\n createSvgElement(\n 'text',\n {\n x: this.width / 2,\n 'text-anchor': 'middle',\n y: this.height - this.fontSizePx * 0.5,\n },\n { fill: '#595959' },\n this.attribution\n )\n );\n\n this.plotLastValue();\n }\n\n plotLastValue() {\n const { data, unit, formatter } = this.series[0];\n\n // If there is no data show a message.\n if (data.length === 0) {\n const x = this.plotWidth / 2;\n const y = this.plotHeight / 2;\n this.el.append(...this.createLargeLabel(x, y, 'No data', 'middle'));\n return;\n }\n\n const [time, value] = data[data.length - 1];\n const { minTime, timeScale, maxValue, minValue } = this.getLimits();\n\n const v = formatter == null ? value : formatter(value);\n const xOffset = this.strokeWidth / 2;\n // const yOffset = this.plotHeight - this.strokeWidth / 2;\n const x = xOffset + (time - minTime) * timeScale;\n const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5;\n const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2;\n\n this.el.append(\n // Value label.\n ...this.createLargeLabel(x, y, `${v} ${unit}`),\n // Time label.\n ...this.createLabel(\n x,\n y,\n `${timeFormatter.format(new Date(time * 1000))}`\n )\n );\n }\n\n plotData() {\n const { data } = this.series[0];\n // Don't do anything we don't have to!\n if (data.length === 0) return;\n\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight - this.strokeWidth / 2;\n const { minTime, timeScale, minValue, valueScale } = this.getLimits();\n // First data point.\n const x = xOffset + (data[0][0] - minTime) * timeScale;\n const y = yOffset - (data[0][1] - minValue) * valueScale;\n const points = [`M${x},${y}`];\n // Remaining data points.\n for (let i = 1; i < data.length; ++i) {\n const x = xOffset + (data[i][0] - minTime) * timeScale;\n const y = yOffset - (data[i][1] - minValue) * valueScale;\n points.push(`L${x},${y}`);\n }\n // Plot the data.\n const path = createSvgElement('path', {\n d: points.join(''),\n stroke: this.plotColor,\n 'stroke-width': this.strokeWidth,\n fill: 'none',\n });\n this.el.append(path);\n }\n\n protected createLabel(x: number, y: number, text: string, anchor = 'end') {\n return [\n // Background for time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor },\n {\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n text\n ),\n // Time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor },\n { fill: this.plotColor },\n text\n ),\n ];\n }\n\n protected createLargeLabel(\n x: number,\n y: number,\n text: string,\n anchor = 'end'\n ) {\n return [\n // Background for value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': anchor },\n {\n 'font-size': '1.5em',\n 'font-weight': 'bold',\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n text\n ),\n // Value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': anchor },\n { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' },\n text\n ),\n ];\n }\n}\n\nexport const getLimits = (data: TimeSeriesValue[]) => {\n if (data.length < 1) {\n return { minTime: 0, maxTime: 1, minValue: 0, maxValue: 0 };\n }\n const minTime = data[0][0];\n const maxTime = data[data.length - 1][0];\n let minValue = Infinity;\n let maxValue = -minValue;\n for (const [, value] of data) {\n minValue = Math.min(minValue, value);\n maxValue = Math.max(maxValue, value);\n }\n return { minTime, maxTime, minValue, maxValue };\n};\n\nexport const getInterval = (range: number, maxDivisions: number) => {\n const exponent = Math.floor(Math.log10(range)) - 1;\n const k = range / (maxDivisions * 10 ** exponent);\n const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10;\n return [mantissa, exponent];\n};\n","// There is no need to be secure about this!\nconst baseUrl = 'http://environment.data.gov.uk/flood-monitoring';\n\nexport interface ApiResponse<T> {\n data: {\n items: T;\n };\n response: Response;\n}\n\nexport interface ApiParameters {\n since?: string; // Time from.\n _sorted?: ''; // Flag for sorting.\n}\n\nexport const apiFetch = async (\n path: string,\n query = {}\n): Promise<ApiResponse<unknown>> => {\n const queryString = new URLSearchParams(query).toString();\n const uri = queryString\n ? `${baseUrl}${path}?${queryString}`\n : `${baseUrl}${path}`;\n const response = await fetch(uri);\n return { data: await response.json(), response };\n};\n\n/**\n * Convert a Date to a format recognized by the EA API for a query parameter.\n *\n * @param date Convert from.\n * @returns A string in the EA API query parameter format.\n */\nexport const toTimeParameter = (date: Date): string => {\n return date.toISOString().substring(0, 19) + 'Z';\n};\n\n/*\nUseful response headers\n Date: 'Sat, 13 May 2023 09:14:07 GMT',\n last-modified: Sat, 13 May 2023 09:03:13 GMT,\nResponse meta:\n publisher: 'Environment Agency',\n license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/',\n documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference',\n version: '0.9',\n comment: 'Status: Beta service',\n hasFormat: [\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z\"\n ],\n*/\n","const prefix = 'riverDataWidget';\n\nconst addPrefix = (key: string): string => `${prefix}|${key}`;\n\nlet instance: Store;\n\nclass Store {\n clear(destroy = false) {\n for (const key of this.keys()) {\n localStorage.removeItem(addPrefix(key));\n }\n if (destroy) {\n localStorage.removeItem(prefix);\n return;\n }\n localStorage.setItem(prefix, JSON.stringify([]));\n }\n\n get(key: string) {\n const value = localStorage.getItem(addPrefix(key));\n return value === null ? null : JSON.parse(value);\n }\n\n has(key: string): boolean {\n return this.keys().includes(key);\n }\n\n /**\n * Detect active localStorage.\n *\n * @returns true iff localStorage for the widget is active.\n */\n isActive() {\n return localStorage.getItem(prefix) !== null;\n }\n\n keys(): string[] {\n const storedKeys = localStorage.getItem(prefix);\n return storedKeys === null ? [] : JSON.parse(storedKeys);\n }\n\n set(key: string, value: unknown) {\n const json = JSON.stringify(value);\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n if (!keys.includes(key)) {\n keys.push(key);\n localStorage.setItem(prefix, JSON.stringify(keys));\n }\n localStorage.setItem(addPrefix(key), json);\n }\n\n unset(key: string): boolean {\n // Remove it before we do anything else.\n localStorage.removeItem(addPrefix(key));\n\n // Then remove it from the list of keys.\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n const index = keys.indexOf(key);\n\n // If it doesn't exist we don't have to remove it.\n if (index === -1) return false;\n\n keys.splice(index, 1);\n localStorage.setItem(prefix, JSON.stringify(keys));\n return true;\n }\n}\n\nexport const useStore = (): Store => {\n if (!instance) {\n instance = new Store();\n }\n return instance;\n};\n","import { apiFetch, toTimeParameter } from './api';\nimport { useStore } from './store';\nimport { MINUTE_MS, startOfDay } from '../helpers/time';\n\nimport type { ApiParameters, ApiResponse } from './api';\n\n// Throttle requests to five minutes.\nconst THROTTLE_MS = 5 * MINUTE_MS;\n\n/**\n * Internal format for readings.\n */\nexport type Reading = [\n timestamp: number, // Unix epoch timestamp (seconds).\n value: number // Value.\n];\n\n/**\n * Internal format for readings.\n */\nexport interface ReadingOptions {\n since?: Date; // Time from.\n}\n\n/**\n * Internal format for readings.\n */\ntype ReadingResponse = [a: Reading[], b: ApiResponse<ReadingDTO[]>];\n\n/**\n * Data transfer object for readings provided by the API.\n */\ninterface ReadingDTO {\n '@id': string; // The URL of this reading.\n dateTime: string; // e.g. '2023-05-13T09:00:00Z'.\n measure: string; // The URL of the measure.\n value: number; // The value in the appropriate units.\n}\n\ninterface StoredReadings {\n storedSince: number;\n lastCheck: number;\n data: Reading[];\n}\n\n/**\n * Fetch the readings for a measure.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nconst fetchMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<ReadingResponse> => {\n // Set the parameters for the request.\n const params: ApiParameters = { _sorted: '' };\n if (options.since) {\n params.since = toTimeParameter(options.since);\n }\n // Get the response, casting the items to ReadingDTOs.\n const response = <ApiResponse<ReadingDTO[]>>(\n await apiFetch(`/id/measures/${id}/readings`, params)\n );\n return [parseReadings(response.data.items)[id] || [], response];\n};\n\nexport const filterSince = (data: Reading[], since: number) => {\n const position = data.findIndex((reading) => reading[0] >= since);\n return position < 0 ? [] : data.slice(position);\n};\n\n/**\n * Get the readings for a measure.\n *\n * @todo Caching and throttling.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nexport const getMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<Reading[]> => {\n // Get the saved readings.\n const key = `readings|${id}`;\n const store = useStore();\n\n const stored: StoredReadings = store.get(key) || {\n data: [],\n lastCheck: 0,\n storedSince: Infinity,\n };\n const { data, lastCheck } = stored;\n let { storedSince } = stored;\n\n const discardBefore = startOfDay(null, -8, true).valueOf() / 1000;\n\n // Discard any older than 30 days.\n while (data.length && data[0][0] < discardBefore) {\n [storedSince] = data[0];\n data.shift();\n }\n\n // If we have data early enough apply throttle.\n const lastStored = data.length ? data[data.length - 1][0] : 0;\n const requestedSince = (options.since && options.since.valueOf() / 1000) || 0;\n if (\n storedSince <= requestedSince &&\n Date.now() < lastCheck * 1000 + THROTTLE_MS\n ) {\n // Throttled.\n return filterSince(data, requestedSince);\n }\n\n const fetchOptions: ReadingOptions = {\n ...options,\n since: new Date(Math.max(requestedSince, lastStored) * 1000),\n };\n\n const [newData] = await fetchMeasureReadings(id, fetchOptions);\n mergeReadings(data, newData);\n storedSince = Math.min(requestedSince, storedSince);\n store.set(key, { lastCheck: Date.now() / 1000, data, storedSince });\n return filterSince(data, requestedSince);\n};\n\nexport const mergeReadings = (first: Reading[], second: Reading[]): void => {\n if (!second.length) return;\n\n let firstPos = first.length - 1;\n while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) {\n --firstPos;\n }\n first.splice(firstPos + 1, Infinity, ...second);\n};\n\nconst parseReadings = (items: ReadingDTO[]): Record<string, Reading[]> => {\n const ranges: Record<string, Reading[]> = {};\n for (const { measure, dateTime, value } of items) {\n if (ranges[measure] == null) {\n ranges[measure] = [];\n }\n ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]);\n }\n\n const rangesById: Record<string, Reading[]> = {};\n for (const [key, range] of Object.entries(ranges)) {\n rangesById[key.substring(key.lastIndexOf('/') + 1)] = range;\n }\n\n return rangesById;\n};\n","import { RiverDataWidgetError } from '../error';\nimport { round3 } from '../helpers/format';\nimport {\n parseMeasureId,\n translateMeasureProperties,\n} from '../flood-monitoring-api/measure';\nimport { Chart } from './chart';\nimport { getMeasureReadings as getFloodMeasureReadings } from '../flood-monitoring-api';\nimport { startOfDay } from '../helpers/time';\n\nimport type { ChartSeries } from './chart';\nimport type { Reading } from '../flood-monitoring-api/reading';\n\nexport const drawMeasureWidget = async (\n parentEl: HTMLElement,\n measureId: string,\n options: Record<string, unknown> = {}\n) => {\n // Get readings for the last 7 days in local time.\n const since = startOfDay(null, -7, true);\n let data: Reading[] = [];\n\n // Get the right API.\n const parts = measureId.split('/');\n const id = parts.pop() ?? '';\n const api = parts.length === 0 ? 'flood' : parts[0];\n switch (api) {\n case 'flood':\n data = await getFloodMeasureReadings(id, { since });\n }\n\n // Clear the GUI deck.\n parentEl.replaceChildren();\n\n const measure = parseMeasureId(measureId);\n const { unit } = translateMeasureProperties(measure);\n\n const series1: ChartSeries = { data, unit, formatter: round3 };\n // Set max/min options for plot from widget options.\n if (options.riverDataWidgetMaxValue != null) {\n series1.max = parseFloat(<string>options.riverDataWidgetMaxValue);\n }\n if (options.riverDataWidgetMinValue != null) {\n series1.min = parseFloat(<string>options.riverDataWidgetMinValue);\n }\n\n // Deal with no data.\n if (data.length === 0) {\n const minTime = since.valueOf() / 1000;\n const maxTime = minTime + 86400 * 7;\n const chartOptions = { minTime, maxTime };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n return;\n }\n\n const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000;\n const maxTime =\n startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000;\n const chartOptions = {\n minTime,\n maxTime,\n // attribution: `www.riverdata.co.uk/station/${measure.stationId}`,\n };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n};\n\n/**\n * Load a widget specified by a DOM element.\n */\nexport const loadWidget = (el: HTMLElement | string) => {\n // Get the target element from a query selector if necessary and check it\n // exists.\n const targetEl =\n typeof el === 'string' ? <HTMLElement>document.querySelector(el) : el;\n if (targetEl === null) {\n throw new Error('Target element not found');\n }\n\n // Parse element for widget type and options.\n const widgetIdParts = targetEl.dataset.riverDataWidget?.split(':') ?? [];\n const type = widgetIdParts.shift();\n const id = widgetIdParts.join(':');\n const options = targetEl.dataset;\n\n switch (type) {\n case 'measure':\n drawMeasureWidget(targetEl, id, options);\n break;\n default:\n throw new RiverDataWidgetError('Unknown widget definition', { type, id });\n }\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\nimport { loadWidget } from './widget/render';\n\nexport { version } from '.';\n\nconst autoload = async () => {\n for (const el of document.querySelectorAll('[data-river-data-widget]')) {\n try {\n loadWidget(<HTMLElement>el);\n } catch (error) {\n console.error(error, { error });\n }\n }\n};\n\nif (document.readyState === 'loading') {\n // Loading hasn't finished yet.\n document.addEventListener('DOMContentLoaded', autoload);\n} else {\n // `DOMContentLoaded` has already fired.\n autoload();\n}\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { drawMeasureWidget } from './widget/render';\n\nexport const version = '1.2.0';\n"],"names":["RiverDataWidgetError","Error","constructor","msg","info","super","this","name","round3","value","toPrecision","Math","round","toString","FloodMonitoringApiError","measureTranslations","unit","m3_s","mAOD","mASD","qualifiedParameter","createSvgElement","attributes","styles","innerHTML","el","document","createElementNS","key","Object","entries","style","setStyles","setAttribute","setAttributes","DAY_MS","startOfDay","date","offset","timeZone","base","Date","now","valueOf","floor","tz","getTimezoneOffset","local","timeFormatter","Intl","DateTimeFormat","hour","minute","hour12","timeZoneName","dddFormatter","weekday","dMmmFormatter","day","month","Chart","series","options","strokeWidth","fontSizePx","width","height","plotHeight","plotWidth","plotColor","labelBg","labelBgWidth","attribution","display","margin","viewBox","_a","append","getLimits","limits","getHorizontalGridlines","minTime","maxTime","timeScale","minValue","maxValue","valueScale","xOffset","yOffset","x1","x2","lines","stroke","labels","valueRange","interval","exponent","getInterval","factor","ceil","i","current","y1","y2","x","y","getTimeScale","labelOffset","fill","d","format","render","data","min","_b","max","_c","_d","assign","timeLines","timeLabels","valueLines","valueLabels","timeAxisLine","plotData","plotLastValue","formatter","length","createLargeLabel","time","v","isHighLabel","createLabel","points","push","path","join","text","anchor","Infinity","range","maxDivisions","log10","k","baseUrl","prefix","addPrefix","instance","Store","clear","destroy","keys","localStorage","removeItem","setItem","JSON","stringify","get","getItem","parse","has","includes","isActive","storedKeys","set","json","unset","index","indexOf","splice","fetchMeasureReadings","async","id","params","_sorted","since","toISOString","substring","response","query","queryString","URLSearchParams","uri","fetch","apiFetch","parseReadings","items","filterSince","position","findIndex","reading","slice","getMeasureReadings","store","stored","lastCheck","storedSince","discardBefore","shift","lastStored","requestedSince","fetchOptions","newData","mergeReadings","first","second","firstPos","ranges","measure","dateTime","unshift","rangesById","lastIndexOf","drawMeasureWidget","parentEl","measureId","parts","split","pop","getFloodMeasureReadings","replaceChildren","matches","match","type","qualifier","parameter","stationId","reverse","parseMeasureId","translated","prop","translateMeasureProperties","series1","riverDataWidgetMaxValue","parseFloat","riverDataWidgetMinValue","loadWidget","targetEl","querySelector","widgetIdParts","dataset","riverDataWidget","autoload","querySelectorAll","error","console","readyState","addEventListener"],"mappings":"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"} |
+594
| /*! RiverDataWidget v1.2.0 2023-06-17 01:06:58 | ||
| *! https://github.com/pb-uk/river-data-widget#readme | ||
| *! Copyright (C) 2023 pbuk (https://github.com/pb-uk). | ||
| *! License MIT. | ||
| */ | ||
| const FONT_STACK = '-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif'; | ||
| const round3 = (value) => value < 100 ? value.toPrecision(3) : Math.round(value).toString(); | ||
| class FloodMonitoringApiError extends Error { | ||
| constructor(msg, info = {}) { | ||
| super(msg); | ||
| this.name = 'FloodMonitoringApiError'; | ||
| this.info = info; | ||
| } | ||
| } | ||
| const parseMeasureId = (measureId) => { | ||
| // ............base/ stat-paramet-qualifi- type -interva-unit | ||
| const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/; | ||
| const matches = measureId.match(regExp); | ||
| if (matches === null) { | ||
| throw new FloodMonitoringApiError('Cannot parse measure id', { measureId }); | ||
| } | ||
| const [unit, interval, type, qualifier, parameter, stationId] = matches.reverse(); | ||
| const qualifiedParameter = qualifier.length | ||
| ? `${parameter}-${qualifier}` | ||
| : parameter; | ||
| return { | ||
| stationId, | ||
| parameter, | ||
| qualifier, | ||
| type, | ||
| interval, | ||
| unit, | ||
| qualifiedParameter, | ||
| }; | ||
| }; | ||
| const measureTranslations = { | ||
| unit: { | ||
| m3_s: 'm³/s', | ||
| mAOD: 'm', | ||
| mASD: 'm', | ||
| }, | ||
| qualifiedParameter: { | ||
| 'level-stage': 'level', | ||
| 'level-downstage': 'downstream level', | ||
| }, | ||
| }; | ||
| const translateMeasureProperties = (measure) => { | ||
| const translated = {}; | ||
| for (const prop in measure) { | ||
| const value = measure[prop]; | ||
| if (measureTranslations[prop] && measureTranslations[prop][value]) { | ||
| translated[prop] = measureTranslations[prop][value]; | ||
| } | ||
| else { | ||
| translated[prop] = value; | ||
| } | ||
| } | ||
| return translated; | ||
| }; | ||
| /** | ||
| * RiverDataWidget https://github.com/pb-uk/river-data-widget. | ||
| * | ||
| * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk. | ||
| * @license AGPL-3.0-or-later see LICENSE.md. | ||
| */ | ||
| const setAttributes = (el, attributes) => { | ||
| for (const [key, value] of Object.entries(attributes)) { | ||
| el.setAttribute(key, `${value}`); | ||
| } | ||
| return el; | ||
| }; | ||
| const setStyles = (el, styles) => { | ||
| for (const [key, value] of Object.entries(styles)) { | ||
| // Workaround (el.style.setProperty uses kebab-case keys). | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| el.style[key] = value; | ||
| } | ||
| return el; | ||
| }; | ||
| const createSvgElement = (name = 'svg', attributes = {}, styles = {}, innerHTML = false) => { | ||
| const el = document.createElementNS('http://www.w3.org/2000/svg', name); | ||
| if (innerHTML !== false) { | ||
| el.innerHTML = innerHTML; | ||
| } | ||
| return setStyles(setAttributes(el, attributes), styles); | ||
| }; | ||
| const MINUTE_MS = 60000; | ||
| // const HOUR_MS = 3600000; | ||
| const DAY_MS = 86400000; | ||
| /** | ||
| * Get the Date at the start of a day in UTC or local time. | ||
| * | ||
| * @param offset | ||
| * @param timeZone The time zone offset in minutes, or set to `true` to use the | ||
| * local time zone (`false`, the default, uses UTC). | ||
| * @returns The reqested date. | ||
| */ | ||
| const startOfDay = (date = null, offset = 0, timeZone = false) => { | ||
| if (timeZone === false) { | ||
| // Use UTC. | ||
| const base = date === null ? Date.now() : date.valueOf(); | ||
| return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS); | ||
| } | ||
| const now = new Date(); | ||
| const tz = timeZone === true ? now.getTimezoneOffset() : timeZone; | ||
| const local = now.valueOf() + tz * MINUTE_MS; | ||
| return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS); | ||
| }; | ||
| const timeFormatter = new Intl.DateTimeFormat('en-GB', { | ||
| hour: '2-digit', | ||
| minute: '2-digit', | ||
| hour12: false, | ||
| timeZoneName: 'short', | ||
| }); | ||
| const dddFormatter = new Intl.DateTimeFormat('en-GB', { | ||
| weekday: 'short', | ||
| }); | ||
| const dMmmFormatter = new Intl.DateTimeFormat('en-GB', { | ||
| day: 'numeric', | ||
| month: 'short', | ||
| }); | ||
| class Chart { | ||
| constructor(el, series, options = {}) { | ||
| var _a; | ||
| this.strokeWidth = 2; | ||
| this.fontSizePx = 14; | ||
| this.width = 480; // 400; | ||
| this.height = 270; // 225; | ||
| this.plotHeight = this.height - this.fontSizePx * 4.5; | ||
| this.plotWidth = this.width - this.strokeWidth; | ||
| this.plotColor = '#77C'; | ||
| this.labelBg = 'rgba(255,255,255,0.5)'; | ||
| this.labelBgWidth = '0.5em'; | ||
| this.attribution = 'Uses Environment Agency data from the real-time API (Beta)'; | ||
| // CSS settings. | ||
| // Just readable at 320x180. | ||
| // Good from 400x225. | ||
| // Perfect at 480x270 (font is 12px); | ||
| this.styles = { | ||
| 'font-family': FONT_STACK, | ||
| 'font-size': `${this.fontSizePx}px`, | ||
| display: 'block', | ||
| margin: 'auto', | ||
| 'max-width': '150vh', | ||
| }; | ||
| this.series = series; | ||
| this.options = options; | ||
| const viewBox = `0 0 ${this.width} ${this.height}`; | ||
| this.attribution = (_a = options.attribution) !== null && _a !== void 0 ? _a : this.attribution; | ||
| this.el = createSvgElement('svg', { viewBox }, this.styles); | ||
| el.append(this.el); | ||
| } | ||
| getLimits() { | ||
| if (this.limits == null) { | ||
| throw new FloodMonitoringApiError('Chart axis limits have not been set'); | ||
| } | ||
| return this.limits; | ||
| } | ||
| getHorizontalGridlines() { | ||
| const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } = this.getLimits(); | ||
| const xOffset = this.strokeWidth / 2; | ||
| const yOffset = this.plotHeight; | ||
| const x1 = xOffset; | ||
| const x2 = xOffset + (maxTime - minTime) * timeScale; | ||
| // Horizontal grid lines. | ||
| const stroke = '#ddd'; | ||
| const lines = createSvgElement('g', { stroke }); | ||
| const labels = createSvgElement('g'); | ||
| const valueRange = maxValue - minValue; | ||
| // Horizontal grid interval. | ||
| const [interval, exponent] = getInterval(valueRange, 9); | ||
| const factor = 10 ** -exponent; | ||
| const base = Math.ceil((minValue * factor) / interval + 1) * interval; | ||
| let i = 0; | ||
| let current = base / factor; | ||
| while (current < maxValue) { | ||
| const y1 = yOffset - (current - minValue) * valueScale; | ||
| lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 })); | ||
| labels.append(createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`)); | ||
| ++i; | ||
| current = (base + i * interval) / factor; | ||
| } | ||
| const timeAxisLine = createSvgElement('line', { x1, y1: yOffset, x2, y2: yOffset }, { stroke: '#777' }); | ||
| return [lines, labels, timeAxisLine]; | ||
| } | ||
| getTimeScale() { | ||
| const { minTime, maxTime, timeScale } = this.getLimits(); | ||
| const xOffset = this.strokeWidth / 2; | ||
| const yOffset = this.plotHeight + this.strokeWidth / 2; | ||
| const y1 = yOffset + this.fontSizePx * 3; | ||
| const y2 = yOffset - this.plotHeight; | ||
| // Vertical grid lines. | ||
| const stroke = '#ddd'; | ||
| const lines = createSvgElement('g', { stroke }); | ||
| const labels = createSvgElement('g'); | ||
| // Vertical grid interval. | ||
| const base = minTime; | ||
| const interval = 86400; | ||
| let i = 0; | ||
| let current = base; | ||
| const labelOffset = 43200 * timeScale; | ||
| const fill = '#444'; | ||
| while (current <= maxTime) { | ||
| const x1 = xOffset + (current - minTime) * timeScale; | ||
| const d = new Date(current * 1000); | ||
| // lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 })); | ||
| lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 })); | ||
| labels.append(createSvgElement('text', { | ||
| x: x1 + labelOffset, | ||
| y: y1 - this.fontSizePx * 1.8, | ||
| 'text-anchor': 'middle', | ||
| }, { fill }, `${dddFormatter.format(d)}`), createSvgElement('text', { | ||
| x: x1 + labelOffset, | ||
| y: y1 - this.fontSizePx * 0.5, | ||
| 'text-anchor': 'middle', | ||
| }, { fill }, `${dMmmFormatter.format(d)}`)); | ||
| ++i; | ||
| current = base + i * interval; | ||
| } | ||
| return [lines, labels]; | ||
| } | ||
| render() { | ||
| var _a, _b, _c, _d; | ||
| // Calculate axis scales. | ||
| const limits = getLimits(this.series[0].data); | ||
| limits.minValue = (_a = this.series[0].min) !== null && _a !== void 0 ? _a : limits.minValue; | ||
| limits.maxValue = (_b = this.series[0].max) !== null && _b !== void 0 ? _b : limits.maxValue; | ||
| limits.minTime = (_c = this.options.minTime) !== null && _c !== void 0 ? _c : limits.minTime; | ||
| limits.maxTime = (_d = this.options.maxTime) !== null && _d !== void 0 ? _d : limits.maxTime; | ||
| this.limits = Object.assign(Object.assign({}, limits), { valueScale: (this.plotHeight - this.strokeWidth) / | ||
| (limits.maxValue - limits.minValue), timeScale: (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime) }); | ||
| // Time axis. | ||
| const [timeLines, timeLabels] = this.getTimeScale(); | ||
| this.el.append(timeLines); | ||
| // Value axis. | ||
| const [valueLines, valueLabels, timeAxisLine] = this.getHorizontalGridlines(); | ||
| this.el.append(valueLines); | ||
| this.el.append(timeAxisLine); | ||
| this.plotData(); | ||
| // Plot labels on top of the line. | ||
| this.el.append(timeLabels); | ||
| this.el.append(valueLabels); | ||
| this.el.append(createSvgElement('text', { | ||
| x: this.width / 2, | ||
| 'text-anchor': 'middle', | ||
| y: this.height - this.fontSizePx * 0.5, | ||
| }, { fill: '#595959' }, this.attribution)); | ||
| this.plotLastValue(); | ||
| } | ||
| plotLastValue() { | ||
| const { data, unit, formatter } = this.series[0]; | ||
| // If there is no data show a message. | ||
| if (data.length === 0) { | ||
| const x = this.plotWidth / 2; | ||
| const y = this.plotHeight / 2; | ||
| this.el.append(...this.createLargeLabel(x, y, 'No data', 'middle')); | ||
| return; | ||
| } | ||
| const [time, value] = data[data.length - 1]; | ||
| const { minTime, timeScale, maxValue, minValue } = this.getLimits(); | ||
| const v = formatter == null ? value : formatter(value); | ||
| const xOffset = this.strokeWidth / 2; | ||
| // const yOffset = this.plotHeight - this.strokeWidth / 2; | ||
| const x = xOffset + (time - minTime) * timeScale; | ||
| const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5; | ||
| const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2; | ||
| this.el.append( | ||
| // Value label. | ||
| ...this.createLargeLabel(x, y, `${v} ${unit}`), | ||
| // Time label. | ||
| ...this.createLabel(x, y, `${timeFormatter.format(new Date(time * 1000))}`)); | ||
| } | ||
| plotData() { | ||
| const { data } = this.series[0]; | ||
| // Don't do anything we don't have to! | ||
| if (data.length === 0) | ||
| return; | ||
| const xOffset = this.strokeWidth / 2; | ||
| const yOffset = this.plotHeight - this.strokeWidth / 2; | ||
| const { minTime, timeScale, minValue, valueScale } = this.getLimits(); | ||
| // First data point. | ||
| const x = xOffset + (data[0][0] - minTime) * timeScale; | ||
| const y = yOffset - (data[0][1] - minValue) * valueScale; | ||
| const points = [`M${x},${y}`]; | ||
| // Remaining data points. | ||
| for (let i = 1; i < data.length; ++i) { | ||
| const x = xOffset + (data[i][0] - minTime) * timeScale; | ||
| const y = yOffset - (data[i][1] - minValue) * valueScale; | ||
| points.push(`L${x},${y}`); | ||
| } | ||
| // Plot the data. | ||
| const path = createSvgElement('path', { | ||
| d: points.join(''), | ||
| stroke: this.plotColor, | ||
| 'stroke-width': this.strokeWidth, | ||
| fill: 'none', | ||
| }); | ||
| this.el.append(path); | ||
| } | ||
| createLabel(x, y, text, anchor = 'end') { | ||
| return [ | ||
| // Background for time label. | ||
| createSvgElement('text', { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor }, { | ||
| stroke: this.labelBg, | ||
| 'stroke-width': this.labelBgWidth, | ||
| }, text), | ||
| // Time label. | ||
| createSvgElement('text', { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor }, { fill: this.plotColor }, text), | ||
| ]; | ||
| } | ||
| createLargeLabel(x, y, text, anchor = 'end') { | ||
| return [ | ||
| // Background for value label. | ||
| createSvgElement('text', { x, y, 'text-anchor': anchor }, { | ||
| 'font-size': '1.5em', | ||
| 'font-weight': 'bold', | ||
| stroke: this.labelBg, | ||
| 'stroke-width': this.labelBgWidth, | ||
| }, text), | ||
| // Value label. | ||
| createSvgElement('text', { x, y, 'text-anchor': anchor }, { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' }, text), | ||
| ]; | ||
| } | ||
| } | ||
| const getLimits = (data) => { | ||
| if (data.length < 1) { | ||
| return { minTime: 0, maxTime: 1, minValue: 0, maxValue: 0 }; | ||
| } | ||
| const minTime = data[0][0]; | ||
| const maxTime = data[data.length - 1][0]; | ||
| let minValue = Infinity; | ||
| let maxValue = -minValue; | ||
| for (const [, value] of data) { | ||
| minValue = Math.min(minValue, value); | ||
| maxValue = Math.max(maxValue, value); | ||
| } | ||
| return { minTime, maxTime, minValue, maxValue }; | ||
| }; | ||
| const getInterval = (range, maxDivisions) => { | ||
| const exponent = Math.floor(Math.log10(range)) - 1; | ||
| const k = range / (maxDivisions * 10 ** exponent); | ||
| const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10; | ||
| return [mantissa, exponent]; | ||
| }; | ||
| // There is no need to be secure about this! | ||
| const baseUrl = 'http://environment.data.gov.uk/flood-monitoring'; | ||
| const apiFetch = async (path, query = {}) => { | ||
| const queryString = new URLSearchParams(query).toString(); | ||
| const uri = queryString | ||
| ? `${baseUrl}${path}?${queryString}` | ||
| : `${baseUrl}${path}`; | ||
| const response = await fetch(uri); | ||
| return { data: await response.json(), response }; | ||
| }; | ||
| /** | ||
| * Convert a Date to a format recognized by the EA API for a query parameter. | ||
| * | ||
| * @param date Convert from. | ||
| * @returns A string in the EA API query parameter format. | ||
| */ | ||
| const toTimeParameter = (date) => { | ||
| return date.toISOString().substring(0, 19) + 'Z'; | ||
| }; | ||
| /* | ||
| Useful response headers | ||
| Date: 'Sat, 13 May 2023 09:14:07 GMT', | ||
| last-modified: Sat, 13 May 2023 09:03:13 GMT, | ||
| Response meta: | ||
| publisher: 'Environment Agency', | ||
| license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/', | ||
| documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference', | ||
| version: '0.9', | ||
| comment: 'Status: Beta service', | ||
| hasFormat: [ | ||
| "http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z", | ||
| "http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z", | ||
| "http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z", | ||
| "http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z" | ||
| ], | ||
| */ | ||
| const prefix = 'riverDataWidget'; | ||
| const addPrefix = (key) => `${prefix}|${key}`; | ||
| let instance; | ||
| class Store { | ||
| clear(destroy = false) { | ||
| for (const key of this.keys()) { | ||
| localStorage.removeItem(addPrefix(key)); | ||
| } | ||
| if (destroy) { | ||
| localStorage.removeItem(prefix); | ||
| return; | ||
| } | ||
| localStorage.setItem(prefix, JSON.stringify([])); | ||
| } | ||
| get(key) { | ||
| const value = localStorage.getItem(addPrefix(key)); | ||
| return value === null ? null : JSON.parse(value); | ||
| } | ||
| has(key) { | ||
| return this.keys().includes(key); | ||
| } | ||
| /** | ||
| * Detect active localStorage. | ||
| * | ||
| * @returns true iff localStorage for the widget is active. | ||
| */ | ||
| isActive() { | ||
| return localStorage.getItem(prefix) !== null; | ||
| } | ||
| keys() { | ||
| const storedKeys = localStorage.getItem(prefix); | ||
| return storedKeys === null ? [] : JSON.parse(storedKeys); | ||
| } | ||
| set(key, value) { | ||
| const json = JSON.stringify(value); | ||
| const storedKeys = localStorage.getItem(prefix); | ||
| const keys = storedKeys === null ? [] : JSON.parse(storedKeys); | ||
| if (!keys.includes(key)) { | ||
| keys.push(key); | ||
| localStorage.setItem(prefix, JSON.stringify(keys)); | ||
| } | ||
| localStorage.setItem(addPrefix(key), json); | ||
| } | ||
| unset(key) { | ||
| // Remove it before we do anything else. | ||
| localStorage.removeItem(addPrefix(key)); | ||
| // Then remove it from the list of keys. | ||
| const storedKeys = localStorage.getItem(prefix); | ||
| const keys = storedKeys === null ? [] : JSON.parse(storedKeys); | ||
| const index = keys.indexOf(key); | ||
| // If it doesn't exist we don't have to remove it. | ||
| if (index === -1) | ||
| return false; | ||
| keys.splice(index, 1); | ||
| localStorage.setItem(prefix, JSON.stringify(keys)); | ||
| return true; | ||
| } | ||
| } | ||
| const useStore = () => { | ||
| if (!instance) { | ||
| instance = new Store(); | ||
| } | ||
| return instance; | ||
| }; | ||
| // Throttle requests to five minutes. | ||
| const THROTTLE_MS = 5 * MINUTE_MS; | ||
| /** | ||
| * Fetch the readings for a measure. | ||
| * | ||
| * @param id The EA measure id. | ||
| * @returns A promise for an array of readings for the measure. | ||
| */ | ||
| const fetchMeasureReadings = async (id, options = {}) => { | ||
| // Set the parameters for the request. | ||
| const params = { _sorted: '' }; | ||
| if (options.since) { | ||
| params.since = toTimeParameter(options.since); | ||
| } | ||
| // Get the response, casting the items to ReadingDTOs. | ||
| const response = (await apiFetch(`/id/measures/${id}/readings`, params)); | ||
| return [parseReadings(response.data.items)[id] || [], response]; | ||
| }; | ||
| const filterSince = (data, since) => { | ||
| const position = data.findIndex((reading) => reading[0] >= since); | ||
| return position < 0 ? [] : data.slice(position); | ||
| }; | ||
| /** | ||
| * Get the readings for a measure. | ||
| * | ||
| * @todo Caching and throttling. | ||
| * | ||
| * @param id The EA measure id. | ||
| * @returns A promise for an array of readings for the measure. | ||
| */ | ||
| const getMeasureReadings = async (id, options = {}) => { | ||
| // Get the saved readings. | ||
| const key = `readings|${id}`; | ||
| const store = useStore(); | ||
| const stored = store.get(key) || { | ||
| data: [], | ||
| lastCheck: 0, | ||
| storedSince: Infinity, | ||
| }; | ||
| const { data, lastCheck } = stored; | ||
| let { storedSince } = stored; | ||
| const discardBefore = startOfDay(null, -8, true).valueOf() / 1000; | ||
| // Discard any older than 30 days. | ||
| while (data.length && data[0][0] < discardBefore) { | ||
| [storedSince] = data[0]; | ||
| data.shift(); | ||
| } | ||
| // If we have data early enough apply throttle. | ||
| const lastStored = data.length ? data[data.length - 1][0] : 0; | ||
| const requestedSince = (options.since && options.since.valueOf() / 1000) || 0; | ||
| if (storedSince <= requestedSince && | ||
| Date.now() < lastCheck * 1000 + THROTTLE_MS) { | ||
| // Throttled. | ||
| return filterSince(data, requestedSince); | ||
| } | ||
| const fetchOptions = Object.assign(Object.assign({}, options), { since: new Date(Math.max(requestedSince, lastStored) * 1000) }); | ||
| const [newData] = await fetchMeasureReadings(id, fetchOptions); | ||
| mergeReadings(data, newData); | ||
| storedSince = Math.min(requestedSince, storedSince); | ||
| store.set(key, { lastCheck: Date.now() / 1000, data, storedSince }); | ||
| return filterSince(data, requestedSince); | ||
| }; | ||
| const mergeReadings = (first, second) => { | ||
| if (!second.length) | ||
| return; | ||
| let firstPos = first.length - 1; | ||
| while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) { | ||
| --firstPos; | ||
| } | ||
| first.splice(firstPos + 1, Infinity, ...second); | ||
| }; | ||
| const parseReadings = (items) => { | ||
| const ranges = {}; | ||
| for (const { measure, dateTime, value } of items) { | ||
| if (ranges[measure] == null) { | ||
| ranges[measure] = []; | ||
| } | ||
| ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]); | ||
| } | ||
| const rangesById = {}; | ||
| for (const [key, range] of Object.entries(ranges)) { | ||
| rangesById[key.substring(key.lastIndexOf('/') + 1)] = range; | ||
| } | ||
| return rangesById; | ||
| }; | ||
| const drawMeasureWidget = async (parentEl, measureId, options = {}) => { | ||
| var _a; | ||
| // Get readings for the last 7 days in local time. | ||
| const since = startOfDay(null, -7, true); | ||
| let data = []; | ||
| // Get the right API. | ||
| const parts = measureId.split('/'); | ||
| const id = (_a = parts.pop()) !== null && _a !== void 0 ? _a : ''; | ||
| const api = parts.length === 0 ? 'flood' : parts[0]; | ||
| switch (api) { | ||
| case 'flood': | ||
| data = await getMeasureReadings(id, { since }); | ||
| } | ||
| // Clear the GUI deck. | ||
| parentEl.replaceChildren(); | ||
| const measure = parseMeasureId(measureId); | ||
| const { unit } = translateMeasureProperties(measure); | ||
| const series1 = { data, unit, formatter: round3 }; | ||
| // Set max/min options for plot from widget options. | ||
| if (options.riverDataWidgetMaxValue != null) { | ||
| series1.max = parseFloat(options.riverDataWidgetMaxValue); | ||
| } | ||
| if (options.riverDataWidgetMinValue != null) { | ||
| series1.min = parseFloat(options.riverDataWidgetMinValue); | ||
| } | ||
| // Deal with no data. | ||
| if (data.length === 0) { | ||
| const minTime = since.valueOf() / 1000; | ||
| const maxTime = minTime + 86400 * 7; | ||
| const chartOptions = { minTime, maxTime }; | ||
| const chart = new Chart(parentEl, [series1], chartOptions); | ||
| chart.render(); | ||
| return; | ||
| } | ||
| const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000; | ||
| const maxTime = startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000; | ||
| const chartOptions = { | ||
| minTime, | ||
| maxTime, | ||
| // attribution: `www.riverdata.co.uk/station/${measure.stationId}`, | ||
| }; | ||
| const chart = new Chart(parentEl, [series1], chartOptions); | ||
| chart.render(); | ||
| }; | ||
| /** | ||
| * RiverDataWidget https://github.com/pb-uk/river-data-widget. | ||
| * | ||
| * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk. | ||
| * @license AGPL-3.0-or-later see LICENSE.md. | ||
| */ | ||
| const version = '1.2.0'; | ||
| export { drawMeasureWidget, version }; | ||
| //# sourceMappingURL=index.mjs.map |
| {"version":3,"file":"index.mjs","sources":["src/helpers/format.ts","src/flood-monitoring-api/error.ts","src/flood-monitoring-api/measure.ts","src/helpers/dom.ts","src/helpers/time.ts","src/widget/chart.ts","src/flood-monitoring-api/api.ts","src/flood-monitoring-api/store.ts","src/flood-monitoring-api/reading.ts","src/widget/render.ts","src/index.ts"],"sourcesContent":["export const FONT_STACK =\n '-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Roboto\",\"Helvetica Neue\",Arial,sans-serif';\n\nexport const round3 = (value: number) =>\n value < 100 ? value.toPrecision(3) : Math.round(value).toString();\n","export class FloodMonitoringApiError extends Error {\n public info: Record<string, unknown>;\n\n constructor(msg: string, info: Record<string, unknown> = {}) {\n super(msg);\n this.name = 'FloodMonitoringApiError';\n this.info = info;\n }\n}\n","import { FloodMonitoringApiError } from './error';\n\nexport { parseMeasureId };\n\nconst parseMeasureId = (measureId: string) => {\n // ............base/ stat-paramet-qualifi- type -interva-unit\n const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/;\n const matches = measureId.match(regExp);\n if (matches === null) {\n throw new FloodMonitoringApiError('Cannot parse measure id', { measureId });\n }\n const [unit, interval, type, qualifier, parameter, stationId] =\n matches.reverse();\n const qualifiedParameter = qualifier.length\n ? `${parameter}-${qualifier}`\n : parameter;\n return {\n stationId,\n parameter,\n qualifier,\n type,\n interval,\n unit,\n qualifiedParameter,\n };\n};\n\nconst measureTranslations: Record<string, Record<string, string>> = {\n unit: {\n m3_s: 'm³/s',\n mAOD: 'm',\n mASD: 'm',\n },\n qualifiedParameter: {\n 'level-stage': 'level',\n 'level-downstage': 'downstream level',\n },\n};\n\nexport const translateMeasureProperties = (measure: Record<string, string>) => {\n const translated: Record<string, string> = {};\n for (const prop in measure) {\n const value = measure[prop];\n if (measureTranslations[prop] && measureTranslations[prop][value]) {\n translated[prop] = measureTranslations[prop][value];\n } else {\n translated[prop] = value;\n }\n }\n return translated;\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { createElement, createSvgElement, setAttributes, setStyles };\n\ntype AttributeList = Record<string, string | number>;\n\nconst setAttributes = <T extends HTMLElement | SVGElement>(\n el: T,\n attributes: AttributeList\n): T => {\n for (const [key, value] of Object.entries(attributes)) {\n el.setAttribute(key, `${value}`);\n }\n return el;\n};\n\nconst setStyles = <T extends HTMLElement | SVGElement>(\n el: T,\n styles: AttributeList\n): T => {\n for (const [key, value] of Object.entries(styles)) {\n // Workaround (el.style.setProperty uses kebab-case keys).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (<any>el.style)[key] = value;\n }\n return el;\n};\n\nconst createElement = (\n name = 'div',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n): HTMLElement => {\n const el = document.createElement(name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n\nconst createSvgElement = (\n name = 'svg',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n) => {\n const el = document.createElementNS('http://www.w3.org/2000/svg', name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n","export const MINUTE_MS = 60000;\n// const HOUR_MS = 3600000;\nexport const DAY_MS = 86400000;\n\n/**\n * Get the Date at the start of a day in UTC or local time.\n *\n * @param offset\n * @param timeZone The time zone offset in minutes, or set to `true` to use the\n * local time zone (`false`, the default, uses UTC).\n * @returns The reqested date.\n */\nexport const startOfDay = (\n date: Date | null = null,\n offset = 0,\n timeZone: boolean | number = false\n): Date => {\n if (timeZone === false) {\n // Use UTC.\n const base = date === null ? Date.now() : date.valueOf();\n return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS);\n }\n\n const now = new Date();\n const tz = timeZone === true ? now.getTimezoneOffset() : timeZone;\n const local = now.valueOf() + tz * MINUTE_MS;\n return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS);\n};\n\n/**\n * | | long |short|narrow|numeric|2-digit|\n * |:-------:|:-----------:|:---:|:----:|:-----:|:-----:|\n * | weekday | Monday | Mon | M | | |\n * | era | Anno Domini | AD | A | | |\n * | year | | | | 2012 | 12 |\n * | month | March | Mar | M | 3 | 03 |\n * | day | | | | 1 | 01 |\n * | hour | | | | 1 | 01 |\n * | minute | | | | 1 | 01 |\n * | second | | | | 1 | 01 |\n *\n * * fractionalSecondDigits: 1, 2 or 3 for number of digits.\n * * timeZoneName: long (Pacific Standard Time), short (PST),\n * longOffset (GMT-0800), shortOffset (GMT-8), longGeneric (Pacific Time),\n * shortGeneric (PT).\n */\n\nexport const dateFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n // year: 'numeric',\n});\n\nexport const timeFormatter = new Intl.DateTimeFormat('en-GB', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n timeZoneName: 'short',\n});\n\nexport const dddFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'short',\n});\n\nexport const dMmmFormatter = new Intl.DateTimeFormat('en-GB', {\n day: 'numeric',\n month: 'short',\n});\n","import { createSvgElement } from '../helpers/dom';\nimport { timeFormatter, dddFormatter, dMmmFormatter } from '../helpers/time';\nimport { FloodMonitoringApiError } from '../flood-monitoring-api/error';\nimport { FONT_STACK } from '../helpers/format';\n\nexport interface ChartOptions {\n minTime?: number;\n maxTime?: number;\n attribution?: string;\n}\n\nexport interface ChartScaleLimits {\n minTime: number;\n maxTime: number;\n timeScale: number;\n minValue: number;\n maxValue: number;\n valueScale: number;\n}\n\nexport interface ChartSeries {\n data: TimeSeriesValue[];\n min?: number;\n max?: number;\n unit?: string;\n formatter?: (value: number) => string;\n}\n\nexport type TimeSeriesValue = [\n ts: number, // Unix time stamp (seconds).\n v: number // Value.\n];\n\nexport class Chart {\n protected strokeWidth = 2;\n protected fontSizePx = 14;\n\n protected el: SVGElement;\n protected series: ChartSeries[];\n protected options: ChartOptions;\n\n protected width = 480; // 400;\n protected height = 270; // 225;\n protected plotHeight = this.height - this.fontSizePx * 4.5;\n protected plotWidth = this.width - this.strokeWidth;\n\n protected limits?: ChartScaleLimits;\n\n protected plotColor = '#77C';\n protected labelBg = 'rgba(255,255,255,0.5)';\n protected labelBgWidth = '0.5em';\n\n protected attribution =\n 'Uses Environment Agency data from the real-time API (Beta)';\n\n // CSS settings.\n // Just readable at 320x180.\n // Good from 400x225.\n // Perfect at 480x270 (font is 12px);\n protected styles = {\n 'font-family': FONT_STACK,\n 'font-size': `${this.fontSizePx}px`,\n display: 'block',\n margin: 'auto',\n 'max-width': '150vh',\n };\n\n constructor(\n el: HTMLElement,\n series: ChartSeries[],\n options: ChartOptions = {}\n ) {\n this.series = series;\n this.options = options;\n const viewBox = `0 0 ${this.width} ${this.height}`;\n this.attribution = options.attribution ?? this.attribution;\n this.el = createSvgElement('svg', { viewBox }, this.styles);\n el.append(this.el);\n }\n\n getLimits(): ChartScaleLimits {\n if (this.limits == null) {\n throw new FloodMonitoringApiError('Chart axis limits have not been set');\n }\n return this.limits;\n }\n\n getHorizontalGridlines(): SVGElement[] {\n const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } =\n this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight;\n const x1 = xOffset;\n const x2 = xOffset + (maxTime - minTime) * timeScale;\n // Horizontal grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n const valueRange = maxValue - minValue;\n // Horizontal grid interval.\n const [interval, exponent] = getInterval(valueRange, 9);\n const factor = 10 ** -exponent;\n const base = Math.ceil((minValue * factor) / interval + 1) * interval;\n let i = 0;\n let current = base / factor;\n while (current < maxValue) {\n const y1 = yOffset - (current - minValue) * valueScale;\n lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n labels.append(\n createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`)\n );\n ++i;\n current = (base + i * interval) / factor;\n }\n const timeAxisLine = createSvgElement(\n 'line',\n { x1, y1: yOffset, x2, y2: yOffset },\n { stroke: '#777' }\n );\n\n return [lines, labels, timeAxisLine];\n }\n\n getTimeScale(): SVGElement[] {\n const { minTime, maxTime, timeScale } = this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight + this.strokeWidth / 2;\n const y1 = yOffset + this.fontSizePx * 3;\n const y2 = yOffset - this.plotHeight;\n // Vertical grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n // Vertical grid interval.\n const base = minTime;\n const interval = 86400;\n let i = 0;\n let current = base;\n const labelOffset = 43200 * timeScale;\n const fill = '#444';\n while (current <= maxTime) {\n const x1 = xOffset + (current - minTime) * timeScale;\n const d = new Date(current * 1000);\n // lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 }));\n labels.append(\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 1.8,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dddFormatter.format(d)}`\n ),\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 0.5,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dMmmFormatter.format(d)}`\n )\n );\n ++i;\n current = base + i * interval;\n }\n return [lines, labels];\n }\n\n render() {\n // Calculate axis scales.\n const limits = getLimits(this.series[0].data);\n limits.minValue = this.series[0].min ?? limits.minValue;\n limits.maxValue = this.series[0].max ?? limits.maxValue;\n limits.minTime = this.options.minTime ?? limits.minTime;\n limits.maxTime = this.options.maxTime ?? limits.maxTime;\n\n this.limits = {\n ...limits,\n valueScale:\n (this.plotHeight - this.strokeWidth) /\n (limits.maxValue - limits.minValue),\n timeScale:\n (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime),\n };\n\n // Time axis.\n const [timeLines, timeLabels] = this.getTimeScale();\n this.el.append(timeLines);\n\n // Value axis.\n const [valueLines, valueLabels, timeAxisLine] =\n this.getHorizontalGridlines();\n this.el.append(valueLines);\n this.el.append(timeAxisLine);\n\n this.plotData();\n\n // Plot labels on top of the line.\n this.el.append(timeLabels);\n this.el.append(valueLabels);\n\n this.el.append(\n createSvgElement(\n 'text',\n {\n x: this.width / 2,\n 'text-anchor': 'middle',\n y: this.height - this.fontSizePx * 0.5,\n },\n { fill: '#595959' },\n this.attribution\n )\n );\n\n this.plotLastValue();\n }\n\n plotLastValue() {\n const { data, unit, formatter } = this.series[0];\n\n // If there is no data show a message.\n if (data.length === 0) {\n const x = this.plotWidth / 2;\n const y = this.plotHeight / 2;\n this.el.append(...this.createLargeLabel(x, y, 'No data', 'middle'));\n return;\n }\n\n const [time, value] = data[data.length - 1];\n const { minTime, timeScale, maxValue, minValue } = this.getLimits();\n\n const v = formatter == null ? value : formatter(value);\n const xOffset = this.strokeWidth / 2;\n // const yOffset = this.plotHeight - this.strokeWidth / 2;\n const x = xOffset + (time - minTime) * timeScale;\n const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5;\n const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2;\n\n this.el.append(\n // Value label.\n ...this.createLargeLabel(x, y, `${v} ${unit}`),\n // Time label.\n ...this.createLabel(\n x,\n y,\n `${timeFormatter.format(new Date(time * 1000))}`\n )\n );\n }\n\n plotData() {\n const { data } = this.series[0];\n // Don't do anything we don't have to!\n if (data.length === 0) return;\n\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight - this.strokeWidth / 2;\n const { minTime, timeScale, minValue, valueScale } = this.getLimits();\n // First data point.\n const x = xOffset + (data[0][0] - minTime) * timeScale;\n const y = yOffset - (data[0][1] - minValue) * valueScale;\n const points = [`M${x},${y}`];\n // Remaining data points.\n for (let i = 1; i < data.length; ++i) {\n const x = xOffset + (data[i][0] - minTime) * timeScale;\n const y = yOffset - (data[i][1] - minValue) * valueScale;\n points.push(`L${x},${y}`);\n }\n // Plot the data.\n const path = createSvgElement('path', {\n d: points.join(''),\n stroke: this.plotColor,\n 'stroke-width': this.strokeWidth,\n fill: 'none',\n });\n this.el.append(path);\n }\n\n protected createLabel(x: number, y: number, text: string, anchor = 'end') {\n return [\n // Background for time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor },\n {\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n text\n ),\n // Time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': anchor },\n { fill: this.plotColor },\n text\n ),\n ];\n }\n\n protected createLargeLabel(\n x: number,\n y: number,\n text: string,\n anchor = 'end'\n ) {\n return [\n // Background for value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': anchor },\n {\n 'font-size': '1.5em',\n 'font-weight': 'bold',\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n text\n ),\n // Value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': anchor },\n { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' },\n text\n ),\n ];\n }\n}\n\nexport const getLimits = (data: TimeSeriesValue[]) => {\n if (data.length < 1) {\n return { minTime: 0, maxTime: 1, minValue: 0, maxValue: 0 };\n }\n const minTime = data[0][0];\n const maxTime = data[data.length - 1][0];\n let minValue = Infinity;\n let maxValue = -minValue;\n for (const [, value] of data) {\n minValue = Math.min(minValue, value);\n maxValue = Math.max(maxValue, value);\n }\n return { minTime, maxTime, minValue, maxValue };\n};\n\nexport const getInterval = (range: number, maxDivisions: number) => {\n const exponent = Math.floor(Math.log10(range)) - 1;\n const k = range / (maxDivisions * 10 ** exponent);\n const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10;\n return [mantissa, exponent];\n};\n","// There is no need to be secure about this!\nconst baseUrl = 'http://environment.data.gov.uk/flood-monitoring';\n\nexport interface ApiResponse<T> {\n data: {\n items: T;\n };\n response: Response;\n}\n\nexport interface ApiParameters {\n since?: string; // Time from.\n _sorted?: ''; // Flag for sorting.\n}\n\nexport const apiFetch = async (\n path: string,\n query = {}\n): Promise<ApiResponse<unknown>> => {\n const queryString = new URLSearchParams(query).toString();\n const uri = queryString\n ? `${baseUrl}${path}?${queryString}`\n : `${baseUrl}${path}`;\n const response = await fetch(uri);\n return { data: await response.json(), response };\n};\n\n/**\n * Convert a Date to a format recognized by the EA API for a query parameter.\n *\n * @param date Convert from.\n * @returns A string in the EA API query parameter format.\n */\nexport const toTimeParameter = (date: Date): string => {\n return date.toISOString().substring(0, 19) + 'Z';\n};\n\n/*\nUseful response headers\n Date: 'Sat, 13 May 2023 09:14:07 GMT',\n last-modified: Sat, 13 May 2023 09:03:13 GMT,\nResponse meta:\n publisher: 'Environment Agency',\n license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/',\n documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference',\n version: '0.9',\n comment: 'Status: Beta service',\n hasFormat: [\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z\"\n ],\n*/\n","const prefix = 'riverDataWidget';\n\nconst addPrefix = (key: string): string => `${prefix}|${key}`;\n\nlet instance: Store;\n\nclass Store {\n clear(destroy = false) {\n for (const key of this.keys()) {\n localStorage.removeItem(addPrefix(key));\n }\n if (destroy) {\n localStorage.removeItem(prefix);\n return;\n }\n localStorage.setItem(prefix, JSON.stringify([]));\n }\n\n get(key: string) {\n const value = localStorage.getItem(addPrefix(key));\n return value === null ? null : JSON.parse(value);\n }\n\n has(key: string): boolean {\n return this.keys().includes(key);\n }\n\n /**\n * Detect active localStorage.\n *\n * @returns true iff localStorage for the widget is active.\n */\n isActive() {\n return localStorage.getItem(prefix) !== null;\n }\n\n keys(): string[] {\n const storedKeys = localStorage.getItem(prefix);\n return storedKeys === null ? [] : JSON.parse(storedKeys);\n }\n\n set(key: string, value: unknown) {\n const json = JSON.stringify(value);\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n if (!keys.includes(key)) {\n keys.push(key);\n localStorage.setItem(prefix, JSON.stringify(keys));\n }\n localStorage.setItem(addPrefix(key), json);\n }\n\n unset(key: string): boolean {\n // Remove it before we do anything else.\n localStorage.removeItem(addPrefix(key));\n\n // Then remove it from the list of keys.\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n const index = keys.indexOf(key);\n\n // If it doesn't exist we don't have to remove it.\n if (index === -1) return false;\n\n keys.splice(index, 1);\n localStorage.setItem(prefix, JSON.stringify(keys));\n return true;\n }\n}\n\nexport const useStore = (): Store => {\n if (!instance) {\n instance = new Store();\n }\n return instance;\n};\n","import { apiFetch, toTimeParameter } from './api';\nimport { useStore } from './store';\nimport { MINUTE_MS, startOfDay } from '../helpers/time';\n\nimport type { ApiParameters, ApiResponse } from './api';\n\n// Throttle requests to five minutes.\nconst THROTTLE_MS = 5 * MINUTE_MS;\n\n/**\n * Internal format for readings.\n */\nexport type Reading = [\n timestamp: number, // Unix epoch timestamp (seconds).\n value: number // Value.\n];\n\n/**\n * Internal format for readings.\n */\nexport interface ReadingOptions {\n since?: Date; // Time from.\n}\n\n/**\n * Internal format for readings.\n */\ntype ReadingResponse = [a: Reading[], b: ApiResponse<ReadingDTO[]>];\n\n/**\n * Data transfer object for readings provided by the API.\n */\ninterface ReadingDTO {\n '@id': string; // The URL of this reading.\n dateTime: string; // e.g. '2023-05-13T09:00:00Z'.\n measure: string; // The URL of the measure.\n value: number; // The value in the appropriate units.\n}\n\ninterface StoredReadings {\n storedSince: number;\n lastCheck: number;\n data: Reading[];\n}\n\n/**\n * Fetch the readings for a measure.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nconst fetchMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<ReadingResponse> => {\n // Set the parameters for the request.\n const params: ApiParameters = { _sorted: '' };\n if (options.since) {\n params.since = toTimeParameter(options.since);\n }\n // Get the response, casting the items to ReadingDTOs.\n const response = <ApiResponse<ReadingDTO[]>>(\n await apiFetch(`/id/measures/${id}/readings`, params)\n );\n return [parseReadings(response.data.items)[id] || [], response];\n};\n\nexport const filterSince = (data: Reading[], since: number) => {\n const position = data.findIndex((reading) => reading[0] >= since);\n return position < 0 ? [] : data.slice(position);\n};\n\n/**\n * Get the readings for a measure.\n *\n * @todo Caching and throttling.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nexport const getMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<Reading[]> => {\n // Get the saved readings.\n const key = `readings|${id}`;\n const store = useStore();\n\n const stored: StoredReadings = store.get(key) || {\n data: [],\n lastCheck: 0,\n storedSince: Infinity,\n };\n const { data, lastCheck } = stored;\n let { storedSince } = stored;\n\n const discardBefore = startOfDay(null, -8, true).valueOf() / 1000;\n\n // Discard any older than 30 days.\n while (data.length && data[0][0] < discardBefore) {\n [storedSince] = data[0];\n data.shift();\n }\n\n // If we have data early enough apply throttle.\n const lastStored = data.length ? data[data.length - 1][0] : 0;\n const requestedSince = (options.since && options.since.valueOf() / 1000) || 0;\n if (\n storedSince <= requestedSince &&\n Date.now() < lastCheck * 1000 + THROTTLE_MS\n ) {\n // Throttled.\n return filterSince(data, requestedSince);\n }\n\n const fetchOptions: ReadingOptions = {\n ...options,\n since: new Date(Math.max(requestedSince, lastStored) * 1000),\n };\n\n const [newData] = await fetchMeasureReadings(id, fetchOptions);\n mergeReadings(data, newData);\n storedSince = Math.min(requestedSince, storedSince);\n store.set(key, { lastCheck: Date.now() / 1000, data, storedSince });\n return filterSince(data, requestedSince);\n};\n\nexport const mergeReadings = (first: Reading[], second: Reading[]): void => {\n if (!second.length) return;\n\n let firstPos = first.length - 1;\n while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) {\n --firstPos;\n }\n first.splice(firstPos + 1, Infinity, ...second);\n};\n\nconst parseReadings = (items: ReadingDTO[]): Record<string, Reading[]> => {\n const ranges: Record<string, Reading[]> = {};\n for (const { measure, dateTime, value } of items) {\n if (ranges[measure] == null) {\n ranges[measure] = [];\n }\n ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]);\n }\n\n const rangesById: Record<string, Reading[]> = {};\n for (const [key, range] of Object.entries(ranges)) {\n rangesById[key.substring(key.lastIndexOf('/') + 1)] = range;\n }\n\n return rangesById;\n};\n","import { RiverDataWidgetError } from '../error';\nimport { round3 } from '../helpers/format';\nimport {\n parseMeasureId,\n translateMeasureProperties,\n} from '../flood-monitoring-api/measure';\nimport { Chart } from './chart';\nimport { getMeasureReadings as getFloodMeasureReadings } from '../flood-monitoring-api';\nimport { startOfDay } from '../helpers/time';\n\nimport type { ChartSeries } from './chart';\nimport type { Reading } from '../flood-monitoring-api/reading';\n\nexport const drawMeasureWidget = async (\n parentEl: HTMLElement,\n measureId: string,\n options: Record<string, unknown> = {}\n) => {\n // Get readings for the last 7 days in local time.\n const since = startOfDay(null, -7, true);\n let data: Reading[] = [];\n\n // Get the right API.\n const parts = measureId.split('/');\n const id = parts.pop() ?? '';\n const api = parts.length === 0 ? 'flood' : parts[0];\n switch (api) {\n case 'flood':\n data = await getFloodMeasureReadings(id, { since });\n }\n\n // Clear the GUI deck.\n parentEl.replaceChildren();\n\n const measure = parseMeasureId(measureId);\n const { unit } = translateMeasureProperties(measure);\n\n const series1: ChartSeries = { data, unit, formatter: round3 };\n // Set max/min options for plot from widget options.\n if (options.riverDataWidgetMaxValue != null) {\n series1.max = parseFloat(<string>options.riverDataWidgetMaxValue);\n }\n if (options.riverDataWidgetMinValue != null) {\n series1.min = parseFloat(<string>options.riverDataWidgetMinValue);\n }\n\n // Deal with no data.\n if (data.length === 0) {\n const minTime = since.valueOf() / 1000;\n const maxTime = minTime + 86400 * 7;\n const chartOptions = { minTime, maxTime };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n return;\n }\n\n const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000;\n const maxTime =\n startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000;\n const chartOptions = {\n minTime,\n maxTime,\n // attribution: `www.riverdata.co.uk/station/${measure.stationId}`,\n };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n};\n\n/**\n * Load a widget specified by a DOM element.\n */\nexport const loadWidget = (el: HTMLElement | string) => {\n // Get the target element from a query selector if necessary and check it\n // exists.\n const targetEl =\n typeof el === 'string' ? <HTMLElement>document.querySelector(el) : el;\n if (targetEl === null) {\n throw new Error('Target element not found');\n }\n\n // Parse element for widget type and options.\n const widgetIdParts = targetEl.dataset.riverDataWidget?.split(':') ?? [];\n const type = widgetIdParts.shift();\n const id = widgetIdParts.join(':');\n const options = targetEl.dataset;\n\n switch (type) {\n case 'measure':\n drawMeasureWidget(targetEl, id, options);\n break;\n default:\n throw new RiverDataWidgetError('Unknown widget definition', { type, id });\n }\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { drawMeasureWidget } from './widget/render';\n\nexport const version = '1.2.0';\n"],"names":["getFloodMeasureReadings"],"mappings":";;;;;;AAAO,MAAM,UAAU,GACrB,wFAAwF,CAAC;AAEpF,MAAM,MAAM,GAAG,CAAC,KAAa,KAClC,KAAK,GAAG,GAAG,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE;;ACJ7D,MAAO,uBAAwB,SAAQ,KAAK,CAAA;IAGhD,WAAY,CAAA,GAAW,EAAE,IAAA,GAAgC,EAAE,EAAA;QACzD,KAAK,CAAC,GAAG,CAAC,CAAC;AACX,QAAA,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;AACtC,QAAA,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;KAClB;AACF;;ACJD,MAAM,cAAc,GAAG,CAAC,SAAiB,KAAI;;IAE3C,MAAM,MAAM,GAAG,+CAA+C,CAAC;IAC/D,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACxC,IAAI,OAAO,KAAK,IAAI,EAAE;QACpB,MAAM,IAAI,uBAAuB,CAAC,yBAAyB,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;AAC7E,KAAA;AACD,IAAA,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC,GAC3D,OAAO,CAAC,OAAO,EAAE,CAAC;AACpB,IAAA,MAAM,kBAAkB,GAAG,SAAS,CAAC,MAAM;AACzC,UAAE,CAAA,EAAG,SAAS,CAAA,CAAA,EAAI,SAAS,CAAE,CAAA;UAC3B,SAAS,CAAC;IACd,OAAO;QACL,SAAS;QACT,SAAS;QACT,SAAS;QACT,IAAI;QACJ,QAAQ;QACR,IAAI;QACJ,kBAAkB;KACnB,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,mBAAmB,GAA2C;AAClE,IAAA,IAAI,EAAE;AACJ,QAAA,IAAI,EAAE,MAAM;AACZ,QAAA,IAAI,EAAE,GAAG;AACT,QAAA,IAAI,EAAE,GAAG;AACV,KAAA;AACD,IAAA,kBAAkB,EAAE;AAClB,QAAA,aAAa,EAAE,OAAO;AACtB,QAAA,iBAAiB,EAAE,kBAAkB;AACtC,KAAA;CACF,CAAC;AAEK,MAAM,0BAA0B,GAAG,CAAC,OAA+B,KAAI;IAC5E,MAAM,UAAU,GAA2B,EAAE,CAAC;AAC9C,IAAA,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE;AAC1B,QAAA,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAC5B,QAAA,IAAI,mBAAmB,CAAC,IAAI,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,EAAE;YACjE,UAAU,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC;AACrD,SAAA;AAAM,aAAA;AACL,YAAA,UAAU,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;AAC1B,SAAA;AACF,KAAA;AACD,IAAA,OAAO,UAAU,CAAC;AACpB,CAAC;;AClDD;;;;;AAKG;AAMH,MAAM,aAAa,GAAG,CACpB,EAAK,EACL,UAAyB,KACpB;AACL,IAAA,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE;QACrD,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,CAAG,EAAA,KAAK,CAAE,CAAA,CAAC,CAAC;AAClC,KAAA;AACD,IAAA,OAAO,EAAE,CAAC;AACZ,CAAC,CAAC;AAEF,MAAM,SAAS,GAAG,CAChB,EAAK,EACL,MAAqB,KAChB;AACL,IAAA,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;;;AAG3C,QAAA,EAAE,CAAC,KAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAC9B,KAAA;AACD,IAAA,OAAO,EAAE,CAAC;AACZ,CAAC,CAAC;AAeF,MAAM,gBAAgB,GAAG,CACvB,IAAI,GAAG,KAAK,EACZ,UAAA,GAA4B,EAAE,EAC9B,SAAwB,EAAE,EAC1B,SAA4B,GAAA,KAAK,KAC/B;IACF,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC,4BAA4B,EAAE,IAAI,CAAC,CAAC;IACxE,IAAI,SAAS,KAAK,KAAK,EAAE;AACvB,QAAA,EAAE,CAAC,SAAS,GAAG,SAAS,CAAC;AAC1B,KAAA;IACD,OAAO,SAAS,CAAC,aAAa,CAAC,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,CAAC;AAC1D,CAAC;;ACzDM,MAAM,SAAS,GAAG,KAAK,CAAC;AAC/B;AACO,MAAM,MAAM,GAAG,QAAQ,CAAC;AAE/B;;;;;;;AAOG;AACI,MAAM,UAAU,GAAG,CACxB,IAAoB,GAAA,IAAI,EACxB,MAAM,GAAG,CAAC,EACV,QAA6B,GAAA,KAAK,KAC1B;IACR,IAAI,QAAQ,KAAK,KAAK,EAAE;;AAEtB,QAAA,MAAM,IAAI,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;AACzD,QAAA,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC;AAC9D,KAAA;AAED,IAAA,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;AACvB,IAAA,MAAM,EAAE,GAAG,QAAQ,KAAK,IAAI,GAAG,GAAG,CAAC,iBAAiB,EAAE,GAAG,QAAQ,CAAC;IAClE,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,SAAS,CAAC;AAC7C,IAAA,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC;AAChE,CAAC,CAAC;AA2BK,MAAM,aAAa,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;AAC5D,IAAA,IAAI,EAAE,SAAS;AACf,IAAA,MAAM,EAAE,SAAS;AACjB,IAAA,MAAM,EAAE,KAAK;AACb,IAAA,YAAY,EAAE,OAAO;AACtB,CAAA,CAAC,CAAC;AAEI,MAAM,YAAY,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;AAC3D,IAAA,OAAO,EAAE,OAAO;AACjB,CAAA,CAAC,CAAC;AAEI,MAAM,aAAa,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;AAC5D,IAAA,GAAG,EAAE,SAAS;AACd,IAAA,KAAK,EAAE,OAAO;AACf,CAAA,CAAC;;MCnCW,KAAK,CAAA;AAkChB,IAAA,WAAA,CACE,EAAe,EACf,MAAqB,EACrB,UAAwB,EAAE,EAAA;;QApClB,IAAW,CAAA,WAAA,GAAG,CAAC,CAAC;QAChB,IAAU,CAAA,UAAA,GAAG,EAAE,CAAC;AAMhB,QAAA,IAAA,CAAA,KAAK,GAAG,GAAG,CAAC;AACZ,QAAA,IAAA,CAAA,MAAM,GAAG,GAAG,CAAC;QACb,IAAU,CAAA,UAAA,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;QACjD,IAAS,CAAA,SAAA,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC;QAI1C,IAAS,CAAA,SAAA,GAAG,MAAM,CAAC;QACnB,IAAO,CAAA,OAAA,GAAG,uBAAuB,CAAC;QAClC,IAAY,CAAA,YAAA,GAAG,OAAO,CAAC;QAEvB,IAAW,CAAA,WAAA,GACnB,4DAA4D,CAAC;;;;;AAMrD,QAAA,IAAA,CAAA,MAAM,GAAG;AACjB,YAAA,aAAa,EAAE,UAAU;AACzB,YAAA,WAAW,EAAE,CAAA,EAAG,IAAI,CAAC,UAAU,CAAI,EAAA,CAAA;AACnC,YAAA,OAAO,EAAE,OAAO;AAChB,YAAA,MAAM,EAAE,MAAM;AACd,YAAA,WAAW,EAAE,OAAO;SACrB,CAAC;AAOA,QAAA,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;AACrB,QAAA,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,MAAM,OAAO,GAAG,CAAA,IAAA,EAAO,IAAI,CAAC,KAAK,CAAA,CAAA,EAAI,IAAI,CAAC,MAAM,CAAA,CAAE,CAAC;QACnD,IAAI,CAAC,WAAW,GAAG,CAAA,EAAA,GAAA,OAAO,CAAC,WAAW,MAAI,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAA,IAAI,CAAC,WAAW,CAAC;AAC3D,QAAA,IAAI,CAAC,EAAE,GAAG,gBAAgB,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;AAC5D,QAAA,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;KACpB;IAED,SAAS,GAAA;AACP,QAAA,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,EAAE;AACvB,YAAA,MAAM,IAAI,uBAAuB,CAAC,qCAAqC,CAAC,CAAC;AAC1E,SAAA;QACD,OAAO,IAAI,CAAC,MAAM,CAAC;KACpB;IAED,sBAAsB,GAAA;AACpB,QAAA,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,GACnE,IAAI,CAAC,SAAS,EAAE,CAAC;AACnB,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;AACrC,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC;QAChC,MAAM,EAAE,GAAG,OAAO,CAAC;QACnB,MAAM,EAAE,GAAG,OAAO,GAAG,CAAC,OAAO,GAAG,OAAO,IAAI,SAAS,CAAC;;QAErD,MAAM,MAAM,GAAG,MAAM,CAAC;QACtB,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;AAChD,QAAA,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;AACrC,QAAA,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;;AAEvC,QAAA,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,WAAW,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;AACxD,QAAA,MAAM,MAAM,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC;AAC/B,QAAA,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,GAAG,MAAM,IAAI,QAAQ,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC;QACtE,IAAI,CAAC,GAAG,CAAC,CAAC;AACV,QAAA,IAAI,OAAO,GAAG,IAAI,GAAG,MAAM,CAAC;QAC5B,OAAO,OAAO,GAAG,QAAQ,EAAE;YACzB,MAAM,EAAE,GAAG,OAAO,GAAG,CAAC,OAAO,GAAG,QAAQ,IAAI,UAAU,CAAC;YACvD,KAAK,CAAC,MAAM,CAAC,gBAAgB,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;AAC/D,YAAA,MAAM,CAAC,MAAM,CACX,gBAAgB,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,CAAA,EAAG,OAAO,CAAA,CAAE,CAAC,CACrE,CAAC;AACF,YAAA,EAAE,CAAC,CAAC;YACJ,OAAO,GAAG,CAAC,IAAI,GAAG,CAAC,GAAG,QAAQ,IAAI,MAAM,CAAC;AAC1C,SAAA;QACD,MAAM,YAAY,GAAG,gBAAgB,CACnC,MAAM,EACN,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EACpC,EAAE,MAAM,EAAE,MAAM,EAAE,CACnB,CAAC;AAEF,QAAA,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC;KACtC;IAED,YAAY,GAAA;AACV,QAAA,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;AACzD,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;QACrC,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;QACvD,MAAM,EAAE,GAAG,OAAO,GAAG,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;AACzC,QAAA,MAAM,EAAE,GAAG,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC;;QAErC,MAAM,MAAM,GAAG,MAAM,CAAC;QACtB,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;AAChD,QAAA,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;;QAErC,MAAM,IAAI,GAAG,OAAO,CAAC;QACrB,MAAM,QAAQ,GAAG,KAAK,CAAC;QACvB,IAAI,CAAC,GAAG,CAAC,CAAC;QACV,IAAI,OAAO,GAAG,IAAI,CAAC;AACnB,QAAA,MAAM,WAAW,GAAG,KAAK,GAAG,SAAS,CAAC;QACtC,MAAM,IAAI,GAAG,MAAM,CAAC;QACpB,OAAO,OAAO,IAAI,OAAO,EAAE;YACzB,MAAM,EAAE,GAAG,OAAO,GAAG,CAAC,OAAO,GAAG,OAAO,IAAI,SAAS,CAAC;YACrD,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;;YAEnC,KAAK,CAAC,MAAM,CAAC,gBAAgB,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;AAC/D,YAAA,MAAM,CAAC,MAAM,CACX,gBAAgB,CACd,MAAM,EACN;gBACE,CAAC,EAAE,EAAE,GAAG,WAAW;AACnB,gBAAA,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,UAAU,GAAG,GAAG;AAC7B,gBAAA,aAAa,EAAE,QAAQ;AACxB,aAAA,EACD,EAAE,IAAI,EAAE,EACR,CAAA,EAAG,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAE,CAAA,CAC5B,EACD,gBAAgB,CACd,MAAM,EACN;gBACE,CAAC,EAAE,EAAE,GAAG,WAAW;AACnB,gBAAA,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,UAAU,GAAG,GAAG;AAC7B,gBAAA,aAAa,EAAE,QAAQ;AACxB,aAAA,EACD,EAAE,IAAI,EAAE,EACR,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAE,CAAA,CAC7B,CACF,CAAC;AACF,YAAA,EAAE,CAAC,CAAC;AACJ,YAAA,OAAO,GAAG,IAAI,GAAG,CAAC,GAAG,QAAQ,CAAC;AAC/B,SAAA;AACD,QAAA,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;KACxB;IAED,MAAM,GAAA;;;AAEJ,QAAA,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAC9C,QAAA,MAAM,CAAC,QAAQ,GAAG,CAAA,EAAA,GAAA,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAI,MAAM,CAAC,QAAQ,CAAC;AACxD,QAAA,MAAM,CAAC,QAAQ,GAAG,CAAA,EAAA,GAAA,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAI,MAAM,CAAC,QAAQ,CAAC;AACxD,QAAA,MAAM,CAAC,OAAO,GAAG,CAAA,EAAA,GAAA,IAAI,CAAC,OAAO,CAAC,OAAO,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAI,MAAM,CAAC,OAAO,CAAC;AACxD,QAAA,MAAM,CAAC,OAAO,GAAG,CAAA,EAAA,GAAA,IAAI,CAAC,OAAO,CAAC,OAAO,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAI,MAAM,CAAC,OAAO,CAAC;AAExD,QAAA,IAAI,CAAC,MAAM,GACN,MAAA,CAAA,MAAA,CAAA,MAAA,CAAA,MAAA,CAAA,EAAA,EAAA,MAAM,KACT,UAAU,EACR,CAAC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,WAAW;AACnC,iBAAC,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,EACrC,SAAS,EACP,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,WAAW,KAAK,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,GACtE,CAAC;;QAGF,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;AACpD,QAAA,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;;AAG1B,QAAA,MAAM,CAAC,UAAU,EAAE,WAAW,EAAE,YAAY,CAAC,GAC3C,IAAI,CAAC,sBAAsB,EAAE,CAAC;AAChC,QAAA,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;AAC3B,QAAA,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QAE7B,IAAI,CAAC,QAAQ,EAAE,CAAC;;AAGhB,QAAA,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;AAC3B,QAAA,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QAE5B,IAAI,CAAC,EAAE,CAAC,MAAM,CACZ,gBAAgB,CACd,MAAM,EACN;AACE,YAAA,CAAC,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC;AACjB,YAAA,aAAa,EAAE,QAAQ;YACvB,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,GAAG,GAAG;SACvC,EACD,EAAE,IAAI,EAAE,SAAS,EAAE,EACnB,IAAI,CAAC,WAAW,CACjB,CACF,CAAC;QAEF,IAAI,CAAC,aAAa,EAAE,CAAC;KACtB;IAED,aAAa,GAAA;AACX,QAAA,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;;AAGjD,QAAA,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE;AACrB,YAAA,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;AAC7B,YAAA,MAAM,CAAC,GAAG,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;AAC9B,YAAA,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC;YACpE,OAAO;AACR,SAAA;AAED,QAAA,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC5C,QAAA,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;AAEpE,QAAA,MAAM,CAAC,GAAG,SAAS,IAAI,IAAI,GAAG,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;AACvD,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;;QAErC,MAAM,CAAC,GAAG,OAAO,GAAG,CAAC,IAAI,GAAG,OAAO,IAAI,SAAS,CAAC;AACjD,QAAA,MAAM,WAAW,GAAG,CAAC,KAAK,GAAG,QAAQ,KAAK,QAAQ,GAAG,QAAQ,CAAC,GAAG,GAAG,CAAC;QACrE,MAAM,CAAC,GAAG,IAAI,CAAC,UAAU,IAAI,WAAW,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;QAE1E,IAAI,CAAC,EAAE,CAAC,MAAM;;AAEZ,QAAA,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAG,EAAA,CAAC,CAAI,CAAA,EAAA,IAAI,EAAE,CAAC;;QAE9C,GAAG,IAAI,CAAC,WAAW,CACjB,CAAC,EACD,CAAC,EACD,CAAG,EAAA,aAAa,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAA,CAAE,CACjD,CACF,CAAC;KACH;IAED,QAAQ,GAAA;QACN,MAAM,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;;AAEhC,QAAA,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;AAE9B,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;QACrC,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;AACvD,QAAA,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;;AAEtE,QAAA,MAAM,CAAC,GAAG,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,SAAS,CAAC;AACvD,QAAA,MAAM,CAAC,GAAG,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,IAAI,UAAU,CAAC;QACzD,MAAM,MAAM,GAAG,CAAC,CAAA,CAAA,EAAI,CAAC,CAAI,CAAA,EAAA,CAAC,CAAE,CAAA,CAAC,CAAC;;AAE9B,QAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE;AACpC,YAAA,MAAM,CAAC,GAAG,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,SAAS,CAAC;AACvD,YAAA,MAAM,CAAC,GAAG,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,IAAI,UAAU,CAAC;YACzD,MAAM,CAAC,IAAI,CAAC,CAAA,CAAA,EAAI,CAAC,CAAI,CAAA,EAAA,CAAC,CAAE,CAAA,CAAC,CAAC;AAC3B,SAAA;;AAED,QAAA,MAAM,IAAI,GAAG,gBAAgB,CAAC,MAAM,EAAE;AACpC,YAAA,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YAClB,MAAM,EAAE,IAAI,CAAC,SAAS;YACtB,cAAc,EAAE,IAAI,CAAC,WAAW;AAChC,YAAA,IAAI,EAAE,MAAM;AACb,SAAA,CAAC,CAAC;AACH,QAAA,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;KACtB;IAES,WAAW,CAAC,CAAS,EAAE,CAAS,EAAE,IAAY,EAAE,MAAM,GAAG,KAAK,EAAA;QACtE,OAAO;;YAEL,gBAAgB,CACd,MAAM,EACN,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,UAAU,GAAG,GAAG,EAAE,aAAa,EAAE,MAAM,EAAE,EAC1D;gBACE,MAAM,EAAE,IAAI,CAAC,OAAO;gBACpB,cAAc,EAAE,IAAI,CAAC,YAAY;AAClC,aAAA,EACD,IAAI,CACL;;AAED,YAAA,gBAAgB,CACd,MAAM,EACN,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,UAAU,GAAG,GAAG,EAAE,aAAa,EAAE,MAAM,EAAE,EAC1D,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,EACxB,IAAI,CACL;SACF,CAAC;KACH;IAES,gBAAgB,CACxB,CAAS,EACT,CAAS,EACT,IAAY,EACZ,MAAM,GAAG,KAAK,EAAA;QAEd,OAAO;;AAEL,YAAA,gBAAgB,CACd,MAAM,EACN,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,EAC/B;AACE,gBAAA,WAAW,EAAE,OAAO;AACpB,gBAAA,aAAa,EAAE,MAAM;gBACrB,MAAM,EAAE,IAAI,CAAC,OAAO;gBACpB,cAAc,EAAE,IAAI,CAAC,YAAY;AAClC,aAAA,EACD,IAAI,CACL;;AAED,YAAA,gBAAgB,CACd,MAAM,EACN,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,EAC/B,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,EACrE,IAAI,CACL;SACF,CAAC;KACH;AACF,CAAA;AAEM,MAAM,SAAS,GAAG,CAAC,IAAuB,KAAI;AACnD,IAAA,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE;AACnB,QAAA,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;AAC7D,KAAA;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3B,IAAA,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzC,IAAI,QAAQ,GAAG,QAAQ,CAAC;AACxB,IAAA,IAAI,QAAQ,GAAG,CAAC,QAAQ,CAAC;AACzB,IAAA,KAAK,MAAM,GAAG,KAAK,CAAC,IAAI,IAAI,EAAE;QAC5B,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QACrC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AACtC,KAAA;IACD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAClD,CAAC,CAAC;AAEK,MAAM,WAAW,GAAG,CAAC,KAAa,EAAE,YAAoB,KAAI;AACjE,IAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC;IACnD,MAAM,CAAC,GAAG,KAAK,IAAI,YAAY,GAAG,EAAE,IAAI,QAAQ,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;AAC9C,IAAA,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC9B,CAAC;;ACnWD;AACA,MAAM,OAAO,GAAG,iDAAiD,CAAC;AAc3D,MAAM,QAAQ,GAAG,OACtB,IAAY,EACZ,KAAK,GAAG,EAAE,KACuB;IACjC,MAAM,WAAW,GAAG,IAAI,eAAe,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC1D,MAAM,GAAG,GAAG,WAAW;AACrB,UAAE,CAAG,EAAA,OAAO,GAAG,IAAI,CAAA,CAAA,EAAI,WAAW,CAAE,CAAA;AACpC,UAAE,CAAG,EAAA,OAAO,CAAG,EAAA,IAAI,EAAE,CAAC;AACxB,IAAA,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;IAClC,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC;AACnD,CAAC,CAAC;AAEF;;;;;AAKG;AACI,MAAM,eAAe,GAAG,CAAC,IAAU,KAAY;AACpD,IAAA,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC;AACnD,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;AAgBE;;ACrDF,MAAM,MAAM,GAAG,iBAAiB,CAAC;AAEjC,MAAM,SAAS,GAAG,CAAC,GAAW,KAAa,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,GAAG,CAAA,CAAE,CAAC;AAE9D,IAAI,QAAe,CAAC;AAEpB,MAAM,KAAK,CAAA;IACT,KAAK,CAAC,OAAO,GAAG,KAAK,EAAA;AACnB,QAAA,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE;YAC7B,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;AACzC,SAAA;AACD,QAAA,IAAI,OAAO,EAAE;AACX,YAAA,YAAY,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;YAChC,OAAO;AACR,SAAA;AACD,QAAA,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC;KAClD;AAED,IAAA,GAAG,CAAC,GAAW,EAAA;QACb,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;AACnD,QAAA,OAAO,KAAK,KAAK,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;KAClD;AAED,IAAA,GAAG,CAAC,GAAW,EAAA;QACb,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;KAClC;AAED;;;;AAIG;IACH,QAAQ,GAAA;QACN,OAAO,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC;KAC9C;IAED,IAAI,GAAA;QACF,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAChD,QAAA,OAAO,UAAU,KAAK,IAAI,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;KAC1D;IAED,GAAG,CAAC,GAAW,EAAE,KAAc,EAAA;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACnC,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAChD,QAAA,MAAM,IAAI,GAAa,UAAU,KAAK,IAAI,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;AACzE,QAAA,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;AACvB,YAAA,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,YAAA,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AACpD,SAAA;QACD,YAAY,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;KAC5C;AAED,IAAA,KAAK,CAAC,GAAW,EAAA;;QAEf,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;;QAGxC,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAChD,QAAA,MAAM,IAAI,GAAa,UAAU,KAAK,IAAI,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACzE,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;;QAGhC,IAAI,KAAK,KAAK,CAAC,CAAC;AAAE,YAAA,OAAO,KAAK,CAAC;AAE/B,QAAA,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AACtB,QAAA,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AACnD,QAAA,OAAO,IAAI,CAAC;KACb;AACF,CAAA;AAEM,MAAM,QAAQ,GAAG,MAAY;IAClC,IAAI,CAAC,QAAQ,EAAE;AACb,QAAA,QAAQ,GAAG,IAAI,KAAK,EAAE,CAAC;AACxB,KAAA;AACD,IAAA,OAAO,QAAQ,CAAC;AAClB,CAAC;;ACrED;AACA,MAAM,WAAW,GAAG,CAAC,GAAG,SAAS,CAAC;AAsClC;;;;;AAKG;AACH,MAAM,oBAAoB,GAAG,OAC3B,EAAU,EACV,OAAA,GAA0B,EAAE,KACA;;AAE5B,IAAA,MAAM,MAAM,GAAkB,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC9C,IAAI,OAAO,CAAC,KAAK,EAAE;QACjB,MAAM,CAAC,KAAK,GAAG,eAAe,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC/C,KAAA;;AAED,IAAA,MAAM,QAAQ,IACZ,MAAM,QAAQ,CAAC,CAAgB,aAAA,EAAA,EAAE,CAAW,SAAA,CAAA,EAAE,MAAM,CAAC,CACtD,CAAC;AACF,IAAA,OAAO,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC;AAClE,CAAC,CAAC;AAEK,MAAM,WAAW,GAAG,CAAC,IAAe,EAAE,KAAa,KAAI;AAC5D,IAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC;AAClE,IAAA,OAAO,QAAQ,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;AAClD,CAAC,CAAC;AAEF;;;;;;;AAOG;AACI,MAAM,kBAAkB,GAAG,OAChC,EAAU,EACV,OAAA,GAA0B,EAAE,KACN;;AAEtB,IAAA,MAAM,GAAG,GAAG,CAAY,SAAA,EAAA,EAAE,EAAE,CAAC;AAC7B,IAAA,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IAEzB,MAAM,MAAM,GAAmB,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI;AAC/C,QAAA,IAAI,EAAE,EAAE;AACR,QAAA,SAAS,EAAE,CAAC;AACZ,QAAA,WAAW,EAAE,QAAQ;KACtB,CAAC;AACF,IAAA,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;AACnC,IAAA,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC;AAE7B,IAAA,MAAM,aAAa,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC;;AAGlE,IAAA,OAAO,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,EAAE;AAChD,QAAA,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACxB,IAAI,CAAC,KAAK,EAAE,CAAC;AACd,KAAA;;IAGD,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAC9D,IAAA,MAAM,cAAc,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,IAAI,KAAK,CAAC,CAAC;IAC9E,IACE,WAAW,IAAI,cAAc;QAC7B,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,GAAG,IAAI,GAAG,WAAW,EAC3C;;AAEA,QAAA,OAAO,WAAW,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;AAC1C,KAAA;IAED,MAAM,YAAY,mCACb,OAAO,CAAA,EAAA,EACV,KAAK,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,UAAU,CAAC,GAAG,IAAI,CAAC,EAAA,CAC7D,CAAC;IAEF,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,oBAAoB,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;AAC/D,IAAA,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC7B,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;IACpD,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;AACpE,IAAA,OAAO,WAAW,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;AAC3C,CAAC,CAAC;AAEK,MAAM,aAAa,GAAG,CAAC,KAAgB,EAAE,MAAiB,KAAU;IACzE,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO;AAE3B,IAAA,IAAI,QAAQ,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;AAChC,IAAA,OAAO,QAAQ,IAAI,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;AAC1D,QAAA,EAAE,QAAQ,CAAC;AACZ,KAAA;AACD,IAAA,KAAK,CAAC,MAAM,CAAC,QAAQ,GAAG,CAAC,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,CAAC;AAClD,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,CAAC,KAAmB,KAA+B;IACvE,MAAM,MAAM,GAA8B,EAAE,CAAC;IAC7C,KAAK,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,KAAK,EAAE;AAChD,QAAA,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE;AAC3B,YAAA,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;AACtB,SAAA;QACD,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;AACvE,KAAA;IAED,MAAM,UAAU,GAA8B,EAAE,CAAC;AACjD,IAAA,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;AACjD,QAAA,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;AAC7D,KAAA;AAED,IAAA,OAAO,UAAU,CAAC;AACpB,CAAC;;AC3IM,MAAM,iBAAiB,GAAG,OAC/B,QAAqB,EACrB,SAAiB,EACjB,OAAmC,GAAA,EAAE,KACnC;;;IAEF,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IACzC,IAAI,IAAI,GAAc,EAAE,CAAC;;IAGzB,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnC,MAAM,EAAE,GAAG,CAAA,EAAA,GAAA,KAAK,CAAC,GAAG,EAAE,MAAI,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAA,EAAE,CAAC;AAC7B,IAAA,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,KAAK,CAAC,GAAG,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;AACpD,IAAA,QAAQ,GAAG;AACT,QAAA,KAAK,OAAO;YACV,IAAI,GAAG,MAAMA,kBAAuB,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;AACvD,KAAA;;IAGD,QAAQ,CAAC,eAAe,EAAE,CAAC;AAE3B,IAAA,MAAM,OAAO,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,EAAE,IAAI,EAAE,GAAG,0BAA0B,CAAC,OAAO,CAAC,CAAC;IAErD,MAAM,OAAO,GAAgB,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;;AAE/D,IAAA,IAAI,OAAO,CAAC,uBAAuB,IAAI,IAAI,EAAE;QAC3C,OAAO,CAAC,GAAG,GAAG,UAAU,CAAS,OAAO,CAAC,uBAAuB,CAAC,CAAC;AACnE,KAAA;AACD,IAAA,IAAI,OAAO,CAAC,uBAAuB,IAAI,IAAI,EAAE;QAC3C,OAAO,CAAC,GAAG,GAAG,UAAU,CAAS,OAAO,CAAC,uBAAuB,CAAC,CAAC;AACnE,KAAA;;AAGD,IAAA,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE;QACrB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC;AACvC,QAAA,MAAM,OAAO,GAAG,OAAO,GAAG,KAAK,GAAG,CAAC,CAAC;AACpC,QAAA,MAAM,YAAY,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAE1C,QAAA,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,CAAC;QAC3D,KAAK,CAAC,MAAM,EAAE,CAAC;QACf,OAAO;AACR,KAAA;IAED,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC;AACzE,IAAA,MAAM,OAAO,GACX,UAAU,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC;AAC5E,IAAA,MAAM,YAAY,GAAG;QACnB,OAAO;QACP,OAAO;;KAER,CAAC;AAEF,IAAA,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,CAAC;IAC3D,KAAK,CAAC,MAAM,EAAE,CAAC;AACjB;;ACpEA;;;;;AAKG;AAII,MAAM,OAAO,GAAG;;;;"} |
| export { version } from '.'; |
| export type RiverDataWidgetErrorInfo = Record<string, unknown>; | ||
| export declare class RiverDataWidgetError extends Error { | ||
| info: RiverDataWidgetErrorInfo; | ||
| constructor(msg: string, info?: RiverDataWidgetErrorInfo); | ||
| } |
| export interface ApiResponse<T> { | ||
| data: { | ||
| items: T; | ||
| }; | ||
| response: Response; | ||
| } | ||
| export interface ApiParameters { | ||
| since?: string; | ||
| _sorted?: ''; | ||
| } | ||
| export declare const apiFetch: (path: string, query?: {}) => Promise<ApiResponse<unknown>>; | ||
| /** | ||
| * Convert a Date to a format recognized by the EA API for a query parameter. | ||
| * | ||
| * @param date Convert from. | ||
| * @returns A string in the EA API query parameter format. | ||
| */ | ||
| export declare const toTimeParameter: (date: Date) => string; |
| export declare class FloodMonitoringApiError extends Error { | ||
| info: Record<string, unknown>; | ||
| constructor(msg: string, info?: Record<string, unknown>); | ||
| } |
| export { getMeasureReadings } from './reading'; |
| export { parseMeasureId }; | ||
| declare const parseMeasureId: (measureId: string) => { | ||
| stationId: string; | ||
| parameter: string; | ||
| qualifier: string; | ||
| type: string; | ||
| interval: string; | ||
| unit: string; | ||
| qualifiedParameter: string; | ||
| }; | ||
| export declare const translateMeasureProperties: (measure: Record<string, string>) => Record<string, string>; |
| /** | ||
| * Internal format for readings. | ||
| */ | ||
| export type Reading = [ | ||
| timestamp: number, | ||
| value: number | ||
| ]; | ||
| /** | ||
| * Internal format for readings. | ||
| */ | ||
| export interface ReadingOptions { | ||
| since?: Date; | ||
| } | ||
| export declare const filterSince: (data: Reading[], since: number) => Reading[]; | ||
| /** | ||
| * Get the readings for a measure. | ||
| * | ||
| * @todo Caching and throttling. | ||
| * | ||
| * @param id The EA measure id. | ||
| * @returns A promise for an array of readings for the measure. | ||
| */ | ||
| export declare const getMeasureReadings: (id: string, options?: ReadingOptions) => Promise<Reading[]>; | ||
| export declare const mergeReadings: (first: Reading[], second: Reading[]) => void; |
| declare class Store { | ||
| clear(destroy?: boolean): void; | ||
| get(key: string): any; | ||
| has(key: string): boolean; | ||
| /** | ||
| * Detect active localStorage. | ||
| * | ||
| * @returns true iff localStorage for the widget is active. | ||
| */ | ||
| isActive(): boolean; | ||
| keys(): string[]; | ||
| set(key: string, value: unknown): void; | ||
| unset(key: string): boolean; | ||
| } | ||
| export declare const useStore: () => Store; | ||
| export {}; |
| /** | ||
| * RiverDataWidget https://github.com/pb-uk/river-data-widget. | ||
| * | ||
| * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk. | ||
| * @license AGPL-3.0-or-later see LICENSE.md. | ||
| */ | ||
| export { createElement, createSvgElement, setAttributes, setStyles }; | ||
| type AttributeList = Record<string, string | number>; | ||
| declare const setAttributes: <T extends HTMLElement | SVGElement>(el: T, attributes: AttributeList) => T; | ||
| declare const setStyles: <T extends HTMLElement | SVGElement>(el: T, styles: AttributeList) => T; | ||
| declare const createElement: (name?: string, attributes?: AttributeList, styles?: AttributeList, innerHTML?: string | false) => HTMLElement; | ||
| declare const createSvgElement: (name?: string, attributes?: AttributeList, styles?: AttributeList, innerHTML?: string | false) => SVGElement; |
| export declare const FONT_STACK = "-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Roboto\",\"Helvetica Neue\",Arial,sans-serif"; | ||
| export declare const round3: (value: number) => string; |
| export declare const MINUTE_MS = 60000; | ||
| export declare const DAY_MS = 86400000; | ||
| /** | ||
| * Get the Date at the start of a day in UTC or local time. | ||
| * | ||
| * @param offset | ||
| * @param timeZone The time zone offset in minutes, or set to `true` to use the | ||
| * local time zone (`false`, the default, uses UTC). | ||
| * @returns The reqested date. | ||
| */ | ||
| export declare const startOfDay: (date?: Date | null, offset?: number, timeZone?: boolean | number) => Date; | ||
| /** | ||
| * | | long |short|narrow|numeric|2-digit| | ||
| * |:-------:|:-----------:|:---:|:----:|:-----:|:-----:| | ||
| * | weekday | Monday | Mon | M | | | | ||
| * | era | Anno Domini | AD | A | | | | ||
| * | year | | | | 2012 | 12 | | ||
| * | month | March | Mar | M | 3 | 03 | | ||
| * | day | | | | 1 | 01 | | ||
| * | hour | | | | 1 | 01 | | ||
| * | minute | | | | 1 | 01 | | ||
| * | second | | | | 1 | 01 | | ||
| * | ||
| * * fractionalSecondDigits: 1, 2 or 3 for number of digits. | ||
| * * timeZoneName: long (Pacific Standard Time), short (PST), | ||
| * longOffset (GMT-0800), shortOffset (GMT-8), longGeneric (Pacific Time), | ||
| * shortGeneric (PT). | ||
| */ | ||
| export declare const dateFormatter: Intl.DateTimeFormat; | ||
| export declare const timeFormatter: Intl.DateTimeFormat; | ||
| export declare const dddFormatter: Intl.DateTimeFormat; | ||
| export declare const dMmmFormatter: Intl.DateTimeFormat; |
| /** | ||
| * RiverDataWidget https://github.com/pb-uk/river-data-widget. | ||
| * | ||
| * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk. | ||
| * @license AGPL-3.0-or-later see LICENSE.md. | ||
| */ | ||
| export { drawMeasureWidget } from './widget/render'; | ||
| export declare const version = "1.2.0"; |
| export interface ChartOptions { | ||
| minTime?: number; | ||
| maxTime?: number; | ||
| attribution?: string; | ||
| } | ||
| export interface ChartScaleLimits { | ||
| minTime: number; | ||
| maxTime: number; | ||
| timeScale: number; | ||
| minValue: number; | ||
| maxValue: number; | ||
| valueScale: number; | ||
| } | ||
| export interface ChartSeries { | ||
| data: TimeSeriesValue[]; | ||
| min?: number; | ||
| max?: number; | ||
| unit?: string; | ||
| formatter?: (value: number) => string; | ||
| } | ||
| export type TimeSeriesValue = [ | ||
| ts: number, | ||
| v: number | ||
| ]; | ||
| export declare class Chart { | ||
| protected strokeWidth: number; | ||
| protected fontSizePx: number; | ||
| protected el: SVGElement; | ||
| protected series: ChartSeries[]; | ||
| protected options: ChartOptions; | ||
| protected width: number; | ||
| protected height: number; | ||
| protected plotHeight: number; | ||
| protected plotWidth: number; | ||
| protected limits?: ChartScaleLimits; | ||
| protected plotColor: string; | ||
| protected labelBg: string; | ||
| protected labelBgWidth: string; | ||
| protected attribution: string; | ||
| protected styles: { | ||
| 'font-family': string; | ||
| 'font-size': string; | ||
| display: string; | ||
| margin: string; | ||
| 'max-width': string; | ||
| }; | ||
| constructor(el: HTMLElement, series: ChartSeries[], options?: ChartOptions); | ||
| getLimits(): ChartScaleLimits; | ||
| getHorizontalGridlines(): SVGElement[]; | ||
| getTimeScale(): SVGElement[]; | ||
| render(): void; | ||
| plotLastValue(): void; | ||
| plotData(): void; | ||
| protected createLabel(x: number, y: number, text: string, anchor?: string): SVGElement[]; | ||
| protected createLargeLabel(x: number, y: number, text: string, anchor?: string): SVGElement[]; | ||
| } | ||
| export declare const getLimits: (data: TimeSeriesValue[]) => { | ||
| minTime: number; | ||
| maxTime: number; | ||
| minValue: number; | ||
| maxValue: number; | ||
| }; | ||
| export declare const getInterval: (range: number, maxDivisions: number) => number[]; |
| import type { WidgetOptions } from './index'; | ||
| export declare const drawFlowGauge: (el: HTMLElement, value: number, options: WidgetOptions) => void; |
| export interface WidgetOptions { | ||
| flowSectors?: number[]; | ||
| flowSectorBackgrounds?: string[]; | ||
| } |
| export declare const drawMeasureWidget: (parentEl: HTMLElement, measureId: string, options?: Record<string, unknown>) => Promise<void>; | ||
| /** | ||
| * Load a widget specified by a DOM element. | ||
| */ | ||
| export declare const loadWidget: (el: HTMLElement | string) => void; |
+12
-9
| { | ||
| "name": "river-data-widget", | ||
| "version": "1.1.0", | ||
| "version": "1.2.0", | ||
| "description": "A web widget to display river flow and other data.", | ||
| "type": "module", | ||
| "browser": "dist/river-data-widget.min.js", | ||
| "types": "types", | ||
| "main": "index.cjs", | ||
| "module": "index.mjs", | ||
| "browser": "index.min.js", | ||
| "scripts": { | ||
| "build": "npm run clean && npm run lint && npm run test && rollup -c", | ||
| "clean": "rimraf dist", | ||
| "build": "npm run clean && npm run lint:check && npm run test && rollup -c && tsc --project tsconfig.types.json", | ||
| "clean": "rimraf types \"index.*\" --glob", | ||
| "coverage": "rimraf coverage && c8 -r html -r text npm run test:unit", | ||
| "lint": "eslint . && prettier . --check", | ||
| "lint:fix": "eslint . --fix && prettier . --write", | ||
| "lint": "eslint . --fix && prettier . --write", | ||
| "lint:check": "eslint . && prettier . --check", | ||
| "test": "npm run test:unit", | ||
@@ -33,7 +36,7 @@ "test:unit": "mocha src/**/*.spec.ts" | ||
| "files": [ | ||
| "dist" | ||
| "index.*", | ||
| "types" | ||
| ], | ||
| "devDependencies": { | ||
| "@istanbuljs/nyc-config-typescript": "^1.0.2", | ||
| "@rollup/plugin-json": "^6.0.0", | ||
| "@rollup/plugin-node-resolve": "^15.0.1", | ||
@@ -47,3 +50,3 @@ "@rollup/plugin-terser": "^0.4.2", | ||
| "@typescript-eslint/parser": "^5.44.0", | ||
| "c8": "^7.12.0", | ||
| "c8": "^8.0.0", | ||
| "camelcase": "^7.0.0", | ||
@@ -50,0 +53,0 @@ "chai": "^4.3.7", |
+1
-1
@@ -12,5 +12,5 @@ # river-data-widget | ||
| style="max-width: 480px; margin: 1em 0;" | ||
| data-river-data-widget="measure:3400TH-flow--i-15_min-m3_s" | ||
| data-river-data-widget="measure:flood/3400TH-flow--i-15_min-m3_s" | ||
| data-river-data-widget-min-value="0" | ||
| ></div> | ||
| ``` |
-610
| /*! RiverDataWidget v1.1.0 2023-05-31 14:34:01 | ||
| *! https://github.com/pb-uk/river-data-widget#readme | ||
| *! Copyright (C) 2023 pbuk (https://github.com/pb-uk). | ||
| *! License MIT. | ||
| */ | ||
| var version = "1.1.0"; | ||
| class RiverDataWidgetError extends Error { | ||
| constructor(msg, info = {}) { | ||
| super(msg); | ||
| this.name = 'RiverDataWidgetError'; | ||
| this.info = info; | ||
| } | ||
| } | ||
| const FONT_STACK = '-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif'; | ||
| const round3 = (value) => value < 100 ? value.toPrecision(3) : Math.round(value).toString(); | ||
| class FloodMonitoringApiError extends Error { | ||
| constructor(msg, info = {}) { | ||
| super(msg); | ||
| this.name = 'FloodMonitoringApiError'; | ||
| this.info = info; | ||
| } | ||
| } | ||
| const parseMeasureId = (measureId) => { | ||
| // ............base/ stat-paramet-qualifi- type -interva-unit | ||
| const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/; | ||
| const matches = measureId.match(regExp); | ||
| if (matches === null) { | ||
| throw new FloodMonitoringApiError('Cannot parse measure id', { measureId }); | ||
| } | ||
| const [unit, interval, type, qualifier, parameter, stationId] = matches.reverse(); | ||
| const qualifiedParameter = qualifier.length | ||
| ? `${parameter}-${qualifier}` | ||
| : parameter; | ||
| return { | ||
| stationId, | ||
| parameter, | ||
| qualifier, | ||
| type, | ||
| interval, | ||
| unit, | ||
| qualifiedParameter, | ||
| }; | ||
| }; | ||
| const measureTranslations = { | ||
| unit: { | ||
| m3_s: 'm³/s', | ||
| mAOD: 'm', | ||
| mASD: 'm', | ||
| }, | ||
| qualifiedParameter: { | ||
| 'level-stage': 'level', | ||
| 'level-downstage': 'downstream level', | ||
| }, | ||
| }; | ||
| const translateMeasureProperties = (measure) => { | ||
| const translated = {}; | ||
| for (const prop in measure) { | ||
| const value = measure[prop]; | ||
| if (measureTranslations[prop] && measureTranslations[prop][value]) { | ||
| translated[prop] = measureTranslations[prop][value]; | ||
| } | ||
| else { | ||
| translated[prop] = value; | ||
| } | ||
| } | ||
| return translated; | ||
| }; | ||
| /** | ||
| * RiverDataWidget https://github.com/pb-uk/river-data-widget. | ||
| * | ||
| * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk. | ||
| * @license AGPL-3.0-or-later see LICENSE.md. | ||
| */ | ||
| const setAttributes = (el, attributes) => { | ||
| Object.entries(attributes).forEach(([key, value]) => { | ||
| el.setAttribute(key, `${value}`); | ||
| }); | ||
| return el; | ||
| }; | ||
| const setStyles = (el, styles) => { | ||
| Object.entries(styles).forEach(([key, value]) => { | ||
| // Workaround (el.style.setProperty uses kebab-case keys). | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| el.style[key] = value; | ||
| }); | ||
| return el; | ||
| }; | ||
| const createSvgElement = (name = 'svg', attributes = {}, styles = {}, innerHTML = false) => { | ||
| const el = document.createElementNS('http://www.w3.org/2000/svg', name); | ||
| if (innerHTML !== false) { | ||
| el.innerHTML = innerHTML; | ||
| } | ||
| return setStyles(setAttributes(el, attributes), styles); | ||
| }; | ||
| const MINUTE_MS = 60000; | ||
| // const HOUR_MS = 3600000; | ||
| const DAY_MS = 86400000; | ||
| /** | ||
| * Get the Date at the start of a day in UTC or local time. | ||
| * | ||
| * @param offset | ||
| * @param timeZone The time zone offset in minutes, or set to `true` to use the | ||
| * local time zone (`false`, the default, uses UTC). | ||
| * @returns The reqested date. | ||
| */ | ||
| const startOfDay = (date = null, offset = 0, timeZone = false) => { | ||
| if (timeZone === false) { | ||
| // Use UTC. | ||
| const base = date === null ? Date.now() : date.valueOf(); | ||
| return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS); | ||
| } | ||
| const now = new Date(); | ||
| const tz = timeZone === true ? now.getTimezoneOffset() : timeZone; | ||
| const local = now.valueOf() + tz * MINUTE_MS; | ||
| return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS); | ||
| }; | ||
| const timeFormatter = new Intl.DateTimeFormat('en-GB', { | ||
| hour: '2-digit', | ||
| minute: '2-digit', | ||
| hour12: false, | ||
| timeZoneName: 'short', | ||
| }); | ||
| const dddFormatter = new Intl.DateTimeFormat('en-GB', { | ||
| weekday: 'short', | ||
| }); | ||
| const dMmmFormatter = new Intl.DateTimeFormat('en-GB', { | ||
| day: 'numeric', | ||
| month: 'short', | ||
| }); | ||
| class Chart { | ||
| constructor(el, series, options = {}) { | ||
| var _a; | ||
| this.fontSizePx = 14; | ||
| this.width = 480; // 400; | ||
| this.height = 270; // 225; | ||
| this.plotHeight = this.height - this.fontSizePx * 4.5; | ||
| this.strokeWidth = 2; | ||
| this.plotColor = '#77C'; | ||
| this.labelBg = 'rgba(255,255,255,0.5)'; | ||
| this.labelBgWidth = '0.5em'; | ||
| this.attribution = 'Uses Environment Agency data from the real-time API (Beta)'; | ||
| // CSS settings. | ||
| // Just readable at 320x180. | ||
| // Good from 400x225. | ||
| // Perfect at 480x270 (font is 12px); | ||
| this.styles = { | ||
| 'font-family': FONT_STACK, | ||
| 'font-size': `${this.fontSizePx}px`, | ||
| display: 'block', | ||
| margin: 'auto', | ||
| 'max-width': '150vh', | ||
| }; | ||
| this.series = series; | ||
| this.options = options; | ||
| const viewBox = `0 0 ${this.width} ${this.height}`; | ||
| this.attribution = (_a = options.attribution) !== null && _a !== void 0 ? _a : this.attribution; | ||
| this.el = createSvgElement('svg', { viewBox }, this.styles); | ||
| el.append(this.el); | ||
| } | ||
| getLimits() { | ||
| if (this.limits == null) { | ||
| throw new FloodMonitoringApiError('Chart axis limits have not been set'); | ||
| } | ||
| return this.limits; | ||
| } | ||
| getHorizontalGridlines() { | ||
| const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } = this.getLimits(); | ||
| const xOffset = this.strokeWidth / 2; | ||
| const yOffset = this.plotHeight; | ||
| const x1 = xOffset; | ||
| const x2 = xOffset + (maxTime - minTime) * timeScale; | ||
| // Horizontal grid lines. | ||
| const stroke = '#ddd'; | ||
| const lines = createSvgElement('g', { stroke }); | ||
| const labels = createSvgElement('g'); | ||
| const valueRange = maxValue - minValue; | ||
| // Horizontal grid interval. | ||
| const [interval, exponent] = getInterval(valueRange, 9); | ||
| const factor = 10 ** -exponent; | ||
| const base = Math.ceil((minValue * factor) / interval + 1) * interval; | ||
| let i = 0; | ||
| let current = base / factor; | ||
| while (current < maxValue) { | ||
| const y1 = yOffset - (current - minValue) * valueScale; | ||
| lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 })); | ||
| labels.append(createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`)); | ||
| ++i; | ||
| current = (base + i * interval) / factor; | ||
| } | ||
| const timeAxisLine = createSvgElement('line', { x1, y1: yOffset, x2, y2: yOffset }, { stroke: '#777' }); | ||
| return [lines, labels, timeAxisLine]; | ||
| } | ||
| getTimeScale() { | ||
| const { minTime, maxTime, timeScale } = this.getLimits(); | ||
| const xOffset = this.strokeWidth / 2; | ||
| const yOffset = this.plotHeight + this.strokeWidth / 2; | ||
| const y1 = yOffset + this.fontSizePx * 3; | ||
| const y2 = yOffset - this.plotHeight; | ||
| // Vertical grid lines. | ||
| const stroke = '#ddd'; | ||
| const lines = createSvgElement('g', { stroke }); | ||
| const labels = createSvgElement('g'); | ||
| // Vertical grid interval. | ||
| const base = minTime; | ||
| const interval = 86400; | ||
| let i = 0; | ||
| let current = base; | ||
| const labelOffset = 43200 * timeScale; | ||
| const fill = '#444'; | ||
| while (current <= maxTime) { | ||
| const x1 = xOffset + (current - minTime) * timeScale; | ||
| const d = new Date(current * 1000); | ||
| // lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 })); | ||
| lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 })); | ||
| labels.append(createSvgElement('text', { | ||
| x: x1 + labelOffset, | ||
| y: y1 - this.fontSizePx * 1.8, | ||
| 'text-anchor': 'middle', | ||
| }, { fill }, `${dddFormatter.format(d)}`), createSvgElement('text', { | ||
| x: x1 + labelOffset, | ||
| y: y1 - this.fontSizePx * 0.5, | ||
| 'text-anchor': 'middle', | ||
| }, { fill }, `${dMmmFormatter.format(d)}`)); | ||
| ++i; | ||
| current = base + i * interval; | ||
| } | ||
| return [lines, labels]; | ||
| } | ||
| render() { | ||
| var _a, _b, _c, _d; | ||
| // Calculate axis scales. | ||
| const limits = getLimits(this.series[0].data); | ||
| limits.minValue = (_a = this.series[0].min) !== null && _a !== void 0 ? _a : limits.minValue; | ||
| limits.maxValue = (_b = this.series[0].max) !== null && _b !== void 0 ? _b : limits.maxValue; | ||
| limits.minTime = (_c = this.options.minTime) !== null && _c !== void 0 ? _c : limits.minTime; | ||
| limits.maxTime = (_d = this.options.maxTime) !== null && _d !== void 0 ? _d : limits.maxTime; | ||
| this.limits = Object.assign(Object.assign({}, limits), { valueScale: (this.plotHeight - this.strokeWidth) / | ||
| (limits.maxValue - limits.minValue), timeScale: (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime) }); | ||
| // Time axis. | ||
| const [timeLines, timeLabels] = this.getTimeScale(); | ||
| this.el.append(timeLines); | ||
| // Value axis. | ||
| const [valueLines, valueLabels, timeAxisLine] = this.getHorizontalGridlines(); | ||
| this.el.append(valueLines); | ||
| this.el.append(timeAxisLine); | ||
| this.plotData(); | ||
| // Plot labels on top of the line. | ||
| this.el.append(timeLabels); | ||
| this.el.append(valueLabels); | ||
| this.el.append(createSvgElement('text', { | ||
| x: this.width / 2, | ||
| 'text-anchor': 'middle', | ||
| y: this.height - this.fontSizePx * 0.5, | ||
| }, { fill: '#595959' }, this.attribution)); | ||
| this.plotLastValue(); | ||
| } | ||
| plotLastValue() { | ||
| const { data, unit, formatter } = this.series[0]; | ||
| const [time, value] = data[data.length - 1]; | ||
| const { minTime, timeScale, maxValue, minValue } = this.getLimits(); | ||
| const v = formatter == null ? value : formatter(value); | ||
| const xOffset = this.strokeWidth / 2; | ||
| // const yOffset = this.plotHeight - this.strokeWidth / 2; | ||
| const x = xOffset + (time - minTime) * timeScale; | ||
| const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5; | ||
| const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2; | ||
| this.el.append( | ||
| // Background for value label. | ||
| createSvgElement('text', { x, y, 'text-anchor': 'end' }, { | ||
| 'font-size': '1.5em', | ||
| 'font-weight': 'bold', | ||
| stroke: this.labelBg, | ||
| 'stroke-width': this.labelBgWidth, | ||
| }, `${v} ${unit}`), | ||
| // Value label. | ||
| createSvgElement('text', { x, y, 'text-anchor': 'end' }, { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' }, `${v} ${unit}`), | ||
| // Background for time label. | ||
| createSvgElement('text', { x, y: y + this.fontSizePx * 1.5, 'text-anchor': 'end' }, { | ||
| stroke: this.labelBg, | ||
| 'stroke-width': this.labelBgWidth, | ||
| }, `${timeFormatter.format(new Date(time * 1000))}`), | ||
| // Time label. | ||
| createSvgElement('text', { x, y: y + this.fontSizePx * 1.5, 'text-anchor': 'end' }, { fill: this.plotColor }, `${timeFormatter.format(new Date(time * 1000))}`)); | ||
| } | ||
| plotData() { | ||
| const xOffset = this.strokeWidth / 2; | ||
| const yOffset = this.plotHeight - this.strokeWidth / 2; | ||
| const { data } = this.series[0]; | ||
| const { minTime, timeScale, minValue, valueScale } = this.getLimits(); | ||
| // First data point. | ||
| const x = xOffset + (data[0][0] - minTime) * timeScale; | ||
| const y = yOffset - (data[0][1] - minValue) * valueScale; | ||
| const points = [`M${x},${y}`]; | ||
| // Remaining data points. | ||
| for (let i = 1; i < data.length; ++i) { | ||
| const x = xOffset + (data[i][0] - minTime) * timeScale; | ||
| const y = yOffset - (data[i][1] - minValue) * valueScale; | ||
| points.push(`L${x},${y}`); | ||
| } | ||
| // Plot the data. | ||
| const path = createSvgElement('path', { | ||
| d: points.join(''), | ||
| stroke: this.plotColor, | ||
| 'stroke-width': this.strokeWidth, | ||
| fill: 'none', | ||
| }); | ||
| this.el.append(path); | ||
| } | ||
| } | ||
| const getLimits = (data) => { | ||
| if (data.length < 1) { | ||
| throw new Error('Readings must not be empty'); | ||
| } | ||
| const minTime = data[0][0]; | ||
| const maxTime = data[data.length - 1][0]; | ||
| let minValue = Infinity; | ||
| let maxValue = -minValue; | ||
| data.forEach(([, value]) => { | ||
| minValue = Math.min(minValue, value); | ||
| maxValue = Math.max(maxValue, value); | ||
| }); | ||
| return { minTime, maxTime, minValue, maxValue }; | ||
| }; | ||
| const getInterval = (range, maxDivisions) => { | ||
| const exponent = Math.floor(Math.log10(range)) - 1; | ||
| const k = range / (maxDivisions * 10 ** exponent); | ||
| const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10; | ||
| return [mantissa, exponent]; | ||
| }; | ||
| // There is no need to be secure about this! | ||
| const baseUrl = 'http://environment.data.gov.uk/flood-monitoring'; | ||
| const apiFetch = async (path, query = {}) => { | ||
| const queryString = new URLSearchParams(query).toString(); | ||
| const uri = queryString | ||
| ? `${baseUrl}${path}?${queryString}` | ||
| : `${baseUrl}${path}`; | ||
| const response = await fetch(uri); | ||
| return { data: await response.json(), response }; | ||
| }; | ||
| /** | ||
| * Convert a Date to a format recognized by the EA API for a query parameter. | ||
| * | ||
| * @param date Convert from. | ||
| * @returns A string in the EA API query parameter format. | ||
| */ | ||
| const toTimeParameter = (date) => { | ||
| return date.toISOString().substring(0, 19) + 'Z'; | ||
| }; | ||
| /* | ||
| Useful response headers | ||
| Date: 'Sat, 13 May 2023 09:14:07 GMT', | ||
| last-modified: Sat, 13 May 2023 09:03:13 GMT, | ||
| Response meta: | ||
| publisher: 'Environment Agency', | ||
| license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/', | ||
| documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference', | ||
| version: '0.9', | ||
| comment: 'Status: Beta service', | ||
| hasFormat: [ | ||
| "http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z", | ||
| "http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z", | ||
| "http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z", | ||
| "http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z" | ||
| ], | ||
| */ | ||
| const prefix = 'riverDataWidget'; | ||
| const addPrefix = (key) => `${prefix}|${key}`; | ||
| let instance; | ||
| class Store { | ||
| clear(destroy = false) { | ||
| for (const key of this.keys()) { | ||
| localStorage.removeItem(addPrefix(key)); | ||
| } | ||
| if (destroy) { | ||
| localStorage.removeItem(prefix); | ||
| return; | ||
| } | ||
| localStorage.setItem(prefix, JSON.stringify([])); | ||
| } | ||
| get(key) { | ||
| const value = localStorage.getItem(addPrefix(key)); | ||
| return value === null ? null : JSON.parse(value); | ||
| } | ||
| has(key) { | ||
| return this.keys().includes(key); | ||
| } | ||
| /** | ||
| * Detect active localStorage. | ||
| * | ||
| * @returns true iff localStorage for the widget is active. | ||
| */ | ||
| isActive() { | ||
| return localStorage.getItem(prefix) !== null; | ||
| } | ||
| keys() { | ||
| const storedKeys = localStorage.getItem(prefix); | ||
| return storedKeys === null ? [] : JSON.parse(storedKeys); | ||
| } | ||
| set(key, value) { | ||
| const json = JSON.stringify(value); | ||
| const storedKeys = localStorage.getItem(prefix); | ||
| const keys = storedKeys === null ? [] : JSON.parse(storedKeys); | ||
| if (!keys.includes(key)) { | ||
| keys.push(key); | ||
| localStorage.setItem(prefix, JSON.stringify(keys)); | ||
| } | ||
| localStorage.setItem(addPrefix(key), json); | ||
| } | ||
| unset(key) { | ||
| // Remove it before we do anything else. | ||
| localStorage.removeItem(addPrefix(key)); | ||
| // Then remove it from the list of keys. | ||
| const storedKeys = localStorage.getItem(prefix); | ||
| const keys = storedKeys === null ? [] : JSON.parse(storedKeys); | ||
| const index = keys.indexOf(key); | ||
| // If it doesn't exist we don't have to remove it. | ||
| if (index === -1) | ||
| return false; | ||
| keys.splice(index, 1); | ||
| localStorage.setItem(prefix, JSON.stringify(keys)); | ||
| return true; | ||
| } | ||
| } | ||
| const useStore = () => { | ||
| if (!instance) { | ||
| instance = new Store(); | ||
| } | ||
| return instance; | ||
| }; | ||
| // Throttle requests to five minutes. | ||
| const THROTTLE_MS = 5 * MINUTE_MS; | ||
| /** | ||
| * Fetch the readings for a measure. | ||
| * | ||
| * @param id The EA measure id. | ||
| * @returns A promise for an array of readings for the measure. | ||
| */ | ||
| const fetchMeasureReadings = async (id, options = {}) => { | ||
| // Set the parameters for the request. | ||
| const params = { _sorted: '' }; | ||
| if (options.since) { | ||
| params.since = toTimeParameter(options.since); | ||
| } | ||
| // Get the response, casting the items to ReadingDTOs. | ||
| const response = (await apiFetch(`/id/measures/${id}/readings`, params)); | ||
| return [parseReadings(response.data.items)[id] || [], response]; | ||
| }; | ||
| const filterSince = (data, since) => { | ||
| const position = data.findIndex((reading) => reading[0] >= since); | ||
| return position < 0 ? [] : data.slice(position); | ||
| }; | ||
| /** | ||
| * Get the readings for a measure. | ||
| * | ||
| * @todo Caching and throttling. | ||
| * | ||
| * @param id The EA measure id. | ||
| * @returns A promise for an array of readings for the measure. | ||
| */ | ||
| const getMeasureReadings = async (id, options = {}) => { | ||
| // Get the saved readings. | ||
| const key = `readings|${id}`; | ||
| const store = useStore(); | ||
| const stored = store.get(key) || { | ||
| data: [], | ||
| lastCheck: 0, | ||
| storedSince: Infinity, | ||
| }; | ||
| const { data, lastCheck } = stored; | ||
| let { storedSince } = stored; | ||
| const discardBefore = startOfDay(null, -8, true).valueOf() / 1000; | ||
| // Discard any older than 30 days. | ||
| while (data.length && data[0][0] < discardBefore) { | ||
| [storedSince] = data[0]; | ||
| data.shift(); | ||
| } | ||
| // If we have data early enough apply throttle. | ||
| const lastStored = data.length ? data[data.length - 1][0] : 0; | ||
| const requestedSince = (options.since && options.since.valueOf() / 1000) || 0; | ||
| if (storedSince <= requestedSince && | ||
| Date.now() < lastCheck * 1000 + THROTTLE_MS) { | ||
| // Throttled. | ||
| return filterSince(data, requestedSince); | ||
| } | ||
| const fetchOptions = Object.assign(Object.assign({}, options), { since: new Date(Math.max(requestedSince, lastStored) * 1000) }); | ||
| const [newData] = await fetchMeasureReadings(id, fetchOptions); | ||
| mergeReadings(data, newData); | ||
| storedSince = Math.min(requestedSince, storedSince); | ||
| store.set(key, { lastCheck: Date.now() / 1000, data, storedSince }); | ||
| return filterSince(data, requestedSince); | ||
| }; | ||
| const mergeReadings = (first, second) => { | ||
| if (!second.length) | ||
| return; | ||
| let firstPos = first.length - 1; | ||
| while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) { | ||
| --firstPos; | ||
| } | ||
| first.splice(firstPos + 1, Infinity, ...second); | ||
| }; | ||
| const parseReadings = (items) => { | ||
| const ranges = {}; | ||
| items.forEach(({ measure, dateTime, value }) => { | ||
| if (ranges[measure] == null) { | ||
| ranges[measure] = []; | ||
| } | ||
| ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]); | ||
| }); | ||
| const rangesById = {}; | ||
| Object.entries(ranges).forEach(([key, range]) => { | ||
| rangesById[key.substring(key.lastIndexOf('/') + 1)] = range; | ||
| }); | ||
| return rangesById; | ||
| }; | ||
| const drawMeasureWidget = async (parentEl, measureId, options = {}) => { | ||
| // Get readings for the last 7 days in local time. | ||
| const since = startOfDay(null, -7, true); | ||
| const data = await getMeasureReadings(measureId, { since }); | ||
| parentEl.replaceChildren(); | ||
| const measure = parseMeasureId(measureId); | ||
| const { unit } = translateMeasureProperties(measure); | ||
| // const [time, value] = data[data.length - 1]; | ||
| // const v = round3(value); | ||
| // const param = m.qualifiedParameter; | ||
| // const station = measure.stationId; | ||
| // const unit = m.unit; | ||
| // let textEl = createElement('div'); | ||
| // const d = dateFormatter.format(new Date(time * 1000)); | ||
| // const t = timeFormatter.format(new Date(time * 1000)); | ||
| // textEl.innerHTML = `The most recent ${param} reading for station ${station} was ${v} ${unit} at ${t} on ${d}.`; | ||
| // widgetEl.append(textEl); | ||
| const series1 = { data, unit, formatter: round3 }; | ||
| // Set max/min options for plot from widget options. | ||
| if (options.riverDataWidgetMaxValue != null) { | ||
| series1.max = parseFloat(options.riverDataWidgetMaxValue); | ||
| } | ||
| if (options.riverDataWidgetMinValue != null) { | ||
| series1.min = parseFloat(options.riverDataWidgetMinValue); | ||
| } | ||
| const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000; | ||
| const maxTime = startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000; | ||
| const chartOptions = { | ||
| minTime, | ||
| maxTime, | ||
| // attribution: `www.riverdata.co.uk/station/${measure.stationId}`, | ||
| }; | ||
| const chart = new Chart(parentEl, [series1], chartOptions); | ||
| chart.render(); | ||
| }; | ||
| /** | ||
| * Load a widget specified by a DOM element. | ||
| */ | ||
| const loadWidget = (el) => { | ||
| var _a, _b; | ||
| // Get the target element from a query selector if necessary and check it | ||
| // exists. | ||
| const targetEl = typeof el === 'string' ? document.querySelector(el) : el; | ||
| if (targetEl === null) { | ||
| throw new Error('Target element not found'); | ||
| } | ||
| // Parse element for widget type and options. | ||
| const widgetIdParts = (_b = (_a = targetEl.dataset.riverDataWidget) === null || _a === void 0 ? void 0 : _a.split(':')) !== null && _b !== void 0 ? _b : []; | ||
| const type = widgetIdParts.shift(); | ||
| const id = widgetIdParts.join(':'); | ||
| const options = targetEl.dataset; | ||
| switch (type) { | ||
| case 'measure': | ||
| drawMeasureWidget(targetEl, id, options); | ||
| break; | ||
| // The 'station' widget is experimental in v1.0 and should not be used. | ||
| // case 'station': | ||
| // break; | ||
| default: | ||
| throw new RiverDataWidgetError('Unknown widget definition', { type, id }); | ||
| } | ||
| }; | ||
| const autoload = async () => { | ||
| document.querySelectorAll('[data-river-data-widget]').forEach((el) => { | ||
| try { | ||
| loadWidget(el); | ||
| } | ||
| catch (error) { | ||
| console.error(error, { error }); | ||
| } | ||
| }); | ||
| }; | ||
| if (document.readyState === 'loading') { | ||
| // Loading hasn't finished yet. | ||
| document.addEventListener('DOMContentLoaded', autoload); | ||
| } | ||
| else { | ||
| // `DOMContentLoaded` has already fired. | ||
| autoload(); | ||
| } | ||
| export { version }; | ||
| //# sourceMappingURL=index.js.map |
| {"version":3,"file":"index.js","sources":["../src/error.ts","../src/helpers/format.ts","../src/flood-monitoring-api/error.ts","../src/flood-monitoring-api/measure.ts","../src/helpers/dom.ts","../src/helpers/time.ts","../src/widget/chart.ts","../src/flood-monitoring-api/api.ts","../src/flood-monitoring-api/store.ts","../src/flood-monitoring-api/reading.ts","../src/widget/render.ts","../src/autoload.ts"],"sourcesContent":["export type RiverDataWidgetErrorInfo = Record<string, unknown>;\n\nexport class RiverDataWidgetError extends Error {\n public info: RiverDataWidgetErrorInfo;\n\n constructor(msg: string, info: RiverDataWidgetErrorInfo = {}) {\n super(msg);\n this.name = 'RiverDataWidgetError';\n this.info = info;\n }\n}\n","export const FONT_STACK =\n '-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Roboto\",\"Helvetica Neue\",Arial,sans-serif';\n\nexport const round3 = (value: number) =>\n value < 100 ? value.toPrecision(3) : Math.round(value).toString();\n","export class FloodMonitoringApiError extends Error {\n public info: Record<string, unknown>;\n\n constructor(msg: string, info: Record<string, unknown> = {}) {\n super(msg);\n this.name = 'FloodMonitoringApiError';\n this.info = info;\n }\n}\n","import { FloodMonitoringApiError } from './error';\n\nexport { parseMeasureId };\n\nconst parseMeasureId = (measureId: string) => {\n // ............base/ stat-paramet-qualifi- type -interva-unit\n const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/;\n const matches = measureId.match(regExp);\n if (matches === null) {\n throw new FloodMonitoringApiError('Cannot parse measure id', { measureId });\n }\n const [unit, interval, type, qualifier, parameter, stationId] =\n matches.reverse();\n const qualifiedParameter = qualifier.length\n ? `${parameter}-${qualifier}`\n : parameter;\n return {\n stationId,\n parameter,\n qualifier,\n type,\n interval,\n unit,\n qualifiedParameter,\n };\n};\n\nconst measureTranslations: Record<string, Record<string, string>> = {\n unit: {\n m3_s: 'm³/s',\n mAOD: 'm',\n mASD: 'm',\n },\n qualifiedParameter: {\n 'level-stage': 'level',\n 'level-downstage': 'downstream level',\n },\n};\n\nexport const translateMeasureProperties = (measure: Record<string, string>) => {\n const translated: Record<string, string> = {};\n for (const prop in measure) {\n const value = measure[prop];\n if (measureTranslations[prop] && measureTranslations[prop][value]) {\n translated[prop] = measureTranslations[prop][value];\n } else {\n translated[prop] = value;\n }\n }\n return translated;\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { createElement, createSvgElement, setAttributes, setStyles };\n\ntype AttributeList = Record<string, string | number>;\n\nconst setAttributes = <T extends HTMLElement | SVGElement>(\n el: T,\n attributes: AttributeList\n): T => {\n Object.entries(attributes).forEach(([key, value]) => {\n el.setAttribute(key, `${value}`);\n });\n return el;\n};\n\nconst setStyles = <T extends HTMLElement | SVGElement>(\n el: T,\n styles: AttributeList\n): T => {\n Object.entries(styles).forEach(([key, value]) => {\n // Workaround (el.style.setProperty uses kebab-case keys).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (<any>el.style)[key] = value;\n });\n return el;\n};\n\nconst createElement = (\n name = 'div',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n): HTMLElement => {\n const el = document.createElement(name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n\nconst createSvgElement = (\n name = 'svg',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n) => {\n const el = document.createElementNS('http://www.w3.org/2000/svg', name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n","export const MINUTE_MS = 60000;\n// const HOUR_MS = 3600000;\nexport const DAY_MS = 86400000;\n\n/**\n * Get the Date at the start of a day in UTC or local time.\n *\n * @param offset\n * @param timeZone The time zone offset in minutes, or set to `true` to use the\n * local time zone (`false`, the default, uses UTC).\n * @returns The reqested date.\n */\nexport const startOfDay = (\n date: Date | null = null,\n offset = 0,\n timeZone: boolean | number = false\n): Date => {\n if (timeZone === false) {\n // Use UTC.\n const base = date === null ? Date.now() : date.valueOf();\n return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS);\n }\n\n const now = new Date();\n const tz = timeZone === true ? now.getTimezoneOffset() : timeZone;\n const local = now.valueOf() + tz * MINUTE_MS;\n return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS);\n};\n\n/**\n * | | long |short|narrow|numeric|2-digit|\n * |:-------:|:-----------:|:---:|:----:|:-----:|:-----:|\n * | weekday | Monday | Mon | M | | |\n * | era | Anno Domini | AD | A | | |\n * | year | | | | 2012 | 12 |\n * | month | March | Mar | M | 3 | 03 |\n * | day | | | | 1 | 01 |\n * | hour | | | | 1 | 01 |\n * | minute | | | | 1 | 01 |\n * | second | | | | 1 | 01 |\n *\n * * fractionalSecondDigits: 1, 2 or 3 for number of digits.\n * * timeZoneName: long (Pacific Standard Time), short (PST),\n * longOffset (GMT-0800), shortOffset (GMT-8), longGeneric (Pacific Time),\n * shortGeneric (PT).\n */\n\nexport const dateFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n // year: 'numeric',\n});\n\nexport const timeFormatter = new Intl.DateTimeFormat('en-GB', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n timeZoneName: 'short',\n});\n\nexport const dddFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'short',\n});\n\nexport const dMmmFormatter = new Intl.DateTimeFormat('en-GB', {\n day: 'numeric',\n month: 'short',\n});\n","import { createSvgElement } from '../helpers/dom';\nimport { timeFormatter, dddFormatter, dMmmFormatter } from '../helpers/time';\nimport { FloodMonitoringApiError } from '../flood-monitoring-api/error';\nimport { FONT_STACK } from '../helpers/format';\n\nexport interface ChartOptions {\n minTime?: number;\n maxTime?: number;\n attribution?: string;\n}\n\nexport interface ChartScaleLimits {\n minTime: number;\n maxTime: number;\n timeScale: number;\n minValue: number;\n maxValue: number;\n valueScale: number;\n}\n\nexport interface ChartSeries {\n data: TimeSeriesValue[];\n min?: number;\n max?: number;\n unit?: string;\n formatter?: (value: number) => string;\n}\n\nexport type TimeSeriesValue = [\n ts: number, // Unix time stamp (seconds).\n v: number // Value.\n];\n\nexport class Chart {\n protected fontSizePx = 14;\n\n protected el: SVGElement;\n protected series: ChartSeries[];\n protected options: ChartOptions;\n protected width = 480; // 400;\n protected height = 270; // 225;\n protected plotHeight = this.height - this.fontSizePx * 4.5;\n protected strokeWidth = 2;\n protected limits?: ChartScaleLimits;\n\n protected plotColor = '#77C';\n protected labelBg = 'rgba(255,255,255,0.5)';\n protected labelBgWidth = '0.5em';\n\n protected attribution =\n 'Uses Environment Agency data from the real-time API (Beta)';\n\n // CSS settings.\n // Just readable at 320x180.\n // Good from 400x225.\n // Perfect at 480x270 (font is 12px);\n protected styles = {\n 'font-family': FONT_STACK,\n 'font-size': `${this.fontSizePx}px`,\n display: 'block',\n margin: 'auto',\n 'max-width': '150vh',\n };\n\n constructor(\n el: HTMLElement,\n series: ChartSeries[],\n options: ChartOptions = {}\n ) {\n this.series = series;\n this.options = options;\n const viewBox = `0 0 ${this.width} ${this.height}`;\n this.attribution = options.attribution ?? this.attribution;\n this.el = createSvgElement('svg', { viewBox }, this.styles);\n el.append(this.el);\n }\n\n getLimits(): ChartScaleLimits {\n if (this.limits == null) {\n throw new FloodMonitoringApiError('Chart axis limits have not been set');\n }\n return this.limits;\n }\n\n getHorizontalGridlines(): SVGElement[] {\n const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } =\n this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight;\n const x1 = xOffset;\n const x2 = xOffset + (maxTime - minTime) * timeScale;\n // Horizontal grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n const valueRange = maxValue - minValue;\n // Horizontal grid interval.\n const [interval, exponent] = getInterval(valueRange, 9);\n const factor = 10 ** -exponent;\n const base = Math.ceil((minValue * factor) / interval + 1) * interval;\n let i = 0;\n let current = base / factor;\n while (current < maxValue) {\n const y1 = yOffset - (current - minValue) * valueScale;\n lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n labels.append(\n createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`)\n );\n ++i;\n current = (base + i * interval) / factor;\n }\n const timeAxisLine = createSvgElement(\n 'line',\n { x1, y1: yOffset, x2, y2: yOffset },\n { stroke: '#777' }\n );\n\n return [lines, labels, timeAxisLine];\n }\n\n getTimeScale(): SVGElement[] {\n const { minTime, maxTime, timeScale } = this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight + this.strokeWidth / 2;\n const y1 = yOffset + this.fontSizePx * 3;\n const y2 = yOffset - this.plotHeight;\n // Vertical grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n // Vertical grid interval.\n const base = minTime;\n const interval = 86400;\n let i = 0;\n let current = base;\n const labelOffset = 43200 * timeScale;\n const fill = '#444';\n while (current <= maxTime) {\n const x1 = xOffset + (current - minTime) * timeScale;\n const d = new Date(current * 1000);\n // lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 }));\n labels.append(\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 1.8,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dddFormatter.format(d)}`\n ),\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 0.5,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dMmmFormatter.format(d)}`\n )\n );\n ++i;\n current = base + i * interval;\n }\n return [lines, labels];\n }\n\n render() {\n // Calculate axis scales.\n const limits = getLimits(this.series[0].data);\n limits.minValue = this.series[0].min ?? limits.minValue;\n limits.maxValue = this.series[0].max ?? limits.maxValue;\n limits.minTime = this.options.minTime ?? limits.minTime;\n limits.maxTime = this.options.maxTime ?? limits.maxTime;\n\n this.limits = {\n ...limits,\n valueScale:\n (this.plotHeight - this.strokeWidth) /\n (limits.maxValue - limits.minValue),\n timeScale:\n (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime),\n };\n\n // Time axis.\n const [timeLines, timeLabels] = this.getTimeScale();\n this.el.append(timeLines);\n\n // Value axis.\n const [valueLines, valueLabels, timeAxisLine] =\n this.getHorizontalGridlines();\n this.el.append(valueLines);\n this.el.append(timeAxisLine);\n\n this.plotData();\n\n // Plot labels on top of the line.\n this.el.append(timeLabels);\n this.el.append(valueLabels);\n\n this.el.append(\n createSvgElement(\n 'text',\n {\n x: this.width / 2,\n 'text-anchor': 'middle',\n y: this.height - this.fontSizePx * 0.5,\n },\n { fill: '#595959' },\n this.attribution\n )\n );\n\n this.plotLastValue();\n }\n\n plotLastValue() {\n const { data, unit, formatter } = this.series[0];\n const [time, value] = data[data.length - 1];\n const { minTime, timeScale, maxValue, minValue } = this.getLimits();\n\n const v = formatter == null ? value : formatter(value);\n const xOffset = this.strokeWidth / 2;\n // const yOffset = this.plotHeight - this.strokeWidth / 2;\n const x = xOffset + (time - minTime) * timeScale;\n const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5;\n const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2;\n\n this.el.append(\n // Background for value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': 'end' },\n {\n 'font-size': '1.5em',\n 'font-weight': 'bold',\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n `${v} ${unit}`\n ),\n // Value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': 'end' },\n { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' },\n `${v} ${unit}`\n ),\n // Background for time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': 'end' },\n {\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n `${timeFormatter.format(new Date(time * 1000))}`\n ),\n // Time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': 'end' },\n { fill: this.plotColor },\n `${timeFormatter.format(new Date(time * 1000))}`\n )\n );\n }\n\n plotData() {\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight - this.strokeWidth / 2;\n const { data } = this.series[0];\n const { minTime, timeScale, minValue, valueScale } = this.getLimits();\n // First data point.\n const x = xOffset + (data[0][0] - minTime) * timeScale;\n const y = yOffset - (data[0][1] - minValue) * valueScale;\n const points = [`M${x},${y}`];\n // Remaining data points.\n for (let i = 1; i < data.length; ++i) {\n const x = xOffset + (data[i][0] - minTime) * timeScale;\n const y = yOffset - (data[i][1] - minValue) * valueScale;\n points.push(`L${x},${y}`);\n }\n // Plot the data.\n const path = createSvgElement('path', {\n d: points.join(''),\n stroke: this.plotColor,\n 'stroke-width': this.strokeWidth,\n fill: 'none',\n });\n this.el.append(path);\n }\n}\n\nexport const getLimits = (data: TimeSeriesValue[]) => {\n if (data.length < 1) {\n throw new Error('Readings must not be empty');\n }\n const minTime = data[0][0];\n const maxTime = data[data.length - 1][0];\n let minValue = Infinity;\n let maxValue = -minValue;\n data.forEach(([, value]) => {\n minValue = Math.min(minValue, value);\n maxValue = Math.max(maxValue, value);\n });\n return { minTime, maxTime, minValue, maxValue };\n};\n\nexport const getInterval = (range: number, maxDivisions: number) => {\n const exponent = Math.floor(Math.log10(range)) - 1;\n const k = range / (maxDivisions * 10 ** exponent);\n const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10;\n return [mantissa, exponent];\n};\n","// There is no need to be secure about this!\nconst baseUrl = 'http://environment.data.gov.uk/flood-monitoring';\n\nexport interface ApiResponse<T> {\n data: {\n items: T;\n };\n response: Response;\n}\n\nexport interface ApiParameters {\n since?: string; // Time from.\n _sorted?: ''; // Flag for sorting.\n}\n\nexport const apiFetch = async (\n path: string,\n query = {}\n): Promise<ApiResponse<unknown>> => {\n const queryString = new URLSearchParams(query).toString();\n const uri = queryString\n ? `${baseUrl}${path}?${queryString}`\n : `${baseUrl}${path}`;\n const response = await fetch(uri);\n return { data: await response.json(), response };\n};\n\n/**\n * Convert a Date to a format recognized by the EA API for a query parameter.\n *\n * @param date Convert from.\n * @returns A string in the EA API query parameter format.\n */\nexport const toTimeParameter = (date: Date): string => {\n return date.toISOString().substring(0, 19) + 'Z';\n};\n\n/*\nUseful response headers\n Date: 'Sat, 13 May 2023 09:14:07 GMT',\n last-modified: Sat, 13 May 2023 09:03:13 GMT,\nResponse meta:\n publisher: 'Environment Agency',\n license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/',\n documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference',\n version: '0.9',\n comment: 'Status: Beta service',\n hasFormat: [\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z\"\n ],\n*/\n","const prefix = 'riverDataWidget';\n\nconst addPrefix = (key: string): string => `${prefix}|${key}`;\n\nlet instance: Store;\n\nclass Store {\n clear(destroy = false) {\n for (const key of this.keys()) {\n localStorage.removeItem(addPrefix(key));\n }\n if (destroy) {\n localStorage.removeItem(prefix);\n return;\n }\n localStorage.setItem(prefix, JSON.stringify([]));\n }\n\n get(key: string) {\n const value = localStorage.getItem(addPrefix(key));\n return value === null ? null : JSON.parse(value);\n }\n\n has(key: string): boolean {\n return this.keys().includes(key);\n }\n\n /**\n * Detect active localStorage.\n *\n * @returns true iff localStorage for the widget is active.\n */\n isActive() {\n return localStorage.getItem(prefix) !== null;\n }\n\n keys(): string[] {\n const storedKeys = localStorage.getItem(prefix);\n return storedKeys === null ? [] : JSON.parse(storedKeys);\n }\n\n set(key: string, value: unknown) {\n const json = JSON.stringify(value);\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n if (!keys.includes(key)) {\n keys.push(key);\n localStorage.setItem(prefix, JSON.stringify(keys));\n }\n localStorage.setItem(addPrefix(key), json);\n }\n\n unset(key: string): boolean {\n // Remove it before we do anything else.\n localStorage.removeItem(addPrefix(key));\n\n // Then remove it from the list of keys.\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n const index = keys.indexOf(key);\n\n // If it doesn't exist we don't have to remove it.\n if (index === -1) return false;\n\n keys.splice(index, 1);\n localStorage.setItem(prefix, JSON.stringify(keys));\n return true;\n }\n}\n\nexport const useStore = (): Store => {\n if (!instance) {\n instance = new Store();\n }\n return instance;\n};\n","import { apiFetch, toTimeParameter } from './api';\nimport { useStore } from './store';\nimport { MINUTE_MS, startOfDay } from '../helpers/time';\n\nimport type { ApiParameters, ApiResponse } from './api';\n\n// Throttle requests to five minutes.\nconst THROTTLE_MS = 5 * MINUTE_MS;\n\n/**\n * Internal format for readings.\n */\nexport type Reading = [\n timestamp: number, // Unix epoch timestamp (seconds).\n value: number // Value.\n];\n\n/**\n * Internal format for readings.\n */\nexport interface ReadingOptions {\n since?: Date; // Time from.\n}\n\n/**\n * Internal format for readings.\n */\ntype ReadingResponse = [a: Reading[], b: ApiResponse<ReadingDTO[]>];\n\n/**\n * Data transfer object for readings provided by the API.\n */\ninterface ReadingDTO {\n '@id': string; // The URL of this reading.\n dateTime: string; // e.g. '2023-05-13T09:00:00Z'.\n measure: string; // The URL of the measure.\n value: number; // The value in the appropriate units.\n}\n\ninterface StoredReadings {\n storedSince: number;\n lastCheck: number;\n data: Reading[];\n}\n\n/**\n * Fetch the readings for a measure.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nconst fetchMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<ReadingResponse> => {\n // Set the parameters for the request.\n const params: ApiParameters = { _sorted: '' };\n if (options.since) {\n params.since = toTimeParameter(options.since);\n }\n // Get the response, casting the items to ReadingDTOs.\n const response = <ApiResponse<ReadingDTO[]>>(\n await apiFetch(`/id/measures/${id}/readings`, params)\n );\n return [parseReadings(response.data.items)[id] || [], response];\n};\n\nexport const filterSince = (data: Reading[], since: number) => {\n const position = data.findIndex((reading) => reading[0] >= since);\n return position < 0 ? [] : data.slice(position);\n};\n\n/**\n * Get the readings for a measure.\n *\n * @todo Caching and throttling.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nexport const getMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<Reading[]> => {\n // Get the saved readings.\n const key = `readings|${id}`;\n const store = useStore();\n\n const stored: StoredReadings = store.get(key) || {\n data: [],\n lastCheck: 0,\n storedSince: Infinity,\n };\n const { data, lastCheck } = stored;\n let { storedSince } = stored;\n\n const discardBefore = startOfDay(null, -8, true).valueOf() / 1000;\n\n // Discard any older than 30 days.\n while (data.length && data[0][0] < discardBefore) {\n [storedSince] = data[0];\n data.shift();\n }\n\n // If we have data early enough apply throttle.\n const lastStored = data.length ? data[data.length - 1][0] : 0;\n const requestedSince = (options.since && options.since.valueOf() / 1000) || 0;\n if (\n storedSince <= requestedSince &&\n Date.now() < lastCheck * 1000 + THROTTLE_MS\n ) {\n // Throttled.\n return filterSince(data, requestedSince);\n }\n\n const fetchOptions: ReadingOptions = {\n ...options,\n since: new Date(Math.max(requestedSince, lastStored) * 1000),\n };\n\n const [newData] = await fetchMeasureReadings(id, fetchOptions);\n mergeReadings(data, newData);\n storedSince = Math.min(requestedSince, storedSince);\n store.set(key, { lastCheck: Date.now() / 1000, data, storedSince });\n return filterSince(data, requestedSince);\n};\n\nexport const mergeReadings = (first: Reading[], second: Reading[]): void => {\n if (!second.length) return;\n\n let firstPos = first.length - 1;\n while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) {\n --firstPos;\n }\n first.splice(firstPos + 1, Infinity, ...second);\n};\n\nconst parseReadings = (items: ReadingDTO[]): Record<string, Reading[]> => {\n const ranges: Record<string, Reading[]> = {};\n items.forEach(({ measure, dateTime, value }) => {\n if (ranges[measure] == null) {\n ranges[measure] = [];\n }\n ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]);\n });\n\n const rangesById: Record<string, Reading[]> = {};\n Object.entries(ranges).forEach(([key, range]) => {\n rangesById[key.substring(key.lastIndexOf('/') + 1)] = range;\n });\n\n return rangesById;\n};\n","import { RiverDataWidgetError } from '../error';\nimport { round3 } from '../helpers/format';\nimport {\n parseMeasureId,\n translateMeasureProperties,\n} from '../flood-monitoring-api/measure';\nimport { Chart } from './chart';\nimport { getMeasureReadings } from '../flood-monitoring-api';\nimport { startOfDay } from '../helpers/time';\n\nimport type { ChartSeries } from './chart';\n\nconst drawMeasureWidget = async (\n parentEl: HTMLElement,\n measureId: string,\n options: Record<string, unknown> = {}\n) => {\n // Get readings for the last 7 days in local time.\n const since = startOfDay(null, -7, true);\n\n const data = await getMeasureReadings(measureId, { since });\n\n parentEl.replaceChildren();\n\n const measure = parseMeasureId(measureId);\n const { unit } = translateMeasureProperties(measure);\n\n // const [time, value] = data[data.length - 1];\n // const v = round3(value);\n // const param = m.qualifiedParameter;\n // const station = measure.stationId;\n // const unit = m.unit;\n\n // let textEl = createElement('div');\n // const d = dateFormatter.format(new Date(time * 1000));\n // const t = timeFormatter.format(new Date(time * 1000));\n // textEl.innerHTML = `The most recent ${param} reading for station ${station} was ${v} ${unit} at ${t} on ${d}.`;\n // widgetEl.append(textEl);\n\n const series1: ChartSeries = { data, unit, formatter: round3 };\n // Set max/min options for plot from widget options.\n if (options.riverDataWidgetMaxValue != null) {\n series1.max = parseFloat(<string>options.riverDataWidgetMaxValue);\n }\n if (options.riverDataWidgetMinValue != null) {\n series1.min = parseFloat(<string>options.riverDataWidgetMinValue);\n }\n const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000;\n const maxTime =\n startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000;\n const chartOptions = {\n minTime,\n maxTime,\n // attribution: `www.riverdata.co.uk/station/${measure.stationId}`,\n };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n};\n\n/**\n * Load a widget specified by a DOM element.\n */\nexport const loadWidget = (el: HTMLElement | string) => {\n // Get the target element from a query selector if necessary and check it\n // exists.\n const targetEl =\n typeof el === 'string' ? <HTMLElement>document.querySelector(el) : el;\n if (targetEl === null) {\n throw new Error('Target element not found');\n }\n\n // Parse element for widget type and options.\n const widgetIdParts = targetEl.dataset.riverDataWidget?.split(':') ?? [];\n const type = widgetIdParts.shift();\n const id = widgetIdParts.join(':');\n const options = targetEl.dataset;\n\n switch (type) {\n case 'measure':\n drawMeasureWidget(targetEl, id, options);\n break;\n\n // The 'station' widget is experimental in v1.0 and should not be used.\n // case 'station':\n // break;\n\n default:\n throw new RiverDataWidgetError('Unknown widget definition', { type, id });\n }\n};\n","import { loadWidget } from './widget/render';\n\nconst autoload = async () => {\n document.querySelectorAll('[data-river-data-widget]').forEach((el) => {\n try {\n loadWidget(<HTMLElement>el);\n } catch (error) {\n console.error(error, { error });\n }\n });\n};\n\nif (document.readyState === 'loading') {\n // Loading hasn't finished yet.\n document.addEventListener('DOMContentLoaded', autoload);\n} else {\n // `DOMContentLoaded` has already fired.\n autoload();\n}\n"],"names":[],"mappings":"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"} |
| /*! RiverDataWidget v1.1.0 2023-05-31 14:34:01 | ||
| *! https://github.com/pb-uk/river-data-widget#readme | ||
| *! Copyright (C) 2023 pbuk (https://github.com/pb-uk). | ||
| *! License MIT. | ||
| */ | ||
| var RiverDataWidget=function(t){"use strict";class e extends Error{constructor(t,e={}){super(t),this.name="RiverDataWidgetError",this.info=e}}const i=t=>t<100?t.toPrecision(3):Math.round(t).toString();class n extends Error{constructor(t,e={}){super(t),this.name="FloodMonitoringApiError",this.info=e}}const s={unit:{m3_s:"m³/s",mAOD:"m",mASD:"m"},qualifiedParameter:{"level-stage":"level","level-downstage":"downstream level"}},a=(t="svg",e={},i={},n=!1)=>{const s=document.createElementNS("http://www.w3.org/2000/svg",t);return!1!==n&&(s.innerHTML=n),((t,e)=>(Object.entries(e).forEach((([e,i])=>{t.style[e]=i})),t))(((t,e)=>(Object.entries(e).forEach((([e,i])=>{t.setAttribute(e,`${i}`)})),t))(s,e),i)},o=864e5,r=(t=null,e=0,i=!1)=>{if(!1===i){const i=null===t?Date.now():t.valueOf();return new Date(Math.floor(i/o+e)*o)}const n=new Date,s=!0===i?n.getTimezoneOffset():i,a=n.valueOf()+6e4*s;return new Date(Math.floor(a/o+e)*o)},l=new Intl.DateTimeFormat("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1,timeZoneName:"short"}),h=new Intl.DateTimeFormat("en-GB",{weekday:"short"}),m=new Intl.DateTimeFormat("en-GB",{day:"numeric",month:"short"});class c{constructor(t,e,i={}){var n;this.fontSizePx=14,this.width=480,this.height=270,this.plotHeight=this.height-4.5*this.fontSizePx,this.strokeWidth=2,this.plotColor="#77C",this.labelBg="rgba(255,255,255,0.5)",this.labelBgWidth="0.5em",this.attribution="Uses Environment Agency data from the real-time API (Beta)",this.styles={"font-family":'-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif',"font-size":`${this.fontSizePx}px`,display:"block",margin:"auto","max-width":"150vh"},this.series=e,this.options=i;const s=`0 0 ${this.width} ${this.height}`;this.attribution=null!==(n=i.attribution)&&void 0!==n?n:this.attribution,this.el=a("svg",{viewBox:s},this.styles),t.append(this.el)}getLimits(){if(null==this.limits)throw new n("Chart axis limits have not been set");return this.limits}getHorizontalGridlines(){const{minTime:t,maxTime:e,timeScale:i,minValue:n,maxValue:s,valueScale:o}=this.getLimits(),r=this.strokeWidth/2,l=this.plotHeight,h=r,m=r+(e-t)*i,c=a("g",{stroke:"#ddd"}),d=a("g"),g=s-n,[f,p]=u(g,9),x=10**-p,w=Math.ceil(n*x/f+1)*f;let S=0,v=w/x;for(;v<s;){const t=l-(v-n)*o;c.append(a("line",{x1:h,y1:t,x2:m,y2:t})),d.append(a("text",{x:h+4,y:t+4},{},`${v}`)),++S,v=(w+S*f)/x}return[c,d,a("line",{x1:h,y1:l,x2:m,y2:l},{stroke:"#777"})]}getTimeScale(){const{minTime:t,maxTime:e,timeScale:i}=this.getLimits(),n=this.strokeWidth/2,s=this.plotHeight+this.strokeWidth/2,o=s+3*this.fontSizePx,r=s-this.plotHeight,l=a("g",{stroke:"#ddd"}),c=a("g"),d=t;let u=0,g=d;const f=43200*i,p="#444";for(;g<=e;){const e=n+(g-t)*i,s=new Date(1e3*g);l.append(a("line",{x1:e,y1:o,x2:e,y2:r})),c.append(a("text",{x:e+f,y:o-1.8*this.fontSizePx,"text-anchor":"middle"},{fill:p},`${h.format(s)}`),a("text",{x:e+f,y:o-.5*this.fontSizePx,"text-anchor":"middle"},{fill:p},`${m.format(s)}`)),++u,g=d+86400*u}return[l,c]}render(){var t,e,i,n;const s=d(this.series[0].data);s.minValue=null!==(t=this.series[0].min)&&void 0!==t?t:s.minValue,s.maxValue=null!==(e=this.series[0].max)&&void 0!==e?e:s.maxValue,s.minTime=null!==(i=this.options.minTime)&&void 0!==i?i:s.minTime,s.maxTime=null!==(n=this.options.maxTime)&&void 0!==n?n:s.maxTime,this.limits=Object.assign(Object.assign({},s),{valueScale:(this.plotHeight-this.strokeWidth)/(s.maxValue-s.minValue),timeScale:(this.width-this.strokeWidth)/(s.maxTime-s.minTime)});const[o,r]=this.getTimeScale();this.el.append(o);const[l,h,m]=this.getHorizontalGridlines();this.el.append(l),this.el.append(m),this.plotData(),this.el.append(r),this.el.append(h),this.el.append(a("text",{x:this.width/2,"text-anchor":"middle",y:this.height-.5*this.fontSizePx},{fill:"#595959"},this.attribution)),this.plotLastValue()}plotLastValue(){const{data:t,unit:e,formatter:i}=this.series[0],[n,s]=t[t.length-1],{minTime:o,timeScale:r,maxValue:h,minValue:m}=this.getLimits(),c=null==i?s:i(s),d=this.strokeWidth/2+(n-o)*r,u=(s-m)/(h-m)<.5,g=this.plotHeight*(u?0:.5)+2*this.fontSizePx;this.el.append(a("text",{x:d,y:g,"text-anchor":"end"},{"font-size":"1.5em","font-weight":"bold",stroke:this.labelBg,"stroke-width":this.labelBgWidth},`${c} ${e}`),a("text",{x:d,y:g,"text-anchor":"end"},{fill:this.plotColor,"font-size":"1.5em","font-weight":"bold"},`${c} ${e}`),a("text",{x:d,y:g+1.5*this.fontSizePx,"text-anchor":"end"},{stroke:this.labelBg,"stroke-width":this.labelBgWidth},`${l.format(new Date(1e3*n))}`),a("text",{x:d,y:g+1.5*this.fontSizePx,"text-anchor":"end"},{fill:this.plotColor},`${l.format(new Date(1e3*n))}`))}plotData(){const t=this.strokeWidth/2,e=this.plotHeight-this.strokeWidth/2,{data:i}=this.series[0],{minTime:n,timeScale:s,minValue:o,valueScale:r}=this.getLimits(),l=[`M${t+(i[0][0]-n)*s},${e-(i[0][1]-o)*r}`];for(let a=1;a<i.length;++a){const h=t+(i[a][0]-n)*s,m=e-(i[a][1]-o)*r;l.push(`L${h},${m}`)}const h=a("path",{d:l.join(""),stroke:this.plotColor,"stroke-width":this.strokeWidth,fill:"none"});this.el.append(h)}}const d=t=>{if(t.length<1)throw new Error("Readings must not be empty");const e=t[0][0],i=t[t.length-1][0];let n=1/0,s=-n;return t.forEach((([,t])=>{n=Math.min(n,t),s=Math.max(s,t)})),{minTime:e,maxTime:i,minValue:n,maxValue:s}},u=(t,e)=>{const i=Math.floor(Math.log10(t))-1,n=t/(e*10**i);return[n<=2?2:n<=5?5:10,i]},g="http://environment.data.gov.uk/flood-monitoring",f="riverDataWidget",p=t=>`${f}|${t}`;let x;class w{clear(t=!1){for(const t of this.keys())localStorage.removeItem(p(t));t?localStorage.removeItem(f):localStorage.setItem(f,JSON.stringify([]))}get(t){const e=localStorage.getItem(p(t));return null===e?null:JSON.parse(e)}has(t){return this.keys().includes(t)}isActive(){return null!==localStorage.getItem(f)}keys(){const t=localStorage.getItem(f);return null===t?[]:JSON.parse(t)}set(t,e){const i=JSON.stringify(e),n=localStorage.getItem(f),s=null===n?[]:JSON.parse(n);s.includes(t)||(s.push(t),localStorage.setItem(f,JSON.stringify(s))),localStorage.setItem(p(t),i)}unset(t){localStorage.removeItem(p(t));const e=localStorage.getItem(f),i=null===e?[]:JSON.parse(e),n=i.indexOf(t);return-1!==n&&(i.splice(n,1),localStorage.setItem(f,JSON.stringify(i)),!0)}}const S=async(t,e={})=>{const i={_sorted:""};e.since&&(i.since=e.since.toISOString().substring(0,19)+"Z");const n=await(async(t,e={})=>{const i=new URLSearchParams(e).toString(),n=i?`${g}${t}?${i}`:`${g}${t}`,s=await fetch(n);return{data:await s.json(),response:s}})(`/id/measures/${t}/readings`,i);return[D(n.data.items)[t]||[],n]},v=(t,e)=>{const i=t.findIndex((t=>t[0]>=e));return i<0?[]:t.slice(i)},y=async(t,e={})=>{const i=`readings|${t}`,n=(x||(x=new w),x),s=n.get(i)||{data:[],lastCheck:0,storedSince:1/0},{data:a,lastCheck:o}=s;let{storedSince:l}=s;const h=r(null,-8,!0).valueOf()/1e3;for(;a.length&&a[0][0]<h;)[l]=a[0],a.shift();const m=a.length?a[a.length-1][0]:0,c=e.since&&e.since.valueOf()/1e3||0;if(l<=c&&Date.now()<1e3*o+3e5)return v(a,c);const d=Object.assign(Object.assign({},e),{since:new Date(1e3*Math.max(c,m))}),[u]=await S(t,d);return k(a,u),l=Math.min(c,l),n.set(i,{lastCheck:Date.now()/1e3,data:a,storedSince:l}),v(a,c)},k=(t,e)=>{if(!e.length)return;let i=t.length-1;for(;i>=0&&t[i][0]>=e[0][0];)--i;t.splice(i+1,1/0,...e)},D=t=>{const e={};t.forEach((({measure:t,dateTime:i,value:n})=>{null==e[t]&&(e[t]=[]),e[t].unshift([new Date(i).valueOf()/1e3,n])}));const i={};return Object.entries(e).forEach((([t,e])=>{i[t.substring(t.lastIndexOf("/")+1)]=e})),i},$=async(t,e,a={})=>{const o=r(null,-7,!0),l=await y(e,{since:o});t.replaceChildren();const h=(t=>{const e=t.match(/(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/);if(null===e)throw new n("Cannot parse measure id",{measureId:t});const[i,s,a,o,r,l]=e.reverse();return{stationId:l,parameter:r,qualifier:o,type:a,interval:s,unit:i,qualifiedParameter:o.length?`${r}-${o}`:r}})(e),{unit:m}=(t=>{const e={};for(const i in t){const n=t[i];s[i]&&s[i][n]?e[i]=s[i][n]:e[i]=n}return e})(h),d={data:l,unit:m,formatter:i};null!=a.riverDataWidgetMaxValue&&(d.max=parseFloat(a.riverDataWidgetMaxValue)),null!=a.riverDataWidgetMinValue&&(d.min=parseFloat(a.riverDataWidgetMinValue));const u=r(new Date(1e3*l[0][0])).valueOf()/1e3,g=r(new Date(1e3*l[l.length-1][0]),1).valueOf()/1e3;new c(t,[d],{minTime:u,maxTime:g}).render()},b=async()=>{document.querySelectorAll("[data-river-data-widget]").forEach((t=>{try{(t=>{var i,n;const s="string"==typeof t?document.querySelector(t):t;if(null===s)throw new Error("Target element not found");const a=null!==(n=null===(i=s.dataset.riverDataWidget)||void 0===i?void 0:i.split(":"))&&void 0!==n?n:[],o=a.shift(),r=a.join(":"),l=s.dataset;if("measure"!==o)throw new e("Unknown widget definition",{type:o,id:r});$(s,r,l)})(t)}catch(t){console.error(t,{error:t})}}))};return"loading"===document.readyState?document.addEventListener("DOMContentLoaded",b):b(),t.version="1.1.0",t}({}); | ||
| //# sourceMappingURL=river-data-widget.min.js.map |
| {"version":3,"file":"river-data-widget.min.js","sources":["../src/error.ts","../src/helpers/format.ts","../src/flood-monitoring-api/error.ts","../src/flood-monitoring-api/measure.ts","../src/helpers/dom.ts","../src/helpers/time.ts","../src/widget/chart.ts","../src/flood-monitoring-api/api.ts","../src/flood-monitoring-api/store.ts","../src/flood-monitoring-api/reading.ts","../src/widget/render.ts","../src/autoload.ts"],"sourcesContent":["export type RiverDataWidgetErrorInfo = Record<string, unknown>;\n\nexport class RiverDataWidgetError extends Error {\n public info: RiverDataWidgetErrorInfo;\n\n constructor(msg: string, info: RiverDataWidgetErrorInfo = {}) {\n super(msg);\n this.name = 'RiverDataWidgetError';\n this.info = info;\n }\n}\n","export const FONT_STACK =\n '-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Roboto\",\"Helvetica Neue\",Arial,sans-serif';\n\nexport const round3 = (value: number) =>\n value < 100 ? value.toPrecision(3) : Math.round(value).toString();\n","export class FloodMonitoringApiError extends Error {\n public info: Record<string, unknown>;\n\n constructor(msg: string, info: Record<string, unknown> = {}) {\n super(msg);\n this.name = 'FloodMonitoringApiError';\n this.info = info;\n }\n}\n","import { FloodMonitoringApiError } from './error';\n\nexport { parseMeasureId };\n\nconst parseMeasureId = (measureId: string) => {\n // ............base/ stat-paramet-qualifi- type -interva-unit\n const regExp = /(.*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)-([^-]*)$/;\n const matches = measureId.match(regExp);\n if (matches === null) {\n throw new FloodMonitoringApiError('Cannot parse measure id', { measureId });\n }\n const [unit, interval, type, qualifier, parameter, stationId] =\n matches.reverse();\n const qualifiedParameter = qualifier.length\n ? `${parameter}-${qualifier}`\n : parameter;\n return {\n stationId,\n parameter,\n qualifier,\n type,\n interval,\n unit,\n qualifiedParameter,\n };\n};\n\nconst measureTranslations: Record<string, Record<string, string>> = {\n unit: {\n m3_s: 'm³/s',\n mAOD: 'm',\n mASD: 'm',\n },\n qualifiedParameter: {\n 'level-stage': 'level',\n 'level-downstage': 'downstream level',\n },\n};\n\nexport const translateMeasureProperties = (measure: Record<string, string>) => {\n const translated: Record<string, string> = {};\n for (const prop in measure) {\n const value = measure[prop];\n if (measureTranslations[prop] && measureTranslations[prop][value]) {\n translated[prop] = measureTranslations[prop][value];\n } else {\n translated[prop] = value;\n }\n }\n return translated;\n};\n","/**\n * RiverDataWidget https://github.com/pb-uk/river-data-widget.\n *\n * @copyright Copyright (C) 2022 pbuk https://github.com/pb-uk.\n * @license AGPL-3.0-or-later see LICENSE.md.\n */\n\nexport { createElement, createSvgElement, setAttributes, setStyles };\n\ntype AttributeList = Record<string, string | number>;\n\nconst setAttributes = <T extends HTMLElement | SVGElement>(\n el: T,\n attributes: AttributeList\n): T => {\n Object.entries(attributes).forEach(([key, value]) => {\n el.setAttribute(key, `${value}`);\n });\n return el;\n};\n\nconst setStyles = <T extends HTMLElement | SVGElement>(\n el: T,\n styles: AttributeList\n): T => {\n Object.entries(styles).forEach(([key, value]) => {\n // Workaround (el.style.setProperty uses kebab-case keys).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (<any>el.style)[key] = value;\n });\n return el;\n};\n\nconst createElement = (\n name = 'div',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n): HTMLElement => {\n const el = document.createElement(name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n\nconst createSvgElement = (\n name = 'svg',\n attributes: AttributeList = {},\n styles: AttributeList = {},\n innerHTML: string | false = false\n) => {\n const el = document.createElementNS('http://www.w3.org/2000/svg', name);\n if (innerHTML !== false) {\n el.innerHTML = innerHTML;\n }\n return setStyles(setAttributes(el, attributes), styles);\n};\n","export const MINUTE_MS = 60000;\n// const HOUR_MS = 3600000;\nexport const DAY_MS = 86400000;\n\n/**\n * Get the Date at the start of a day in UTC or local time.\n *\n * @param offset\n * @param timeZone The time zone offset in minutes, or set to `true` to use the\n * local time zone (`false`, the default, uses UTC).\n * @returns The reqested date.\n */\nexport const startOfDay = (\n date: Date | null = null,\n offset = 0,\n timeZone: boolean | number = false\n): Date => {\n if (timeZone === false) {\n // Use UTC.\n const base = date === null ? Date.now() : date.valueOf();\n return new Date(Math.floor(base / DAY_MS + offset) * DAY_MS);\n }\n\n const now = new Date();\n const tz = timeZone === true ? now.getTimezoneOffset() : timeZone;\n const local = now.valueOf() + tz * MINUTE_MS;\n return new Date(Math.floor(local / DAY_MS + offset) * DAY_MS);\n};\n\n/**\n * | | long |short|narrow|numeric|2-digit|\n * |:-------:|:-----------:|:---:|:----:|:-----:|:-----:|\n * | weekday | Monday | Mon | M | | |\n * | era | Anno Domini | AD | A | | |\n * | year | | | | 2012 | 12 |\n * | month | March | Mar | M | 3 | 03 |\n * | day | | | | 1 | 01 |\n * | hour | | | | 1 | 01 |\n * | minute | | | | 1 | 01 |\n * | second | | | | 1 | 01 |\n *\n * * fractionalSecondDigits: 1, 2 or 3 for number of digits.\n * * timeZoneName: long (Pacific Standard Time), short (PST),\n * longOffset (GMT-0800), shortOffset (GMT-8), longGeneric (Pacific Time),\n * shortGeneric (PT).\n */\n\nexport const dateFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'long',\n day: 'numeric',\n month: 'long',\n // year: 'numeric',\n});\n\nexport const timeFormatter = new Intl.DateTimeFormat('en-GB', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n timeZoneName: 'short',\n});\n\nexport const dddFormatter = new Intl.DateTimeFormat('en-GB', {\n weekday: 'short',\n});\n\nexport const dMmmFormatter = new Intl.DateTimeFormat('en-GB', {\n day: 'numeric',\n month: 'short',\n});\n","import { createSvgElement } from '../helpers/dom';\nimport { timeFormatter, dddFormatter, dMmmFormatter } from '../helpers/time';\nimport { FloodMonitoringApiError } from '../flood-monitoring-api/error';\nimport { FONT_STACK } from '../helpers/format';\n\nexport interface ChartOptions {\n minTime?: number;\n maxTime?: number;\n attribution?: string;\n}\n\nexport interface ChartScaleLimits {\n minTime: number;\n maxTime: number;\n timeScale: number;\n minValue: number;\n maxValue: number;\n valueScale: number;\n}\n\nexport interface ChartSeries {\n data: TimeSeriesValue[];\n min?: number;\n max?: number;\n unit?: string;\n formatter?: (value: number) => string;\n}\n\nexport type TimeSeriesValue = [\n ts: number, // Unix time stamp (seconds).\n v: number // Value.\n];\n\nexport class Chart {\n protected fontSizePx = 14;\n\n protected el: SVGElement;\n protected series: ChartSeries[];\n protected options: ChartOptions;\n protected width = 480; // 400;\n protected height = 270; // 225;\n protected plotHeight = this.height - this.fontSizePx * 4.5;\n protected strokeWidth = 2;\n protected limits?: ChartScaleLimits;\n\n protected plotColor = '#77C';\n protected labelBg = 'rgba(255,255,255,0.5)';\n protected labelBgWidth = '0.5em';\n\n protected attribution =\n 'Uses Environment Agency data from the real-time API (Beta)';\n\n // CSS settings.\n // Just readable at 320x180.\n // Good from 400x225.\n // Perfect at 480x270 (font is 12px);\n protected styles = {\n 'font-family': FONT_STACK,\n 'font-size': `${this.fontSizePx}px`,\n display: 'block',\n margin: 'auto',\n 'max-width': '150vh',\n };\n\n constructor(\n el: HTMLElement,\n series: ChartSeries[],\n options: ChartOptions = {}\n ) {\n this.series = series;\n this.options = options;\n const viewBox = `0 0 ${this.width} ${this.height}`;\n this.attribution = options.attribution ?? this.attribution;\n this.el = createSvgElement('svg', { viewBox }, this.styles);\n el.append(this.el);\n }\n\n getLimits(): ChartScaleLimits {\n if (this.limits == null) {\n throw new FloodMonitoringApiError('Chart axis limits have not been set');\n }\n return this.limits;\n }\n\n getHorizontalGridlines(): SVGElement[] {\n const { minTime, maxTime, timeScale, minValue, maxValue, valueScale } =\n this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight;\n const x1 = xOffset;\n const x2 = xOffset + (maxTime - minTime) * timeScale;\n // Horizontal grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n const valueRange = maxValue - minValue;\n // Horizontal grid interval.\n const [interval, exponent] = getInterval(valueRange, 9);\n const factor = 10 ** -exponent;\n const base = Math.ceil((minValue * factor) / interval + 1) * interval;\n let i = 0;\n let current = base / factor;\n while (current < maxValue) {\n const y1 = yOffset - (current - minValue) * valueScale;\n lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n labels.append(\n createSvgElement('text', { x: x1 + 4, y: y1 + 4 }, {}, `${current}`)\n );\n ++i;\n current = (base + i * interval) / factor;\n }\n const timeAxisLine = createSvgElement(\n 'line',\n { x1, y1: yOffset, x2, y2: yOffset },\n { stroke: '#777' }\n );\n\n return [lines, labels, timeAxisLine];\n }\n\n getTimeScale(): SVGElement[] {\n const { minTime, maxTime, timeScale } = this.getLimits();\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight + this.strokeWidth / 2;\n const y1 = yOffset + this.fontSizePx * 3;\n const y2 = yOffset - this.plotHeight;\n // Vertical grid lines.\n const stroke = '#ddd';\n const lines = createSvgElement('g', { stroke });\n const labels = createSvgElement('g');\n // Vertical grid interval.\n const base = minTime;\n const interval = 86400;\n let i = 0;\n let current = base;\n const labelOffset = 43200 * timeScale;\n const fill = '#444';\n while (current <= maxTime) {\n const x1 = xOffset + (current - minTime) * timeScale;\n const d = new Date(current * 1000);\n // lines.append(createSvgElement('line', { x1, y1, x2, y2: y1 }));\n lines.append(createSvgElement('line', { x1, y1, x2: x1, y2 }));\n labels.append(\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 1.8,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dddFormatter.format(d)}`\n ),\n createSvgElement(\n 'text',\n {\n x: x1 + labelOffset,\n y: y1 - this.fontSizePx * 0.5,\n 'text-anchor': 'middle',\n },\n { fill },\n `${dMmmFormatter.format(d)}`\n )\n );\n ++i;\n current = base + i * interval;\n }\n return [lines, labels];\n }\n\n render() {\n // Calculate axis scales.\n const limits = getLimits(this.series[0].data);\n limits.minValue = this.series[0].min ?? limits.minValue;\n limits.maxValue = this.series[0].max ?? limits.maxValue;\n limits.minTime = this.options.minTime ?? limits.minTime;\n limits.maxTime = this.options.maxTime ?? limits.maxTime;\n\n this.limits = {\n ...limits,\n valueScale:\n (this.plotHeight - this.strokeWidth) /\n (limits.maxValue - limits.minValue),\n timeScale:\n (this.width - this.strokeWidth) / (limits.maxTime - limits.minTime),\n };\n\n // Time axis.\n const [timeLines, timeLabels] = this.getTimeScale();\n this.el.append(timeLines);\n\n // Value axis.\n const [valueLines, valueLabels, timeAxisLine] =\n this.getHorizontalGridlines();\n this.el.append(valueLines);\n this.el.append(timeAxisLine);\n\n this.plotData();\n\n // Plot labels on top of the line.\n this.el.append(timeLabels);\n this.el.append(valueLabels);\n\n this.el.append(\n createSvgElement(\n 'text',\n {\n x: this.width / 2,\n 'text-anchor': 'middle',\n y: this.height - this.fontSizePx * 0.5,\n },\n { fill: '#595959' },\n this.attribution\n )\n );\n\n this.plotLastValue();\n }\n\n plotLastValue() {\n const { data, unit, formatter } = this.series[0];\n const [time, value] = data[data.length - 1];\n const { minTime, timeScale, maxValue, minValue } = this.getLimits();\n\n const v = formatter == null ? value : formatter(value);\n const xOffset = this.strokeWidth / 2;\n // const yOffset = this.plotHeight - this.strokeWidth / 2;\n const x = xOffset + (time - minTime) * timeScale;\n const isHighLabel = (value - minValue) / (maxValue - minValue) < 0.5;\n const y = this.plotHeight * (isHighLabel ? 0 : 0.5) + this.fontSizePx * 2;\n\n this.el.append(\n // Background for value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': 'end' },\n {\n 'font-size': '1.5em',\n 'font-weight': 'bold',\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n `${v} ${unit}`\n ),\n // Value label.\n createSvgElement(\n 'text',\n { x, y, 'text-anchor': 'end' },\n { fill: this.plotColor, 'font-size': '1.5em', 'font-weight': 'bold' },\n `${v} ${unit}`\n ),\n // Background for time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': 'end' },\n {\n stroke: this.labelBg,\n 'stroke-width': this.labelBgWidth,\n },\n `${timeFormatter.format(new Date(time * 1000))}`\n ),\n // Time label.\n createSvgElement(\n 'text',\n { x, y: y + this.fontSizePx * 1.5, 'text-anchor': 'end' },\n { fill: this.plotColor },\n `${timeFormatter.format(new Date(time * 1000))}`\n )\n );\n }\n\n plotData() {\n const xOffset = this.strokeWidth / 2;\n const yOffset = this.plotHeight - this.strokeWidth / 2;\n const { data } = this.series[0];\n const { minTime, timeScale, minValue, valueScale } = this.getLimits();\n // First data point.\n const x = xOffset + (data[0][0] - minTime) * timeScale;\n const y = yOffset - (data[0][1] - minValue) * valueScale;\n const points = [`M${x},${y}`];\n // Remaining data points.\n for (let i = 1; i < data.length; ++i) {\n const x = xOffset + (data[i][0] - minTime) * timeScale;\n const y = yOffset - (data[i][1] - minValue) * valueScale;\n points.push(`L${x},${y}`);\n }\n // Plot the data.\n const path = createSvgElement('path', {\n d: points.join(''),\n stroke: this.plotColor,\n 'stroke-width': this.strokeWidth,\n fill: 'none',\n });\n this.el.append(path);\n }\n}\n\nexport const getLimits = (data: TimeSeriesValue[]) => {\n if (data.length < 1) {\n throw new Error('Readings must not be empty');\n }\n const minTime = data[0][0];\n const maxTime = data[data.length - 1][0];\n let minValue = Infinity;\n let maxValue = -minValue;\n data.forEach(([, value]) => {\n minValue = Math.min(minValue, value);\n maxValue = Math.max(maxValue, value);\n });\n return { minTime, maxTime, minValue, maxValue };\n};\n\nexport const getInterval = (range: number, maxDivisions: number) => {\n const exponent = Math.floor(Math.log10(range)) - 1;\n const k = range / (maxDivisions * 10 ** exponent);\n const mantissa = k <= 2 ? 2 : k <= 5 ? 5 : 10;\n return [mantissa, exponent];\n};\n","// There is no need to be secure about this!\nconst baseUrl = 'http://environment.data.gov.uk/flood-monitoring';\n\nexport interface ApiResponse<T> {\n data: {\n items: T;\n };\n response: Response;\n}\n\nexport interface ApiParameters {\n since?: string; // Time from.\n _sorted?: ''; // Flag for sorting.\n}\n\nexport const apiFetch = async (\n path: string,\n query = {}\n): Promise<ApiResponse<unknown>> => {\n const queryString = new URLSearchParams(query).toString();\n const uri = queryString\n ? `${baseUrl}${path}?${queryString}`\n : `${baseUrl}${path}`;\n const response = await fetch(uri);\n return { data: await response.json(), response };\n};\n\n/**\n * Convert a Date to a format recognized by the EA API for a query parameter.\n *\n * @param date Convert from.\n * @returns A string in the EA API query parameter format.\n */\nexport const toTimeParameter = (date: Date): string => {\n return date.toISOString().substring(0, 19) + 'Z';\n};\n\n/*\nUseful response headers\n Date: 'Sat, 13 May 2023 09:14:07 GMT',\n last-modified: Sat, 13 May 2023 09:03:13 GMT,\nResponse meta:\n publisher: 'Environment Agency',\n license: 'http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/',\n documentation: 'http://environment.data.gov.uk/flood-monitoring/doc/reference',\n version: '0.9',\n comment: 'Status: Beta service',\n hasFormat: [\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.csv?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.rdf?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.ttl?_sorted=&since=2023-05-12T08%3A00%3A00Z\",\n \"http://environment.data.gov.uk/flood-monitoring/id/measures/3400TH-level-stage-i-15_min-mAOD/readings.html?_sorted=&since=2023-05-12T08%3A00%3A00Z\"\n ],\n*/\n","const prefix = 'riverDataWidget';\n\nconst addPrefix = (key: string): string => `${prefix}|${key}`;\n\nlet instance: Store;\n\nclass Store {\n clear(destroy = false) {\n for (const key of this.keys()) {\n localStorage.removeItem(addPrefix(key));\n }\n if (destroy) {\n localStorage.removeItem(prefix);\n return;\n }\n localStorage.setItem(prefix, JSON.stringify([]));\n }\n\n get(key: string) {\n const value = localStorage.getItem(addPrefix(key));\n return value === null ? null : JSON.parse(value);\n }\n\n has(key: string): boolean {\n return this.keys().includes(key);\n }\n\n /**\n * Detect active localStorage.\n *\n * @returns true iff localStorage for the widget is active.\n */\n isActive() {\n return localStorage.getItem(prefix) !== null;\n }\n\n keys(): string[] {\n const storedKeys = localStorage.getItem(prefix);\n return storedKeys === null ? [] : JSON.parse(storedKeys);\n }\n\n set(key: string, value: unknown) {\n const json = JSON.stringify(value);\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n if (!keys.includes(key)) {\n keys.push(key);\n localStorage.setItem(prefix, JSON.stringify(keys));\n }\n localStorage.setItem(addPrefix(key), json);\n }\n\n unset(key: string): boolean {\n // Remove it before we do anything else.\n localStorage.removeItem(addPrefix(key));\n\n // Then remove it from the list of keys.\n const storedKeys = localStorage.getItem(prefix);\n const keys: string[] = storedKeys === null ? [] : JSON.parse(storedKeys);\n const index = keys.indexOf(key);\n\n // If it doesn't exist we don't have to remove it.\n if (index === -1) return false;\n\n keys.splice(index, 1);\n localStorage.setItem(prefix, JSON.stringify(keys));\n return true;\n }\n}\n\nexport const useStore = (): Store => {\n if (!instance) {\n instance = new Store();\n }\n return instance;\n};\n","import { apiFetch, toTimeParameter } from './api';\nimport { useStore } from './store';\nimport { MINUTE_MS, startOfDay } from '../helpers/time';\n\nimport type { ApiParameters, ApiResponse } from './api';\n\n// Throttle requests to five minutes.\nconst THROTTLE_MS = 5 * MINUTE_MS;\n\n/**\n * Internal format for readings.\n */\nexport type Reading = [\n timestamp: number, // Unix epoch timestamp (seconds).\n value: number // Value.\n];\n\n/**\n * Internal format for readings.\n */\nexport interface ReadingOptions {\n since?: Date; // Time from.\n}\n\n/**\n * Internal format for readings.\n */\ntype ReadingResponse = [a: Reading[], b: ApiResponse<ReadingDTO[]>];\n\n/**\n * Data transfer object for readings provided by the API.\n */\ninterface ReadingDTO {\n '@id': string; // The URL of this reading.\n dateTime: string; // e.g. '2023-05-13T09:00:00Z'.\n measure: string; // The URL of the measure.\n value: number; // The value in the appropriate units.\n}\n\ninterface StoredReadings {\n storedSince: number;\n lastCheck: number;\n data: Reading[];\n}\n\n/**\n * Fetch the readings for a measure.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nconst fetchMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<ReadingResponse> => {\n // Set the parameters for the request.\n const params: ApiParameters = { _sorted: '' };\n if (options.since) {\n params.since = toTimeParameter(options.since);\n }\n // Get the response, casting the items to ReadingDTOs.\n const response = <ApiResponse<ReadingDTO[]>>(\n await apiFetch(`/id/measures/${id}/readings`, params)\n );\n return [parseReadings(response.data.items)[id] || [], response];\n};\n\nexport const filterSince = (data: Reading[], since: number) => {\n const position = data.findIndex((reading) => reading[0] >= since);\n return position < 0 ? [] : data.slice(position);\n};\n\n/**\n * Get the readings for a measure.\n *\n * @todo Caching and throttling.\n *\n * @param id The EA measure id.\n * @returns A promise for an array of readings for the measure.\n */\nexport const getMeasureReadings = async (\n id: string,\n options: ReadingOptions = {}\n): Promise<Reading[]> => {\n // Get the saved readings.\n const key = `readings|${id}`;\n const store = useStore();\n\n const stored: StoredReadings = store.get(key) || {\n data: [],\n lastCheck: 0,\n storedSince: Infinity,\n };\n const { data, lastCheck } = stored;\n let { storedSince } = stored;\n\n const discardBefore = startOfDay(null, -8, true).valueOf() / 1000;\n\n // Discard any older than 30 days.\n while (data.length && data[0][0] < discardBefore) {\n [storedSince] = data[0];\n data.shift();\n }\n\n // If we have data early enough apply throttle.\n const lastStored = data.length ? data[data.length - 1][0] : 0;\n const requestedSince = (options.since && options.since.valueOf() / 1000) || 0;\n if (\n storedSince <= requestedSince &&\n Date.now() < lastCheck * 1000 + THROTTLE_MS\n ) {\n // Throttled.\n return filterSince(data, requestedSince);\n }\n\n const fetchOptions: ReadingOptions = {\n ...options,\n since: new Date(Math.max(requestedSince, lastStored) * 1000),\n };\n\n const [newData] = await fetchMeasureReadings(id, fetchOptions);\n mergeReadings(data, newData);\n storedSince = Math.min(requestedSince, storedSince);\n store.set(key, { lastCheck: Date.now() / 1000, data, storedSince });\n return filterSince(data, requestedSince);\n};\n\nexport const mergeReadings = (first: Reading[], second: Reading[]): void => {\n if (!second.length) return;\n\n let firstPos = first.length - 1;\n while (firstPos >= 0 && first[firstPos][0] >= second[0][0]) {\n --firstPos;\n }\n first.splice(firstPos + 1, Infinity, ...second);\n};\n\nconst parseReadings = (items: ReadingDTO[]): Record<string, Reading[]> => {\n const ranges: Record<string, Reading[]> = {};\n items.forEach(({ measure, dateTime, value }) => {\n if (ranges[measure] == null) {\n ranges[measure] = [];\n }\n ranges[measure].unshift([new Date(dateTime).valueOf() / 1000, value]);\n });\n\n const rangesById: Record<string, Reading[]> = {};\n Object.entries(ranges).forEach(([key, range]) => {\n rangesById[key.substring(key.lastIndexOf('/') + 1)] = range;\n });\n\n return rangesById;\n};\n","import { RiverDataWidgetError } from '../error';\nimport { round3 } from '../helpers/format';\nimport {\n parseMeasureId,\n translateMeasureProperties,\n} from '../flood-monitoring-api/measure';\nimport { Chart } from './chart';\nimport { getMeasureReadings } from '../flood-monitoring-api';\nimport { startOfDay } from '../helpers/time';\n\nimport type { ChartSeries } from './chart';\n\nconst drawMeasureWidget = async (\n parentEl: HTMLElement,\n measureId: string,\n options: Record<string, unknown> = {}\n) => {\n // Get readings for the last 7 days in local time.\n const since = startOfDay(null, -7, true);\n\n const data = await getMeasureReadings(measureId, { since });\n\n parentEl.replaceChildren();\n\n const measure = parseMeasureId(measureId);\n const { unit } = translateMeasureProperties(measure);\n\n // const [time, value] = data[data.length - 1];\n // const v = round3(value);\n // const param = m.qualifiedParameter;\n // const station = measure.stationId;\n // const unit = m.unit;\n\n // let textEl = createElement('div');\n // const d = dateFormatter.format(new Date(time * 1000));\n // const t = timeFormatter.format(new Date(time * 1000));\n // textEl.innerHTML = `The most recent ${param} reading for station ${station} was ${v} ${unit} at ${t} on ${d}.`;\n // widgetEl.append(textEl);\n\n const series1: ChartSeries = { data, unit, formatter: round3 };\n // Set max/min options for plot from widget options.\n if (options.riverDataWidgetMaxValue != null) {\n series1.max = parseFloat(<string>options.riverDataWidgetMaxValue);\n }\n if (options.riverDataWidgetMinValue != null) {\n series1.min = parseFloat(<string>options.riverDataWidgetMinValue);\n }\n const minTime = startOfDay(new Date(data[0][0] * 1000)).valueOf() / 1000;\n const maxTime =\n startOfDay(new Date(data[data.length - 1][0] * 1000), 1).valueOf() / 1000;\n const chartOptions = {\n minTime,\n maxTime,\n // attribution: `www.riverdata.co.uk/station/${measure.stationId}`,\n };\n\n const chart = new Chart(parentEl, [series1], chartOptions);\n chart.render();\n};\n\n/**\n * Load a widget specified by a DOM element.\n */\nexport const loadWidget = (el: HTMLElement | string) => {\n // Get the target element from a query selector if necessary and check it\n // exists.\n const targetEl =\n typeof el === 'string' ? <HTMLElement>document.querySelector(el) : el;\n if (targetEl === null) {\n throw new Error('Target element not found');\n }\n\n // Parse element for widget type and options.\n const widgetIdParts = targetEl.dataset.riverDataWidget?.split(':') ?? [];\n const type = widgetIdParts.shift();\n const id = widgetIdParts.join(':');\n const options = targetEl.dataset;\n\n switch (type) {\n case 'measure':\n drawMeasureWidget(targetEl, id, options);\n break;\n\n // The 'station' widget is experimental in v1.0 and should not be used.\n // case 'station':\n // break;\n\n default:\n throw new RiverDataWidgetError('Unknown widget definition', { type, id });\n }\n};\n","import { loadWidget } from './widget/render';\n\nconst autoload = async () => {\n document.querySelectorAll('[data-river-data-widget]').forEach((el) => {\n try {\n loadWidget(<HTMLElement>el);\n } catch (error) {\n console.error(error, { error });\n }\n });\n};\n\nif (document.readyState === 'loading') {\n // Loading hasn't finished yet.\n document.addEventListener('DOMContentLoaded', autoload);\n} else {\n // `DOMContentLoaded` has already fired.\n autoload();\n}\n"],"names":["RiverDataWidgetError","Error","constructor","msg","info","super","this","name","round3","value","toPrecision","Math","round","toString","FloodMonitoringApiError","measureTranslations","unit","m3_s","mAOD","mASD","qualifiedParameter","createSvgElement","attributes","styles","innerHTML","el","document","createElementNS","Object","entries","forEach","key","style","setStyles","setAttribute","setAttributes","DAY_MS","startOfDay","date","offset","timeZone","base","Date","now","valueOf","floor","tz","getTimezoneOffset","local","timeFormatter","Intl","DateTimeFormat","hour","minute","hour12","timeZoneName","dddFormatter","weekday","dMmmFormatter","day","month","Chart","series","options","fontSizePx","width","height","plotHeight","strokeWidth","plotColor","labelBg","labelBgWidth","attribution","display","margin","viewBox","_a","append","getLimits","limits","getHorizontalGridlines","minTime","maxTime","timeScale","minValue","maxValue","valueScale","xOffset","yOffset","x1","x2","lines","stroke","labels","valueRange","interval","exponent","getInterval","factor","ceil","i","current","y1","y2","x","y","getTimeScale","labelOffset","fill","d","format","render","data","min","_b","max","_c","_d","assign","timeLines","timeLabels","valueLines","valueLabels","timeAxisLine","plotData","plotLastValue","formatter","time","length","v","isHighLabel","points","push","path","join","Infinity","range","maxDivisions","log10","k","baseUrl","prefix","addPrefix","instance","Store","clear","destroy","keys","localStorage","removeItem","setItem","JSON","stringify","get","getItem","parse","has","includes","isActive","storedKeys","set","json","unset","index","indexOf","splice","fetchMeasureReadings","async","id","params","_sorted","since","toISOString","substring","response","query","queryString","URLSearchParams","uri","fetch","apiFetch","parseReadings","items","filterSince","position","findIndex","reading","slice","getMeasureReadings","store","stored","lastCheck","storedSince","discardBefore","shift","lastStored","requestedSince","fetchOptions","newData","mergeReadings","first","second","firstPos","ranges","measure","dateTime","unshift","rangesById","lastIndexOf","drawMeasureWidget","parentEl","measureId","replaceChildren","matches","match","type","qualifier","parameter","stationId","reverse","parseMeasureId","translated","prop","translateMeasureProperties","series1","riverDataWidgetMaxValue","parseFloat","riverDataWidgetMinValue","autoload","querySelectorAll","targetEl","querySelector","widgetIdParts","dataset","riverDataWidget","split","loadWidget","error","console","readyState","addEventListener"],"mappings":"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"} |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
211632
64.89%21
-4.55%25
257.14%1417
122.45%4
100%