chiitiler 1.14.2 → 1.16.0
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/README.md +258 -225
- package/dist/cache/gcs.d.ts +10 -0
- package/dist/cache/gcs.js +66 -0
- package/dist/cache/index.d.ts +2 -1
- package/dist/cache/index.js +2 -1
- package/dist/cli.js +26 -0
- package/dist/gcs.d.ts +7 -0
- package/dist/gcs.js +14 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +6 -1
- package/dist/render/index.d.ts +14 -1
- package/dist/render/index.js +29 -2
- package/dist/server/index.d.ts +0 -2
- package/dist/server/index.js +28 -215
- package/dist/server/routes/camera.d.ts +36 -0
- package/dist/server/routes/camera.js +128 -0
- package/dist/server/routes/clip.d.ts +32 -0
- package/dist/server/routes/clip.js +104 -0
- package/dist/server/routes/tiles.d.ts +40 -0
- package/dist/server/routes/tiles.js +113 -0
- package/dist/server/utils.d.ts +7 -0
- package/dist/server/utils.js +28 -0
- package/dist/source/gcs.d.ts +2 -0
- package/dist/source/gcs.js +21 -0
- package/dist/source/index.js +3 -0
- package/dist/source/pmtiles.js +1 -2
- package/package.json +21 -17
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,14 @@ function parseCacheStrategy(method, options) {
|
|
|
21
21
|
endpoint: options.s3Endpoint,
|
|
22
22
|
forcePathStyle: options.s3ForcePathStyle,
|
|
23
23
|
});
|
|
24
|
+
if (method === 'gcs')
|
|
25
|
+
return caches.gcsCache({
|
|
26
|
+
bucket: options.gcsCacheBucket,
|
|
27
|
+
projectId: options.gcsProjectId,
|
|
28
|
+
keyFilename: options.gcsKeyFilename,
|
|
29
|
+
prefix: options.gcsCachePrefix,
|
|
30
|
+
apiEndpoint: options.gcsApiEndpoint,
|
|
31
|
+
});
|
|
24
32
|
// command-line is not specified -> try to read from env
|
|
25
33
|
const cacheEnv = process.env.CHIITILER_CACHE_METHOD;
|
|
26
34
|
if (cacheEnv === 'memory')
|
|
@@ -40,6 +48,14 @@ function parseCacheStrategy(method, options) {
|
|
|
40
48
|
endpoint: process.env.CHIITILER_S3_ENDPOINT,
|
|
41
49
|
forcePathStyle: process.env.CHIITILER_S3_FORCE_PATH_STYLE === 'true',
|
|
42
50
|
});
|
|
51
|
+
if (cacheEnv === 'gcs')
|
|
52
|
+
return caches.gcsCache({
|
|
53
|
+
bucket: process.env.CHIITILER_GCS_CACHE_BUCKET ?? '',
|
|
54
|
+
prefix: process.env.CHIITILER_GCS_CACHE_PREFIX,
|
|
55
|
+
projectId: process.env.CHIITILER_GCS_PROJECT_ID,
|
|
56
|
+
keyFilename: process.env.CHIITILER_GCS_KEY_FILENAME,
|
|
57
|
+
apiEndpoint: process.env.CHIITILER_GCS_API_ENDPOINT,
|
|
58
|
+
});
|
|
43
59
|
// undefined or invalid
|
|
44
60
|
return caches.noneCache();
|
|
45
61
|
}
|
|
@@ -88,6 +104,11 @@ export function createProgram() {
|
|
|
88
104
|
.option('-s3b --s3-cache-bucket <bucket-name>', 's3 cache bucket name', '')
|
|
89
105
|
.option('-s3e --s3-endpoint <url>', 's3 endpoint url', '')
|
|
90
106
|
.option('-3p --s3-force-path-style', 's3 force path style', '')
|
|
107
|
+
.option('-gcsb --gcs-cache-bucket <bucket-name>', 'gcs cache bucket name', '')
|
|
108
|
+
.option('-gcsp --gcs-project-id <project-id>', 'gcs project id', '')
|
|
109
|
+
.option('-gcsk --gcs-key-filename <key-filename>', 'gcs key filename', '')
|
|
110
|
+
.option('-gcsp --gcs-cache-prefix <prefix>', 'gcs cache prefix', '')
|
|
111
|
+
.option('-gcse --gcs-api-endpoint <api-endpoint>', 'gcs api endpoint', '')
|
|
91
112
|
.option('-p --port <port>', 'port number')
|
|
92
113
|
.option('-r --stream', 'stream mode')
|
|
93
114
|
.option('-D --debug', 'debug mode')
|
|
@@ -101,6 +122,11 @@ export function createProgram() {
|
|
|
101
122
|
s3Region: options.s3Region,
|
|
102
123
|
s3Endpoint: options.s3Endpoint,
|
|
103
124
|
s3ForcePathStyle: options.s3ForcePathStyle === 'true',
|
|
125
|
+
gcsCacheBucket: options.gcsCacheBucket,
|
|
126
|
+
gcsCachePrefix: options.gcsCachePrefix,
|
|
127
|
+
gcsProjectId: options.gcsProjectId,
|
|
128
|
+
gcsKeyFilename: options.gcsKeyFilename,
|
|
129
|
+
gcsApiEndpoint: options.gcsApiEndpoint,
|
|
104
130
|
}),
|
|
105
131
|
port: parsePort(options.port),
|
|
106
132
|
debug: parseDebug(options.debug),
|
package/dist/gcs.d.ts
ADDED
package/dist/gcs.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Storage } from '@google-cloud/storage';
|
|
2
|
+
let storageClient; // singleton
|
|
3
|
+
const getStorageClient = function ({ projectId, keyFilename, apiEndpoint }) {
|
|
4
|
+
if (storageClient !== undefined)
|
|
5
|
+
return storageClient;
|
|
6
|
+
const storageOptions = {
|
|
7
|
+
projectId,
|
|
8
|
+
keyFilename,
|
|
9
|
+
apiEndpoint
|
|
10
|
+
};
|
|
11
|
+
storageClient = new Storage(storageOptions);
|
|
12
|
+
return storageClient;
|
|
13
|
+
};
|
|
14
|
+
export { getStorageClient };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { getRenderedBbox, getRenderedTile, GetRenderedBboxOptions, GetRenderedTileOptions } from './render/index.js';
|
|
1
|
+
import { getRenderedBbox, getRenderedTile, getRenderedImage, GetRenderedBboxOptions, GetRenderedTileOptions, GetRenderedImageOptions } from './render/index.js';
|
|
2
2
|
export declare function getRenderedBboxBuffer(options: GetRenderedBboxOptions): Promise<Buffer>;
|
|
3
3
|
export { getRenderedBbox as getRenderedBboxStream };
|
|
4
|
+
export declare function getRenderedImageBuffer(options: GetRenderedImageOptions): Promise<Buffer>;
|
|
5
|
+
export { getRenderedImage as getRenderedImageStream };
|
|
4
6
|
export declare function getRenderedTileBuffer(options: GetRenderedTileOptions): Promise<Buffer>;
|
|
5
7
|
export { getRenderedTile as getRenderedTileStream };
|
|
6
8
|
export * as ChiitilerCache from './cache/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { getRenderedBbox, getRenderedTile, } from './render/index.js';
|
|
1
|
+
import { getRenderedBbox, getRenderedTile, getRenderedImage, } from './render/index.js';
|
|
2
2
|
export async function getRenderedBboxBuffer(options) {
|
|
3
3
|
const sharp = await getRenderedBbox(options);
|
|
4
4
|
return sharp.toBuffer();
|
|
5
5
|
}
|
|
6
6
|
export { getRenderedBbox as getRenderedBboxStream };
|
|
7
|
+
export async function getRenderedImageBuffer(options) {
|
|
8
|
+
const sharp = await getRenderedImage(options);
|
|
9
|
+
return sharp.toBuffer();
|
|
10
|
+
}
|
|
11
|
+
export { getRenderedImage as getRenderedImageStream };
|
|
7
12
|
export async function getRenderedTileBuffer(options) {
|
|
8
13
|
const sharp = await getRenderedTile(options);
|
|
9
14
|
return sharp.toBuffer();
|
package/dist/render/index.d.ts
CHANGED
|
@@ -23,4 +23,17 @@ type GetRenderedBboxOptions = {
|
|
|
23
23
|
quality: number;
|
|
24
24
|
};
|
|
25
25
|
declare function getRenderedBbox({ stylejson, bbox, size, cache, ext, quality, }: GetRenderedBboxOptions): Promise<sharp.Sharp>;
|
|
26
|
-
|
|
26
|
+
type GetRenderedImageOptions = {
|
|
27
|
+
stylejson: string | StyleSpecification;
|
|
28
|
+
cache: Cache;
|
|
29
|
+
ext: SupportedFormat;
|
|
30
|
+
quality: number;
|
|
31
|
+
bearing: number;
|
|
32
|
+
pitch: number;
|
|
33
|
+
zoom: number;
|
|
34
|
+
center: [number, number];
|
|
35
|
+
height: number;
|
|
36
|
+
width: number;
|
|
37
|
+
};
|
|
38
|
+
declare function getRenderedImage(options: GetRenderedImageOptions): Promise<sharp.Sharp>;
|
|
39
|
+
export { getRenderedTile, getRenderedBbox, getRenderedImage, type GetRenderedBboxOptions, type GetRenderedTileOptions, type GetRenderedImageOptions, type SupportedFormat, };
|
package/dist/render/index.js
CHANGED
|
@@ -121,7 +121,7 @@ async function getRenderedBbox({ stylejson, bbox, size, cache, ext, quality, })
|
|
|
121
121
|
height,
|
|
122
122
|
center,
|
|
123
123
|
}, cache, 'static');
|
|
124
|
-
|
|
124
|
+
const _sharp = sharp(pixels, {
|
|
125
125
|
raw: {
|
|
126
126
|
width,
|
|
127
127
|
height,
|
|
@@ -138,4 +138,31 @@ async function getRenderedBbox({ stylejson, bbox, size, cache, ext, quality, })
|
|
|
138
138
|
return _sharp.webp({ quality, effort: 0 });
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
-
|
|
141
|
+
async function getRenderedImage(options) {
|
|
142
|
+
const style = await loadStyle(options.stylejson, options.cache);
|
|
143
|
+
const pixels = await render(style, {
|
|
144
|
+
center: options.center,
|
|
145
|
+
height: options.height,
|
|
146
|
+
width: options.width,
|
|
147
|
+
zoom: options.zoom,
|
|
148
|
+
bearing: options.bearing,
|
|
149
|
+
pitch: options.pitch,
|
|
150
|
+
}, options.cache, 'static');
|
|
151
|
+
const _sharp = sharp(pixels, {
|
|
152
|
+
raw: {
|
|
153
|
+
width: options.width,
|
|
154
|
+
height: options.height,
|
|
155
|
+
channels: 4,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
switch (options.ext) {
|
|
159
|
+
case 'png':
|
|
160
|
+
return _sharp.png();
|
|
161
|
+
case 'jpeg':
|
|
162
|
+
case 'jpg':
|
|
163
|
+
return _sharp.jpeg({ quality: options.quality });
|
|
164
|
+
case 'webp':
|
|
165
|
+
return _sharp.webp({ quality: options.quality, effort: 0 });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export { getRenderedTile, getRenderedBbox, getRenderedImage, };
|
package/dist/server/index.d.ts
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -1,230 +1,43 @@
|
|
|
1
1
|
import { Hono } from 'hono/quick';
|
|
2
|
-
import { stream } from 'hono/streaming';
|
|
3
2
|
import { serve } from '@hono/node-server';
|
|
4
|
-
import { validateStyleMin, } from '@maplibre/maplibre-gl-style-spec';
|
|
5
3
|
import { getDebugPage, getEditorgPage } from './debug.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
function isValidXyz(x, y, z) {
|
|
11
|
-
if (x < 0 || y < 0 || z < 0)
|
|
12
|
-
return false;
|
|
13
|
-
if (x >= 2 ** z || y >= 2 ** z)
|
|
14
|
-
return false;
|
|
15
|
-
return true;
|
|
16
|
-
}
|
|
17
|
-
function isSupportedFormat(ext) {
|
|
18
|
-
return ['png', 'jpeg', 'jpg', 'webp'].includes(ext);
|
|
19
|
-
}
|
|
4
|
+
import { createClipRouter } from './routes/clip.js';
|
|
5
|
+
import { createTilesRouter } from './routes/tiles.js';
|
|
6
|
+
import { createCameraRouter } from './routes/camera.js';
|
|
20
7
|
function initServer(options) {
|
|
21
|
-
const tiles = new Hono()
|
|
22
|
-
.get('/:z/:x/:y_ext', async (c) => {
|
|
23
|
-
const url = c.req.query('url');
|
|
24
|
-
if (url === undefined)
|
|
25
|
-
return c.body('url is required', 400);
|
|
26
|
-
// path params
|
|
27
|
-
const z = Number(c.req.param('z'));
|
|
28
|
-
const x = Number(c.req.param('x'));
|
|
29
|
-
let [_y, ext] = c.req.param('y_ext').split('.');
|
|
30
|
-
const y = Number(_y);
|
|
31
|
-
if (!isValidXyz(x, y, z))
|
|
32
|
-
return c.body('invalid xyz', 400);
|
|
33
|
-
if (!isSupportedFormat(ext))
|
|
34
|
-
return c.body('invalid format', 400);
|
|
35
|
-
// query params
|
|
36
|
-
const tileSize = Number(c.req.query('tileSize') ?? 512);
|
|
37
|
-
const quality = Number(c.req.query('quality') ?? 100);
|
|
38
|
-
const margin = Number(c.req.query('margin') ?? 0);
|
|
39
|
-
c.header('Content-Type', `image/${ext}`);
|
|
40
|
-
try {
|
|
41
|
-
const sharp = await getRenderedTile({
|
|
42
|
-
stylejson: url,
|
|
43
|
-
z,
|
|
44
|
-
x,
|
|
45
|
-
y,
|
|
46
|
-
tileSize,
|
|
47
|
-
cache: options.cache,
|
|
48
|
-
margin,
|
|
49
|
-
ext,
|
|
50
|
-
quality,
|
|
51
|
-
});
|
|
52
|
-
if (options.stream) {
|
|
53
|
-
// stream mode
|
|
54
|
-
return stream(c, async (stream) => {
|
|
55
|
-
for await (const chunk of sharp) {
|
|
56
|
-
stream.write(chunk);
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
const buf = await sharp.toBuffer();
|
|
62
|
-
return c.body(buf);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
catch (e) {
|
|
66
|
-
console.error(`render error: ${e}`);
|
|
67
|
-
return c.body('failed to render tile', 400);
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
.post('/:z/:x/:y_ext', async (c) => {
|
|
71
|
-
// body
|
|
72
|
-
const { style } = await c.req.json();
|
|
73
|
-
if (!isValidStylejson(style))
|
|
74
|
-
return c.body('invalid stylejson', 400);
|
|
75
|
-
// path params
|
|
76
|
-
const z = Number(c.req.param('z'));
|
|
77
|
-
const x = Number(c.req.param('x'));
|
|
78
|
-
let [_y, ext] = c.req.param('y_ext').split('.');
|
|
79
|
-
const y = Number(_y);
|
|
80
|
-
if (!isValidXyz(x, y, z))
|
|
81
|
-
return c.body('invalid xyz', 400);
|
|
82
|
-
if (!isSupportedFormat(ext))
|
|
83
|
-
return c.body('invalid format', 400);
|
|
84
|
-
// query params
|
|
85
|
-
const tileSize = Number(c.req.query('tileSize') ?? 512);
|
|
86
|
-
const quality = Number(c.req.query('quality') ?? 100);
|
|
87
|
-
const margin = Number(c.req.query('margin') ?? 0);
|
|
88
|
-
c.header('Content-Type', `image/${ext}`);
|
|
89
|
-
try {
|
|
90
|
-
const sharp = await getRenderedTile({
|
|
91
|
-
stylejson: style,
|
|
92
|
-
z,
|
|
93
|
-
x,
|
|
94
|
-
y,
|
|
95
|
-
tileSize,
|
|
96
|
-
cache: options.cache,
|
|
97
|
-
margin,
|
|
98
|
-
ext,
|
|
99
|
-
quality,
|
|
100
|
-
});
|
|
101
|
-
if (options.stream) {
|
|
102
|
-
// stream mode
|
|
103
|
-
return stream(c, async (stream) => {
|
|
104
|
-
for await (const chunk of sharp) {
|
|
105
|
-
stream.write(chunk);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
const buf = await sharp.toBuffer();
|
|
111
|
-
return c.body(buf);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
catch (e) {
|
|
115
|
-
console.error(`render error: ${e}`);
|
|
116
|
-
return c.body('failed to render tile', 400);
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
const clip = new Hono()
|
|
120
|
-
.get('/:filename_ext', async (c) => {
|
|
121
|
-
// path params
|
|
122
|
-
const [filename, ext] = c.req.param('filename_ext').split('.');
|
|
123
|
-
if (filename !== 'clip')
|
|
124
|
-
return c.body('not found', 404);
|
|
125
|
-
if (!isSupportedFormat(ext))
|
|
126
|
-
return c.body('invalid format', 400);
|
|
127
|
-
// query params
|
|
128
|
-
const bbox = c.req.query('bbox'); // ?bbox=minx,miny,maxx,maxy
|
|
129
|
-
if (bbox === undefined)
|
|
130
|
-
return c.body('bbox is required', 400);
|
|
131
|
-
const [minx, miny, maxx, maxy] = bbox.split(',').map(Number);
|
|
132
|
-
if (minx >= maxx || miny >= maxy)
|
|
133
|
-
return c.body('invalid bbox', 400);
|
|
134
|
-
const url = c.req.query('url');
|
|
135
|
-
if (url === undefined)
|
|
136
|
-
return c.body('url is required', 400);
|
|
137
|
-
const quality = Number(c.req.query('quality') ?? 100);
|
|
138
|
-
const size = Number(c.req.query('size') ?? 1024);
|
|
139
|
-
c.header('Content-Type', `image/${ext}`);
|
|
140
|
-
try {
|
|
141
|
-
const sharp = await getRenderedBbox({
|
|
142
|
-
stylejson: url,
|
|
143
|
-
bbox: [minx, miny, maxx, maxy],
|
|
144
|
-
size,
|
|
145
|
-
cache: options.cache,
|
|
146
|
-
ext,
|
|
147
|
-
quality,
|
|
148
|
-
});
|
|
149
|
-
if (options.stream) {
|
|
150
|
-
// stream mode
|
|
151
|
-
return stream(c, async (stream) => {
|
|
152
|
-
for await (const chunk of sharp) {
|
|
153
|
-
stream.write(chunk);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
const buf = await sharp.toBuffer();
|
|
159
|
-
return c.body(buf);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
catch (e) {
|
|
163
|
-
console.error(`render error: ${e}`);
|
|
164
|
-
return c.body('failed to render tile', 400);
|
|
165
|
-
}
|
|
166
|
-
})
|
|
167
|
-
.post('/:filename_ext', async (c) => {
|
|
168
|
-
// body
|
|
169
|
-
const { style } = await c.req.json();
|
|
170
|
-
if (!isValidStylejson(style))
|
|
171
|
-
return c.body('invalid stylejson', 400);
|
|
172
|
-
// path params
|
|
173
|
-
const [filename, ext] = c.req.param('filename_ext').split('.');
|
|
174
|
-
if (filename !== 'clip')
|
|
175
|
-
return c.body('not found', 404);
|
|
176
|
-
if (!isSupportedFormat(ext))
|
|
177
|
-
return c.body('invalid format', 400);
|
|
178
|
-
// query params
|
|
179
|
-
const bbox = c.req.query('bbox'); // ?bbox=minx,miny,maxx,maxy
|
|
180
|
-
if (bbox === undefined)
|
|
181
|
-
return c.body('bbox is required', 400);
|
|
182
|
-
const [minx, miny, maxx, maxy] = bbox.split(',').map(Number);
|
|
183
|
-
if (minx >= maxx || miny >= maxy)
|
|
184
|
-
return c.body('invalid bbox', 400);
|
|
185
|
-
const quality = Number(c.req.query('quality') ?? 100);
|
|
186
|
-
const size = Number(c.req.query('size') ?? 1024);
|
|
187
|
-
c.header('Content-Type', `image/${ext}`);
|
|
188
|
-
try {
|
|
189
|
-
const sharp = await getRenderedBbox({
|
|
190
|
-
stylejson: style,
|
|
191
|
-
bbox: [minx, miny, maxx, maxy],
|
|
192
|
-
size,
|
|
193
|
-
cache: options.cache,
|
|
194
|
-
ext,
|
|
195
|
-
quality,
|
|
196
|
-
});
|
|
197
|
-
if (options.stream) {
|
|
198
|
-
// stream mode
|
|
199
|
-
return stream(c, async (stream) => {
|
|
200
|
-
for await (const chunk of sharp) {
|
|
201
|
-
stream.write(chunk);
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
const buf = await sharp.toBuffer();
|
|
207
|
-
return c.body(buf);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
catch (e) {
|
|
211
|
-
console.error(`render error: ${e}`);
|
|
212
|
-
return c.body('failed to render tile', 400);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
8
|
const hono = new Hono();
|
|
216
9
|
if (options.debug) {
|
|
217
10
|
hono.get('/debug', getDebugPage);
|
|
218
11
|
hono.get('/editor', getEditorgPage);
|
|
219
12
|
}
|
|
220
13
|
hono.get('/health', (c) => c.text('OK'));
|
|
221
|
-
hono.route('/
|
|
222
|
-
|
|
14
|
+
hono.route('/static', createCameraRouter({
|
|
15
|
+
cache: options.cache,
|
|
16
|
+
stream: options.stream,
|
|
17
|
+
}));
|
|
18
|
+
hono.route('/tiles', createTilesRouter({
|
|
19
|
+
cache: options.cache,
|
|
20
|
+
stream: options.stream,
|
|
21
|
+
}));
|
|
22
|
+
hono.route('/', createClipRouter({
|
|
23
|
+
cache: options.cache,
|
|
24
|
+
stream: options.stream,
|
|
25
|
+
}));
|
|
223
26
|
return {
|
|
224
27
|
app: hono,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
28
|
+
start: () => {
|
|
29
|
+
const server = serve({ port: options.port, fetch: hono.fetch });
|
|
30
|
+
process.on('SIGINT', () => {
|
|
31
|
+
console.log('shutting down server...');
|
|
32
|
+
server.close();
|
|
33
|
+
process.exit(0);
|
|
34
|
+
});
|
|
35
|
+
process.on('SIGTERM', () => {
|
|
36
|
+
console.log('shutting down server...');
|
|
37
|
+
server.close();
|
|
38
|
+
process.exit(0);
|
|
39
|
+
});
|
|
40
|
+
},
|
|
228
41
|
};
|
|
229
42
|
}
|
|
230
43
|
export { initServer };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Cache } from '../../cache/index.js';
|
|
2
|
+
declare function createCameraRouter(options: {
|
|
3
|
+
cache: Cache;
|
|
4
|
+
stream: boolean;
|
|
5
|
+
}): import("hono/hono-base").HonoBase<import("hono/types").BlankEnv, {
|
|
6
|
+
"/:camera/:dimensions_ext": {
|
|
7
|
+
$get: {
|
|
8
|
+
input: {
|
|
9
|
+
param: {
|
|
10
|
+
camera: string;
|
|
11
|
+
} & {
|
|
12
|
+
dimensions_ext: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
output: {};
|
|
16
|
+
outputFormat: string;
|
|
17
|
+
status: import("hono/utils/http-status").StatusCode;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
} & {
|
|
21
|
+
"/:camera/:dimensions_ext": {
|
|
22
|
+
$post: {
|
|
23
|
+
input: {
|
|
24
|
+
param: {
|
|
25
|
+
camera: string;
|
|
26
|
+
} & {
|
|
27
|
+
dimensions_ext: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
output: {};
|
|
31
|
+
outputFormat: string;
|
|
32
|
+
status: import("hono/utils/http-status").StatusCode;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
}, "/", "/:camera/:dimensions_ext">;
|
|
36
|
+
export { createCameraRouter };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { stream } from 'hono/streaming';
|
|
3
|
+
import { isSupportedFormat, isValidCamera, isValidDimensions, isValidStylejson, } from '../utils.js';
|
|
4
|
+
import { getRenderedImage } from '../../render/index.js';
|
|
5
|
+
function createCameraRouter(options) {
|
|
6
|
+
const camera = new Hono()
|
|
7
|
+
.get('/:camera/:dimensions_ext', async (c) => {
|
|
8
|
+
// path params
|
|
9
|
+
const camera = c.req
|
|
10
|
+
.param('camera')
|
|
11
|
+
.match(/([\d.]+),([\d.]+),([\d.]+)(?:@(\d+)(?:,(\d+))?)?/);
|
|
12
|
+
const [_dimensions, ext] = c.req.param('dimensions_ext').split('.');
|
|
13
|
+
const dimensions = _dimensions.match(/(\d+)x(\d+)?/);
|
|
14
|
+
if (!camera || !isValidCamera(camera))
|
|
15
|
+
return c.body('invalid camera', 400);
|
|
16
|
+
if (!dimensions || !isValidDimensions(dimensions))
|
|
17
|
+
return c.body('invalid dimensions', 400);
|
|
18
|
+
if (!isSupportedFormat(ext))
|
|
19
|
+
return c.body('invalid format', 400);
|
|
20
|
+
const [, _lon, _lat, _zoom, _bearing, _pitch] = camera;
|
|
21
|
+
const [, _width, _height] = dimensions;
|
|
22
|
+
const lat = Number(_lat);
|
|
23
|
+
const lon = Number(_lon);
|
|
24
|
+
const zoom = Number(_zoom);
|
|
25
|
+
const bearing = Number(_bearing ?? 0);
|
|
26
|
+
const pitch = Number(_pitch ?? 0);
|
|
27
|
+
const height = Number(_height);
|
|
28
|
+
const width = Number(_width);
|
|
29
|
+
// query params
|
|
30
|
+
const url = c.req.query('url');
|
|
31
|
+
if (url === undefined)
|
|
32
|
+
return c.body('url is required', 400);
|
|
33
|
+
const quality = Number(c.req.query('quality') ?? 100);
|
|
34
|
+
c.header('Content-Type', `image/${ext}`);
|
|
35
|
+
try {
|
|
36
|
+
const sharp = await getRenderedImage({
|
|
37
|
+
stylejson: url,
|
|
38
|
+
cache: options.cache,
|
|
39
|
+
ext,
|
|
40
|
+
quality,
|
|
41
|
+
center: [lon, lat],
|
|
42
|
+
zoom,
|
|
43
|
+
bearing,
|
|
44
|
+
pitch,
|
|
45
|
+
height,
|
|
46
|
+
width,
|
|
47
|
+
});
|
|
48
|
+
if (options.stream) {
|
|
49
|
+
// stream mode
|
|
50
|
+
return stream(c, async (stream) => {
|
|
51
|
+
for await (const chunk of sharp) {
|
|
52
|
+
stream.write(chunk);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const buf = await sharp.toBuffer();
|
|
58
|
+
return c.body(buf);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
console.error(`render error: ${e}`);
|
|
63
|
+
return c.body('failed to render static image', 400);
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.post('/:camera/:dimensions_ext', async (c) => {
|
|
67
|
+
// body
|
|
68
|
+
const { style } = await c.req.json();
|
|
69
|
+
if (!isValidStylejson(style))
|
|
70
|
+
return c.body('invalid stylejson', 400);
|
|
71
|
+
// path params
|
|
72
|
+
const camera = c.req
|
|
73
|
+
.param('camera')
|
|
74
|
+
.match(/([\d.]+),([\d.]+),([\d.]+)(?:@(\d+)(?:,(\d+))?)?/);
|
|
75
|
+
const [_dimensions, ext] = c.req.param('dimensions_ext').split('.');
|
|
76
|
+
const dimensions = _dimensions.match(/(\d+)x(\d+)?/);
|
|
77
|
+
if (!camera || !isValidCamera(camera))
|
|
78
|
+
return c.body('invalid camera', 400);
|
|
79
|
+
if (!dimensions || !isValidDimensions(dimensions))
|
|
80
|
+
return c.body('invalid dimensions', 400);
|
|
81
|
+
if (!isSupportedFormat(ext))
|
|
82
|
+
return c.body('invalid format', 400);
|
|
83
|
+
const [, _lon, _lat, _zoom, _bearing, _pitch] = camera;
|
|
84
|
+
const [, _width, _height] = dimensions;
|
|
85
|
+
const lat = Number(_lat);
|
|
86
|
+
const lon = Number(_lon);
|
|
87
|
+
const zoom = Number(_zoom);
|
|
88
|
+
const bearing = Number(_bearing ?? 0);
|
|
89
|
+
const pitch = Number(_pitch ?? 0);
|
|
90
|
+
const height = Number(_height);
|
|
91
|
+
const width = Number(_width);
|
|
92
|
+
// query params
|
|
93
|
+
const quality = Number(c.req.query('quality') ?? 100);
|
|
94
|
+
c.header('Content-Type', `image/${ext}`);
|
|
95
|
+
try {
|
|
96
|
+
const sharp = await getRenderedImage({
|
|
97
|
+
stylejson: style,
|
|
98
|
+
cache: options.cache,
|
|
99
|
+
ext,
|
|
100
|
+
quality,
|
|
101
|
+
center: [lon, lat],
|
|
102
|
+
zoom,
|
|
103
|
+
bearing,
|
|
104
|
+
pitch,
|
|
105
|
+
height,
|
|
106
|
+
width,
|
|
107
|
+
});
|
|
108
|
+
if (options.stream) {
|
|
109
|
+
// stream mode
|
|
110
|
+
return stream(c, async (stream) => {
|
|
111
|
+
for await (const chunk of sharp) {
|
|
112
|
+
stream.write(chunk);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const buf = await sharp.toBuffer();
|
|
118
|
+
return c.body(buf);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
console.error(`render error: ${e}`);
|
|
123
|
+
return c.body('failed to render static image', 400);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return camera;
|
|
127
|
+
}
|
|
128
|
+
export { createCameraRouter };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Cache } from '../../cache/index.js';
|
|
2
|
+
declare function createClipRouter(options: {
|
|
3
|
+
cache: Cache;
|
|
4
|
+
stream: boolean;
|
|
5
|
+
}): import("hono/hono-base").HonoBase<import("hono/types").BlankEnv, {
|
|
6
|
+
"/:filename_ext": {
|
|
7
|
+
$get: {
|
|
8
|
+
input: {
|
|
9
|
+
param: {
|
|
10
|
+
filename_ext: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
output: {};
|
|
14
|
+
outputFormat: string;
|
|
15
|
+
status: import("hono/utils/http-status").StatusCode;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
} & {
|
|
19
|
+
"/:filename_ext": {
|
|
20
|
+
$post: {
|
|
21
|
+
input: {
|
|
22
|
+
param: {
|
|
23
|
+
filename_ext: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
output: {};
|
|
27
|
+
outputFormat: string;
|
|
28
|
+
status: import("hono/utils/http-status").StatusCode;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
}, "/", "/:filename_ext">;
|
|
32
|
+
export { createClipRouter };
|