/**
* 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,
};
}