chiitiler 1.20.3 → 1.21.1

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.
@@ -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,
@@ -54,6 +53,10 @@ async function getRenderedTile({ stylejson, z, x, y, tileSize, cache, margin, ex
54
53
  width: 512,
55
54
  height: 512,
56
55
  channels: 4,
56
+ // maplibre-native returns premultiplied alpha; tell sharp so it
57
+ // unpremultiplies to straight alpha, otherwise antialiased edges
58
+ // (e.g. white text-halo) come out gray on transparent backgrounds.
59
+ premultiplied: true,
57
60
  },
58
61
  }).resize(256, 256);
59
62
  }
@@ -63,6 +66,7 @@ async function getRenderedTile({ stylejson, z, x, y, tileSize, cache, margin, ex
63
66
  width: tileSize,
64
67
  height: tileSize,
65
68
  channels: 4,
69
+ premultiplied: true,
66
70
  },
67
71
  });
68
72
  }
@@ -72,6 +76,7 @@ async function getRenderedTile({ stylejson, z, x, y, tileSize, cache, margin, ex
72
76
  width: tileSize + margin,
73
77
  height: tileSize + margin,
74
78
  channels: 4,
79
+ premultiplied: true,
75
80
  },
76
81
  })
77
82
  .extract({
@@ -93,22 +98,23 @@ async function getRenderedTile({ stylejson, z, x, y, tileSize, cache, margin, ex
93
98
  }
94
99
  }
95
100
  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,
101
+ // bbox width/height as fractions of the whole world ([0, 1] mercator)
102
+ const x1 = mercX(bbox[0]);
103
+ const x2 = mercX(bbox[2]);
104
+ const y1 = mercY(bbox[3]); // north -> smaller y
105
+ const y2 = mercY(bbox[1]);
106
+ const dx = x2 - x1;
107
+ const dy = y2 - y1;
108
+ // maplibre-native's world is 512px wide at zoom 0, so at zoom z it is
109
+ // 512 * 2^z px. The zoom that fits the longer side into `size` px solves
110
+ // 512 * 2^z * max(dx, dy) = size.
111
+ const zoom = Math.log2(size / (512 * Math.max(dx, dy)));
112
+ const width = dx >= dy ? size : Math.ceil((dx / dy) * size);
113
+ const height = dx >= dy ? Math.ceil((dy / dx) * size) : size;
114
+ const center = [
115
+ (bbox[0] + bbox[2]) / 2, // longitude is linear in mercator x
116
+ invMercY((y1 + y2) / 2), // latitude: invert the mercator-space midpoint
110
117
  ];
111
- const center = mercator.ll(mercCenter, 25); // latlon
112
118
  return { zoom, width, height, center };
113
119
  };
114
120
  async function getRenderedClip({ stylejson, bbox, size, cache, ext, quality, }) {
@@ -125,6 +131,8 @@ async function getRenderedClip({ stylejson, bbox, size, cache, ext, quality, })
125
131
  width,
126
132
  height,
127
133
  channels: 4,
134
+ // see getRenderedTile: maplibre-native returns premultiplied alpha
135
+ premultiplied: true,
128
136
  },
129
137
  });
130
138
  switch (ext) {
@@ -152,6 +160,8 @@ async function getRenderedCamera(options) {
152
160
  width: options.width,
153
161
  height: options.height,
154
162
  channels: 4,
163
+ // see getRenderedTile: maplibre-native returns premultiplied alpha
164
+ premultiplied: true,
155
165
  },
156
166
  });
157
167
  switch (options.ext) {
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "chiitiler",
4
- "version": "1.20.3",
4
+ "version": "1.21.1",
5
5
  "description": "Tiny map rendering server for MapLibre Style Spec",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -26,7 +26,6 @@
26
26
  "devDependencies": {
27
27
  "@tsconfig/node24": "^24.0.4",
28
28
  "@types/autocannon": "^7.12.7",
29
- "@types/better-sqlite3": "^7.6.13",
30
29
  "@types/node": "^25.6.0",
31
30
  "@vitest/coverage-v8": "^4.1.4",
32
31
  "autocannon": "^8.0.0",
@@ -40,11 +39,9 @@
40
39
  "@aws-sdk/client-s3": "^3.1032.0",
41
40
  "@google-cloud/storage": "^7.19.0",
42
41
  "@hono/node-server": "^2.0.0",
43
- "@mapbox/sphericalmercator": "^2.0.2",
44
42
  "@mapbox/tilebelt": "^2.0.3",
45
43
  "@maplibre/maplibre-gl-native": "^6.4.1",
46
44
  "@maplibre/maplibre-gl-style-spec": "^24.8.1",
47
- "better-sqlite3": "12.9.0",
48
45
  "commander": "^14.0.3",
49
46
  "file-system-cache": "^2.4.7",
50
47
  "higuruma": "^0.1.6",