UNPKG

@opengis/bi

Version:

BI data visualization module

184 lines (147 loc) 5.16 kB
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 }; } }