@opengis/bi 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bi.js +1 -1
- package/dist/bi.umd.cjs +40 -40
- package/dist/import-file-MEpI7PGd.js +3509 -0
- package/dist/{vs-funnel-bar-T330oJNS.js → vs-funnel-bar-BdVcgrYG.js} +1 -1
- package/dist/{vs-list-DeHF_Oaf.js → vs-list-_1Ub562I.js} +1 -1
- package/dist/{vs-map-Skt608pM.js → vs-map-BQwf_Vmm.js} +2 -2
- package/dist/{vs-map-cluster-BRUiY_90.js → vs-map-cluster-Bu0fgucv.js} +2 -2
- package/dist/{vs-number-Dd_21nn-.js → vs-number-Dke5ThxE.js} +1 -1
- package/dist/{vs-table-BwC29Zyc.js → vs-table-C95epuWz.js} +1 -1
- package/dist/{vs-text-DEJjWxDu.js → vs-text-DAq6rCjJ.js} +1 -1
- package/package.json +3 -2
- package/server/plugins/vite.js +69 -69
- package/server/routes/dashboard/controllers/utils/yaml.js +11 -11
- package/server/routes/data/controllers/data.js +2 -2
- package/server/routes/data/index.mjs +7 -1
- package/server/routes/map/controllers/cluster.js +125 -125
- package/server/routes/map/controllers/clusterVtile.js +166 -166
- package/server/routes/map/controllers/geojson.js +127 -127
- package/server/routes/map/controllers/map.js +69 -69
- package/server/routes/map/controllers/utils/downloadClusterData.js +44 -44
- package/server/routes/map/controllers/vtile.js +183 -183
- package/utils.js +12 -12
- package/dist/import-file-D8jh74Dz.js +0 -3543
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
import { pgClients, getFilterSQL, getSelectVal } from '@opengis/fastify-table/utils.js';
|
|
2
|
-
|
|
3
|
-
import { getWidget } from '../../../../utils.js';
|
|
4
|
-
|
|
5
|
-
export default async function map(req) {
|
|
6
|
-
const { query = {} } = req;
|
|
7
|
-
const { dashboard, widget } = query;
|
|
8
|
-
|
|
9
|
-
const { pg = req.pg || pgClients.client, data, type, layers, style, controls } = await getWidget({ pg: req.pg, dashboard, widget });
|
|
10
|
-
|
|
11
|
-
if (!['map'].includes(type)) {
|
|
12
|
-
return { message: 'access restricted: invalid widget type', status: 403 };
|
|
13
|
-
}
|
|
14
|
-
if (!data?.table) {
|
|
15
|
-
return { message: 'invalid widget: param table is required', status: 400 };
|
|
16
|
-
}
|
|
17
|
-
if (!pg.pk[data?.table]) {
|
|
18
|
-
return { message: 'invalid widget: table pkey not found', status: 400 };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const { q = '' } = await getFilterSQL({
|
|
22
|
-
pg,
|
|
23
|
-
table: data?.table,
|
|
24
|
-
filter: query.filter,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const res = {};
|
|
28
|
-
if (data?.color) {
|
|
29
|
-
const { rows = [] } = await pg.query(
|
|
30
|
-
`select count(*), "${data.color}" as val from ${data.table} where ${data.query || '1=1'} group by "${data.color}"`
|
|
31
|
-
);
|
|
32
|
-
if (data?.cls) {
|
|
33
|
-
const vals = await getSelectVal({
|
|
34
|
-
pg, name: data.cls, values: rows.map(el => el.val), ar: true,
|
|
35
|
-
});
|
|
36
|
-
rows.forEach(row => Object.assign(row, { ...vals?.find?.(el => el.id === row.val) || { text: row.val } }));
|
|
37
|
-
}
|
|
38
|
-
Object.assign(res, { colors: rows }); // кольори для легенди
|
|
39
|
-
}
|
|
40
|
-
if (data?.metrics?.length) {
|
|
41
|
-
const metric = data?.metrics[0];
|
|
42
|
-
const q1 = `select PERCENTILE_CONT(0) WITHIN GROUP (ORDER BY "${metric}") as "0",
|
|
43
|
-
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "${metric}") as "25",
|
|
44
|
-
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY "${metric}") as "50",
|
|
45
|
-
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "${metric}") as "75",
|
|
46
|
-
PERCENTILE_CONT(1) WITHIN GROUP (ORDER BY "${metric}") as "100" from ${data.table} where ${data.query || '1=1'} and ${q || '1=1'}`;
|
|
47
|
-
const sizes = await pg
|
|
48
|
-
.query(q1)
|
|
49
|
-
.then(el => Object.values(el.rows?.[0] || {}));
|
|
50
|
-
Object.assign(res, { sizes }); // розміри для легенди
|
|
51
|
-
}
|
|
52
|
-
const { bounds, extentStr } = await pg.query(`select count(*),
|
|
53
|
-
st_asgeojson(st_extent(geom))::json as bounds,
|
|
54
|
-
replace(regexp_replace(st_extent(geom)::box2d::text,'BOX\\(|\\)','','g'),' ',',') as "extentStr"
|
|
55
|
-
from ${data.table} where ${data.query || '1=1'}`).then(el => el.rows?.[0] || {});
|
|
56
|
-
const extent = extentStr ? extentStr.split(',') : undefined;
|
|
57
|
-
|
|
58
|
-
Object.assign(res, {
|
|
59
|
-
layers,
|
|
60
|
-
style,
|
|
61
|
-
controls,
|
|
62
|
-
columns: data.columns,
|
|
63
|
-
bounds, // Map bounds
|
|
64
|
-
extent,
|
|
65
|
-
top: [], // 10 найкращих
|
|
66
|
-
bottom: [], // 10 найгірших
|
|
67
|
-
});
|
|
68
|
-
return res;
|
|
69
|
-
}
|
|
1
|
+
import { pgClients, getFilterSQL, getSelectVal } from '@opengis/fastify-table/utils.js';
|
|
2
|
+
|
|
3
|
+
import { getWidget } from '../../../../utils.js';
|
|
4
|
+
|
|
5
|
+
export default async function map(req) {
|
|
6
|
+
const { query = {} } = req;
|
|
7
|
+
const { dashboard, widget } = query;
|
|
8
|
+
|
|
9
|
+
const { pg = req.pg || pgClients.client, data, type, layers, style, controls } = await getWidget({ pg: req.pg, dashboard, widget });
|
|
10
|
+
|
|
11
|
+
if (!['map'].includes(type)) {
|
|
12
|
+
return { message: 'access restricted: invalid widget type', status: 403 };
|
|
13
|
+
}
|
|
14
|
+
if (!data?.table) {
|
|
15
|
+
return { message: 'invalid widget: param table is required', status: 400 };
|
|
16
|
+
}
|
|
17
|
+
if (!pg.pk[data?.table]) {
|
|
18
|
+
return { message: 'invalid widget: table pkey not found', status: 400 };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { q = '' } = await getFilterSQL({
|
|
22
|
+
pg,
|
|
23
|
+
table: data?.table,
|
|
24
|
+
filter: query.filter,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const res = {};
|
|
28
|
+
if (data?.color) {
|
|
29
|
+
const { rows = [] } = await pg.query(
|
|
30
|
+
`select count(*), "${data.color}" as val from ${data.table} where ${data.query || '1=1'} group by "${data.color}"`
|
|
31
|
+
);
|
|
32
|
+
if (data?.cls) {
|
|
33
|
+
const vals = await getSelectVal({
|
|
34
|
+
pg, name: data.cls, values: rows.map(el => el.val), ar: true,
|
|
35
|
+
});
|
|
36
|
+
rows.forEach(row => Object.assign(row, { ...vals?.find?.(el => el.id === row.val) || { text: row.val } }));
|
|
37
|
+
}
|
|
38
|
+
Object.assign(res, { colors: rows }); // кольори для легенди
|
|
39
|
+
}
|
|
40
|
+
if (data?.metrics?.length) {
|
|
41
|
+
const metric = data?.metrics[0];
|
|
42
|
+
const q1 = `select PERCENTILE_CONT(0) WITHIN GROUP (ORDER BY "${metric}") as "0",
|
|
43
|
+
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "${metric}") as "25",
|
|
44
|
+
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY "${metric}") as "50",
|
|
45
|
+
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "${metric}") as "75",
|
|
46
|
+
PERCENTILE_CONT(1) WITHIN GROUP (ORDER BY "${metric}") as "100" from ${data.table} where ${data.query || '1=1'} and ${q || '1=1'}`;
|
|
47
|
+
const sizes = await pg
|
|
48
|
+
.query(q1)
|
|
49
|
+
.then(el => Object.values(el.rows?.[0] || {}));
|
|
50
|
+
Object.assign(res, { sizes }); // розміри для легенди
|
|
51
|
+
}
|
|
52
|
+
const { bounds, extentStr } = await pg.query(`select count(*),
|
|
53
|
+
st_asgeojson(st_extent(geom))::json as bounds,
|
|
54
|
+
replace(regexp_replace(st_extent(geom)::box2d::text,'BOX\\(|\\)','','g'),' ',',') as "extentStr"
|
|
55
|
+
from ${data.table} where ${data.query || '1=1'}`).then(el => el.rows?.[0] || {});
|
|
56
|
+
const extent = extentStr ? extentStr.split(',') : undefined;
|
|
57
|
+
|
|
58
|
+
Object.assign(res, {
|
|
59
|
+
layers,
|
|
60
|
+
style,
|
|
61
|
+
controls,
|
|
62
|
+
columns: data.columns,
|
|
63
|
+
bounds, // Map bounds
|
|
64
|
+
extent,
|
|
65
|
+
top: [], // 10 найкращих
|
|
66
|
+
bottom: [], // 10 найгірших
|
|
67
|
+
});
|
|
68
|
+
return res;
|
|
69
|
+
}
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
import { config, logger, pgClients } from '@opengis/fastify-table/utils.js';
|
|
2
|
-
|
|
3
|
-
export default async function downloadClusterData({ pg = pgClients.client, cluster }) {
|
|
4
|
-
if (!pg || !cluster) return null;
|
|
5
|
-
const res = await fetch(`https://cdn.softpro.ua/data/bi/${cluster}-ua.geojson`);
|
|
6
|
-
if (res?.status !== 200) {
|
|
7
|
-
return {
|
|
8
|
-
message: `cluster file not found: ${cluster}-ua.json`,
|
|
9
|
-
status: 404,
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
try {
|
|
13
|
-
const geojson = await res.json();
|
|
14
|
-
const features = geojson?.features?.filter(
|
|
15
|
-
(el, idx, arr) => el?.geometry &&
|
|
16
|
-
arr.map((item) => item.properties.name)
|
|
17
|
-
.indexOf(el.properties.name) === idx
|
|
18
|
-
); // unique
|
|
19
|
-
if (!features?.length) {
|
|
20
|
-
return {
|
|
21
|
-
message: `cluster file empty: ${cluster}-ua.json`,
|
|
22
|
-
status: 400,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
const { count = 0 } = await pg.query(`select count(*)::int from bi.cluster where type=$1`, [cluster])
|
|
26
|
-
.then((res1) => res1.rows?.[0] || {});
|
|
27
|
-
if (count !== features?.length || config.debug) {
|
|
28
|
-
// await pg.query(`delete from bi.cluster where type=$1`, [cluster]);
|
|
29
|
-
const values = features?.map((el) => `('${el.properties.codifier?.replace(/'/g, "''") || ''}','${el.properties.name?.replace(/'/g, "''") || ''}', '${cluster}', ST_GeomFromGeoJSON('${JSON.stringify(el.geometry)}')::geometry)`).join(',');
|
|
30
|
-
|
|
31
|
-
const { rowCount } = await pg.query(`insert into bi.cluster (codifier,title,type,geom)
|
|
32
|
-
values ${values} on conflict(title,type) do update set codifier=excluded.codifier, geom=excluded.geom`);
|
|
33
|
-
logger.file('bi/clusterVtile', { cluster, rowCount });
|
|
34
|
-
}
|
|
35
|
-
} catch (err) {
|
|
36
|
-
logger.file('bi/clusterVtile/error', {
|
|
37
|
-
error: err.toString(),
|
|
38
|
-
filename: `${cluster}-ua.json`,
|
|
39
|
-
});
|
|
40
|
-
return {
|
|
41
|
-
message: `cluster file import error: ${cluster}-ua.json`,
|
|
42
|
-
status: 500,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
1
|
+
import { config, logger, pgClients } from '@opengis/fastify-table/utils.js';
|
|
2
|
+
|
|
3
|
+
export default async function downloadClusterData({ pg = pgClients.client, cluster }) {
|
|
4
|
+
if (!pg || !cluster) return null;
|
|
5
|
+
const res = await fetch(`https://cdn.softpro.ua/data/bi/${cluster}-ua.geojson`);
|
|
6
|
+
if (res?.status !== 200) {
|
|
7
|
+
return {
|
|
8
|
+
message: `cluster file not found: ${cluster}-ua.json`,
|
|
9
|
+
status: 404,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const geojson = await res.json();
|
|
14
|
+
const features = geojson?.features?.filter(
|
|
15
|
+
(el, idx, arr) => el?.geometry &&
|
|
16
|
+
arr.map((item) => item.properties.name)
|
|
17
|
+
.indexOf(el.properties.name) === idx
|
|
18
|
+
); // unique
|
|
19
|
+
if (!features?.length) {
|
|
20
|
+
return {
|
|
21
|
+
message: `cluster file empty: ${cluster}-ua.json`,
|
|
22
|
+
status: 400,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const { count = 0 } = await pg.query(`select count(*)::int from bi.cluster where type=$1`, [cluster])
|
|
26
|
+
.then((res1) => res1.rows?.[0] || {});
|
|
27
|
+
if (count !== features?.length || config.debug) {
|
|
28
|
+
// await pg.query(`delete from bi.cluster where type=$1`, [cluster]);
|
|
29
|
+
const values = features?.map((el) => `('${el.properties.codifier?.replace(/'/g, "''") || ''}','${el.properties.name?.replace(/'/g, "''") || ''}', '${cluster}', ST_GeomFromGeoJSON('${JSON.stringify(el.geometry)}')::geometry)`).join(',');
|
|
30
|
+
|
|
31
|
+
const { rowCount } = await pg.query(`insert into bi.cluster (codifier,title,type,geom)
|
|
32
|
+
values ${values} on conflict(title,type) do update set codifier=excluded.codifier, geom=excluded.geom`);
|
|
33
|
+
logger.file('bi/clusterVtile', { cluster, rowCount });
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
logger.file('bi/clusterVtile/error', {
|
|
37
|
+
error: err.toString(),
|
|
38
|
+
filename: `${cluster}-ua.json`,
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
message: `cluster file import error: ${cluster}-ua.json`,
|
|
42
|
+
status: 500,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
45
|
}
|
|
@@ -1,183 +1,183 @@
|
|
|
1
|
-
import Sphericalmercator from '@mapbox/sphericalmercator';
|
|
2
|
-
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { createHash } from 'crypto';
|
|
5
|
-
import { writeFile, mkdir } from 'fs/promises';
|
|
6
|
-
import { existsSync, /* readdirSync, */ readFileSync } from 'fs';
|
|
7
|
-
import { getWidget } from '../../../../utils.js';
|
|
8
|
-
|
|
9
|
-
import { getFolder, getFilterSQL, logger, pgClients } from '@opengis/fastify-table/utils.js';
|
|
10
|
-
|
|
11
|
-
import normalizeData from '../../data/controllers/util/normalizeData.js';
|
|
12
|
-
|
|
13
|
-
const mercator = new Sphericalmercator({ size: 256 });
|
|
14
|
-
|
|
15
|
-
const types = {
|
|
16
|
-
point: 'ST_Point' /* ,ST_MultiPoint */,
|
|
17
|
-
polygon: 'ST_Polygon,ST_MultiPolygon',
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const area = {
|
|
21
|
-
1: 1000000,
|
|
22
|
-
2: 100000,
|
|
23
|
-
3: 100000,
|
|
24
|
-
4: 100000,
|
|
25
|
-
5: 100000,
|
|
26
|
-
6: 100000,
|
|
27
|
-
7: 100000,
|
|
28
|
-
8: 100000,
|
|
29
|
-
9: 100000,
|
|
30
|
-
10: 50000,
|
|
31
|
-
11: 40000,
|
|
32
|
-
12: 20000,
|
|
33
|
-
13: 20000,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export default async function vtile(req, reply) {
|
|
37
|
-
const { params = {}, query = {} } = req;
|
|
38
|
-
|
|
39
|
-
const {
|
|
40
|
-
filter,
|
|
41
|
-
widget,
|
|
42
|
-
dashboard,
|
|
43
|
-
sql,
|
|
44
|
-
cluster,
|
|
45
|
-
type,
|
|
46
|
-
nocache,
|
|
47
|
-
geom = 'geom',
|
|
48
|
-
pointZoom = 0,
|
|
49
|
-
} = query;
|
|
50
|
-
|
|
51
|
-
if (!widget) {
|
|
52
|
-
return { message: 'not enough params: widget', status: 400 };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const { y, z } = params;
|
|
56
|
-
const x = params.x?.split('.')[0] - 0;
|
|
57
|
-
|
|
58
|
-
if (!x || !y || !z) {
|
|
59
|
-
return { message: 'not enough params: xyz', status: 400 };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const { pg = req.pg || pgClients.client, data } = await getWidget({ pg: req.pg, widget, dashboard });
|
|
63
|
-
|
|
64
|
-
const headers = {
|
|
65
|
-
'Content-Type': 'application/x-protobuf',
|
|
66
|
-
'Cache-Control': nocache || sql ? 'no-cache' : 'public, max-age=86400',
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const hash = [pointZoom, filter].filter((el) => el).join();
|
|
70
|
-
|
|
71
|
-
const root = getFolder(req);
|
|
72
|
-
const file = path.join(
|
|
73
|
-
root,
|
|
74
|
-
`/map/vtile/${widget}/${hash ? `${createHash('sha1').update(hash).digest('base64')}/` : ''}${z}/${x}/${y}.mvt`
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const pkey = pg.pk?.[data?.table];
|
|
79
|
-
if (!pkey) {
|
|
80
|
-
return {
|
|
81
|
-
message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`,
|
|
82
|
-
status: 400,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// data param
|
|
87
|
-
const {
|
|
88
|
-
table,
|
|
89
|
-
where = '1=1',
|
|
90
|
-
xName = {},
|
|
91
|
-
metric,
|
|
92
|
-
} = normalizeData(data, query);
|
|
93
|
-
|
|
94
|
-
// get sql
|
|
95
|
-
const columns = data.columns?.map((el) => el.name || el)?.join(',') || '1';
|
|
96
|
-
const filterQ = filter
|
|
97
|
-
? await getFilterSQL({ pg, table, filter })
|
|
98
|
-
: undefined;
|
|
99
|
-
const q = `select "${pkey}",
|
|
100
|
-
${data?.color ? `"${data?.color}"` : '0'} as x,
|
|
101
|
-
${data.metrics?.[0] ? `"${data.metrics[0]}"::float` : '0'} as metric,
|
|
102
|
-
${columns},
|
|
103
|
-
${geom} as geom
|
|
104
|
-
from ${filterQ ? `(${filterQ})` : table} q where ${where}`;
|
|
105
|
-
|
|
106
|
-
if (sql === '1') return q;
|
|
107
|
-
|
|
108
|
-
const koef =
|
|
109
|
-
{
|
|
110
|
-
10: 0.1,
|
|
111
|
-
11: 0.05,
|
|
112
|
-
12: 0.005,
|
|
113
|
-
13: 0.0002,
|
|
114
|
-
14: 0.00005,
|
|
115
|
-
}[z] || 0.000001;
|
|
116
|
-
|
|
117
|
-
const geomCol =
|
|
118
|
-
parseInt(z, 10) < parseInt(pointZoom, 10) || true
|
|
119
|
-
? `ST_Centroid(${geom})`
|
|
120
|
-
: geom;
|
|
121
|
-
|
|
122
|
-
const bbox = mercator.bbox(+y, +x, +z, false /* , '900913' */);
|
|
123
|
-
const bbox2d = `'BOX(${bbox[0]} ${bbox[1]},${bbox[2]} ${bbox[3]})'::box2d`;
|
|
124
|
-
|
|
125
|
-
const areaZoom =
|
|
126
|
-
area[z] && false
|
|
127
|
-
? ` and (st_area(st_transform(${geom},3857)))>'${area[z]}'`
|
|
128
|
-
: '';
|
|
129
|
-
|
|
130
|
-
const q1 =
|
|
131
|
-
cluster > z
|
|
132
|
-
? `SELECT ST_AsMVT(q, 'bi', 4096, 'geom','row') as tile
|
|
133
|
-
FROM (
|
|
134
|
-
SELECT floor(random() * 100000 + 1)::int + row_number() over() as row, count(*) as point_count,
|
|
135
|
-
ST_AsMVTGeom(st_transform(st_centroid(ST_Union(geom)),3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
|
|
136
|
-
FROM (
|
|
137
|
-
SELECT geom, ST_ClusterDBSCAN(geom,${koef},1) OVER () AS cluster
|
|
138
|
-
FROM (${q})q where ${geom} && ${bbox2d} ) j
|
|
139
|
-
WHERE cluster IS NOT NULL
|
|
140
|
-
GROUP BY cluster
|
|
141
|
-
ORDER BY 1 DESC
|
|
142
|
-
)q`
|
|
143
|
-
: `SELECT ST_AsMVT(q, 'bi', 4096, 'geom','row') as tile
|
|
144
|
-
FROM (
|
|
145
|
-
SELECT
|
|
146
|
-
floor(random() * 100000 + 1)::int + row_number() over() as row,
|
|
147
|
-
|
|
148
|
-
${pkey} as id,
|
|
149
|
-
x,
|
|
150
|
-
metric,
|
|
151
|
-
${columns},
|
|
152
|
-
ST_AsMVTGeom(st_transform(${geomCol}, 3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
|
|
153
|
-
|
|
154
|
-
FROM (select * from (${q})q where ${geom} && ${bbox2d}
|
|
155
|
-
|
|
156
|
-
and ${geom} is not null and st_srid(${geom}) >0
|
|
157
|
-
|
|
158
|
-
${areaZoom}
|
|
159
|
-
|
|
160
|
-
${types[type] ? ` and ST_GeometryType(geom) = any ('{ ${types[type]} }') ` : ''}
|
|
161
|
-
|
|
162
|
-
limit 3000)q
|
|
163
|
-
) q`;
|
|
164
|
-
|
|
165
|
-
if (sql === '2') return q1;
|
|
166
|
-
|
|
167
|
-
const { rows = [] } = await pg.query(q1);
|
|
168
|
-
|
|
169
|
-
if (sql === '3') return rows.map((el) => el.tile);
|
|
170
|
-
|
|
171
|
-
const buffer = Buffer.concat(rows.map((el) => Buffer.from(el.tile)));
|
|
172
|
-
|
|
173
|
-
if (!nocache) {
|
|
174
|
-
await mkdir(path.dirname(file), { recursive: true });
|
|
175
|
-
await writeFile(file, buffer, 'binary');
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return reply.headers(headers).send(buffer);
|
|
179
|
-
} catch (err) {
|
|
180
|
-
logger.file('bi/vtile', { level: 'ERROR', error: err.toString(), query, params });
|
|
181
|
-
return { error: err.toString(), status: 500 };
|
|
182
|
-
}
|
|
183
|
-
}
|
|
1
|
+
import Sphericalmercator from '@mapbox/sphericalmercator';
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
6
|
+
import { existsSync, /* readdirSync, */ readFileSync } from 'fs';
|
|
7
|
+
import { getWidget } from '../../../../utils.js';
|
|
8
|
+
|
|
9
|
+
import { getFolder, getFilterSQL, logger, pgClients } from '@opengis/fastify-table/utils.js';
|
|
10
|
+
|
|
11
|
+
import normalizeData from '../../data/controllers/util/normalizeData.js';
|
|
12
|
+
|
|
13
|
+
const mercator = new Sphericalmercator({ size: 256 });
|
|
14
|
+
|
|
15
|
+
const types = {
|
|
16
|
+
point: 'ST_Point' /* ,ST_MultiPoint */,
|
|
17
|
+
polygon: 'ST_Polygon,ST_MultiPolygon',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const area = {
|
|
21
|
+
1: 1000000,
|
|
22
|
+
2: 100000,
|
|
23
|
+
3: 100000,
|
|
24
|
+
4: 100000,
|
|
25
|
+
5: 100000,
|
|
26
|
+
6: 100000,
|
|
27
|
+
7: 100000,
|
|
28
|
+
8: 100000,
|
|
29
|
+
9: 100000,
|
|
30
|
+
10: 50000,
|
|
31
|
+
11: 40000,
|
|
32
|
+
12: 20000,
|
|
33
|
+
13: 20000,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default async function vtile(req, reply) {
|
|
37
|
+
const { params = {}, query = {} } = req;
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
filter,
|
|
41
|
+
widget,
|
|
42
|
+
dashboard,
|
|
43
|
+
sql,
|
|
44
|
+
cluster,
|
|
45
|
+
type,
|
|
46
|
+
nocache,
|
|
47
|
+
geom = 'geom',
|
|
48
|
+
pointZoom = 0,
|
|
49
|
+
} = query;
|
|
50
|
+
|
|
51
|
+
if (!widget) {
|
|
52
|
+
return { message: 'not enough params: widget', status: 400 };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { y, z } = params;
|
|
56
|
+
const x = params.x?.split('.')[0] - 0;
|
|
57
|
+
|
|
58
|
+
if (!x || !y || !z) {
|
|
59
|
+
return { message: 'not enough params: xyz', status: 400 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { pg = req.pg || pgClients.client, data } = await getWidget({ pg: req.pg, widget, dashboard });
|
|
63
|
+
|
|
64
|
+
const headers = {
|
|
65
|
+
'Content-Type': 'application/x-protobuf',
|
|
66
|
+
'Cache-Control': nocache || sql ? 'no-cache' : 'public, max-age=86400',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const hash = [pointZoom, filter].filter((el) => el).join();
|
|
70
|
+
|
|
71
|
+
const root = getFolder(req);
|
|
72
|
+
const file = path.join(
|
|
73
|
+
root,
|
|
74
|
+
`/map/vtile/${widget}/${hash ? `${createHash('sha1').update(hash).digest('base64')}/` : ''}${z}/${x}/${y}.mvt`
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const pkey = pg.pk?.[data?.table];
|
|
79
|
+
if (!pkey) {
|
|
80
|
+
return {
|
|
81
|
+
message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`,
|
|
82
|
+
status: 400,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// data param
|
|
87
|
+
const {
|
|
88
|
+
table,
|
|
89
|
+
where = '1=1',
|
|
90
|
+
xName = {},
|
|
91
|
+
metric,
|
|
92
|
+
} = normalizeData(data, query);
|
|
93
|
+
|
|
94
|
+
// get sql
|
|
95
|
+
const columns = data.columns?.map((el) => el.name || el)?.join(',') || '1';
|
|
96
|
+
const filterQ = filter
|
|
97
|
+
? await getFilterSQL({ pg, table, filter })
|
|
98
|
+
: undefined;
|
|
99
|
+
const q = `select "${pkey}",
|
|
100
|
+
${data?.color ? `"${data?.color}"` : '0'} as x,
|
|
101
|
+
${data.metrics?.[0] ? `"${data.metrics[0]}"::float` : '0'} as metric,
|
|
102
|
+
${columns},
|
|
103
|
+
${geom} as geom
|
|
104
|
+
from ${filterQ ? `(${filterQ})` : table} q where ${where}`;
|
|
105
|
+
|
|
106
|
+
if (sql === '1') return q;
|
|
107
|
+
|
|
108
|
+
const koef =
|
|
109
|
+
{
|
|
110
|
+
10: 0.1,
|
|
111
|
+
11: 0.05,
|
|
112
|
+
12: 0.005,
|
|
113
|
+
13: 0.0002,
|
|
114
|
+
14: 0.00005,
|
|
115
|
+
}[z] || 0.000001;
|
|
116
|
+
|
|
117
|
+
const geomCol =
|
|
118
|
+
parseInt(z, 10) < parseInt(pointZoom, 10) || true
|
|
119
|
+
? `ST_Centroid(${geom})`
|
|
120
|
+
: geom;
|
|
121
|
+
|
|
122
|
+
const bbox = mercator.bbox(+y, +x, +z, false /* , '900913' */);
|
|
123
|
+
const bbox2d = `'BOX(${bbox[0]} ${bbox[1]},${bbox[2]} ${bbox[3]})'::box2d`;
|
|
124
|
+
|
|
125
|
+
const areaZoom =
|
|
126
|
+
area[z] && false
|
|
127
|
+
? ` and (st_area(st_transform(${geom},3857)))>'${area[z]}'`
|
|
128
|
+
: '';
|
|
129
|
+
|
|
130
|
+
const q1 =
|
|
131
|
+
cluster > z
|
|
132
|
+
? `SELECT ST_AsMVT(q, 'bi', 4096, 'geom','row') as tile
|
|
133
|
+
FROM (
|
|
134
|
+
SELECT floor(random() * 100000 + 1)::int + row_number() over() as row, count(*) as point_count,
|
|
135
|
+
ST_AsMVTGeom(st_transform(st_centroid(ST_Union(geom)),3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
|
|
136
|
+
FROM (
|
|
137
|
+
SELECT geom, ST_ClusterDBSCAN(geom,${koef},1) OVER () AS cluster
|
|
138
|
+
FROM (${q})q where ${geom} && ${bbox2d} ) j
|
|
139
|
+
WHERE cluster IS NOT NULL
|
|
140
|
+
GROUP BY cluster
|
|
141
|
+
ORDER BY 1 DESC
|
|
142
|
+
)q`
|
|
143
|
+
: `SELECT ST_AsMVT(q, 'bi', 4096, 'geom','row') as tile
|
|
144
|
+
FROM (
|
|
145
|
+
SELECT
|
|
146
|
+
floor(random() * 100000 + 1)::int + row_number() over() as row,
|
|
147
|
+
|
|
148
|
+
${pkey} as id,
|
|
149
|
+
x,
|
|
150
|
+
metric,
|
|
151
|
+
${columns},
|
|
152
|
+
ST_AsMVTGeom(st_transform(${geomCol}, 3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
|
|
153
|
+
|
|
154
|
+
FROM (select * from (${q})q where ${geom} && ${bbox2d}
|
|
155
|
+
|
|
156
|
+
and ${geom} is not null and st_srid(${geom}) >0
|
|
157
|
+
|
|
158
|
+
${areaZoom}
|
|
159
|
+
|
|
160
|
+
${types[type] ? ` and ST_GeometryType(geom) = any ('{ ${types[type]} }') ` : ''}
|
|
161
|
+
|
|
162
|
+
limit 3000)q
|
|
163
|
+
) q`;
|
|
164
|
+
|
|
165
|
+
if (sql === '2') return q1;
|
|
166
|
+
|
|
167
|
+
const { rows = [] } = await pg.query(q1);
|
|
168
|
+
|
|
169
|
+
if (sql === '3') return rows.map((el) => el.tile);
|
|
170
|
+
|
|
171
|
+
const buffer = Buffer.concat(rows.map((el) => Buffer.from(el.tile)));
|
|
172
|
+
|
|
173
|
+
if (!nocache) {
|
|
174
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
175
|
+
await writeFile(file, buffer, 'binary');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return reply.headers(headers).send(buffer);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
logger.file('bi/vtile', { level: 'ERROR', error: err.toString(), query, params });
|
|
181
|
+
return { error: err.toString(), status: 500 };
|
|
182
|
+
}
|
|
183
|
+
}
|
package/utils.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// This file contains code that we reuse
|
|
2
|
-
// between our tests.
|
|
3
|
-
|
|
4
|
-
// import getTemplatePath from '@opengis/fastify-table/table/controllers/utils/getTemplatePath.js';
|
|
5
|
-
import getWidget from './server/utils/getWidget.js';
|
|
6
|
-
import yamlSafe from './server/routes/dashboard/controllers/utils/yaml.js';
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
// getTemplatePath,
|
|
10
|
-
yamlSafe,
|
|
11
|
-
getWidget,
|
|
12
|
-
};
|
|
1
|
+
// This file contains code that we reuse
|
|
2
|
+
// between our tests.
|
|
3
|
+
|
|
4
|
+
// import getTemplatePath from '@opengis/fastify-table/table/controllers/utils/getTemplatePath.js';
|
|
5
|
+
import getWidget from './server/utils/getWidget.js';
|
|
6
|
+
import yamlSafe from './server/routes/dashboard/controllers/utils/yaml.js';
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
// getTemplatePath,
|
|
10
|
+
yamlSafe,
|
|
11
|
+
getWidget,
|
|
12
|
+
};
|