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.
@@ -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,2 @@
1
+ declare function getGCSSource(uri: string): Promise<Buffer<ArrayBufferLike> | null>;
2
+ export { getGCSSource };
@@ -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 };
@@ -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://'))
@@ -50,8 +50,7 @@ class S3Source {
50
50
  }));
51
51
  }
52
52
  catch (e) {
53
- if (e instanceof Error &&
54
- e.name === 'PreconditionFailed') {
53
+ if (e instanceof Error && e.name === 'PreconditionFailed') {
55
54
  throw new Error('etag mismatch');
56
55
  }
57
56
  throw e;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "chiitiler",
4
- "version": "1.14.2",
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/node20": "^20.1.4",
24
- "@types/better-sqlite3": "^7.6.10",
25
- "@types/node": "^20.6.0",
26
- "@vitest/coverage-v8": "^1.6.0",
27
- "esbuild": "^0.25.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
- "ts-node": "^10.9.1",
30
- "tsx": "^4.7.1",
31
- "typescript": "^5.2.2",
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
- "@hono/node-server": "^1.12.0",
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": "^5.4.0",
40
- "@maplibre/maplibre-gl-style-spec": "^20.2.0",
41
- "better-sqlite3": "^9.6.0",
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.5.4",
49
+ "hono": "^4.11.3",
46
50
  "lightning-pool": "^4.2.2",
47
51
  "lru-cache": "^11.0.0",
48
- "maplibre-gl": "^3.3.1",
52
+ "maplibre-gl": "^5.15.0",
49
53
  "pmtiles": "^3.0.5",
50
- "sharp": "^0.32.5"
54
+ "sharp": "^0.34.5"
51
55
  }
52
56
  }