UNPKG

@opengis/bi

Version:

BI data visualization module

119 lines (86 loc) 4.27 kB
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; }