@opengis/gis 0.2.44 → 0.2.46
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 +7874 -7072
- package/dist/index.umd.cjs +73 -53
- package/module/gis/table/gis.cartocss.table.json +3 -2
- package/module/gis/table/gis.maps.table.json +10 -2
- package/module/gis/table/gis.ogc_service.table.json +1 -1
- package/module/gis/table/gis.rasters.table.json +3 -2
- package/module/gis/table/gis.registers.table.json +1 -0
- package/module/gis/table/gis.services.table.json +1 -0
- package/package.json +75 -75
- package/plugin.js +1 -0
- package/server/plugins/mapnik/funcs/mapnik.js +83 -78
- package/server/plugins/mapnik/map.proto +151 -0
- package/server/routes/gis/cartocss/add.cartocss.js +138 -4
- package/server/routes/map/index.mjs +4 -4
- package/server/routes/map/maps/get.map.js +1 -1
- package/server/routes/mapnik/controllers/createXML.js +61 -0
- package/server/routes/mapnik/controllers/rtile.js +86 -0
- package/server/routes/mapnik/controllers/uploadRaster.js +159 -0
- package/server/routes/mapnik/index.js +16 -0
- package/server/plugins/mapnik/funcs/createXML.js +0 -72
- package/server/plugins/mapnik/funcs/gdalWrapper.js +0 -72
- package/server/plugins/mapnik/funcs/map.proto +0 -241
- package/server/plugins/mapnik/funcs/rasterConfig.js +0 -11
- package/server/plugins/mapnik/funcs/rasterExists.js +0 -21
- package/server/plugins/mapnik/funcs/rasterInfo.js +0 -109
- package/server/plugins/mapnik/funcs/rasterVrt.js +0 -56
- package/server/plugins/mapnik/funcs/rasterXML.js +0 -65
- package/server/plugins/mapnik/utils/map.proto +0 -241
- package/server/routes/map/controllers/rtile.js +0 -134
|
@@ -1,18 +1,152 @@
|
|
|
1
|
+
import carto from 'carto';
|
|
2
|
+
|
|
3
|
+
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
|
+
|
|
30
|
+
const bounds = geom
|
|
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
|
+
}
|
|
127
|
+
|
|
1
128
|
export default async function addCartocss(req, reply) {
|
|
2
129
|
const { pg, body, params } = req;
|
|
3
130
|
|
|
4
131
|
const { id } = params;
|
|
5
132
|
if (!id) return reply.status(400).send({ message: 'id is empty', status: 400 });
|
|
6
133
|
|
|
7
|
-
const { config, style } = body;
|
|
8
|
-
if (!
|
|
134
|
+
const { config: dataset, style } = body;
|
|
135
|
+
if (!dataset && !style) return reply.status(400).send({ message: 'config, style is empty', status: 400 });
|
|
9
136
|
|
|
10
137
|
const selectCartocss = `select cartocss_id from gis.cartocss where cartocss_id=$1`;
|
|
11
138
|
const cartocss = await pg.one(selectCartocss, [id]);
|
|
12
139
|
if (!cartocss) return reply.status(404).send({ message: 'cartocss not found', status: 404 });
|
|
13
140
|
|
|
14
|
-
|
|
15
|
-
|
|
141
|
+
await dataUpdate({
|
|
142
|
+
pg,
|
|
143
|
+
id,
|
|
144
|
+
table: 'gis.cartocss',
|
|
145
|
+
data: { config: dataset, style },
|
|
146
|
+
uid: req.user?.uid,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await uploadXML({ id, dataset, style }, pg);
|
|
16
150
|
|
|
17
151
|
return reply.status(200).send({ message: 'cartocss updated', status: 200 });
|
|
18
152
|
}
|
|
@@ -4,7 +4,7 @@ import mapFeatures from './controllers/mapFeatures.js';
|
|
|
4
4
|
|
|
5
5
|
import mapCatalog from './controllers/mapCatalog.js';
|
|
6
6
|
import mapCatalogAttribute from './controllers/mapCatalogAttribute.js';
|
|
7
|
-
import rtile from './controllers/rtile.js';
|
|
7
|
+
// import rtile from './controllers/rtile.js';
|
|
8
8
|
import vtile from './controllers/vtile.js';
|
|
9
9
|
import vtile1 from './vtile1.js';
|
|
10
10
|
import geojson from './controllers/geojson.js';
|
|
@@ -99,9 +99,9 @@ export default async function route(app) {
|
|
|
99
99
|
if (!app.hasRoute({ method: 'GET', url: '/map-catalog/:service/:attr' })) {
|
|
100
100
|
app.get('/map-catalog/:service/:attr', publicParams, mapCatalogAttribute);
|
|
101
101
|
}
|
|
102
|
-
if (!app.hasRoute({ method: 'GET', url: '/layer-rtile/:id/:z/:y/:x' })) {
|
|
103
|
-
|
|
104
|
-
}
|
|
102
|
+
// if (!app.hasRoute({ method: 'GET', url: '/layer-rtile/:id/:z/:y/:x' })) {
|
|
103
|
+
// app.get('/layer-rtile/:id/:z/:y/:x', publicParams, rtile);
|
|
104
|
+
// }
|
|
105
105
|
if (!app.hasRoute({ method: 'GET', url: '/layer-vtile/:id/:z/:y/:x' })) {
|
|
106
106
|
app.get('/layer-vtile/:id/:z/:y/:x', publicParams, vtile);
|
|
107
107
|
}
|
|
@@ -32,7 +32,7 @@ export default async function getMap({ params = {}, pg = pgClients.client }, rep
|
|
|
32
32
|
const totals = pg.queryCache ? await pg.queryCache('select json_object_agg(oid::regclass, reltuples) from pg_class')
|
|
33
33
|
.then(el => el.rows?.[0]?.json_object_agg || {}) : {};
|
|
34
34
|
const maps = pg.pk?.['gis.maps'] ? await pg.query(
|
|
35
|
-
'SELECT map_id as id, map_key as slug, name
|
|
35
|
+
'SELECT map_id as id, map_key as slug, name FROM gis.maps where is_active and is_public order by name',
|
|
36
36
|
).then(el => el.rows || []) : [];
|
|
37
37
|
|
|
38
38
|
if (params.id) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { pgClients, eventStream } from '@opengis/fastify-table/utils.js';
|
|
4
|
+
|
|
5
|
+
import mapnik from '../../../plugins/mapnik/funcs/mapnik.js';
|
|
6
|
+
|
|
7
|
+
const { GetRasterStatus, CreateXML } = mapnik();
|
|
8
|
+
|
|
9
|
+
export default async function createXml({
|
|
10
|
+
pg = pgClients.client, params, query,
|
|
11
|
+
}, reply) {
|
|
12
|
+
if (!GetRasterStatus || !CreateXML) {
|
|
13
|
+
return reply.status(400).send({ error: 'mapnik server address needed', code: 400 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { id } = params;
|
|
17
|
+
|
|
18
|
+
if (!id) {
|
|
19
|
+
return reply.status(400).send({ error: 'not enough params: id', code: 400 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const data = pg.pk?.['gis.rasters']
|
|
23
|
+
? await pg.query('select raster_id as id, source_path, srid, raster_zoom from gis.rasters where raster_id=$1::text', [id])
|
|
24
|
+
.then(el => el.rows?.[0])
|
|
25
|
+
: null;
|
|
26
|
+
|
|
27
|
+
if (!data?.id) {
|
|
28
|
+
return reply.status(404).send({ error: 'raster not found', code: 404 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!data.source_path) {
|
|
32
|
+
return reply.status(400).send({ error: 'raster source_path not set', code: 400 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const relpath = `/map/raster/${data.source_path}`;
|
|
36
|
+
|
|
37
|
+
// check raster upload status, skip for directories
|
|
38
|
+
const uploadStatus = path.extname(relpath) ? await GetRasterStatus({ path: relpath }) : {};
|
|
39
|
+
|
|
40
|
+
if (uploadStatus.exists === false) {
|
|
41
|
+
return reply.status(400).send({ error: 'raster not uploaded', code: 400 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (uploadStatus.finished === false) {
|
|
45
|
+
return reply.status(400).send({ error: 'upload not finished', code: 400 });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const callback = eventStream(reply);
|
|
49
|
+
|
|
50
|
+
// create at xml/:id with name only, always - by path
|
|
51
|
+
const resp = await CreateXML({
|
|
52
|
+
path: data.source_path,
|
|
53
|
+
name: id,
|
|
54
|
+
previewZoom: data.raster_zoom,
|
|
55
|
+
srid: data.srid,
|
|
56
|
+
vrt: query.vrt === '1' /* || !data.source_path.toLowerCase().endsWith('.tif') */,
|
|
57
|
+
}, callback);
|
|
58
|
+
|
|
59
|
+
callback('finish', 1);
|
|
60
|
+
return reply.status(200).send(resp);
|
|
61
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import Sphericalmercator from '@mapbox/sphericalmercator';
|
|
2
|
+
|
|
3
|
+
import { config, logger, pgClients } from '@opengis/fastify-table/utils.js';
|
|
4
|
+
|
|
5
|
+
import mapnik from '../../../plugins/mapnik/funcs/mapnik.js';
|
|
6
|
+
|
|
7
|
+
const { RenderTile } = mapnik();
|
|
8
|
+
|
|
9
|
+
const mercator = new Sphericalmercator({ size: 256 });
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Формування растрового tile cartoCss
|
|
13
|
+
*
|
|
14
|
+
* @method GET
|
|
15
|
+
* @alias rtile
|
|
16
|
+
* @param {String} bbox - bbox
|
|
17
|
+
* @param {Number} height - висота по координатам
|
|
18
|
+
* @param {Number} width - ширина по координатам
|
|
19
|
+
* @param {String} data - стилізація
|
|
20
|
+
* @param {String} lang - мова
|
|
21
|
+
* @param {String} z - координата z
|
|
22
|
+
* @param {String} x - координата y
|
|
23
|
+
* @param {String} y - координата x
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export default async function rtile({
|
|
27
|
+
pg = pgClients.client, params, query,
|
|
28
|
+
}, reply) {
|
|
29
|
+
if (!RenderTile) {
|
|
30
|
+
return reply.status(400).send({ error: 'mapnik server address needed', code: 400 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { id, z, y } = params;
|
|
34
|
+
|
|
35
|
+
const x = params.x.split('.')[0] - 0;
|
|
36
|
+
|
|
37
|
+
if (!id) {
|
|
38
|
+
return reply.status(400).send({ error: 'not enough params: id', code: 400 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!x || !y || !z) {
|
|
42
|
+
return reply.status(400).send({ error: 'not enough params: xyz', code: 400 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const raster = pg.pk?.['gis.rasters']
|
|
46
|
+
? await pg.query('select raster_id, source_path from gis.rasters where raster_id=$1::text', [id])
|
|
47
|
+
.then(el => el.rows?.[0])
|
|
48
|
+
: null;
|
|
49
|
+
|
|
50
|
+
// const relpath = raster ? `/map/raster/${raster.source_path}` : null;
|
|
51
|
+
|
|
52
|
+
const cartoExists = pg.pk?.['gis.cartocss']
|
|
53
|
+
? await pg.query('select cartocss_id from gis.cartocss where cartocss_id=$1::text', [id])
|
|
54
|
+
.then(el => el.rows?.[0]?.cartocss_id)
|
|
55
|
+
: null;
|
|
56
|
+
|
|
57
|
+
if (!raster && !cartoExists) {
|
|
58
|
+
return reply.status(404).send({ error: 'raster / cartocss not found', code: 404 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const bbox = mercator.bbox(y, x, z, false, '900913');
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const data = await RenderTile({
|
|
65
|
+
path: raster ? raster.source_path : null,
|
|
66
|
+
name: id,
|
|
67
|
+
width: 256,
|
|
68
|
+
height: 256,
|
|
69
|
+
bbox,
|
|
70
|
+
ttl: query.nocache ? '0' : '1h',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (data.err) {
|
|
74
|
+
logger.file('rtile/error', { error: data.err });
|
|
75
|
+
return reply.status(500).send({ error: config.local ? data.err : 'render error', code: 500 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const buffer = Buffer.from(data.base64, 'base64');
|
|
79
|
+
|
|
80
|
+
return reply.headers({ 'Content-Type': 'image/png' }).send(buffer);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
logger.file('rtile/error', { error: err.toString(), stack: err.stack });
|
|
84
|
+
return reply.status(500).send({ error: config.local ? err.toString() : 'rtile error', code: 500 });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ListObjectsV2Command } from '@aws-sdk/client-s3';
|
|
3
|
+
|
|
4
|
+
/* eslint-disable no-await-in-loop */
|
|
5
|
+
import {
|
|
6
|
+
config, isFileExists, logger, pgClients, eventStream, downloadFile, s3Client,
|
|
7
|
+
} from '@opengis/fastify-table/utils.js';
|
|
8
|
+
|
|
9
|
+
import mapnik from '../../../plugins/mapnik/funcs/mapnik.js';
|
|
10
|
+
|
|
11
|
+
const { UploadRaster, GetRasterStatus } = mapnik();
|
|
12
|
+
|
|
13
|
+
const CHUNK_SIZE = 1024 * 1024; // 1 MB per chunk
|
|
14
|
+
|
|
15
|
+
function sequence(files, data, fn) {
|
|
16
|
+
return files.reduce(
|
|
17
|
+
(promise, relpath) => promise.then(() => fn({
|
|
18
|
+
...data,
|
|
19
|
+
relpath, // allow upload multiple to one directory for vrt
|
|
20
|
+
})),
|
|
21
|
+
Promise.resolve(),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function uploadRasterFile({
|
|
26
|
+
id, prefix, relpath: relpathOriginal, callback = () => { },
|
|
27
|
+
}) {
|
|
28
|
+
const name = path.basename(relpathOriginal);
|
|
29
|
+
const metadata = await isFileExists(relpathOriginal);
|
|
30
|
+
const relpath = relpathOriginal.replace(config.folder, '').replace(prefix, '');
|
|
31
|
+
|
|
32
|
+
if (!metadata || !metadata.ContentLength) {
|
|
33
|
+
callback(`File not found at s3: ${relpath}`);
|
|
34
|
+
return { error: `File not found at s3: ${relpath}`, code: 404 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let offset = 0;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const uploadStatus = await GetRasterStatus({ path: relpath, md5: true });
|
|
41
|
+
|
|
42
|
+
offset = uploadStatus.exists ? +uploadStatus.size : 0;
|
|
43
|
+
|
|
44
|
+
if (offset === metadata.ContentLength) {
|
|
45
|
+
callback(`Already uploaded: ${relpath}`);
|
|
46
|
+
return { message: 'already uploaded', status: 200 };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
while (offset < metadata.ContentLength) {
|
|
50
|
+
const end = Math.min(offset + CHUNK_SIZE - 1, metadata.ContentLength - 1);
|
|
51
|
+
const Range = `bytes=${offset}-${end}`;
|
|
52
|
+
|
|
53
|
+
callback(`Uploading chunk ${(end / 1024 / 1024).toFixed(0)}/${(metadata.ContentLength / 1024 / 1024).toFixed(0)} MB...`);
|
|
54
|
+
|
|
55
|
+
const chunk = await downloadFile(relpathOriginal, { Range, fallback: false });
|
|
56
|
+
// const { Body: chunk } = await s3Client.send(new GetObjectCommand({ Bucket, Key, Range }));
|
|
57
|
+
const bufferPart = chunk ? await chunk.transformToByteArray() : [];
|
|
58
|
+
|
|
59
|
+
if (!bufferPart.length) {
|
|
60
|
+
callback(`Server error: file not found / zero bytes: ${relpath}`);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = await UploadRaster({
|
|
65
|
+
path: relpath,
|
|
66
|
+
filesize: metadata.ContentLength,
|
|
67
|
+
data: bufferPart,
|
|
68
|
+
chunked: true,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (result.finished) {
|
|
72
|
+
callback(`Upload completed: ${relpath}`);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!result.uploaded) {
|
|
77
|
+
callback(`Chunked upload error: ${relpath}`);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
offset = +result.uploaded;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
logger.file('mapnik/upload/error', { error: err.toString(), stack: err.stack });
|
|
88
|
+
callback(config.local ? err.toString() : `Upload error: ${relpath}`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default async function uploadRaster({
|
|
94
|
+
pg = pgClients.client, params,
|
|
95
|
+
}, reply) {
|
|
96
|
+
if (!UploadRaster || !GetRasterStatus) {
|
|
97
|
+
return reply.status(400).send({ error: 'mapnik server address needed', code: 400 });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { id } = params;
|
|
101
|
+
|
|
102
|
+
if (!id) {
|
|
103
|
+
return reply.status(400).send({ error: 'not enough params: id', code: 400 });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const data = pg.pk?.['gis.rasters']
|
|
107
|
+
? await pg.query('select raster_id as id, source_path from gis.rasters where raster_id=$1::text', [id])
|
|
108
|
+
.then(el => el.rows?.[0])
|
|
109
|
+
: null;
|
|
110
|
+
|
|
111
|
+
if (!data?.id) {
|
|
112
|
+
return reply.status(404).send({ error: 'raster not found', code: 404 });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!data.source_path) {
|
|
116
|
+
return reply.status(400).send({ error: 'raster source_path not set', code: 400 });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const prefix = '/map/raster';
|
|
120
|
+
const relpath = `${prefix}/${data.source_path}`;
|
|
121
|
+
|
|
122
|
+
if (!path.extname(relpath)) {
|
|
123
|
+
const metadata = await s3Client.send(new ListObjectsV2Command({
|
|
124
|
+
Bucket: config.s3?.containerName || 'work',
|
|
125
|
+
Prefix: path.join(config.folder, relpath).replace(/\\/g, '/'),
|
|
126
|
+
}));
|
|
127
|
+
if (!metadata.Contents?.length) {
|
|
128
|
+
return reply.status(404).send({
|
|
129
|
+
error: 'invalid: raster source_path: not found / empty directory',
|
|
130
|
+
code: 404,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// list rasters, skip preview subdir content, skip folders from s3 directory name of which only starts with same characters, but does not exactly match
|
|
135
|
+
const files = metadata.Contents.map(file => file.Key).filter((file) => path.basename(path.dirname(file)) === path.basename(relpath) && !path.basename(path.dirname(file)).startsWith('preview') && path.extname(file).toLowerCase() === '.tif');
|
|
136
|
+
|
|
137
|
+
if (!files.length) {
|
|
138
|
+
return reply.status(400).send({
|
|
139
|
+
error: 'invalid: raster source_path: no raster files at directory',
|
|
140
|
+
code: 400,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const callback = eventStream(reply);
|
|
144
|
+
await sequence(files, { id, prefix, callback }, uploadRasterFile);
|
|
145
|
+
return callback('finish', 1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const metadata = await isFileExists(relpath);
|
|
149
|
+
|
|
150
|
+
if (!metadata || !metadata.ContentLength) {
|
|
151
|
+
return reply.status(404).send({ error: `File not found at s3: ${data.source_path}`, code: 404 });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const callback = eventStream(reply);
|
|
155
|
+
await uploadRasterFile({
|
|
156
|
+
id, prefix, callback, relpath,
|
|
157
|
+
});
|
|
158
|
+
return callback('finish', 1);
|
|
159
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import uploadRaster from './controllers/uploadRaster.js';
|
|
2
|
+
import createXML from './controllers/createXML.js';
|
|
3
|
+
import rtile from './controllers/rtile.js';
|
|
4
|
+
|
|
5
|
+
const publicParams = { config: { policy: 'L0' }, package: 'gis' }; // L0 === public
|
|
6
|
+
const params = { config: { policy: 'L1' }, package: 'gis' }; // L1 === authorized only
|
|
7
|
+
|
|
8
|
+
export default async function route(app) {
|
|
9
|
+
app.get('/raster-upload/:id', params, uploadRaster);
|
|
10
|
+
app.get('/raster-xml/:id', publicParams, createXML);
|
|
11
|
+
app.get('/raster-tile/:id/:z/:y/:x', publicParams, rtile);
|
|
12
|
+
// temporary support of old alias
|
|
13
|
+
if (!app.hasRoute({ method: 'GET', url: '/layer-rtile/:id/:z/:y/:x' })) {
|
|
14
|
+
app.get('/layer-rtile/:id/:z/:y/:x', publicParams, rtile);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import carto from 'carto';
|
|
2
|
-
|
|
3
|
-
import rasterExists from './rasterExists.js';
|
|
4
|
-
import rasterConfig from './rasterConfig.js';
|
|
5
|
-
|
|
6
|
-
export default async function createXML({
|
|
7
|
-
fullPath, srs, extent: bounds, send, size
|
|
8
|
-
}) {
|
|
9
|
-
|
|
10
|
-
const configFilePath = fullPath.replace('/preview1', '').replace('mosaic.vrt', 'config.yml')
|
|
11
|
-
const configData = await rasterConfig(configFilePath);
|
|
12
|
-
|
|
13
|
-
const rasterPreview = await rasterExists(fullPath, 1);
|
|
14
|
-
|
|
15
|
-
if (rasterPreview) {
|
|
16
|
-
send('preview:' + rasterPreview);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const rasterZoom = configData?.previewZoom ?? (rasterPreview && size?.includes('G') ? 16 : 0);
|
|
20
|
-
|
|
21
|
-
const rasterScaling = 'bilinear';
|
|
22
|
-
|
|
23
|
-
const Layer = [].concat([
|
|
24
|
-
{
|
|
25
|
-
id: 'test',
|
|
26
|
-
name: 'test',
|
|
27
|
-
geometry: 'raster',
|
|
28
|
-
properties: { minzoom: +rasterZoom + 1 },
|
|
29
|
-
srs: srs || '+proj=longlat +datum=WGS84 +no_defs ',
|
|
30
|
-
Datasource: {
|
|
31
|
-
// extent: bounds.join(','),
|
|
32
|
-
file: fullPath,
|
|
33
|
-
type: 'gdal',
|
|
34
|
-
},
|
|
35
|
-
}]).concat(
|
|
36
|
-
rasterZoom && rasterPreview ? [{
|
|
37
|
-
id: 'testpreview',
|
|
38
|
-
name: 'testpreview',
|
|
39
|
-
geometry: 'raster',
|
|
40
|
-
properties: { maxzoom: +rasterZoom },
|
|
41
|
-
srs: srs || '+proj=longlat +datum=WGS84 +no_defs ',
|
|
42
|
-
Datasource: {
|
|
43
|
-
// extent: bounds.join(','),
|
|
44
|
-
file: rasterPreview,
|
|
45
|
-
type: 'gdal',
|
|
46
|
-
},
|
|
47
|
-
}]
|
|
48
|
-
: [],
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const data = `#testpreview{raster-opacity:1; raster-scaling:${rasterScaling};} #test{raster-opacity:1;raster-scaling:${rasterScaling}; } `;
|
|
52
|
-
|
|
53
|
-
const configJson = {
|
|
54
|
-
format: 'png',
|
|
55
|
-
Layer,
|
|
56
|
-
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',
|
|
57
|
-
bounds: bounds || [-180, -85.05112877980659, 180, 85.05112877980659],
|
|
58
|
-
'maximum-extent': '-20037508.34,-20037508.34,20037508.34,20037508.34',
|
|
59
|
-
center: [0, 0, 2],
|
|
60
|
-
minzoom: 1,
|
|
61
|
-
maxzoom: 25,
|
|
62
|
-
Stylesheet: [{ id: 'carto_css_style', data }],
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const xmlFileText = new carto.Renderer().render(configJson);
|
|
66
|
-
|
|
67
|
-
if (!xmlFileText) {
|
|
68
|
-
return { status: 500, error: `Cant create xml for ${fullPath}` };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return xmlFileText;
|
|
72
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import { randomUUID as uuidv4 } from 'crypto';
|
|
3
|
-
import mapnik from './mapnik.js';
|
|
4
|
-
|
|
5
|
-
import { config } from '@opengis/fastify-table/utils.js';
|
|
6
|
-
|
|
7
|
-
const { mapServerAddress } = mapnik();
|
|
8
|
-
|
|
9
|
-
function unixPath(data) {
|
|
10
|
-
if (mapServerAddress && !mapServerAddress?.includes('localhost') && data?.[1] === ':') {
|
|
11
|
-
return path.posix.join('/data/softpro', data.substr(3).replace(/\\/g, '/'));
|
|
12
|
-
}
|
|
13
|
-
return data;
|
|
14
|
-
}
|
|
15
|
-
const gdalWrapper = async ({
|
|
16
|
-
name, pathIn, pathOut, parameters, funcs, send = console.log,
|
|
17
|
-
}) => {
|
|
18
|
-
const timeStart = new Date();
|
|
19
|
-
const pref = name === 'gdalbuildvrt' ? ' -o ' : '';
|
|
20
|
-
const cmd = `${name} ${parameters || ''} ${unixPath(pathIn) || ''} ${pref} ${unixPath(pathOut) || ''}`;
|
|
21
|
-
|
|
22
|
-
if (mapServerAddress) {
|
|
23
|
-
const out = pref + (unixPath(pathOut) || '');
|
|
24
|
-
const obj = {
|
|
25
|
-
name, path: unixPath(pathIn), out, params: parameters,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const time = Date.now();
|
|
29
|
-
const { result, err } = await mapnik().gdal(obj);
|
|
30
|
-
|
|
31
|
-
if (config.local && false) {
|
|
32
|
-
send(cmd);
|
|
33
|
-
send(JSON.stringify(obj));
|
|
34
|
-
send(`${Date.now() - time}`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (err) {
|
|
38
|
-
const logObj = {
|
|
39
|
-
level: 'ERROR',
|
|
40
|
-
uuid: uuidv4(),
|
|
41
|
-
name: 'GDAL',
|
|
42
|
-
subname: 'GDALWRAPPER',
|
|
43
|
-
msec: new Date() - timeStart,
|
|
44
|
-
param: { cmd },
|
|
45
|
-
status: '500',
|
|
46
|
-
response: { error: err },
|
|
47
|
-
};
|
|
48
|
-
// log.error({ name: 'gdal', ...logObj });
|
|
49
|
-
send(err);
|
|
50
|
-
}
|
|
51
|
-
return { err, result };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const result = await funcs.exec(cmd, { send });
|
|
55
|
-
if (!result) {
|
|
56
|
-
const logObj = {
|
|
57
|
-
level: 'ERROR',
|
|
58
|
-
uuid: uuidv4(),
|
|
59
|
-
name: 'GDAL',
|
|
60
|
-
subname: 'GDALWRAPPER',
|
|
61
|
-
msec: new Date() - timeStart,
|
|
62
|
-
param: { cmd },
|
|
63
|
-
status: '500',
|
|
64
|
-
response: { error: `Cant execute ${cmd}` },
|
|
65
|
-
};
|
|
66
|
-
// log.error({ name: 'gdal', ...logObj });
|
|
67
|
-
return { err: `Cant execute ${cmd}` };
|
|
68
|
-
}
|
|
69
|
-
return { result };
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export default gdalWrapper;
|