@opengis/bi
Version:
BI data visualization module
119 lines (86 loc) • 4.27 kB
JavaScript
import path from 'node:path';
import { createHash } from 'node:crypto';
import { existsSync } from 'node:fs';
import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
import { pgClients, getFilterSQL, getMeta, getFolder } from '@opengis/fastify-table/utils.js';
import { getWidget } from '../../../../utils.js';
const hourMs = 3.6e6;
const maxLimit = 2500;
export default async function heatmap(req, reply) {
const { query = {}, user = {} } = req;
const { widget, dashboard, filter, search, size = 0.1 } = query;
if (query.size && (+query.size || 0) <= 0) {
return { message: 'param size is invalid', status: 400 };
}
if (!dashboard || !widget) {
return { message: 'not enough params: dashboard / widget', status: 400 };
}
const { data } = await getWidget({ pg: req.pg, widget, dashboard });
if (!data?.table) {
return { message: 'widget not found: ' + widget, status: 400 };
}
const limit = Math.min(+query.limit || maxLimit, maxLimit);
const hash = [search, filter, limit].filter((el) => el).join();
const root = getFolder(req, 'local');
const file = path.join(
root,
`/map/geojson/heatmap/${data.table}/${hash ? `${createHash('sha1').update(hash).digest('base64')}/` : ''}.geojson`
);
if (existsSync(file) && !query.nocache && !query.sql) {
const timeNow = Date.now();
const stats = await stat(file);
const birthTime = new Date(stats.birthtime).getTime();
if (!(birthTime - timeNow > hourMs * 24)) {
const geojson = JSON.parse((await readFile(file, 'utf-8')) || {});
return geojson;
}
}
const pg = data.pg || req.pg || pgClients.client;
if (!pg.pk?.[data.table]) {
return { message: `table not found: ${data.table}`, status: 404 };
}
const metric = query.metric || data.metrics?.[0]?.name || (Array.isArray(data.metrics) ? data.metrics?.[0] : data.metrics);
const operator = metric
? (['sum', 'min', 'max', 'avg'].find(el => el === query.operator) || 'sum')
: undefined;
const aggregator = metric
? `${operator}(${metric})`
: 'count(*)';
const { geom, columns } = await getMeta({ pg, table: data.table });
const { dataTypeID } = columns.find(col => col.name === metric) || {};
if (metric && !dataTypeID) {
return { message: `metric column not found: ${metric}`, status: 404 };
}
if (!['integer', 'numeric', 'double precision'].includes(pg.pgType[dataTypeID])) {
return { message: `metric column invalid type: ${metric} (${pg.pgType[dataTypeID]})`, status: 404 };
}
if (!geom) {
return { message: `geometry column not found: ${data.table}`, status: 404 };
}
const { optimizedSQL = `select * from ${data.table} where 1=1` } = hash ? await getFilterSQL({ pg, table: data.table, filter, search }) : {};
const subQuery = `SELECT ${aggregator} AS metric, hex.geom FROM (
SELECT ST_SetSRID( (ST_HexagonGrid(${size}, ST_Extent(q.${geom})) ).geom, 4326 ) as geom FROM ( ${optimizedSQL})q
)hex
LEFT JOIN ( ${optimizedSQL} )pts
ON ST_Within(pts.${geom}, hex.geom)
JOIN ( SELECT ST_ConvexHull(ST_Collect(${geom})) AS mask FROM ( ${optimizedSQL} )q )point_mask
ON ST_Intersects(hex.geom, point_mask.mask)
WHERE 1=1 /*and pts.${geom} is not null AND st_srid(pts.${geom}) > 0*/
GROUP BY hex.geom
limit ${limit}`;
if (query.sql === '1' && user?.user_type?.includes('admin')) return subQuery;
const q = `SELECT 'FeatureCollection' As type, json_agg(f) As features FROM (
SELECT
'Feature' As type,
row_number() over() as id,
st_asgeojson(geom, 6, 0)::json as geometry,
json_build_object( 'metric', metric ) as properties
from (${subQuery})sq
)f`;
if (query.sql === '2' && user?.user_type?.includes('admin')) return q;
const geojson = await pg.query(q)
.then(el => el.rows?.[0] || {});
await mkdir(path.dirname(file), { recursive: true });
await writeFile(file, JSON.stringify(geojson));
return geojson;
}