chiitiler 1.15.0 → 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 +256 -268
- 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/pmtiles.js +1 -2
- package/package.json +20 -17
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 };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { stream } from 'hono/streaming';
|
|
3
|
+
import { isSupportedFormat, isValidStylejson } from '../utils.js';
|
|
4
|
+
import { getRenderedBbox } from '../../render/index.js';
|
|
5
|
+
function createClipRouter(options) {
|
|
6
|
+
const clip = new Hono()
|
|
7
|
+
.get('/:filename_ext', async (c) => {
|
|
8
|
+
// path params
|
|
9
|
+
const [filename, ext] = c.req.param('filename_ext').split('.');
|
|
10
|
+
if (filename !== 'clip')
|
|
11
|
+
return c.body('not found', 404);
|
|
12
|
+
if (!isSupportedFormat(ext))
|
|
13
|
+
return c.body('invalid format', 400);
|
|
14
|
+
// query params
|
|
15
|
+
const bbox = c.req.query('bbox'); // ?bbox=minx,miny,maxx,maxy
|
|
16
|
+
if (bbox === undefined)
|
|
17
|
+
return c.body('bbox is required', 400);
|
|
18
|
+
const [minx, miny, maxx, maxy] = bbox.split(',').map(Number);
|
|
19
|
+
if (minx >= maxx || miny >= maxy)
|
|
20
|
+
return c.body('invalid bbox', 400);
|
|
21
|
+
const url = c.req.query('url');
|
|
22
|
+
if (url === undefined)
|
|
23
|
+
return c.body('url is required', 400);
|
|
24
|
+
const quality = Number(c.req.query('quality') ?? 100);
|
|
25
|
+
const size = Number(c.req.query('size') ?? 1024);
|
|
26
|
+
c.header('Content-Type', `image/${ext}`);
|
|
27
|
+
try {
|
|
28
|
+
const sharp = await getRenderedBbox({
|
|
29
|
+
stylejson: url,
|
|
30
|
+
bbox: [minx, miny, maxx, maxy],
|
|
31
|
+
size,
|
|
32
|
+
cache: options.cache,
|
|
33
|
+
ext,
|
|
34
|
+
quality,
|
|
35
|
+
});
|
|
36
|
+
if (options.stream) {
|
|
37
|
+
// stream mode
|
|
38
|
+
return stream(c, async (stream) => {
|
|
39
|
+
for await (const chunk of sharp) {
|
|
40
|
+
stream.write(chunk);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const buf = await sharp.toBuffer();
|
|
46
|
+
return c.body(buf);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
console.error(`render error: ${e}`);
|
|
51
|
+
return c.body('failed to render tile', 400);
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
.post('/:filename_ext', async (c) => {
|
|
55
|
+
// body
|
|
56
|
+
const { style } = await c.req.json();
|
|
57
|
+
if (!isValidStylejson(style))
|
|
58
|
+
return c.body('invalid stylejson', 400);
|
|
59
|
+
// path params
|
|
60
|
+
const [filename, ext] = c.req.param('filename_ext').split('.');
|
|
61
|
+
if (filename !== 'clip')
|
|
62
|
+
return c.body('not found', 404);
|
|
63
|
+
if (!isSupportedFormat(ext))
|
|
64
|
+
return c.body('invalid format', 400);
|
|
65
|
+
// query params
|
|
66
|
+
const bbox = c.req.query('bbox'); // ?bbox=minx,miny,maxx,maxy
|
|
67
|
+
if (bbox === undefined)
|
|
68
|
+
return c.body('bbox is required', 400);
|
|
69
|
+
const [minx, miny, maxx, maxy] = bbox.split(',').map(Number);
|
|
70
|
+
if (minx >= maxx || miny >= maxy)
|
|
71
|
+
return c.body('invalid bbox', 400);
|
|
72
|
+
const quality = Number(c.req.query('quality') ?? 100);
|
|
73
|
+
const size = Number(c.req.query('size') ?? 1024);
|
|
74
|
+
c.header('Content-Type', `image/${ext}`);
|
|
75
|
+
try {
|
|
76
|
+
const sharp = await getRenderedBbox({
|
|
77
|
+
stylejson: style,
|
|
78
|
+
bbox: [minx, miny, maxx, maxy],
|
|
79
|
+
size,
|
|
80
|
+
cache: options.cache,
|
|
81
|
+
ext,
|
|
82
|
+
quality,
|
|
83
|
+
});
|
|
84
|
+
if (options.stream) {
|
|
85
|
+
// stream mode
|
|
86
|
+
return stream(c, async (stream) => {
|
|
87
|
+
for await (const chunk of sharp) {
|
|
88
|
+
stream.write(chunk);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const buf = await sharp.toBuffer();
|
|
94
|
+
return c.body(buf);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
console.error(`render error: ${e}`);
|
|
99
|
+
return c.body('failed to render tile', 400);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return clip;
|
|
103
|
+
}
|
|
104
|
+
export { createClipRouter };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Cache } from '../../cache/index.js';
|
|
2
|
+
declare function createTilesRouter(options: {
|
|
3
|
+
cache: Cache;
|
|
4
|
+
stream: boolean;
|
|
5
|
+
}): import("hono/hono-base").HonoBase<import("hono/types").BlankEnv, {
|
|
6
|
+
"/:z/:x/:y_ext": {
|
|
7
|
+
$get: {
|
|
8
|
+
input: {
|
|
9
|
+
param: {
|
|
10
|
+
z: string;
|
|
11
|
+
} & {
|
|
12
|
+
x: string;
|
|
13
|
+
} & {
|
|
14
|
+
y_ext: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
output: {};
|
|
18
|
+
outputFormat: string;
|
|
19
|
+
status: import("hono/utils/http-status").StatusCode;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
} & {
|
|
23
|
+
"/:z/:x/:y_ext": {
|
|
24
|
+
$post: {
|
|
25
|
+
input: {
|
|
26
|
+
param: {
|
|
27
|
+
z: string;
|
|
28
|
+
} & {
|
|
29
|
+
x: string;
|
|
30
|
+
} & {
|
|
31
|
+
y_ext: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
output: {};
|
|
35
|
+
outputFormat: string;
|
|
36
|
+
status: import("hono/utils/http-status").StatusCode;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}, "/", "/:z/:x/:y_ext">;
|
|
40
|
+
export { createTilesRouter };
|