@opengis/gis 0.2.8 → 0.2.10

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.
@@ -1,32 +1,32 @@
1
- [
2
- {
3
- "id": "1",
4
- "text": "Діючий",
5
- "color": "#1ab394"
6
- },
7
- {
8
- "id": "2",
9
- "text": "Зупинений",
10
- "color": "#ed82c8"
11
- },
12
- {
13
- "id": "3",
14
- "text": "Скасований",
15
- "color": "#4a4a4a"
16
- },
17
- {
18
- "id": "5",
19
- "text": "Проектний",
20
- "color": "#6495ed"
21
- },
22
- {
23
- "id": "7",
24
- "text": "Очікує розгляду",
25
- "color": "#92b8ef"
26
- },
27
- {
28
- "id": "10",
29
- "text": "Архівний",
30
- "color": "#d48428"
31
- }
1
+ [
2
+ {
3
+ "id": "1",
4
+ "text": "Діючий",
5
+ "color": "#1ab394"
6
+ },
7
+ {
8
+ "id": "2",
9
+ "text": "Зупинений",
10
+ "color": "#ed82c8"
11
+ },
12
+ {
13
+ "id": "3",
14
+ "text": "Скасований",
15
+ "color": "#4a4a4a"
16
+ },
17
+ {
18
+ "id": "5",
19
+ "text": "Проектний",
20
+ "color": "#6495ed"
21
+ },
22
+ {
23
+ "id": "7",
24
+ "text": "Очікує розгляду",
25
+ "color": "#92b8ef"
26
+ },
27
+ {
28
+ "id": "10",
29
+ "text": "Архівний",
30
+ "color": "#d48428"
31
+ }
32
32
  ]
@@ -1 +1 @@
1
- select uid, coalesce(coalesce(sur_name,'')||coalesce(' '||user_name,'') ||coalesce(' '||father_name,''),login) as text from admin.users
1
+ select uid, coalesce(coalesce(sur_name,'')||coalesce(' '||user_name,'') ||coalesce(' '||father_name,''),login) as text from admin.users
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengis/gis",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "type": "module",
5
5
  "author": "Softpro",
6
6
  "main": "./dist/index.js",
package/plugin.js CHANGED
@@ -26,6 +26,7 @@ const columnType = {
26
26
  async function plugin(app, opts = config) {
27
27
  // API
28
28
  app.register(import('./server/routes/map/index.mjs'), opts);
29
+ app.register(import('./server/plugins/crons.js'), opts);
29
30
  app.register(import('./server/routes/gis/index.mjs'), opts);
30
31
 
31
32
  addHook('afterData', async ({
@@ -0,0 +1,21 @@
1
+ import path from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { rm } from 'node:fs/promises';
4
+
5
+ import { addCron } from '@opengis/fastify-table/utils.js';
6
+ import rootFolder from './mapnik/funcs/rootFolder.mjs';
7
+
8
+ async function clearOldVectorTiles() {
9
+ if (existsSync(path.join(rootFolder, 'vtile'))) {
10
+ await rm(path.join(rootFolder, 'vtile'), { recursive: true, force: true });
11
+ console.log('clear tiles...', rootFolder, 'vtile');
12
+ return 'vector tiles cleared';
13
+ }
14
+ return 'nothing to clear';
15
+ }
16
+
17
+ async function plugin() {
18
+ addCron(clearOldVectorTiles, 60 * 60 * 24);
19
+ }
20
+
21
+ export default plugin;
@@ -1,11 +1,15 @@
1
1
  import path from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { createHash } from 'node:crypto';
4
- import { readFile, stat, mkdir, writeFile } from 'node:fs/promises';
4
+ import {
5
+ readFile, stat, mkdir, writeFile,
6
+ } from 'node:fs/promises';
5
7
 
6
8
  import Sphericalmercator from '@mapbox/sphericalmercator';
7
9
 
8
- import { getTemplate, pgClients, getFilterSQL, getMeta, yml2json, getColumnCLS, getSelect } from '@opengis/fastify-table/utils.js';
10
+ import {
11
+ getTemplate, pgClients, getFilterSQL, getMeta, yml2json, getColumnCLS, getSelect,
12
+ } from '@opengis/fastify-table/utils.js';
9
13
 
10
14
  import rootFolder from '../../../plugins/mapnik/funcs/rootFolder.mjs';
11
15
 
@@ -14,9 +18,13 @@ const mercator = new Sphericalmercator({ size: 256 });
14
18
  const types = { point: 'ST_Point', polygon: 'ST_Polygon,ST_MultiPolygon' };
15
19
  const systemColumns = ['uid', 'cdate', 'editor_id', 'editor_date', 'created_by', 'created_at', 'updated_by', 'updated_at'];
16
20
 
17
- export default async function vtile({ pg = pgClients.client, params = {}, query = {}, unittest }, reply) {
21
+ export default async function vtile({
22
+ pg = pgClients.client, params = {}, query = {}, unittest,
23
+ }, reply) {
18
24
  const { id, y, z } = params;
19
- const { sql, nocache, filter, pointZoom, type, layers } = query;
25
+ const {
26
+ sql, nocache, filter, pointZoom, type, layers,
27
+ } = query;
20
28
  const x = params.x?.split?.('.')?.[0] - 0;
21
29
 
22
30
  if (!id) {
@@ -39,7 +47,7 @@ export default async function vtile({ pg = pgClients.client, params = {}, query
39
47
 
40
48
  const filepath = path.join(
41
49
  rootFolder,
42
- `/map/vtile/${id}/${hash ? `${createHash('md5').update(hash).digest('hex')}/` : ''}${z}/${x}/${y}.vmt`
50
+ `/vtile/${id}/${hash ? `${createHash('md5').update(hash).digest('hex')}/` : ''}${z}/${x}/${y}.vmt`,
43
51
  );
44
52
 
45
53
  if (existsSync(filepath) && z < 13 && !nocache && !sql && !unittest) {
@@ -57,7 +65,7 @@ export default async function vtile({ pg = pgClients.client, params = {}, query
57
65
 
58
66
  const data = await pg.query(
59
67
  'select source_path as source, style, geom_type as gt, geometry_column as gc from gis.services where service_id=$1 and is_active and is_public',
60
- [id]
68
+ [id],
61
69
  ).then(el => el.rows?.[0]);
62
70
 
63
71
  if (!data) {
@@ -96,26 +104,24 @@ export default async function vtile({ pg = pgClients.client, params = {}, query
96
104
  const columnsFiltered = columns
97
105
  .filter(el => !['json', 'geometry'].includes(pg.pgType?.[el.dataTypeID]) && !systemColumns.includes(el.name));
98
106
 
99
- const props = columnsFiltered.map(el => {
100
- return el.type?.[0] === '_'
101
- ? `${el.name}[1] as ${el.name}`
102
- : `${el.name}${['int4'].includes(el.type) ? '::text' : ''}`;
103
- });
107
+ const props = columnsFiltered.map(el => (el.type?.[0] === '_'
108
+ ? `${el.name}[1] as ${el.name}`
109
+ : `${el.name}${['int4'].includes(el.type) ? '::text' : ''}`));
104
110
 
105
111
  const propsColumns = props.length ? await Promise.all(props.map(async colname => {
106
112
  if (!cls?.[colname]) return `"${colname}"`;
107
- const { type, data } = await pg.queryCache('select type, data from admin.cls where parent is null and name=$1 limit 1', { args: [cls[colname]], table: 'admin.cls' })
113
+ const { type: type2, data: data2 } = await pg.queryCache('select type, data from admin.cls where parent is null and name=$1 limit 1', { args: [cls[colname]], table: 'admin.cls' })
108
114
  .then(el => el.rows?.[0] || {}) || {};
109
115
 
110
- if (!type || type === 'sql' && !data) {
116
+ if (!type2 || (type2 === 'sql' && !data2)) {
111
117
  return `"${colname}"`;
112
118
  }
113
119
 
114
- if (type === 'json') {
120
+ if (type2 === 'json') {
115
121
  return `(select name from admin.cls where parent='${cls[colname]}' and code="${colname}"::text) as "${colname}"`;
116
122
  }
117
123
 
118
- return `(with c(id,text) as (select * from (${data})q limit 1) select text from c where id::text="${colname}"::text) as "${colname}"`;
124
+ return `(with c(id,text) as (select * from (${data2})q limit 1) select text from c where id::text="${colname}"::text) as "${colname}"`;
119
125
  })) : '';
120
126
 
121
127
  const q = `SELECT ST_AsMVT(q, '${id.replace(/'/g, "''")}', 4096, 'geom','row') as tile
@@ -163,4 +169,4 @@ export default async function vtile({ pg = pgClients.client, params = {}, query
163
169
  }
164
170
 
165
171
  return reply.status(200).headers(headers).send(buffer);
166
- }
172
+ }
@@ -1,14 +1,27 @@
1
1
  const headers = {
2
2
  'Content-Type': 'application/x-protobuf',
3
- 'Cache-Control': 'no-cache',
3
+ 'Cache-Control': '86400',
4
4
  };
5
+
6
+ import path from 'node:path';
7
+ import { existsSync } from 'node:fs';
8
+ import { createHash } from 'node:crypto';
9
+ import {
10
+ readFile, stat, mkdir, writeFile,
11
+ } from 'node:fs/promises';
12
+
5
13
  import Sphericalmercator from '@mapbox/sphericalmercator';
6
14
 
7
15
  const mercator = new Sphericalmercator({ size: 256 });
16
+
17
+ // import { SphericalMercator } from '@mapbox/sphericalmercator';
18
+ // const mercator = new SphericalMercator({ size: 256 });
19
+
8
20
  import {
9
21
  getColumnCLS, getTemplate, getMeta, getFilterSQL, getSelect,
10
22
  } from '@opengis/fastify-table/utils.js';
11
23
  import yaml from 'js-yaml';
24
+ import rootFolder from '../../plugins/mapnik/funcs/rootFolder.mjs';
12
25
 
13
26
  const propsCache = {};
14
27
 
@@ -17,6 +30,25 @@ export default async function vtile({
17
30
  }, reply) {
18
31
  const { y, z } = params;
19
32
  const x = params.x.split('.')[0] - 0;
33
+ const {
34
+ type, nocache, sql, clusterZoom, filter,
35
+ } = query;
36
+
37
+ const hash = [type, clusterZoom, filter].join();
38
+
39
+ const filepath = path.join(
40
+ rootFolder,
41
+ `/vtile/${params.layer}/${hash ? `${createHash('md5').update(hash).digest('hex')}/` : ''}${z}/${x}/${y}.vmt`,
42
+ );
43
+
44
+ if (existsSync(filepath) && +z < 13 && !nocache && !sql && process.env.NODE_ENV !== 'test') {
45
+ const { birthtimeMs = Date.now() } = await stat(filepath);
46
+ const ageInMs = Date.now() - birthtimeMs;
47
+ if (ageInMs < 86400000) {
48
+ const buffer = await readFile(filepath);
49
+ return reply.status(200).headers(headers).send(buffer);
50
+ }
51
+ }
20
52
 
21
53
  // layer
22
54
  const [map, layer] = params.layer.includes(':') ? params.layer.split(':') : [null, params.layer];
@@ -41,7 +73,7 @@ export default async function vtile({
41
73
  // meta
42
74
  const metaTable = await getMeta({ pg, table });
43
75
  if (metaTable.error) { return metaTable; }
44
- const { pk: pkey, columns } = metaTable;
76
+ const { pk: pkey = data.key, columns } = metaTable; // if view - specify via template
45
77
 
46
78
  // props
47
79
 
@@ -54,49 +86,51 @@ export default async function vtile({
54
86
  // console.log(select);
55
87
  const clsData = select?.data || col.data;
56
88
  const cls = clsData ? await getSelect(clsData).then(el => ({ name: clsData, type: el.arr ? 'cls' : 'select', sql: el.sql })) : null;
57
- const { name, type, sql } = cls || getColumnCLS(table, col.data || key) || {};
89
+ const { name, type: type1, sql: sql1 } = cls || getColumnCLS(table, col.data || key) || {};
58
90
  // console.log({ name, type, sql });
59
- if (name && type === 'cls') {
91
+ if (name && type1 === 'cls') {
60
92
  return `(select name from admin.cls where parent='${name}' and code::text=${key}::text limit 1) as "${key}_text","${key}"::text`;
61
93
  }
62
- if (type === 'select' && sql) {
63
- const { fields = [] } = await pg.query(`${sql} limit 0`);
64
- return `(select "${fields[1].name}" from (${sql})s${idx} where "${fields[0].name}"::text="${key}"::text limit 1) as "${key}_text","${key}"::text`;
94
+ if (type1 === 'select' && sql1) {
95
+ const { fields = [] } = await pg.query(`${sql1} limit 0`);
96
+ return `(select "${fields[1].name}" from (${sql1})s${idx} where "${fields[0].name}"::text="${key}"::text limit 1) as "${key}_text","${key}"::text`;
65
97
  }
66
98
  return `"${key}"::text`;
67
99
  }));
68
100
  propsCache[cache] = propsCache[cache] || propsWithCls;
69
101
 
70
102
  const geomCol = (style?.type === 'point' ? `ST_Centroid(${geom})` : null)
71
- || (parseInt(z, 10) < parseInt(query.pointZoom || style?.pointZoom || style?.iconZoom || '0', 10) ? `ST_Centroid(${geom})` : geom);
103
+ || (parseInt(z, 10) < parseInt(query.pointZoom || style?.pointZoom || style?.iconZoom || '0', 10) ? `ST_Centroid(${geom})` : geom);
72
104
 
73
105
  const koef = {
74
106
  10: 0.1, 11: 0.05, 12: 0.005, 13: 0.001, 14: 0.001, 15: 0.0005,
75
107
  }[z] || 0.0005;
76
108
 
77
- const { type } = query;
78
109
  const types = { point: 'ST_Point' /* ,ST_MultiPoint */, polygon: 'ST_Polygon,ST_MultiPolygon' };
79
- const sql = ((query.clusterZoom - z) > 2
80
- ? `SELECT ST_AsMVT(q, '${layer}', 4096, 'geom','row') as tile
110
+ const queryMode = ((clusterZoom - z) > 2 ? 1 : null) || ((clusterZoom - z) > 0 ? 2 : null) || 3;
111
+ // console.log(z, clusterZoom, queryMode);
112
+ const props1 = props.filter((el, idx, arr) => idx === arr.findIndex(item => item.name === el.name));
113
+ const q = {
114
+ 1: `SELECT ST_AsMVT(q, '${layer}', 4096, 'geom','row') as tile
81
115
  FROM (
82
116
  SELECT floor(random() * 100000 + 1)::int + row_number() over() as row, point_count, ST_AsMVTGeom(st_transform(geom,3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) geom
83
117
  FROM (
84
118
  SELECT st_centroid(st_union(geom)) as geom, count(*) as point_count
85
119
  FROM ${table} where ${layerQuery || 'true'} and ${filterData?.q || 'true'} and ${geom} && ${bbox2d} ) j
86
- )q` : null
87
- ) || ((query.clusterZoom - z) > 0
88
- ? `SELECT ST_AsMVT(q, '${layer}', 4096, 'geom','row') as tile
120
+ )q`,
121
+ 2: `SELECT ST_AsMVT(q, '${layer}', 4096, 'geom','row') as tile
89
122
  FROM (
90
- 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) geom
123
+ SELECT floor(random() * 100000 + 1)::int +row_number() over() as row, count(*) as point_count,
124
+ ${props?.length ? `${props1.map(prop => `case when count(*)=1 then min(${prop.name.replace(/'/g, "''")}) else null end as "${prop.name.replace(/'/g, "''")}"`).join(',')},` : ''}
125
+ ST_AsMVTGeom(st_transform(st_centroid(ST_Union(geom)),3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
91
126
  FROM (
92
- SELECT geom, ST_ClusterDBSCAN(geom,${koef},1) OVER () AS cluster
127
+ SELECT ${props?.length ? `${props1.map(prop => prop.name).join(',')},` : ''} geom, ST_ClusterDBSCAN(geom,${koef},1) OVER () AS cluster
93
128
  FROM ${table} where ${layerQuery || 'true'} and ${filterData?.q || 'true'} and ${geom} && ${bbox2d} ) j
94
129
  WHERE cluster IS NOT NULL
95
130
  GROUP BY cluster
96
131
  ORDER BY 1 DESC
97
- )q`
98
-
99
- : `SELECT ST_AsMVT(q, '${layer}', 4096, 'geom','row') as tile
132
+ )q`,
133
+ 3: `SELECT ST_AsMVT(q, '${layer}', 4096, 'geom','row') as tile
100
134
  FROM (
101
135
  SELECT
102
136
 
@@ -120,10 +154,17 @@ export default async function vtile({
120
154
  ${(style?.type || data?.geometry_type) === 'polygon' ? `order by st_area(st_transform(${geom},3857)) desc` : ''}
121
155
 
122
156
  limit 3000)q
123
- ) q`);
157
+ ) q`,
158
+ }[queryMode];
124
159
 
125
- if (query.sql === '1') { return sql; }
126
- const tile = await pg.query(sql);
160
+ if (query.sql === '1') { return q; }
161
+ const tile = await pg.query(q);
127
162
  const buffer = Buffer.concat(tile.rows.map((el) => Buffer.from(el.tile)));
128
- return reply.headers(headers).send(buffer);
163
+
164
+ if (!nocache) {
165
+ await mkdir(path.dirname(filepath), { recursive: true });
166
+ await writeFile(filepath, buffer, 'binary');
167
+ }
168
+
169
+ return reply.headers({ ...headers, 'Cache-Control': nocache ? 'no-cache' : headers['Cache-Control'] }).send(buffer);
129
170
  }
package/utils.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import gisRegistry from './server/routes/gis/registers/gis.registry.js';
2
2
  import gisRegistryList from './server/routes/gis/registers/gis.registry.list.js';
3
3
  import mapRegistry from './server/routes/gis/registers/map.registry.js';
4
+ import rootFolder from './server/plugins/mapnik/funcs/rootFolder.mjs';
4
5
 
5
6
  export {
6
7
  gisRegistry,
7
8
  gisRegistryList,
8
- mapRegistry
9
+ mapRegistry,
10
+ rootFolder,
9
11
  };