chiitiler 1.20.2 → 1.21.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 CHANGED
@@ -142,7 +142,7 @@ Chiitiler caches *source assets* (vector tiles, glyphs, sprites) — not final r
142
142
  ## Deployment
143
143
 
144
144
  - **Docker** — `ghcr.io/kanahiro/chiitiler:latest` (entrypoint: `tile-server`)
145
- - **Docker Compose** — see [`docker-compose.yml`](./docker-compose.yml) (includes MinIO + fake-gcs-server for local testing)
145
+ - **Docker Compose** — see [`docker-compose.yml`](./docker-compose.yml) (includes RustFS + fake-gcs-server for local testing)
146
146
  - **AWS Lambda** — ready-to-deploy CDK app in [`cdk/`](./cdk)
147
147
 
148
148
  ## Develop
@@ -1,8 +1,7 @@
1
1
  import sharp from 'sharp';
2
- import { SphericalMercator } from '@mapbox/sphericalmercator';
3
- const mercator = new SphericalMercator();
4
2
  import { LRUCache } from 'lru-cache';
5
3
  import { renderTile, render } from './rasterize.js';
4
+ import { mercX, mercY, invMercY } from './mercator.js';
6
5
  import { getSource } from '../source/index.js';
7
6
  const styleCache = new LRUCache({
8
7
  max: 5,
@@ -93,22 +92,23 @@ async function getRenderedTile({ stylejson, z, x, y, tileSize, cache, margin, ex
93
92
  }
94
93
  }
95
94
  const calcRenderingParams = (bbox, size) => {
96
- // reference: https://github.com/maptiler/tileserver-gl/blob/cc4b8f7954069fd0e1db731ff07f5349f7b9c8cd/src/serve_rendered.js#L346
97
- // very hacky and it might be wrong
98
- let zoom = 25;
99
- const minCorner = mercator.px([bbox[0], bbox[3]], zoom);
100
- const maxCorner = mercator.px([bbox[2], bbox[1]], zoom);
101
- const dx = maxCorner[0] - minCorner[0];
102
- const dy = maxCorner[1] - minCorner[1];
103
- zoom -= Math.max(Math.log(dx / size), Math.log(dy / size)) / Math.LN2;
104
- zoom = Math.max(Math.log(size / 256) / Math.LN2, Math.min(25, zoom)) - 1;
105
- const width = dx > dy ? size : Math.ceil((dx / dy) * size);
106
- const height = dx > dy ? Math.ceil((dy / dx) * size) : size;
107
- const mercCenter = [
108
- (maxCorner[0] + minCorner[0]) / 2,
109
- (maxCorner[1] + minCorner[1]) / 2,
95
+ // bbox width/height as fractions of the whole world ([0, 1] mercator)
96
+ const x1 = mercX(bbox[0]);
97
+ const x2 = mercX(bbox[2]);
98
+ const y1 = mercY(bbox[3]); // north -> smaller y
99
+ const y2 = mercY(bbox[1]);
100
+ const dx = x2 - x1;
101
+ const dy = y2 - y1;
102
+ // maplibre-native's world is 512px wide at zoom 0, so at zoom z it is
103
+ // 512 * 2^z px. The zoom that fits the longer side into `size` px solves
104
+ // 512 * 2^z * max(dx, dy) = size.
105
+ const zoom = Math.log2(size / (512 * Math.max(dx, dy)));
106
+ const width = dx >= dy ? size : Math.ceil((dx / dy) * size);
107
+ const height = dx >= dy ? Math.ceil((dy / dx) * size) : size;
108
+ const center = [
109
+ (bbox[0] + bbox[2]) / 2, // longitude is linear in mercator x
110
+ invMercY((y1 + y2) / 2), // latitude: invert the mercator-space midpoint
110
111
  ];
111
- const center = mercator.ll(mercCenter, 25); // latlon
112
112
  return { zoom, width, height, center };
113
113
  };
114
114
  async function getRenderedClip({ stylejson, bbox, size, cache, ext, quality, }) {
@@ -0,0 +1,5 @@
1
+ declare const mercX: (lon: number) => number;
2
+ declare const mercY: (lat: number) => number;
3
+ declare const invMercX: (x: number) => number;
4
+ declare const invMercY: (y: number) => number;
5
+ export { mercX, mercY, invMercX, invMercY };
@@ -0,0 +1,8 @@
1
+ const DEG2RAD = Math.PI / 180;
2
+ // lon/lat <-> world coords normalized to [0, 1] (Web Mercator).
3
+ // north is small y; antimeridian is x=0/1.
4
+ const mercX = (lon) => (lon + 180) / 360;
5
+ const mercY = (lat) => 0.5 - Math.log(Math.tan(Math.PI / 4 + (lat * DEG2RAD) / 2)) / (2 * Math.PI);
6
+ const invMercX = (x) => x * 360 - 180;
7
+ const invMercY = (y) => (2 * Math.atan(Math.exp((0.5 - y) * 2 * Math.PI)) - Math.PI / 2) / DEG2RAD;
8
+ export { mercX, mercY, invMercX, invMercY };
@@ -1,13 +1,10 @@
1
- import { SphericalMercator } from '@mapbox/sphericalmercator';
2
1
  import { getRenderPool } from './pool.js';
3
- function getTileCenter(z, x, y, tileSize = 256) {
4
- const mercator = new SphericalMercator({
5
- size: tileSize,
6
- });
7
- const px = tileSize / 2 + x * tileSize;
8
- const py = tileSize / 2 + y * tileSize;
9
- const tileCenter = mercator.ll([px, py], z);
10
- return tileCenter;
2
+ import { invMercX, invMercY } from './mercator.js';
3
+ function getTileCenter(z, x, y) {
4
+ // tile (x, y) center as a world fraction is (x + 0.5) / 2^z, independent
5
+ // of tile pixel size; convert that back to lon/lat.
6
+ const n = 2 ** z;
7
+ return [invMercX((x + 0.5) / n), invMercY((y + 0.5) / n)];
11
8
  }
12
9
  async function render(style, renderOptions, cache, mode) {
13
10
  const pool = await getRenderPool(style, cache, mode);
@@ -56,7 +53,7 @@ async function renderTile(style, z, x, y, options) {
56
53
  zoom: renderingParams.zoom,
57
54
  width: renderingParams.width + (options.margin ?? 0),
58
55
  height: renderingParams.height + (options.margin ?? 0),
59
- center: getTileCenter(z, x, y, options.tileSize),
56
+ center: getTileCenter(z, x, y),
60
57
  bearing: 0,
61
58
  pitch: 0,
62
59
  }, options.cache, tileMode ? 'tile' : 'static');
@@ -115,8 +115,8 @@ declare function createCameraRouter(options: {
115
115
  };
116
116
  };
117
117
  output: "failed to render static image";
118
- outputFormat: "body";
119
- status: 400;
118
+ outputFormat: "text";
119
+ status: 500;
120
120
  };
121
121
  };
122
122
  } & {
@@ -214,8 +214,8 @@ declare function createCameraRouter(options: {
214
214
  };
215
215
  };
216
216
  output: "failed to render static image";
217
- outputFormat: "body";
218
- status: 400;
217
+ outputFormat: "text";
218
+ status: 500;
219
219
  } | {
220
220
  input: {
221
221
  param: {
@@ -68,7 +68,9 @@ function createCameraRouter(options) {
68
68
  }
69
69
  catch (e) {
70
70
  console.error(`render error: ${e}`);
71
- return c.body('failed to render static image', 400);
71
+ // c.text overrides the image/* Content-Type set above; a render
72
+ // failure is a server-side error so respond 500, not 400.
73
+ return c.text('failed to render static image', 500);
72
74
  }
73
75
  })
74
76
  .post('/:zoom/:lat/:lon/:bearing/:pitch/:dimensions_ext', async (c) => {
@@ -122,7 +124,9 @@ function createCameraRouter(options) {
122
124
  }
123
125
  catch (e) {
124
126
  console.error(`render error: ${e}`);
125
- return c.body('failed to render static image', 400);
127
+ // c.text overrides the image/* Content-Type set above; a render
128
+ // failure is a server-side error so respond 500, not 400.
129
+ return c.text('failed to render static image', 500);
126
130
  }
127
131
  });
128
132
  return camera;
@@ -37,8 +37,8 @@ declare function createClipRouter(options: {
37
37
  };
38
38
  };
39
39
  output: "failed to render tile";
40
- outputFormat: "body";
41
- status: 400;
40
+ outputFormat: "text";
41
+ status: 500;
42
42
  } | {
43
43
  input: {
44
44
  param: {
@@ -95,8 +95,8 @@ declare function createClipRouter(options: {
95
95
  };
96
96
  };
97
97
  output: "failed to render tile";
98
- outputFormat: "body";
99
- status: 400;
98
+ outputFormat: "text";
99
+ status: 500;
100
100
  } | {
101
101
  input: {
102
102
  param: {
@@ -37,7 +37,9 @@ function createClipRouter(options) {
37
37
  }
38
38
  catch (e) {
39
39
  console.error(`render error: ${e}`);
40
- return c.body('failed to render tile', 400);
40
+ // c.text overrides the image/* Content-Type set above; a render
41
+ // failure is a server-side error so respond 500, not 400.
42
+ return c.text('failed to render tile', 500);
41
43
  }
42
44
  })
43
45
  .post('/:filename_ext', async (c) => {
@@ -76,7 +78,9 @@ function createClipRouter(options) {
76
78
  }
77
79
  catch (e) {
78
80
  console.error(`render error: ${e}`);
79
- return c.body('failed to render tile', 400);
81
+ // c.text overrides the image/* Content-Type set above; a render
82
+ // failure is a server-side error so respond 500, not 400.
83
+ return c.text('failed to render tile', 500);
80
84
  }
81
85
  });
82
86
  return clip;
@@ -53,8 +53,8 @@ declare function createTilesRouter(options: {
53
53
  };
54
54
  };
55
55
  output: "failed to render tile";
56
- outputFormat: "body";
57
- status: 400;
56
+ outputFormat: "text";
57
+ status: 500;
58
58
  } | {
59
59
  input: {
60
60
  param: {
@@ -109,8 +109,8 @@ declare function createTilesRouter(options: {
109
109
  };
110
110
  };
111
111
  output: "failed to render tile";
112
- outputFormat: "body";
113
- status: 400;
112
+ outputFormat: "text";
113
+ status: 500;
114
114
  } | {
115
115
  input: {
116
116
  param: {
@@ -45,7 +45,9 @@ function createTilesRouter(options) {
45
45
  }
46
46
  catch (e) {
47
47
  console.error(`render error: ${e}`);
48
- return c.body('failed to render tile', 400);
48
+ // c.text overrides the image/* Content-Type set above; a render
49
+ // failure is a server-side error so respond 500, not 400.
50
+ return c.text('failed to render tile', 500);
49
51
  }
50
52
  })
51
53
  .post('/:z/:x/:y_ext', async (c) => {
@@ -85,7 +87,9 @@ function createTilesRouter(options) {
85
87
  }
86
88
  catch (e) {
87
89
  console.error(`render error: ${e}`);
88
- return c.body('failed to render tile', 400);
90
+ // c.text overrides the image/* Content-Type set above; a render
91
+ // failure is a server-side error so respond 500, not 400.
92
+ return c.text('failed to render tile', 500);
89
93
  }
90
94
  });
91
95
  return tiles;
@@ -1,8 +1,12 @@
1
- import Database from 'better-sqlite3';
1
+ import { DatabaseSync } from 'node:sqlite';
2
2
  import { unzip } from 'zlib';
3
3
  import { LRUCache } from 'lru-cache';
4
+ // A StatementSync is only valid while its DatabaseSync is alive, and node:sqlite
5
+ // does not guarantee the statement keeps the database from being GC'd. Cache the
6
+ // database alongside the statement so both share the entry's lifetime.
4
7
  const mbtilesCache = new LRUCache({
5
8
  max: 20,
9
+ dispose: ({ db }) => db.close(),
6
10
  });
7
11
  /**
8
12
  * uri = mbtiles://path/to/file.mbtiles/{z}/{x}/{y}
@@ -11,12 +15,14 @@ async function getMbtilesSource(uri) {
11
15
  const mbtilesFilepath = uri
12
16
  .replace('mbtiles://', '')
13
17
  .replace(/\/\d+\/\d+\/\d+$/, '');
14
- let statement = mbtilesCache.get(mbtilesFilepath);
15
- if (statement === undefined) {
16
- const db = new Database(mbtilesFilepath, { readonly: true });
17
- statement = db.prepare('SELECT tile_data FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?');
18
- mbtilesCache.set(mbtilesFilepath, statement);
18
+ let entry = mbtilesCache.get(mbtilesFilepath);
19
+ if (entry === undefined) {
20
+ const db = new DatabaseSync(mbtilesFilepath, { readOnly: true });
21
+ const statement = db.prepare('SELECT tile_data FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?');
22
+ entry = { db, statement };
23
+ mbtilesCache.set(mbtilesFilepath, entry);
19
24
  }
25
+ const { statement } = entry;
20
26
  const [z, x, y] = uri
21
27
  .replace(`mbtiles://${mbtilesFilepath}/`, '')
22
28
  .split('/');
package/package.json CHANGED
@@ -1,56 +1,55 @@
1
1
  {
2
- "type": "module",
3
- "name": "chiitiler",
4
- "version": "1.20.2",
5
- "description": "Tiny map rendering server for MapLibre Style Spec",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "files": [
9
- "dist"
10
- ],
11
- "scripts": {
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",
14
- "dev": "tsx watch src/main.ts tile-server -D",
15
- "test:unit": "vitest src",
16
- "test:coverage": "vitest src --coverage --coverage.provider=v8",
17
- "test:integration": "vitest tests/integration.test.ts",
18
- "test:benchmark": "vitest bench"
19
- },
20
- "keywords": [],
21
- "author": "Kanahiro Iguchi",
22
- "license": "MIT",
23
- "repository": {
24
- "url": "https://github.com/Kanahiro/chiitiler"
25
- },
26
- "devDependencies": {
27
- "@tsconfig/node24": "^24.0.4",
28
- "@types/better-sqlite3": "^7.6.13",
29
- "@types/node": "^25.6.0",
30
- "@vitest/coverage-v8": "^4.1.4",
31
- "esbuild": "^0.28.0",
32
- "image-size": "^2.0.2",
33
- "tsx": "^4.21.0",
34
- "typescript": "^6.0.3",
35
- "vitest": "^4.1.4"
36
- },
37
- "dependencies": {
38
- "@aws-sdk/client-s3": "^3.1032.0",
39
- "@google-cloud/storage": "^7.19.0",
40
- "@hono/node-server": "^2.0.0",
41
- "@mapbox/sphericalmercator": "^2.0.2",
42
- "@mapbox/tilebelt": "^2.0.3",
43
- "@maplibre/maplibre-gl-native": "^6.4.1",
44
- "@maplibre/maplibre-gl-style-spec": "^24.8.1",
45
- "better-sqlite3": "12.9.0",
46
- "commander": "^14.0.3",
47
- "file-system-cache": "^2.4.7",
48
- "higuruma": "^0.1.6",
49
- "hono": "^4.12.14",
50
- "lightning-pool": "^4.12.0",
51
- "lru-cache": "^11.3.5",
52
- "maplibre-gl": "^5.23.0",
53
- "pmtiles": "^4.4.1",
54
- "sharp": "^0.34.5"
55
- }
2
+ "type": "module",
3
+ "name": "chiitiler",
4
+ "version": "1.21.0",
5
+ "description": "Tiny map rendering server for MapLibre Style Spec",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
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",
14
+ "dev": "tsx watch src/main.ts tile-server -D",
15
+ "test:unit": "vitest src",
16
+ "test:coverage": "vitest src --coverage --coverage.provider=v8",
17
+ "test:integration": "vitest tests/integration.test.ts",
18
+ "test:benchmark": "tsx tests/benchmark.ts"
19
+ },
20
+ "keywords": [],
21
+ "author": "Kanahiro Iguchi",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "url": "https://github.com/Kanahiro/chiitiler"
25
+ },
26
+ "devDependencies": {
27
+ "@tsconfig/node24": "^24.0.4",
28
+ "@types/autocannon": "^7.12.7",
29
+ "@types/node": "^25.6.0",
30
+ "@vitest/coverage-v8": "^4.1.4",
31
+ "autocannon": "^8.0.0",
32
+ "esbuild": "^0.28.0",
33
+ "image-size": "^2.0.2",
34
+ "tsx": "^4.21.0",
35
+ "typescript": "^6.0.3",
36
+ "vitest": "^4.1.4"
37
+ },
38
+ "dependencies": {
39
+ "@aws-sdk/client-s3": "^3.1032.0",
40
+ "@google-cloud/storage": "^7.19.0",
41
+ "@hono/node-server": "^2.0.0",
42
+ "@mapbox/tilebelt": "^2.0.3",
43
+ "@maplibre/maplibre-gl-native": "^6.4.1",
44
+ "@maplibre/maplibre-gl-style-spec": "^24.8.1",
45
+ "commander": "^14.0.3",
46
+ "file-system-cache": "^2.4.7",
47
+ "higuruma": "^0.1.6",
48
+ "hono": "^4.12.14",
49
+ "lightning-pool": "^4.12.0",
50
+ "lru-cache": "^11.3.5",
51
+ "maplibre-gl": "^5.23.0",
52
+ "pmtiles": "^4.4.1",
53
+ "sharp": "^0.34.5"
54
+ }
56
55
  }