UNPKG

air-monitor

Version:

Utilities for working with air quality monitoring data.

138 lines (123 loc) 4.99 kB
/** * Utility functions for computing current status summaries and generating GeoJSON * FeatureCollections from Monitor metadata. These are used to spatially visualize the * most recent valid air quality measurements and data latencies from each deployed sensor. * * Internal functions: * - `internal_getCurrentStatus()` – Appends current data statistics to the Monitor's metadata table. * - `internal_createGeoJSON()` – Creates a .geojson file with current data statistics for each time series. */ import * as aq from "arquero"; /** * Compute the most recent valid timestamp and value for each deviceDeploymentID, * and append them to the metadata table. * * @param {Monitor} monitor - The Monitor instance. * @returns {aq.Table} An enhanced metadata table with last valid datetime and value. */ export function internal_getCurrentStatus(monitor) { const { data, meta } = monitor; const ids = monitor.getIDs(); // For each series, find the index of the most recent finite (valid) value. // Using -1 as the sentinel so a series whose only valid value is at row 0 // is distinguishable from a fully-empty series (which stays at -1). // Plain JS array walk avoids string-interpolated Arquero expressions, which // break for deviceDeploymentIDs containing quotes or backslashes. const n = data.numRows(); const lastValidIndices = ids.map(id => { const arr = data.array(id); let lastIdx = -1; for (let i = 0; i < n; i++) { if (Number.isFinite(arr[i])) lastIdx = i; } return lastIdx; }); // For series with no valid observations (index === -1), report null rather than // falsely reporting row 0 as the most recent valid status. const datetimeColumn = data.array("datetime"); const lastValidDatetime = lastValidIndices.map(i => (i < 0 ? null : datetimeColumn[i])); // Round to 1 decimal place to match getPM25()/daily stats. Load-time data is // not rounded, so reading it raw here would otherwise report more decimals // than the rest of the API. const lastValidValues = ids.map((id, i) => { if (lastValidIndices[i] < 0) return null; const value = data.get(id, lastValidIndices[i]); return value == null ? null : Math.round(value * 10) / 10; }); const statusTable = aq.table({ lastValidDatetime, lastValidPM_25: lastValidValues, // Generic, though field is named for PM2.5 }); return meta.assign(statusTable); } /** * Convert the current metadata table to a GeoJSON FeatureCollection. * * @param {Monitor} monitor - The Monitor instance. * @returns {Object} A GeoJSON FeatureCollection object. */ export function internal_createGeoJSON(monitor) { const meta = internal_getCurrentStatus(monitor); const features = []; // From: mv4_wrcc_PM2.5_latest.geojson // // { // "type": "FeatureCollection", // "features": [ // { // "type": "Feature", // "geometry": { // "type": "Point", // "coordinates": [-114.0909, 46.5135] // }, // "properties": { // "deviceDeploymentID": "aaef057f3e4d83c4_wrcc.s139", // "locationName": "Smoke USFS R1-39", // "timezone": "America/Denver", // "dataIngestSource": "WRCC", // "dataIngestUnitID": "s139", // "currentStatus_processingTime": "2022-12-12 13:54:13", // "last_validTime": "2022-12-12 12:00:00", // "last_validLocalTimestamp": "2022-12-12 05:00:00 MST", // "last_nowcast": "2.9", // "last_PM2.5": "2", // "last_latency": "1", // "yesterday_PM2.5_avg": "4.7" // } // } // ] // } // Format any DateTime objects to human-readable strings with timezone const formatDT = (dt) => dt?.toFormat?.('yyyy-MM-dd HH:mm:ss ZZZZ') ?? null; for (let i = 0; i < meta.numRows(); i++) { const site = meta.slice(i, i + 1).object(); features.push({ type: "Feature", geometry: { type: "Point", coordinates: [site.longitude, site.latitude], }, properties: { deviceDeploymentID: site.deviceDeploymentID, locationName: site.locationName, last_time: formatDT(site.lastValidDatetime), last_pm25: site.lastValidPM_25?.toString() ?? null, // timezone: site.timezone, // dataIngestSource: site.dataIngestSource, // dataIngestUnitID: site.dataIngestUnitID, // currentStatus_processingTime: formatDT(site.currentStatus_processingTime), // last_validTime: formatDT(site.lastValidDatetime), // last_validLocalTimestamp: formatDT(site.lastValidLocalTimestamp), // last_nowcast: site.lastNowcast?.toString() ?? null, // last_PM2_5: site.lastValidPM_25?.toString() ?? null, // last_latency: site.lastLatency?.toString() ?? null, // yesterday_PM2_5_avg: site.yesterdayAvgPM_25?.toString() ?? null, }, }); } return { type: "FeatureCollection", features, }; }