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
|
@@ -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 };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { stream } from 'hono/streaming';
|
|
3
|
+
import { isSupportedFormat, isValidStylejson } from '../utils.js';
|
|
4
|
+
import { getRenderedTile } from '../../render/index.js';
|
|
5
|
+
function isValidXyz(x, y, z) {
|
|
6
|
+
if (x < 0 || y < 0 || z < 0)
|
|
7
|
+
return false;
|
|
8
|
+
if (x >= 2 ** z || y >= 2 ** z)
|
|
9
|
+
return false;
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
function createTilesRouter(options) {
|
|
13
|
+
const tiles = new Hono()
|
|
14
|
+
.get('/:z/:x/:y_ext', async (c) => {
|
|
15
|
+
const url = c.req.query('url');
|
|
16
|
+
if (url === undefined)
|
|
17
|
+
return c.body('url is required', 400);
|
|
18
|
+
// path params
|
|
19
|
+
const z = Number(c.req.param('z'));
|
|
20
|
+
const x = Number(c.req.param('x'));
|
|
21
|
+
let [_y, ext] = c.req.param('y_ext').split('.');
|
|
22
|
+
const y = Number(_y);
|
|
23
|
+
if (!isValidXyz(x, y, z))
|
|
24
|
+
return c.body('invalid xyz', 400);
|
|
25
|
+
if (!isSupportedFormat(ext))
|
|
26
|
+
return c.body('invalid format', 400);
|
|
27
|
+
// query params
|
|
28
|
+
const tileSize = Number(c.req.query('tileSize') ?? 512);
|
|
29
|
+
const quality = Number(c.req.query('quality') ?? 100);
|
|
30
|
+
const margin = Number(c.req.query('margin') ?? 0);
|
|
31
|
+
c.header('Content-Type', `image/${ext}`);
|
|
32
|
+
try {
|
|
33
|
+
const sharp = await getRenderedTile({
|
|
34
|
+
stylejson: url,
|
|
35
|
+
z,
|
|
36
|
+
x,
|
|
37
|
+
y,
|
|
38
|
+
tileSize,
|
|
39
|
+
cache: options.cache,
|
|
40
|
+
margin,
|
|
41
|
+
ext,
|
|
42
|
+
quality,
|
|
43
|
+
});
|
|
44
|
+
if (options.stream) {
|
|
45
|
+
// stream mode
|
|
46
|
+
return stream(c, async (stream) => {
|
|
47
|
+
for await (const chunk of sharp) {
|
|
48
|
+
stream.write(chunk);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const buf = await sharp.toBuffer();
|
|
54
|
+
return c.body(buf);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
console.error(`render error: ${e}`);
|
|
59
|
+
return c.body('failed to render tile', 400);
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.post('/:z/:x/:y_ext', async (c) => {
|
|
63
|
+
// body
|
|
64
|
+
const { style } = await c.req.json();
|
|
65
|
+
if (!isValidStylejson(style))
|
|
66
|
+
return c.body('invalid stylejson', 400);
|
|
67
|
+
// path params
|
|
68
|
+
const z = Number(c.req.param('z'));
|
|
69
|
+
const x = Number(c.req.param('x'));
|
|
70
|
+
let [_y, ext] = c.req.param('y_ext').split('.');
|
|
71
|
+
const y = Number(_y);
|
|
72
|
+
if (!isValidXyz(x, y, z))
|
|
73
|
+
return c.body('invalid xyz', 400);
|
|
74
|
+
if (!isSupportedFormat(ext))
|
|
75
|
+
return c.body('invalid format', 400);
|
|
76
|
+
// query params
|
|
77
|
+
const tileSize = Number(c.req.query('tileSize') ?? 512);
|
|
78
|
+
const quality = Number(c.req.query('quality') ?? 100);
|
|
79
|
+
const margin = Number(c.req.query('margin') ?? 0);
|
|
80
|
+
c.header('Content-Type', `image/${ext}`);
|
|
81
|
+
try {
|
|
82
|
+
const sharp = await getRenderedTile({
|
|
83
|
+
stylejson: style,
|
|
84
|
+
z,
|
|
85
|
+
x,
|
|
86
|
+
y,
|
|
87
|
+
tileSize,
|
|
88
|
+
cache: options.cache,
|
|
89
|
+
margin,
|
|
90
|
+
ext,
|
|
91
|
+
quality,
|
|
92
|
+
});
|
|
93
|
+
if (options.stream) {
|
|
94
|
+
// stream mode
|
|
95
|
+
return stream(c, async (stream) => {
|
|
96
|
+
for await (const chunk of sharp) {
|
|
97
|
+
stream.write(chunk);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const buf = await sharp.toBuffer();
|
|
103
|
+
return c.body(buf);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
console.error(`render error: ${e}`);
|
|
108
|
+
return c.body('failed to render tile', 400);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return tiles;
|
|
112
|
+
}
|
|
113
|
+
export { createTilesRouter };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type StyleSpecification } from '@maplibre/maplibre-gl-style-spec';
|
|
2
|
+
import type { SupportedFormat } from '../render/index.js';
|
|
3
|
+
declare function isValidStylejson(stylejson: any): stylejson is StyleSpecification;
|
|
4
|
+
declare function isValidCamera([, lon, lat, zoom, bearing, pitch]: string[]): boolean;
|
|
5
|
+
declare function isValidDimensions([, width, height]: string[]): boolean;
|
|
6
|
+
declare function isSupportedFormat(ext: string | undefined): ext is SupportedFormat;
|
|
7
|
+
export { isValidStylejson, isValidCamera, isValidDimensions, isSupportedFormat, };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { validateStyleMin, } from '@maplibre/maplibre-gl-style-spec';
|
|
2
|
+
function isValidStylejson(stylejson) {
|
|
3
|
+
return validateStyleMin(stylejson).length === 0;
|
|
4
|
+
}
|
|
5
|
+
function isValidCamera([, lon, lat, zoom, bearing, pitch]) {
|
|
6
|
+
if (Number.isNaN(Number(lat)) || Number(lat) < -90 || Number(lat) > 90)
|
|
7
|
+
return false;
|
|
8
|
+
if (Number.isNaN(Number(lon)) || Number(lon) < -180 || Number(lon) > 180)
|
|
9
|
+
return false;
|
|
10
|
+
if (Number.isNaN(Number(zoom)) || Number(zoom) < 0 || Number(zoom) > 24)
|
|
11
|
+
return false;
|
|
12
|
+
if (bearing &&
|
|
13
|
+
(Number.isNaN(Number(bearing)) ||
|
|
14
|
+
Number(bearing) < 0 ||
|
|
15
|
+
Number(bearing) > 360))
|
|
16
|
+
return false;
|
|
17
|
+
if (pitch &&
|
|
18
|
+
(Number.isNaN(Number(pitch)) || Number(pitch) < 0 || Number(pitch) > 180))
|
|
19
|
+
return false;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
function isValidDimensions([, width, height]) {
|
|
23
|
+
return !Number.isNaN(Number(width)) && !Number.isNaN(Number(height));
|
|
24
|
+
}
|
|
25
|
+
function isSupportedFormat(ext) {
|
|
26
|
+
return Boolean(ext && ['png', 'jpeg', 'jpg', 'webp'].includes(ext));
|
|
27
|
+
}
|
|
28
|
+
export { isValidStylejson, isValidCamera, isValidDimensions, isSupportedFormat, };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getStorageClient } from '../gcs.js';
|
|
2
|
+
async function getGCSSource(uri) {
|
|
3
|
+
const storageClient = getStorageClient({
|
|
4
|
+
projectId: process.env.CHIITILER_GCS_PROJECT_ID,
|
|
5
|
+
keyFilename: process.env.CHIITILER_GCS_KEY_FILENAME,
|
|
6
|
+
apiEndpoint: process.env.CHIITILER_GCS_API_ENDPOINT,
|
|
7
|
+
});
|
|
8
|
+
const bucket = uri.replace('gs://', '').split('/')[0];
|
|
9
|
+
const path = uri.replace(`gs://${bucket}/`, '');
|
|
10
|
+
try {
|
|
11
|
+
const file = storageClient.bucket(bucket).file(path);
|
|
12
|
+
const [buffer] = await file.download();
|
|
13
|
+
return buffer;
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
if (e.code !== 404)
|
|
17
|
+
console.log(e);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export { getGCSSource };
|
package/dist/source/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { getHttpSource } from './http.js';
|
|
|
3
3
|
import { getPmtilesSource } from './pmtiles.js';
|
|
4
4
|
import { getMbtilesSource } from './mbtiles.js';
|
|
5
5
|
import { getS3Source } from './s3.js';
|
|
6
|
+
import { getGCSSource } from './gcs.js';
|
|
6
7
|
import { getCogSource } from './cog.js';
|
|
7
8
|
import { noneCache } from '../cache/index.js';
|
|
8
9
|
/**
|
|
@@ -19,6 +20,8 @@ async function getSource(uri, cache = noneCache()) {
|
|
|
19
20
|
data = await getFilesystemSource(uri);
|
|
20
21
|
else if (uri.startsWith('s3://'))
|
|
21
22
|
data = await getS3Source(uri);
|
|
23
|
+
else if (uri.startsWith('gs://'))
|
|
24
|
+
data = await getGCSSource(uri);
|
|
22
25
|
else if (uri.startsWith('mbtiles://'))
|
|
23
26
|
data = await getMbtilesSource(uri);
|
|
24
27
|
else if (uri.startsWith('pmtiles://'))
|
package/dist/source/pmtiles.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "chiitiler",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.16.0",
|
|
5
5
|
"description": "Tiny map rendering server for MapLibre Style Spec",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "npx esbuild --bundle src/main.ts --minify --outfile=build/main.cjs --platform=node --external:@maplibre/maplibre-gl-native --external:sharp",
|
|
13
|
+
"check": "tsc --noEmit",
|
|
13
14
|
"dev": "tsx watch src/main.ts tile-server -D",
|
|
14
15
|
"test:unit": "vitest src",
|
|
15
16
|
"test:coverage": "vitest src --coverage --coverage.provider=v8",
|
|
@@ -19,34 +20,37 @@
|
|
|
19
20
|
"keywords": [],
|
|
20
21
|
"author": "Kanahiro Iguchi",
|
|
21
22
|
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"url": "https://github.com/Kanahiro/chiitiler"
|
|
25
|
+
},
|
|
22
26
|
"devDependencies": {
|
|
23
|
-
"@tsconfig/
|
|
24
|
-
"@types/better-sqlite3": "^7.6.
|
|
25
|
-
"@types/node": "^
|
|
26
|
-
"@vitest/coverage-v8": "^
|
|
27
|
-
"esbuild": "^0.
|
|
27
|
+
"@tsconfig/node24": "^24.0.0",
|
|
28
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
29
|
+
"@types/node": "^24.0.0",
|
|
30
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
31
|
+
"esbuild": "^0.27.2",
|
|
28
32
|
"image-size": "^1.1.1",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"vitest": "^1.6.0"
|
|
33
|
+
"tsx": "^4.21.0",
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"vitest": "^4.0.16"
|
|
33
36
|
},
|
|
34
37
|
"dependencies": {
|
|
35
38
|
"@aws-sdk/client-s3": "^3.418.0",
|
|
36
|
-
"@
|
|
39
|
+
"@google-cloud/storage": "^7.15.2",
|
|
40
|
+
"@hono/node-server": "^1.19.8",
|
|
37
41
|
"@mapbox/sphericalmercator": "^1.2.0",
|
|
38
42
|
"@mapbox/tilebelt": "^1.0.2",
|
|
39
|
-
"@maplibre/maplibre-gl-native": "^
|
|
40
|
-
"@maplibre/maplibre-gl-style-spec": "^
|
|
41
|
-
"better-sqlite3": "
|
|
43
|
+
"@maplibre/maplibre-gl-native": "^6.3.0",
|
|
44
|
+
"@maplibre/maplibre-gl-style-spec": "^24.4.1",
|
|
45
|
+
"better-sqlite3": "12.6.0",
|
|
42
46
|
"commander": "^11.0.0",
|
|
43
47
|
"file-system-cache": "^2.4.4",
|
|
44
48
|
"higuruma": "^0.1.6",
|
|
45
|
-
"hono": "^4.
|
|
49
|
+
"hono": "^4.11.3",
|
|
46
50
|
"lightning-pool": "^4.2.2",
|
|
47
51
|
"lru-cache": "^11.0.0",
|
|
48
|
-
"maplibre-gl": "^
|
|
52
|
+
"maplibre-gl": "^5.15.0",
|
|
49
53
|
"pmtiles": "^3.0.5",
|
|
50
|
-
"sharp": "^0.
|
|
54
|
+
"sharp": "^0.34.5"
|
|
51
55
|
}
|
|
52
56
|
}
|