@opengis/bi
Version:
BI data visualization module
167 lines (126 loc) • 5.2 kB
JavaScript
import Sphericalmercator from '@mapbox/sphericalmercator';
import path from 'path';
import { createHash } from 'crypto';
import { writeFile, mkdir } from 'fs/promises';
import { logger, getFolder, getFilterSQL, autoIndex, pgClients } from '@opengis/fastify-table/utils.js';
import { getWidget } from '../../../../utils.js';
import downloadClusterData from './utils/downloadClusterData.js';
const mercator = new Sphericalmercator({ size: 256 });
const clusterExists = {};
export default async function clusterVtile(req, reply) {
const { params = {}, query = {} } = req;
const { z, y } = params;
const x = params.x?.split('.')[0] - 0;
if (!x || !y || !z) {
return { message: 'not enough params: xyz', status: 400 };
}
const { widget, filter, dashboard, search, clusterZoom, nocache, pointZoom } =
query;
if (!widget) {
return { message: 'not enough params: widget', status: 400 };
}
const { pg = req.pg || pgClients.client, data } = await getWidget({ pg: req.pg, dashboard, widget });
const headers = {
'Content-Type': 'application/x-protobuf',
'Cache-Control':
nocache || query.sql ? 'no-cache' : 'public, max-age=86400',
};
const hash = [pointZoom, filter].filter((el) => el).join();
const root = getFolder(req);
const file = path.join(
root,
`/map/vtile/${widget}/${hash ? `${createHash('sha1').update(hash).digest('base64')}/` : ''}${z}/${x}/${y}.mvt`
);
try {
if (!data?.table) {
return {
message: `invalid ${widget ? 'widget' : 'dashboard'}: table not specified`,
status: 400,
};
}
const pkey = pg.pk?.[data?.table];
if (!pkey) {
return {
message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`,
status: 400,
};
}
// data param
const {
table,
query: where = '1=1',
metrics = [],
cluster,
clusterTable = {},
} = data;
if (!clusterTable?.name) {
Object.assign(clusterTable, {
name: 'bi.cluster',
title: 'title',
query: `type='${data.cluster}'`,
});
}
if (cluster && !clusterExists[data.cluster]) {
const res = await downloadClusterData({ pg, cluster });
if (res) return res;
clusterExists[cluster] = 1;
}
if (!cluster) {
return {
message: `invalid ${widget ? 'widget' : 'dashboard'}: cluster column not specified`,
status: 400,
};
}
if (!metrics.length) {
return {
message: `invalid ${widget ? 'widget' : 'dashboard'}: metric columns not found`,
status: 400,
};
}
// get sql
const { optimizedSQL } =
filter || search
? await getFilterSQL({ pg, table, filter, search })
: {};
const q = `select ${clusterTable?.column || cluster} as name, ${clusterTable?.operator || 'sum'}("${metrics[0]}")::float as metric, b.*
from ${optimizedSQL ? `(${optimizedSQL})` : table} q
left join lateral (select "${pg.pk?.[clusterTable?.name]}" as id, ${clusterTable?.title} as title,
${clusterTable?.geom || 'geom'} as geom from ${clusterTable?.name}
where ${clusterTable?.query || '1=1'} and ${clusterTable?.codifierColumn || 'codifier'}=q."${clusterTable?.column || cluster}" limit 1
)b on 1=1
where ${where} group by
q."${clusterTable?.column || cluster}", b.id, b.title, b.geom`;
if (query.sql === '1') return q;
const geomCol =
parseInt(z, 10) < parseInt(pointZoom, 10)
? `ST_Centroid(${clusterTable?.geom || data?.geom || 'geom'})`
: clusterTable?.geom || data?.geom || 'geom';
const bbox = mercator.bbox(+y, +x, +z, false /* , '900913' */);
const bbox2d = `'BOX(${bbox[0]} ${bbox[1]},${bbox[2]} ${bbox[3]})'::box2d`;
const q1 = `SELECT ST_AsMVT(q, 'bi', 4096, 'geom','row') as tile
FROM (
SELECT
floor(random() * 100000 + 1)::int + row_number() over() as row,
${pg.pk?.[clusterTable?.name] ? 'id,' : ''} name, metric, title,
ST_AsMVTGeom(st_transform(${geomCol}, 3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
FROM (select * from (${q})q where geom && ${bbox2d}
and geom is not null and st_srid(geom) >0
and ST_GeometryType(geom) = any ('{ "ST_Polygon", "ST_MultiPolygon" }')
limit 3000)q
) q`;
if (query.sql === '2') return q1;
// auto Index
autoIndex({ table, columns: (metrics || []).concat([cluster]) });
const { rows = [] } = await pg.query(q1);
if (query.sql === '3') return rows.map((el) => el.tile);
const buffer = Buffer.concat(rows.map((el) => Buffer.from(el.tile)));
if (!nocache) {
await mkdir(path.dirname(file), { recursive: true });
await writeFile(file, buffer, 'binary');
}
return reply.headers(headers).send(buffer);
} catch (err) {
logger.file('bi/clusterVtile/error', { error: err.toString(), query, params });
return { error: err.toString(), status: 500 };
}
}