@opengis/bi
Version:
BI data visualization module
184 lines (147 loc) • 5.16 kB
JavaScript
import Sphericalmercator from '@mapbox/sphericalmercator';
import path from 'path';
import { createHash } from 'crypto';
import { writeFile, mkdir } from 'fs/promises';
import { existsSync, /* readdirSync, */ readFileSync } from 'fs';
import { getWidget } from '../../../../utils.js';
import { getFolder, getFilterSQL, logger, pgClients } from '@opengis/fastify-table/utils.js';
import normalizeData from '../../data/controllers/util/normalizeData.js';
const mercator = new Sphericalmercator({ size: 256 });
const types = {
point: 'ST_Point' /* ,ST_MultiPoint */,
polygon: 'ST_Polygon,ST_MultiPolygon',
};
const area = {
1: 1000000,
2: 100000,
3: 100000,
4: 100000,
5: 100000,
6: 100000,
7: 100000,
8: 100000,
9: 100000,
10: 50000,
11: 40000,
12: 20000,
13: 20000,
};
export default async function vtile(req, reply) {
const { params = {}, query = {} } = req;
const {
filter,
widget,
dashboard,
sql,
cluster,
type,
nocache,
geom = 'geom',
pointZoom = 0,
} = query;
if (!widget) {
return { message: 'not enough params: widget', status: 400 };
}
const { y, z } = params;
const x = params.x?.split('.')[0] - 0;
if (!x || !y || !z) {
return { message: 'not enough params: xyz', status: 400 };
}
const { pg = req.pg || pgClients.client, data } = await getWidget({ pg: req.pg, widget, dashboard });
const headers = {
'Content-Type': 'application/x-protobuf',
'Cache-Control': nocache || 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 {
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,
where = '1=1',
xName = {},
metric,
} = normalizeData(data, query);
// get sql
const columns = data.columns?.map((el) => el.name || el)?.join(',') || '1';
const filterQ = filter
? await getFilterSQL({ pg, table, filter })
: undefined;
const q = `select "${pkey}",
${data?.color ? `"${data?.color}"` : '0'} as x,
${data.metrics?.[0] ? `"${data.metrics[0]}"::float` : '0'} as metric,
${columns},
${geom} as geom
from ${filterQ ? `(${filterQ})` : table} q where ${where}`;
if (sql === '1') return q;
const koef =
{
10: 0.1,
11: 0.05,
12: 0.005,
13: 0.0002,
14: 0.00005,
}[z] || 0.000001;
const geomCol =
parseInt(z, 10) < parseInt(pointZoom, 10) || true
? `ST_Centroid(${geom})`
: geom;
const bbox = mercator.bbox(+y, +x, +z, false /* , '900913' */);
const bbox2d = `'BOX(${bbox[0]} ${bbox[1]},${bbox[2]} ${bbox[3]})'::box2d`;
const areaZoom =
area[z] && false
? ` and (st_area(st_transform(${geom},3857)))>'${area[z]}'`
: '';
const q1 =
cluster > z
? `SELECT ST_AsMVT(q, 'bi', 4096, 'geom','row') as tile
FROM (
SELECT floor(random() * 100000 + 1)::int + row_number() over() as row, count(*) as point_count,
ST_AsMVTGeom(st_transform(st_centroid(ST_Union(geom)),3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
FROM (
SELECT geom, ST_ClusterDBSCAN(geom,${koef},1) OVER () AS cluster
FROM (${q})q where ${geom} && ${bbox2d} ) j
WHERE cluster IS NOT NULL
GROUP BY cluster
ORDER BY 1 DESC
)q`
: `SELECT ST_AsMVT(q, 'bi', 4096, 'geom','row') as tile
FROM (
SELECT
floor(random() * 100000 + 1)::int + row_number() over() as row,
${pkey} as id,
x,
metric,
${columns},
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
${areaZoom}
${types[type] ? ` and ST_GeometryType(geom) = any ('{ ${types[type]} }') ` : ''}
limit 3000)q
) q`;
if (sql === '2') return q1;
const { rows = [] } = await pg.query(q1);
if (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/vtile', { level: 'ERROR', error: err.toString(), query, params });
return { error: err.toString(), status: 500 };
}
}