@opengis/gis 0.2.50 → 0.2.52
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/index.css +1 -1
- package/dist/index.js +750 -750
- package/dist/index.umd.cjs +41 -41
- package/module/gis/form/gis.rasters.form.json +5 -32
- package/package.json +2 -2
- package/server/migrations/cartocss.sql +20 -0
- package/server/plugins/mapnik/funcs/mapnik.js +35 -1
- package/server/plugins/mapnik/map.proto +33 -0
- package/server/routes/gis/cartocss/add.cartocss.js +1 -124
- package/server/routes/map/controllers/mapFormat.js +3 -1
- package/server/routes/mapnik/controllers/checkCarto.js +40 -0
- package/server/routes/mapnik/controllers/mapnikLogger.js +23 -0
- package/server/routes/mapnik/controllers/mapnikStat.js +20 -0
- package/server/routes/mapnik/controllers/rasterInfo.js +36 -0
- package/server/routes/mapnik/controllers/rtile.js +4 -14
- package/server/routes/mapnik/functions/cartoBounds.js +22 -0
- package/server/routes/mapnik/functions/uploadXML.js +106 -0
- package/server/routes/mapnik/index.js +12 -7
|
@@ -20,10 +20,7 @@
|
|
|
20
20
|
"raster_key": {
|
|
21
21
|
"type": "Text",
|
|
22
22
|
"ua": "Унікальний ключ",
|
|
23
|
-
"col": 4
|
|
24
|
-
"validators": [
|
|
25
|
-
"required"
|
|
26
|
-
]
|
|
23
|
+
"col": 4
|
|
27
24
|
},
|
|
28
25
|
"group_id": {
|
|
29
26
|
"type": "Autocomplete",
|
|
@@ -57,7 +54,8 @@
|
|
|
57
54
|
"ua": "Просторова референтна система",
|
|
58
55
|
"type": "Autocomplete",
|
|
59
56
|
"data": "gis.srid_from_setting",
|
|
60
|
-
"
|
|
57
|
+
"info": "EPSG",
|
|
58
|
+
"col": 12,
|
|
61
59
|
"validators": [
|
|
62
60
|
"required"
|
|
63
61
|
]
|
|
@@ -65,39 +63,14 @@
|
|
|
65
63
|
"proj4": {
|
|
66
64
|
"type": "Text",
|
|
67
65
|
"ua": "proj4",
|
|
68
|
-
"col": 6
|
|
69
|
-
},
|
|
70
|
-
"source_type": {
|
|
71
|
-
"type": "Text",
|
|
72
|
-
"ua": "Тип джерела, де зберігається растр",
|
|
73
66
|
"col": 12
|
|
74
67
|
},
|
|
75
68
|
"raster_zoom": {
|
|
76
69
|
"type": "Number",
|
|
77
70
|
"min": 0,
|
|
78
|
-
"ua": "
|
|
71
|
+
"ua": "previewZoom",
|
|
79
72
|
"placeholder": "15 за замовченням",
|
|
80
|
-
"info": "
|
|
81
|
-
"col": 6
|
|
82
|
-
},
|
|
83
|
-
"resolution_x": {
|
|
84
|
-
"type": "Number",
|
|
85
|
-
"ua": "Координати x центру",
|
|
86
|
-
"col": 6
|
|
87
|
-
},
|
|
88
|
-
"resolution_y": {
|
|
89
|
-
"type": "Number",
|
|
90
|
-
"ua": "Координати y центру",
|
|
91
|
-
"col": 6
|
|
92
|
-
},
|
|
93
|
-
"format": {
|
|
94
|
-
"type": "Text",
|
|
95
|
-
"ua": "Формат (розширення) растру",
|
|
96
|
-
"col": 12
|
|
97
|
-
},
|
|
98
|
-
"raster_date": {
|
|
99
|
-
"type": "DatePicker",
|
|
100
|
-
"ua": "Дата створення растру",
|
|
73
|
+
"info": "Зум для відображення оригіналу",
|
|
101
74
|
"col": 12
|
|
102
75
|
}
|
|
103
76
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengis/gis",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.52",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Softpro",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"dump": "bun ./node_modules/@opengis/fastify-table/dist/script/dump.js",
|
|
19
19
|
"migrate": "MIGRATE=true bun ./node_modules/@opengis/fastify-table/dist/script/migrate.js",
|
|
20
|
-
"
|
|
20
|
+
"start": "bun server",
|
|
21
21
|
"start:kyiv-demo": "bun --env-file=.env.kyiv-demo server",
|
|
22
22
|
"start:kamyanske": "bun --env-file=.env.kamyanske server",
|
|
23
23
|
"start:kr": "bun --env-file=.env.kr server",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
create schema if not exists gis;
|
|
2
|
+
|
|
3
|
+
create table if not exists gis.cartocss(
|
|
4
|
+
cartocss_id text default next_id() not null primary key,
|
|
5
|
+
cartocss_key text not null constraint cartocss_key_unique unique,
|
|
6
|
+
name text not null,
|
|
7
|
+
description text,
|
|
8
|
+
config jsonb,
|
|
9
|
+
style text,
|
|
10
|
+
created_at timestamp default now(),
|
|
11
|
+
created_by text,
|
|
12
|
+
updated_by text,
|
|
13
|
+
updated_at timestamp,
|
|
14
|
+
group_id text constraint services_group_id_fkey references group_list,
|
|
15
|
+
is_public boolean default false not null,
|
|
16
|
+
enabled boolean default true not null,
|
|
17
|
+
geom public.geometry
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
alter table gis.cartocss add column if not exists geom public.geometry;
|
|
@@ -61,6 +61,24 @@ mapClient.waitForReady(Date.now() + 5000, (err) => {
|
|
|
61
61
|
'GetRasterStatus', // for resumable upload support
|
|
62
62
|
]
|
|
63
63
|
*/
|
|
64
|
+
|
|
65
|
+
function formatObject(obj1) {
|
|
66
|
+
return Object.keys(obj1 || {}).reduce((acc1, key) => {
|
|
67
|
+
const { kind } = obj1[key];
|
|
68
|
+
const value = obj1[key][kind];
|
|
69
|
+
if (key === 'timestamp') {
|
|
70
|
+
Object.assign(acc1, { [key]: new Date(value * 1000).toISOString() });
|
|
71
|
+
}
|
|
72
|
+
else if (kind === 'structValue') {
|
|
73
|
+
Object.assign(acc1, { [key]: formatObject(value.fields) });
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
Object.assign(acc1, { [key]: value });
|
|
77
|
+
}
|
|
78
|
+
return acc1;
|
|
79
|
+
}, {});
|
|
80
|
+
}
|
|
81
|
+
|
|
64
82
|
const grpcMethods = Object.getOwnPropertyNames(
|
|
65
83
|
Object.getPrototypeOf(mapClient),
|
|
66
84
|
)
|
|
@@ -89,6 +107,22 @@ const grpcMethods = Object.getOwnPropertyNames(
|
|
|
89
107
|
});
|
|
90
108
|
stream.on("end", () => res('finish'));
|
|
91
109
|
}
|
|
110
|
+
else if (curr === 'GetStat') {
|
|
111
|
+
mapClient[curr].call(
|
|
112
|
+
mapClient,
|
|
113
|
+
inputData,
|
|
114
|
+
(err, data) => {
|
|
115
|
+
if (err) {
|
|
116
|
+
return rej(err || 'invalid response');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { fields } = data?.result || {};
|
|
120
|
+
// google.protobuf.Struct to js object
|
|
121
|
+
const obj = formatObject(fields);
|
|
122
|
+
return res(obj);
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
}
|
|
92
126
|
else {
|
|
93
127
|
mapClient[curr].call(
|
|
94
128
|
mapClient,
|
|
@@ -97,7 +131,7 @@ const grpcMethods = Object.getOwnPropertyNames(
|
|
|
97
131
|
if (err) {
|
|
98
132
|
return rej(err);
|
|
99
133
|
}
|
|
100
|
-
return res(data
|
|
134
|
+
return res(data);
|
|
101
135
|
},
|
|
102
136
|
);
|
|
103
137
|
}
|
|
@@ -18,6 +18,7 @@ service MapService {
|
|
|
18
18
|
rpc GetStat(GetStatRequest) returns (GetStatOut);
|
|
19
19
|
rpc GetRender(GetRenderRequest) returns (GetRenderOut);
|
|
20
20
|
rpc GetLogs(GetLogsRequest) returns (GetLogsOut);
|
|
21
|
+
rpc GetRasterInfo(GetRasterInfoRequest) returns (GetRasterInfoOut);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
// ----------------------------
|
|
@@ -114,6 +115,7 @@ message GetStatusOut {
|
|
|
114
115
|
|
|
115
116
|
message GetStatRequest {
|
|
116
117
|
string period = 1; // "5m", "1h", "1d"
|
|
118
|
+
string ttl = 2; // "0"
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
message GetStatOut {
|
|
@@ -138,6 +140,37 @@ message GetLogsOut {
|
|
|
138
140
|
repeated string result = 1;
|
|
139
141
|
}
|
|
140
142
|
|
|
143
|
+
message GetRasterInfoRequest {
|
|
144
|
+
string path = 1;
|
|
145
|
+
string ttl = 2;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
message FileItem {
|
|
149
|
+
string name = 1;
|
|
150
|
+
string size = 2;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
message GetRasterInfoData {
|
|
154
|
+
string total_size = 1;
|
|
155
|
+
string extension = 2;
|
|
156
|
+
int64 srid = 3;
|
|
157
|
+
string proj4 = 4;
|
|
158
|
+
repeated float extent = 5;
|
|
159
|
+
repeated FileItem files = 6;
|
|
160
|
+
int64 width = 7;
|
|
161
|
+
int64 height = 8;
|
|
162
|
+
int64 bands_count = 9;
|
|
163
|
+
string resolution = 10;
|
|
164
|
+
repeated string bands = 11;
|
|
165
|
+
int64 color_depth = 12;
|
|
166
|
+
string compression = 13;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
message GetRasterInfoOut {
|
|
170
|
+
GetRasterInfoData data = 1;
|
|
171
|
+
bool cache = 2;
|
|
172
|
+
}
|
|
173
|
+
|
|
141
174
|
// ----------------------------
|
|
142
175
|
// Job Service Messages
|
|
143
176
|
// ----------------------------
|
|
@@ -1,129 +1,6 @@
|
|
|
1
|
-
import carto from 'carto';
|
|
2
|
-
|
|
3
1
|
import { dataUpdate } from '@opengis/fastify-table/utils.js';
|
|
4
|
-
import mapnik from '../../../plugins/mapnik/funcs/mapnik.js';
|
|
5
|
-
|
|
6
|
-
const { UploadXML } = mapnik();
|
|
7
|
-
|
|
8
|
-
const srsList = {};
|
|
9
|
-
|
|
10
|
-
const getGeomColumn = (geoms, customColumn) => {
|
|
11
|
-
if (customColumn && geoms.includes(customColumn)) {
|
|
12
|
-
return customColumn;
|
|
13
|
-
}
|
|
14
|
-
if (geoms.includes('geom')) {
|
|
15
|
-
return 'geom';
|
|
16
|
-
}
|
|
17
|
-
return geoms[0];
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
async function cartoBounds({ id, dataset }, pg) {
|
|
21
|
-
const sqlBounds = dataset.map(el => ({ tbl: el.table.split('.'), geom: el.gcol || 'geom' })).map(({ tbl, geom }) => `select ST_EstimatedExtent('${tbl[0]}','${tbl[1]}', '${geom}')`);
|
|
22
|
-
|
|
23
|
-
const geom = await pg.query(
|
|
24
|
-
`update gis.style set
|
|
25
|
-
geom=(select st_extent(st_estimatedextent) from ( ${sqlBounds.join(' union all ')})q )
|
|
26
|
-
where style_id=$1 returning geom::box2d`,
|
|
27
|
-
[id],
|
|
28
|
-
).then(e => e.rows?.[0]?.geom);
|
|
29
2
|
|
|
30
|
-
|
|
31
|
-
? geom.replace(/[A-Z\)\(]+/g, '').split(/[ ,]+/).map(el => el - 0)
|
|
32
|
-
: null;
|
|
33
|
-
|
|
34
|
-
if (!bounds) {
|
|
35
|
-
throw new Error('empty bounds');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return bounds;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function cartoData(el, extent, pg) {
|
|
42
|
-
const minzoom = el.zoom ? (+el.zoom || 5) : 5;
|
|
43
|
-
const maxzoom = el.maxzoom ? (+el.maxzoom || 22) : 22;
|
|
44
|
-
const view = `(select ${el.columns || '1'}, ${el.gcol} from ${el.table} where ${el.query || 'true'} ) as data`;
|
|
45
|
-
const layerBound = extent || [-180, -85, 180, 85];
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
id: el.caption,
|
|
49
|
-
name: el.caption,
|
|
50
|
-
properties: { minzoom, maxzoom },
|
|
51
|
-
srs: el.proj4text || '+proj=longlat +datum=WGS84 +no_defs',
|
|
52
|
-
Datasource: {
|
|
53
|
-
simplify_geometries: true,
|
|
54
|
-
type: 'postgis',
|
|
55
|
-
extent: layerBound.join(','),
|
|
56
|
-
table: view,
|
|
57
|
-
geometry_field: el.gcol,
|
|
58
|
-
host: pg.options?.host || 'localhost',
|
|
59
|
-
dbname: pg.options?.database || 'postgres',
|
|
60
|
-
port: pg.options?.port || 5432,
|
|
61
|
-
user: pg.options?.user || 'postgres',
|
|
62
|
-
password: pg.options?.password || 'postgres',
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function uploadXML({ id, dataset, style }, pg) {
|
|
68
|
-
if (!id || !dataset?.length) {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
await Promise.all(dataset.filter((el) => el.active !== false).map(async el => {
|
|
72
|
-
const { table } = el;
|
|
73
|
-
|
|
74
|
-
const geoms = await pg.query(`SELECT json_agg(attname) as geoms FROM pg_attribute
|
|
75
|
-
where attrelid = to_regclass($1::text)
|
|
76
|
-
and attnum > 0
|
|
77
|
-
AND NOT attisdropped
|
|
78
|
-
and atttypid::regtype::text ='geometry'`, [table]).then(e => e.rows?.[0]?.geoms || []);
|
|
79
|
-
|
|
80
|
-
const gcol = getGeomColumn(geoms, el.gcol);
|
|
81
|
-
Object.assign(el, { gcol });
|
|
82
|
-
|
|
83
|
-
const srid = el.srid ? el.srid
|
|
84
|
-
: await pg.query(`select st_srid(${gcol}) as srid from ${table} where ${gcol} is not null limit 1`).then(e => e.rows[0]?.srid);
|
|
85
|
-
|
|
86
|
-
if (srid && !srsList[srid] && (srid - 0 > 0)) {
|
|
87
|
-
srsList[srid] = await pg.query(`SELECT proj4text from spatial_ref_sys where srid=$1`, [srid])
|
|
88
|
-
.then(e => e.rows[0]?.proj4text);
|
|
89
|
-
}
|
|
90
|
-
Object.assign(el, { gcol, proj4text: srsList[srid] });
|
|
91
|
-
}));
|
|
92
|
-
|
|
93
|
-
const bounds = await cartoBounds({ id, dataset }, pg);
|
|
94
|
-
|
|
95
|
-
const arrayLayers = dataset.filter((el) => el.active !== false).map((el) => cartoData(el, bounds, pg));
|
|
96
|
-
|
|
97
|
-
const configJson = {
|
|
98
|
-
format: 'png',
|
|
99
|
-
Layer: arrayLayers.reverse(),
|
|
100
|
-
srs: '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over',
|
|
101
|
-
bounds,
|
|
102
|
-
'maximum-extent': '-20037508.34,-20037508.34,20037508.34,20037508.34',
|
|
103
|
-
center: [0, 0, 2],
|
|
104
|
-
minzoom: 1,
|
|
105
|
-
maxzoom: 25,
|
|
106
|
-
Stylesheet: [{
|
|
107
|
-
id: 'carto_css_style',
|
|
108
|
-
data: style || '',
|
|
109
|
-
}],
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// generate
|
|
113
|
-
const res = new carto.Renderer().render(configJson);
|
|
114
|
-
const xmlFileText = typeof res === 'string' ? res : res?.data;
|
|
115
|
-
if (!xmlFileText) {
|
|
116
|
-
throw new Error('xml generation error');
|
|
117
|
-
}
|
|
118
|
-
const { status } = await UploadXML({
|
|
119
|
-
name: id,
|
|
120
|
-
xml: xmlFileText,
|
|
121
|
-
});
|
|
122
|
-
if (status !== 'success') {
|
|
123
|
-
throw new Error('xml upload error');
|
|
124
|
-
}
|
|
125
|
-
return xmlFileText;
|
|
126
|
-
}
|
|
3
|
+
import uploadXML from '../../mapnik/functions/uploadXML.js';
|
|
127
4
|
|
|
128
5
|
export default async function addCartocss(req, reply) {
|
|
129
6
|
const { pg, body, params } = req;
|
|
@@ -28,7 +28,7 @@ export default async function mapFormat(req, reply) {
|
|
|
28
28
|
const filteredColumns = columnNames.filter(name => !excluded.includes(name));
|
|
29
29
|
|
|
30
30
|
const rows = await pg.query(
|
|
31
|
-
`SELECT ${pk} as "id", geom::json ${filteredColumns?.length > 0 ? ',' : ''} ${filteredColumns.map((n) => `"${n}"`).join(', ')} FROM ${tname} WHERE ${pk} = $1`,
|
|
31
|
+
`SELECT ${pk} as "id", geom::json, geom::box2d as box2d ${filteredColumns?.length > 0 ? ',' : ''} ${filteredColumns.map((n) => `"${n}"`).join(', ')} FROM ${tname} WHERE ${pk} = $1`,
|
|
32
32
|
[id],
|
|
33
33
|
).then(el => el.rows || []);
|
|
34
34
|
|
|
@@ -38,6 +38,8 @@ export default async function mapFormat(req, reply) {
|
|
|
38
38
|
.filter(col => col.data && ['select', 'badge', 'tags'].includes(col.format))
|
|
39
39
|
.reduce((acc, curr) => ({ ...acc, [curr.name]: curr.data }), {});
|
|
40
40
|
|
|
41
|
+
rows.forEach(row => Object.assign(row, { box2d: undefined, bounds: row.box2d ? row.box2d.replace(/[A-Z\)\(]+/g, '').split(/[ ,]+/).map(el => el - 0) : null }));
|
|
42
|
+
|
|
41
43
|
await metaFormat({
|
|
42
44
|
rows, table: tname, cls: classifiers, sufix: false,
|
|
43
45
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { config, pgClients } from '@opengis/fastify-table/utils.js';
|
|
2
|
+
|
|
3
|
+
import mapnik from '../../../plugins/mapnik/funcs/mapnik.js';
|
|
4
|
+
import cartoBounds from '../functions/cartoBounds.js';
|
|
5
|
+
|
|
6
|
+
const { RenderTile } = mapnik();
|
|
7
|
+
|
|
8
|
+
const { prefix = '/api' } = config;
|
|
9
|
+
|
|
10
|
+
export default async function checkCarto({
|
|
11
|
+
pg = pgClients.client, params,
|
|
12
|
+
}, reply) {
|
|
13
|
+
if (!config.mapServerAddress) {
|
|
14
|
+
return reply.status(400).send({ error: 'mapnik server address needed', code: 400 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const cartocss = await pg.query('select config, geom::box2d from gis.cartocss where cartocss_id=$1', [params.id]).then(el => el.rows?.[0]);
|
|
18
|
+
|
|
19
|
+
if (!cartocss) {
|
|
20
|
+
return reply.status(404).send({ error: `cartocss not found: ${params.id}`, code: 404 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const rtile = await RenderTile({
|
|
24
|
+
name: params.id,
|
|
25
|
+
width: 256,
|
|
26
|
+
height: 256,
|
|
27
|
+
bbox: [3713463.7081504324, 6088362.176970857, 3713616.5822070027, 6088515.051027427],
|
|
28
|
+
ttl: '0',
|
|
29
|
+
// debug: query.debug,
|
|
30
|
+
}).catch(err => ({ err: err.toString() }));
|
|
31
|
+
|
|
32
|
+
const bounds = cartocss.geom || await cartoBounds({ id: params.id, dataset: cartocss.config }, pg);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
url: `${prefix}/gis-rtile/${params.id}/{z}/{x}/{y}.png`,
|
|
36
|
+
bounds,
|
|
37
|
+
render: !!rtile.base64,
|
|
38
|
+
renderError: !!rtile.err,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { config } from '@opengis/fastify-table/utils.js';
|
|
2
|
+
|
|
3
|
+
import mapnik from '../../../plugins/mapnik/funcs/mapnik.js';
|
|
4
|
+
|
|
5
|
+
const { GetLogs } = mapnik();
|
|
6
|
+
|
|
7
|
+
export default async function mapnikLogger({
|
|
8
|
+
params, query,
|
|
9
|
+
}, reply) {
|
|
10
|
+
if (!config.mapServerAddress) {
|
|
11
|
+
return reply.status(400).send({ error: 'mapnik server address needed', code: 400 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!['render', 'error', 'info', 'critical'].includes(params.type)) {
|
|
15
|
+
return reply.status(400).send({ error: 'invalid params: type', code: 400 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { result } = await GetLogs({
|
|
19
|
+
period: query.period,
|
|
20
|
+
type: params.type,
|
|
21
|
+
});
|
|
22
|
+
return reply.status(200).send(result.join('\n'));
|
|
23
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { config } from '@opengis/fastify-table/utils.js';
|
|
2
|
+
|
|
3
|
+
import mapnik from '../../../plugins/mapnik/funcs/mapnik.js';
|
|
4
|
+
|
|
5
|
+
const { GetStat } = mapnik();
|
|
6
|
+
|
|
7
|
+
export default async function mapnikStat({
|
|
8
|
+
params, query,
|
|
9
|
+
}, reply) {
|
|
10
|
+
if (!config.mapServerAddress) {
|
|
11
|
+
return reply.status(400).send({ error: 'mapnik server address needed', code: 400 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const stat = await GetStat({
|
|
15
|
+
period: params.period,
|
|
16
|
+
ttl: query.nocache ? '0' : undefined,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return reply.status(200).send(stat);
|
|
20
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { config, pgClients } from '@opengis/fastify-table/utils.js';
|
|
2
|
+
|
|
3
|
+
import mapnik from '../../../plugins/mapnik/funcs/mapnik.js';
|
|
4
|
+
|
|
5
|
+
const { GetRasterInfo } = mapnik();
|
|
6
|
+
|
|
7
|
+
const { prefix = '/api' } = config;
|
|
8
|
+
|
|
9
|
+
export default async function rtile({
|
|
10
|
+
pg = pgClients.client, params, query,
|
|
11
|
+
}, reply) {
|
|
12
|
+
if (!config.mapServerAddress) {
|
|
13
|
+
return reply.status(400).send({ error: 'mapnik server address needed', code: 400 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const raster = pg.pk?.['gis.rasters']
|
|
17
|
+
? await pg.query('select raster_id, source_path from gis.rasters where raster_id=$1::text', [params.id])
|
|
18
|
+
.then(el => el.rows?.[0] || {})
|
|
19
|
+
: {};
|
|
20
|
+
|
|
21
|
+
if (raster.raster_id && !raster.source_path) {
|
|
22
|
+
return reply.status(400).send({ error: 'empty source_path', code: 400 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!raster.raster_id) {
|
|
26
|
+
const decodedPath = Buffer.from(params.id, 'base64url').toString('utf-8');
|
|
27
|
+
Object.assign(raster, { source_path: decodedPath });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const data = await GetRasterInfo({
|
|
31
|
+
path: raster.source_path,
|
|
32
|
+
ttl: query.nocache ? '0' : '1h',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return { ...data, url: `${prefix}/gis-rtile/${Buffer.from(raster.source_path).toString('base64url')}/{z}/{x}/{y}.png` };
|
|
36
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import Sphericalmercator from '@mapbox/sphericalmercator';
|
|
3
2
|
|
|
4
3
|
import { config, logger, pgClients } from '@opengis/fastify-table/utils.js';
|
|
5
4
|
|
|
6
5
|
import mapnik from '../../../plugins/mapnik/funcs/mapnik.js';
|
|
7
6
|
|
|
8
|
-
const { RenderTile
|
|
7
|
+
const { RenderTile } = mapnik();
|
|
9
8
|
|
|
10
9
|
const mercator = new Sphericalmercator({ size: 256 });
|
|
11
10
|
|
|
@@ -27,7 +26,7 @@ const mercator = new Sphericalmercator({ size: 256 });
|
|
|
27
26
|
export default async function rtile({
|
|
28
27
|
pg = pgClients.client, params, query,
|
|
29
28
|
}, reply) {
|
|
30
|
-
if (!RenderTile
|
|
29
|
+
if (!RenderTile) {
|
|
31
30
|
return reply.status(400).send({ error: 'mapnik server address needed', code: 400 });
|
|
32
31
|
}
|
|
33
32
|
|
|
@@ -57,19 +56,10 @@ export default async function rtile({
|
|
|
57
56
|
|
|
58
57
|
if (!raster.raster_id && !cartoExists) {
|
|
59
58
|
const decodedPath = Buffer.from(id, 'base64url').toString('utf-8');
|
|
60
|
-
const uploadStatus = path.extname(decodedPath) ? await GetRasterStatus({ path: decodedPath }) : {};
|
|
61
|
-
|
|
62
|
-
if (uploadStatus.exists === false) {
|
|
63
|
-
return reply.status(400).send({ error: 'raster not uploaded', code: 400 });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (uploadStatus.finished === false) {
|
|
67
|
-
return reply.status(400).send({ error: 'upload not finished', code: 400 });
|
|
68
|
-
}
|
|
69
59
|
Object.assign(raster, { source_path: decodedPath });
|
|
70
60
|
}
|
|
71
61
|
|
|
72
|
-
if (!raster && !cartoExists) {
|
|
62
|
+
if (!raster.source_path && !cartoExists) {
|
|
73
63
|
return reply.status(404).send({ error: 'raster / cartocss not found', code: 404 });
|
|
74
64
|
}
|
|
75
65
|
|
|
@@ -97,7 +87,7 @@ export default async function rtile({
|
|
|
97
87
|
|
|
98
88
|
const buffer = Buffer.from(data.base64, 'base64');
|
|
99
89
|
|
|
100
|
-
return reply.headers({ 'Content-Type': 'image/png' }).send(buffer);
|
|
90
|
+
return reply.headers({ 'Content-Type': 'image/png', 'Cache-Control': query.nocache ? 'no-cache' : '30d' }).send(buffer);
|
|
101
91
|
}
|
|
102
92
|
catch (err) {
|
|
103
93
|
logger.file('rtile/error', { error: err.toString(), stack: err.stack });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { pgClients } from "@opengis/fastify-table/utils.js";
|
|
2
|
+
|
|
3
|
+
export default async function cartoBounds({ id, dataset }, pg = pgClients.client) {
|
|
4
|
+
const sqlBounds = dataset.map(el => ({ tbl: el.table.split('.'), geom: el.gcol || 'geom' })).map(({ tbl, geom }) => `select ST_EstimatedExtent('${tbl[0]}','${tbl[1]}', '${geom}')`);
|
|
5
|
+
|
|
6
|
+
const geom = await pg.query(
|
|
7
|
+
`update gis.cartocss set
|
|
8
|
+
geom=(select st_extent(st_estimatedextent) from ( ${sqlBounds.join(' union all ')})q )
|
|
9
|
+
where cartocss_id=$1 returning geom::box2d`,
|
|
10
|
+
[id],
|
|
11
|
+
).then(e => e.rows?.[0]?.geom);
|
|
12
|
+
|
|
13
|
+
const bounds = geom
|
|
14
|
+
? geom.replace(/[A-Z\)\(]+/g, '').split(/[ ,]+/).map(el => el - 0)
|
|
15
|
+
: null;
|
|
16
|
+
|
|
17
|
+
if (!bounds) {
|
|
18
|
+
// throw new Error('empty bounds');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return bounds || [-180, -85.05112877980659, 180, 85.05112877980659];
|
|
22
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import carto from 'carto';
|
|
2
|
+
|
|
3
|
+
import mapnik from '../../../plugins/mapnik/funcs/mapnik.js';
|
|
4
|
+
|
|
5
|
+
const { UploadXML } = mapnik();
|
|
6
|
+
|
|
7
|
+
const srsList = {};
|
|
8
|
+
|
|
9
|
+
const getGeomColumn = (geoms, customColumn) => {
|
|
10
|
+
if (customColumn && geoms.includes(customColumn)) {
|
|
11
|
+
return customColumn;
|
|
12
|
+
}
|
|
13
|
+
if (geoms.includes('geom')) {
|
|
14
|
+
return 'geom';
|
|
15
|
+
}
|
|
16
|
+
return geoms[0];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
import cartoBounds from './cartoBounds.js';
|
|
20
|
+
|
|
21
|
+
function cartoData(el, extent, pg) {
|
|
22
|
+
const minzoom = el.zoom ? (+el.zoom || 5) : 5;
|
|
23
|
+
const maxzoom = el.maxzoom ? (+el.maxzoom || 22) : 22;
|
|
24
|
+
const view = `(select ${el.columns || '1'}, ${el.gcol} from ${el.table} where ${el.query || 'true'} ) as data`;
|
|
25
|
+
const layerBound = extent || [-180, -85, 180, 85];
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
id: el.caption,
|
|
29
|
+
name: el.caption,
|
|
30
|
+
properties: { minzoom, maxzoom },
|
|
31
|
+
srs: el.proj4text || '+proj=longlat +datum=WGS84 +no_defs',
|
|
32
|
+
Datasource: {
|
|
33
|
+
simplify_geometries: true,
|
|
34
|
+
type: 'postgis',
|
|
35
|
+
extent: layerBound.join(','),
|
|
36
|
+
table: view,
|
|
37
|
+
geometry_field: el.gcol,
|
|
38
|
+
host: pg.options?.host || 'localhost',
|
|
39
|
+
dbname: pg.options?.database || 'postgres',
|
|
40
|
+
port: pg.options?.port || 5432,
|
|
41
|
+
user: pg.options?.user || 'postgres',
|
|
42
|
+
password: pg.options?.password || 'postgres',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default async function uploadXML({ id, dataset, style }, pg) {
|
|
48
|
+
if (!id || !dataset?.length || Array.isArray(dataset) === false) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
await Promise.all(dataset.filter((el) => el.active !== false).map(async el => {
|
|
52
|
+
const { table } = el;
|
|
53
|
+
|
|
54
|
+
const geoms = await pg.query(`SELECT json_agg(attname) as geoms FROM pg_attribute
|
|
55
|
+
where attrelid = to_regclass($1::text)
|
|
56
|
+
and attnum > 0
|
|
57
|
+
AND NOT attisdropped
|
|
58
|
+
and atttypid::regtype::text ='geometry'`, [table]).then(e => e.rows?.[0]?.geoms || []);
|
|
59
|
+
|
|
60
|
+
const gcol = getGeomColumn(geoms, el.gcol);
|
|
61
|
+
Object.assign(el, { gcol });
|
|
62
|
+
|
|
63
|
+
const srid = el.srid ? el.srid
|
|
64
|
+
: await pg.query(`select st_srid(${gcol}) as srid from ${table} where ${gcol} is not null limit 1`).then(e => e.rows[0]?.srid);
|
|
65
|
+
|
|
66
|
+
if (srid && !srsList[srid] && (srid - 0 > 0)) {
|
|
67
|
+
srsList[srid] = await pg.query(`SELECT proj4text from spatial_ref_sys where srid=$1`, [srid])
|
|
68
|
+
.then(e => e.rows[0]?.proj4text);
|
|
69
|
+
}
|
|
70
|
+
Object.assign(el, { gcol, proj4text: srsList[srid] });
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
const bounds = await cartoBounds({ id, dataset }, pg);
|
|
74
|
+
|
|
75
|
+
const arrayLayers = dataset.filter((el) => el.active !== false).map((el) => cartoData(el, bounds, pg));
|
|
76
|
+
|
|
77
|
+
const configJson = {
|
|
78
|
+
format: 'png',
|
|
79
|
+
Layer: arrayLayers.reverse(),
|
|
80
|
+
srs: '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over',
|
|
81
|
+
bounds,
|
|
82
|
+
'maximum-extent': '-20037508.34,-20037508.34,20037508.34,20037508.34',
|
|
83
|
+
center: [0, 0, 2],
|
|
84
|
+
minzoom: 1,
|
|
85
|
+
maxzoom: 25,
|
|
86
|
+
Stylesheet: [{
|
|
87
|
+
id: 'carto_css_style',
|
|
88
|
+
data: style || '',
|
|
89
|
+
}],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// generate
|
|
93
|
+
const res = new carto.Renderer().render(configJson);
|
|
94
|
+
const xmlFileText = typeof res === 'string' ? res : res?.data;
|
|
95
|
+
if (!xmlFileText) {
|
|
96
|
+
throw new Error('xml generation error');
|
|
97
|
+
}
|
|
98
|
+
const { status } = await UploadXML({
|
|
99
|
+
name: id,
|
|
100
|
+
xml: xmlFileText,
|
|
101
|
+
});
|
|
102
|
+
if (status !== 'success') {
|
|
103
|
+
throw new Error('xml upload error');
|
|
104
|
+
}
|
|
105
|
+
return xmlFileText;
|
|
106
|
+
}
|