/**
* 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";
const op = aq.op;
/**
* 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();
// Add a zero-based index to non-datetime columns
const dataWithIndex = data
.select(aq.not("datetime"))
.derive({ index: d => op.row_number() - 1 });
// Replace valid values with their row index; else 0
const valueExprs = {};
ids.forEach(id => {
valueExprs[id] = `d => op.is_finite(d['${id}']) ? d.index : 0`;
});
// Find max index per ID (i.e., most recent valid row)
const maxIndexExprs = {};
ids.forEach(id => {
maxIndexExprs[id] = `d => op.max(d['${id}'])`;
});
const lastValidIndexObj = dataWithIndex.derive(valueExprs).rollup(maxIndexExprs).object(0);
const lastValidIndices = Object.values(lastValidIndexObj);
const lastValidDatetime = lastValidIndices.map(i => data.array("datetime")[i]);
const lastValidValues = ids.map((id, i) => data.get(id, lastValidIndices[i]));
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,
};
}