tileserver-gl 5.6.1-pre.0 → 5.7.0-pre.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.
- package/CHANGELOG.md +14 -1
- package/docs/config.rst +43 -2
- package/docs/usage.rst +2 -1
- package/package.json +30 -24
- package/src/main.js +62 -7
- package/src/mbtiles_wrapper.js +9 -0
- package/src/metrics.js +70 -0
- package/src/pmtiles_adapter.js +44 -0
- package/src/render_mode.js +20 -0
- package/src/serve_data.js +65 -10
- package/src/serve_font.js +10 -0
- package/src/serve_rendered.js +463 -269
- package/src/serve_style.js +15 -0
- package/src/server.js +147 -36
- package/src/utils.js +51 -0
- package/test/data_decorator.js +156 -0
- package/test/fixtures/data-decorator-async.js +17 -0
- package/test/fixtures/data-decorator.js +29 -0
- package/test/fixtures/reload-config.json +34 -0
- package/test/fixtures/visual/diffs/static-bearing-pitch-diff.png +0 -0
- package/test/fixtures/visual/static-512px-path-alignment-2x.png +0 -0
- package/test/metrics.js +102 -0
- package/test/reload.js +98 -0
- package/test/render_mode.js +27 -0
- package/test/sparse.js +411 -0
- package/test/static_images.js +5 -0
- package/test/style.js +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,9 +7,22 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
-
## 5.
|
|
10
|
+
## 5.7.0-pre.1
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
- feat: add opt-in Prometheus metrics endpoint ([#2211](https://github.com/maptiler/tileserver-gl/pull/2211)) (by [navidnabavi](https://github.com/navidnabavi))
|
|
13
|
+
- feat: allow the data decorator to be asynchronous ([#2352](https://github.com/maptiler/tileserver-gl/pull/2352)) (by [acalcutt](https://github.com/acalcutt))
|
|
14
|
+
|
|
11
15
|
### 🐞 Bug fixes
|
|
12
16
|
- fix: TypeError when style source value is a string (e.g. sprite path) ([#2179](https://github.com/maptiler/tileserver-gl/pull/2179)) (by [app/copilot-swe-agent](https://github.com/app/copilot-swe-agent))
|
|
17
|
+
- fix: clean stale tile-source state on SIGHUP reload ([#2158](https://github.com/maptiler/tileserver-gl/pull/2158)) (by [bvitlas](https://github.com/bvitlas))
|
|
18
|
+
- fix: correctly handle public_url in wmts endpoint ([#2205](https://github.com/maptiler/tileserver-gl/pull/2205)) (by [andrewlaguna824](https://github.com/andrewlaguna824))
|
|
19
|
+
- fix: never leave a renderer request or pool slot unsettled ([#2347](https://github.com/maptiler/tileserver-gl/pull/2347)) (by [acalcutt](https://github.com/acalcutt))
|
|
20
|
+
- fix: honour sparse set in archive metadata ([#2350](https://github.com/maptiler/tileserver-gl/pull/2350)) (by [acalcutt](https://github.com/acalcutt))
|
|
21
|
+
- fix: data decorator never ran on Windows, or on PMTiles style sources ([#2351](https://github.com/maptiler/tileserver-gl/pull/2351)) (by [acalcutt](https://github.com/acalcutt))
|
|
22
|
+
- chore: drop unused host-validation imports from server.js ([#2349](https://github.com/maptiler/tileserver-gl/pull/2349)) (by [acalcutt](https://github.com/acalcutt))
|
|
23
|
+
- fix: restore the format-based sparse default ([#2348](https://github.com/maptiler/tileserver-gl/pull/2348)) (by [acalcutt](https://github.com/acalcutt))
|
|
24
|
+
- fix: align 512px static map overlays ([#2344](https://github.com/maptiler/tileserver-gl/pull/2344)) (by [miakh](https://github.com/miakh))
|
|
25
|
+
- fix: allow native install scripts under npm 12 so canvas builds ([#2343](https://github.com/maptiler/tileserver-gl/pull/2343)) (by [mikey923](https://github.com/mikey923))
|
|
13
26
|
|
|
14
27
|
## 5.6.0
|
|
15
28
|
### ✨ Features and improvements
|
package/docs/config.rst
CHANGED
|
@@ -214,6 +214,41 @@ Allows the rendering of inline marker icons or base64 urls.
|
|
|
214
214
|
For security reasons only allow this if you can control the origins from where the markers are fetched!
|
|
215
215
|
Not used by default.
|
|
216
216
|
|
|
217
|
+
``dataDecorator``
|
|
218
|
+
-----------------
|
|
219
|
+
|
|
220
|
+
Path (relative to ``root``) to a JavaScript module whose default export is called to modify tile data and TileJSON on the fly. Not used by default.
|
|
221
|
+
|
|
222
|
+
The module must be an ES module with a default export. The function is called as ``decorate(id, type, data, z, x, y)`` and **must return the value**, modified or not - returning nothing discards the data.
|
|
223
|
+
|
|
224
|
+
The function may be ``async``, or return a promise. The return value is awaited, so the decorator can do asynchronous work such as a lookup or a fetch. It is awaited on every call, including once per tile for ``'data'``, so keep it fast or cache inside the module.
|
|
225
|
+
|
|
226
|
+
``id``
|
|
227
|
+
The data source id from the config, or the source name from the style.
|
|
228
|
+
|
|
229
|
+
``type``
|
|
230
|
+
Either ``'tilejson'`` or ``'data'``.
|
|
231
|
+
|
|
232
|
+
``data``
|
|
233
|
+
For ``'tilejson'``, the TileJSON object served at ``/data/{id}.json``, or a style's local source object. For ``'data'``, a ``Buffer`` of vector tile bytes, already un-gzipped.
|
|
234
|
+
|
|
235
|
+
``z``, ``x``, ``y``
|
|
236
|
+
Tile coordinates. Only passed for ``'data'``.
|
|
237
|
+
|
|
238
|
+
``'data'`` is called for vector (pbf) sources only, both for ``/data/{id}/{z}/{x}/{y}.pbf`` and for the tiles fed to the raster renderer. Raster tiles are never passed through it.
|
|
239
|
+
|
|
240
|
+
.. code-block:: js
|
|
241
|
+
|
|
242
|
+
// decorator.js
|
|
243
|
+
export default function (id, type, data, z, x, y) {
|
|
244
|
+
if (type === 'tilejson') {
|
|
245
|
+
data.attribution = 'Example attribution';
|
|
246
|
+
}
|
|
247
|
+
return data;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
Errors loading the module are logged and the server starts without a decorator, so check the startup output if it does not appear to run.
|
|
251
|
+
|
|
217
252
|
|
|
218
253
|
``styles``
|
|
219
254
|
==========
|
|
@@ -294,8 +329,14 @@ Here are the available options for each data source:
|
|
|
294
329
|
* ``true`` - Returns HTTP 404, allowing clients like MapLibre to overzoom and use parent tiles. Use this for terrain or datasets with uneven zoom coverage.
|
|
295
330
|
* ``false`` - Returns HTTP 204 (No Content), signaling an intentionally empty tile and preventing overzoom.
|
|
296
331
|
|
|
297
|
-
|
|
298
|
-
|
|
332
|
+
Resolved from the first of these that is set, highest priority first:
|
|
333
|
+
|
|
334
|
+
1. ``sparse`` on the data source in this config file
|
|
335
|
+
2. ``sparse`` in the top-level ``options``
|
|
336
|
+
3. ``sparse`` in the archive's own metadata (the MBTiles ``metadata`` table, or the PMTiles metadata JSON)
|
|
337
|
+
4. The tile format - ``false`` for vector tiles (pbf), ``true`` for raster tiles (png, webp, jpg, etc.)
|
|
338
|
+
|
|
339
|
+
Config outranks archive metadata, so a source that declares the wrong value for your deployment can be corrected without rewriting the file. MBTiles metadata values are text; ``"true"``/``"false"`` and ``"1"``/``"0"`` are both understood, and anything else is ignored rather than guessed at.
|
|
299
340
|
|
|
300
341
|
``s3Profile`` (string)
|
|
301
342
|
Specifies the AWS credential profile to use for S3 PMTiles sources. The profile must be defined in your ``~/.aws/credentials`` file.
|
package/docs/usage.rst
CHANGED
|
@@ -119,7 +119,8 @@ TileServer GL can fetch tiles from remote HTTP/HTTPS sources referenced in your
|
|
|
119
119
|
|
|
120
120
|
**Default behavior:**
|
|
121
121
|
- Default timeout is 15 seconds (15000 milliseconds)
|
|
122
|
-
- If a remote tile request exceeds this timeout, an error is logged and
|
|
122
|
+
- If a remote tile request exceeds this timeout, an error is logged and the tile is treated as missing
|
|
123
|
+
- What "missing" means follows the source's ``sparse`` setting: raster sources are sparse by default, so MapLibre overzooms from a parent tile, while vector sources are not, so an empty tile is returned to the renderer. See ``sparse`` in :doc:`/config`.
|
|
123
124
|
|
|
124
125
|
**Tuning the timeout:**
|
|
125
126
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tileserver-gl",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.7.0-pre.1",
|
|
4
4
|
"description": "Map tile server for JSON GL styles - vector and server side generated raster tiles",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -37,23 +37,23 @@
|
|
|
37
37
|
"docker": "docker build . && docker run --rm -i -p 8080:8080 $(docker build -q .)"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@aws-sdk/client-s3": "^3.
|
|
41
|
-
"@jsse/pbfont": "^0.3.
|
|
40
|
+
"@aws-sdk/client-s3": "^3.1081.0",
|
|
41
|
+
"@jsse/pbfont": "^0.3.4",
|
|
42
42
|
"@mapbox/mapbox-gl-rtl-text": "0.4.0",
|
|
43
43
|
"@mapbox/mbtiles": "0.12.1",
|
|
44
44
|
"@mapbox/polyline": "^1.2.1",
|
|
45
45
|
"@mapbox/sphericalmercator": "2.0.2",
|
|
46
|
-
"@mapbox/vector-tile": "
|
|
46
|
+
"@mapbox/vector-tile": "3.0.0",
|
|
47
47
|
"@maplibre/maplibre-gl-inspect": "1.8.2",
|
|
48
48
|
"@maplibre/maplibre-gl-native": "6.4.1",
|
|
49
|
-
"@maplibre/maplibre-gl-style-spec": "24.
|
|
49
|
+
"@maplibre/maplibre-gl-style-spec": "24.10.0",
|
|
50
50
|
"@sindresorhus/fnv1a": "3.1.0",
|
|
51
51
|
"advanced-pool": "0.3.3",
|
|
52
52
|
"canvas": "3.2.3",
|
|
53
53
|
"chokidar": "5.0.0",
|
|
54
54
|
"clone": "2.1.2",
|
|
55
55
|
"color": "5.0.3",
|
|
56
|
-
"commander": "
|
|
56
|
+
"commander": "15.0.0",
|
|
57
57
|
"copyfiles": "2.4.1",
|
|
58
58
|
"cors": "2.8.6",
|
|
59
59
|
"express": "5.2.1",
|
|
@@ -62,35 +62,36 @@
|
|
|
62
62
|
"leaflet": "1.9.4",
|
|
63
63
|
"leaflet-hash": "0.2.1",
|
|
64
64
|
"maplibre-gl": "5.24.0",
|
|
65
|
-
"morgan": "1.
|
|
66
|
-
"pbf": "
|
|
65
|
+
"morgan": "1.11.0",
|
|
66
|
+
"pbf": "5.1.0",
|
|
67
67
|
"pmtiles": "4.4.1",
|
|
68
|
-
"proj4": "2.20.
|
|
68
|
+
"proj4": "2.20.9",
|
|
69
|
+
"prom-client": "^15.1.3",
|
|
69
70
|
"sanitize-filename": "1.6.4",
|
|
70
71
|
"secure-json-parse": "^4.1.0",
|
|
71
|
-
"semver": "^7.8.
|
|
72
|
-
"sharp": "0.
|
|
72
|
+
"semver": "^7.8.5",
|
|
73
|
+
"sharp": "0.35.3",
|
|
73
74
|
"tileserver-gl-styles": "2.0.0"
|
|
74
75
|
},
|
|
75
76
|
"devDependencies": {
|
|
76
|
-
"@commitlint/cli": "^21.0
|
|
77
|
-
"@commitlint/config-conventional": "^21.
|
|
77
|
+
"@commitlint/cli": "^21.2.0",
|
|
78
|
+
"@commitlint/config-conventional": "^21.2.0",
|
|
78
79
|
"@eslint/js": "^10.0.1",
|
|
79
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
80
|
-
"@typescript-eslint/parser": "^8.
|
|
80
|
+
"@typescript-eslint/eslint-plugin": "^8.63.0",
|
|
81
|
+
"@typescript-eslint/parser": "^8.61.1",
|
|
81
82
|
"chai": "6.2.2",
|
|
82
83
|
"cross-env": "^10.1.0",
|
|
83
|
-
"eslint": "^10.
|
|
84
|
+
"eslint": "^10.6.0",
|
|
84
85
|
"eslint-config-prettier": "^10.1.8",
|
|
85
|
-
"eslint-plugin-jsdoc": "^
|
|
86
|
-
"eslint-plugin-prettier": "^5.5.
|
|
87
|
-
"eslint-plugin-security": "^4.0.
|
|
88
|
-
"globals": "^17.
|
|
89
|
-
"lint-staged": "^17.0.
|
|
90
|
-
"mocha": "^11.7.
|
|
86
|
+
"eslint-plugin-jsdoc": "^63.0.12",
|
|
87
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
88
|
+
"eslint-plugin-security": "^4.0.1",
|
|
89
|
+
"globals": "^17.7.0",
|
|
90
|
+
"lint-staged": "^17.0.8",
|
|
91
|
+
"mocha": "^11.7.6",
|
|
91
92
|
"node-addon-api": "^8",
|
|
92
93
|
"pixelmatch": "^7.2.0",
|
|
93
|
-
"prettier": "^3.
|
|
94
|
+
"prettier": "^3.9.4",
|
|
94
95
|
"should": "^13.2.3",
|
|
95
96
|
"supertest": "^7.2.2",
|
|
96
97
|
"yaml-lint": "^1.7.0"
|
|
@@ -112,5 +113,10 @@
|
|
|
112
113
|
"bugs": {
|
|
113
114
|
"url": "https://github.com/maptiler/tileserver-gl/issues"
|
|
114
115
|
},
|
|
115
|
-
"homepage": "https://github.com/maptiler/tileserver-gl#readme"
|
|
116
|
+
"homepage": "https://github.com/maptiler/tileserver-gl#readme",
|
|
117
|
+
"allowScripts": {
|
|
118
|
+
"canvas": true,
|
|
119
|
+
"sqlite3": true,
|
|
120
|
+
"@maplibre/maplibre-gl-native": true
|
|
121
|
+
}
|
|
116
122
|
}
|
package/src/main.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
import os from 'os';
|
|
5
|
+
import { inspect } from 'node:util';
|
|
5
6
|
|
|
6
7
|
const envSize = parseInt(process.env.UV_THREADPOOL_SIZE, 10);
|
|
7
8
|
process.env.UV_THREADPOOL_SIZE = Math.ceil(
|
|
@@ -25,18 +26,63 @@ import { openMbTilesWrapper } from './mbtiles_wrapper.js';
|
|
|
25
26
|
// Global Error Handlers - Prevent server crashes from unhandled errors
|
|
26
27
|
// ============================================================================
|
|
27
28
|
|
|
29
|
+
// Keeping the process alive hides the failure unless the log says what broke,
|
|
30
|
+
// so report the stack, any cause chain, and how often this has happened. A
|
|
31
|
+
// repeating count is the signal that something is looping, not misfiring once.
|
|
32
|
+
const survivedErrorCounts = new Map();
|
|
33
|
+
const SURVIVED_ERROR_KEYS_MAX = 500;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Renders a thrown value as text, following up to `depth` levels of `cause`.
|
|
37
|
+
* @param {unknown} err - The thrown value or rejection reason.
|
|
38
|
+
* @param {number} [depth] - Remaining cause levels to follow.
|
|
39
|
+
* @returns {string} A printable description of the error.
|
|
40
|
+
*/
|
|
41
|
+
function describeError(err, depth = 3) {
|
|
42
|
+
if (!(err instanceof Error)) {
|
|
43
|
+
return `Non-Error value: ${inspect(err, { depth: 3 })}`;
|
|
44
|
+
}
|
|
45
|
+
let text = err.stack || `${err.name}: ${err.message}`;
|
|
46
|
+
if (err.cause !== undefined && depth > 0) {
|
|
47
|
+
text += `\nCaused by: ${describeError(err.cause, depth - 1)}`;
|
|
48
|
+
}
|
|
49
|
+
return text;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Reports an error the process deliberately survived.
|
|
54
|
+
* @param {string} kind - The handler that fired.
|
|
55
|
+
* @param {unknown} err - The thrown value or rejection reason.
|
|
56
|
+
* @returns {void}
|
|
57
|
+
*/
|
|
58
|
+
function reportSurvivedError(kind, err) {
|
|
59
|
+
const detail = describeError(err);
|
|
60
|
+
const key = `${kind} :: ${err instanceof Error ? err.message : detail}`;
|
|
61
|
+
|
|
62
|
+
let occurrence = '';
|
|
63
|
+
if (survivedErrorCounts.has(key)) {
|
|
64
|
+
const count = survivedErrorCounts.get(key) + 1;
|
|
65
|
+
survivedErrorCounts.set(key, count);
|
|
66
|
+
occurrence = ` (occurrence ${count})`;
|
|
67
|
+
} else if (survivedErrorCounts.size < SURVIVED_ERROR_KEYS_MAX) {
|
|
68
|
+
// Bounded: distinct messages can carry tile coordinates or URLs, and an
|
|
69
|
+
// unbounded tally of them would be a leak of its own.
|
|
70
|
+
survivedErrorCounts.set(key, 1);
|
|
71
|
+
occurrence = ' (occurrence 1)';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.error(`[${kind}] server continuing after error${occurrence}:`);
|
|
75
|
+
console.error(detail);
|
|
76
|
+
}
|
|
77
|
+
|
|
28
78
|
// Prevent unhandled promise rejections from crashing the server
|
|
29
|
-
process.on('unhandledRejection', (reason
|
|
30
|
-
|
|
31
|
-
console.error('Reason:', reason);
|
|
32
|
-
// Don't exit - keep server running
|
|
79
|
+
process.on('unhandledRejection', (reason) => {
|
|
80
|
+
reportSurvivedError('unhandledRejection', reason);
|
|
33
81
|
});
|
|
34
82
|
|
|
35
83
|
// Prevent uncaught exceptions from crashing the server
|
|
36
84
|
process.on('uncaughtException', (error) => {
|
|
37
|
-
|
|
38
|
-
console.error('Stack:', error.stack);
|
|
39
|
-
// Don't exit - keep server running
|
|
85
|
+
reportSurvivedError('uncaughtException', error);
|
|
40
86
|
});
|
|
41
87
|
|
|
42
88
|
// ============================================================================
|
|
@@ -113,6 +159,10 @@ program
|
|
|
113
159
|
'--ignore-missing-files',
|
|
114
160
|
'Continue startup even if configured mbtiles/pmtiles files are missing',
|
|
115
161
|
)
|
|
162
|
+
.option(
|
|
163
|
+
'--metrics',
|
|
164
|
+
'Enable Prometheus metrics endpoint (env: TILESERVER_GL_METRICS)',
|
|
165
|
+
)
|
|
116
166
|
.version(packageJson.version, '-v, --version');
|
|
117
167
|
program.parse(process.argv);
|
|
118
168
|
const opts = program.opts();
|
|
@@ -166,6 +216,11 @@ const startServer = (configPath, config) => {
|
|
|
166
216
|
publicUrl,
|
|
167
217
|
allowedHosts,
|
|
168
218
|
ignoreMissingFiles: opts.ignoreMissingFiles,
|
|
219
|
+
metrics: opts.metrics || process.env.TILESERVER_GL_METRICS === 'true',
|
|
220
|
+
metricsPort: (() => {
|
|
221
|
+
const port = parseInt(process.env.METRICS_PORT, 10);
|
|
222
|
+
return !isNaN(port) && port > 0 ? port : 9090;
|
|
223
|
+
})(),
|
|
169
224
|
});
|
|
170
225
|
};
|
|
171
226
|
|
package/src/mbtiles_wrapper.js
CHANGED
|
@@ -8,6 +8,7 @@ class MBTilesWrapper {
|
|
|
8
8
|
constructor(mbtiles) {
|
|
9
9
|
this._mbtiles = mbtiles;
|
|
10
10
|
this._getInfoP = util.promisify(mbtiles.getInfo.bind(mbtiles));
|
|
11
|
+
this._closeP = util.promisify(mbtiles.close.bind(mbtiles));
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -25,6 +26,14 @@ class MBTilesWrapper {
|
|
|
25
26
|
getInfo() {
|
|
26
27
|
return this._getInfoP();
|
|
27
28
|
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Closes the underlying MBTiles database handle.
|
|
32
|
+
* @returns {Promise<void>} A promise that resolves when the database is closed.
|
|
33
|
+
*/
|
|
34
|
+
close() {
|
|
35
|
+
return this._closeP();
|
|
36
|
+
}
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
/**
|
package/src/metrics.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// src/metrics.js
|
|
2
|
+
import {
|
|
3
|
+
Registry,
|
|
4
|
+
Counter,
|
|
5
|
+
Histogram,
|
|
6
|
+
Gauge,
|
|
7
|
+
collectDefaultMetrics,
|
|
8
|
+
} from 'prom-client';
|
|
9
|
+
|
|
10
|
+
export const registry = new Registry();
|
|
11
|
+
|
|
12
|
+
collectDefaultMetrics({ register: registry });
|
|
13
|
+
|
|
14
|
+
export const httpRequestsTotal = new Counter({
|
|
15
|
+
name: 'tileserver_http_requests_total',
|
|
16
|
+
help: 'Total number of HTTP requests',
|
|
17
|
+
labelNames: ['method', 'route', 'status_code'],
|
|
18
|
+
registers: [registry],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const httpRequestDuration = new Histogram({
|
|
22
|
+
name: 'tileserver_http_request_duration_seconds',
|
|
23
|
+
help: 'HTTP request duration in seconds',
|
|
24
|
+
labelNames: ['method', 'route', 'status_code'],
|
|
25
|
+
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
|
|
26
|
+
registers: [registry],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const tilesServedTotal = new Counter({
|
|
30
|
+
name: 'tileserver_tiles_served_total',
|
|
31
|
+
help: 'Total number of tiles served',
|
|
32
|
+
labelNames: ['type', 'name'],
|
|
33
|
+
registers: [registry],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const tileRenderDuration = new Histogram({
|
|
37
|
+
name: 'tileserver_tile_render_duration_seconds',
|
|
38
|
+
help: 'Tile pipeline duration in seconds (includes pool wait + render + encode)',
|
|
39
|
+
labelNames: ['name', 'zoom'],
|
|
40
|
+
buckets: [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
|
|
41
|
+
registers: [registry],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const tileErrorsTotal = new Counter({
|
|
45
|
+
name: 'tileserver_tile_errors_total',
|
|
46
|
+
help: 'Total number of tile errors',
|
|
47
|
+
labelNames: ['type', 'name'],
|
|
48
|
+
registers: [registry],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const renderPoolSize = new Gauge({
|
|
52
|
+
name: 'tileserver_render_pool_size',
|
|
53
|
+
help: 'Total objects in render pool',
|
|
54
|
+
labelNames: ['name'],
|
|
55
|
+
registers: [registry],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const renderPoolActive = new Gauge({
|
|
59
|
+
name: 'tileserver_render_pool_active',
|
|
60
|
+
help: 'Active (borrowed) objects in render pool',
|
|
61
|
+
labelNames: ['name'],
|
|
62
|
+
registers: [registry],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const renderPoolWaiting = new Gauge({
|
|
66
|
+
name: 'tileserver_render_pool_waiting',
|
|
67
|
+
help: 'Clients waiting for a render pool object',
|
|
68
|
+
labelNames: ['name'],
|
|
69
|
+
registers: [registry],
|
|
70
|
+
});
|
package/src/pmtiles_adapter.js
CHANGED
|
@@ -292,6 +292,23 @@ class PMTilesFileSource {
|
|
|
292
292
|
);
|
|
293
293
|
return { data: ab };
|
|
294
294
|
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Closes the underlying file descriptor for local PMTiles sources.
|
|
298
|
+
* @returns {void}
|
|
299
|
+
*/
|
|
300
|
+
close() {
|
|
301
|
+
if (typeof this.fd === 'number') {
|
|
302
|
+
const fd = this.fd;
|
|
303
|
+
try {
|
|
304
|
+
fs.closeSync(fd);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.warn(`Failed to close PMTiles file descriptor ${fd}:`, err);
|
|
307
|
+
} finally {
|
|
308
|
+
this.fd = null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
295
312
|
}
|
|
296
313
|
|
|
297
314
|
/**
|
|
@@ -315,6 +332,22 @@ async function readFileBytes(fd, buffer, offset) {
|
|
|
315
332
|
// Cache for PMTiles objects to avoid creating multiple instances for the same URL
|
|
316
333
|
const pmtilesCache = new Map();
|
|
317
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Closes a PMTiles instance if it owns a closeable local file source.
|
|
337
|
+
* @param {PMTiles} pmtiles - The PMTiles instance to close.
|
|
338
|
+
* @returns {void}
|
|
339
|
+
*/
|
|
340
|
+
function closePMTiles(pmtiles) {
|
|
341
|
+
if (!pmtiles) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const source = pmtiles.source;
|
|
346
|
+
if (source && typeof source.close === 'function') {
|
|
347
|
+
source.close();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
318
351
|
/**
|
|
319
352
|
* Opens a PMTiles file from local filesystem, HTTP URL, or S3 URL.
|
|
320
353
|
* Uses caching to avoid creating multiple PMTiles instances for the same file.
|
|
@@ -388,6 +421,17 @@ export function openPMtiles(
|
|
|
388
421
|
return pmtiles;
|
|
389
422
|
}
|
|
390
423
|
|
|
424
|
+
/**
|
|
425
|
+
* Clears the PMTiles cache and closes any local file descriptors owned by cached sources.
|
|
426
|
+
* @returns {void}
|
|
427
|
+
*/
|
|
428
|
+
export function clearPMtilesCache() {
|
|
429
|
+
for (const pmtiles of pmtilesCache.values()) {
|
|
430
|
+
closePMTiles(pmtiles);
|
|
431
|
+
}
|
|
432
|
+
pmtilesCache.clear();
|
|
433
|
+
}
|
|
434
|
+
|
|
391
435
|
/**
|
|
392
436
|
* Retrieves metadata and header information from a PMTiles archive with retry logic for rate limiting.
|
|
393
437
|
* @param {PMTiles} pmtiles - The PMTiles instance.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts a TileServer zoom level to the zoom expected by MapLibre Native.
|
|
5
|
+
* @param {number} zoom TileServer zoom level.
|
|
6
|
+
* @param {number} logicalWidth Unscaled request width in pixels.
|
|
7
|
+
* @param {'tile'|'static'} mode Rendering mode.
|
|
8
|
+
* @returns {number} MapLibre Native zoom level.
|
|
9
|
+
*/
|
|
10
|
+
export function getMapLibreRenderZoom(zoom, logicalWidth, mode) {
|
|
11
|
+
if (mode === 'static') {
|
|
12
|
+
return Math.max(0, zoom - 1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (mode === 'tile') {
|
|
16
|
+
return Math.max(0, logicalWidth === 512 ? zoom : zoom - 1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
throw new Error(`Unsupported render mode: ${mode}`);
|
|
20
|
+
}
|
package/src/serve_data.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'path';
|
|
|
5
5
|
|
|
6
6
|
import clone from 'clone';
|
|
7
7
|
import express from 'express';
|
|
8
|
-
import
|
|
8
|
+
import { PbfReader } from 'pbf';
|
|
9
9
|
import { VectorTile } from '@mapbox/vector-tile';
|
|
10
10
|
import { SphericalMercator } from '@mapbox/sphericalmercator';
|
|
11
11
|
|
|
@@ -15,12 +15,16 @@ import {
|
|
|
15
15
|
isValidRemoteUrl,
|
|
16
16
|
fetchTileData,
|
|
17
17
|
lonLatToTilePixel,
|
|
18
|
+
parseOptionalBoolean,
|
|
19
|
+
resolveSparse,
|
|
18
20
|
} from './utils.js';
|
|
19
21
|
import { getPMtilesInfo, openPMtiles } from './pmtiles_adapter.js';
|
|
20
22
|
import { gunzipP, gzipP } from './promises.js';
|
|
21
23
|
import { openMbTilesWrapper } from './mbtiles_wrapper.js';
|
|
22
24
|
|
|
23
25
|
import fs from 'node:fs';
|
|
26
|
+
|
|
27
|
+
let metricsModule = null;
|
|
24
28
|
import { fileURLToPath } from 'url';
|
|
25
29
|
|
|
26
30
|
const packageJson = JSON.parse(
|
|
@@ -45,6 +49,16 @@ export const serve_data = {
|
|
|
45
49
|
*/
|
|
46
50
|
init: function (options, repo, programOpts) {
|
|
47
51
|
const { verbose, allowedHosts } = programOpts;
|
|
52
|
+
// Cache metrics module if enabled. Safe because tests verify before production.
|
|
53
|
+
if (programOpts.metrics) {
|
|
54
|
+
import('./metrics.js')
|
|
55
|
+
.then((m) => {
|
|
56
|
+
metricsModule = m;
|
|
57
|
+
})
|
|
58
|
+
.catch((err) => {
|
|
59
|
+
console.error('Failed to import metrics module:', err);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
48
62
|
const app = express().disable('x-powered-by');
|
|
49
63
|
app.use(express.json());
|
|
50
64
|
|
|
@@ -127,7 +141,7 @@ export const serve_data = {
|
|
|
127
141
|
|
|
128
142
|
if (tileJSONFormat === 'pbf') {
|
|
129
143
|
if (options.dataDecoratorFunc) {
|
|
130
|
-
data = options.dataDecoratorFunc(
|
|
144
|
+
data = await options.dataDecoratorFunc(
|
|
131
145
|
req.params.id,
|
|
132
146
|
'data',
|
|
133
147
|
data,
|
|
@@ -142,7 +156,7 @@ export const serve_data = {
|
|
|
142
156
|
headers['Content-Type'] = 'application/x-protobuf';
|
|
143
157
|
} else if (format === 'geojson') {
|
|
144
158
|
headers['Content-Type'] = 'application/json';
|
|
145
|
-
const tile = new VectorTile(new
|
|
159
|
+
const tile = new VectorTile(new PbfReader(data));
|
|
146
160
|
const geojson = {
|
|
147
161
|
type: 'FeatureCollection',
|
|
148
162
|
features: [],
|
|
@@ -167,6 +181,12 @@ export const serve_data = {
|
|
|
167
181
|
|
|
168
182
|
data = await gzipP(data);
|
|
169
183
|
|
|
184
|
+
if (metricsModule) {
|
|
185
|
+
metricsModule.tilesServedTotal.inc({
|
|
186
|
+
type: 'vector',
|
|
187
|
+
name: req.params.id,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
170
190
|
return res.status(200).send(data);
|
|
171
191
|
});
|
|
172
192
|
|
|
@@ -601,19 +621,29 @@ export const serve_data = {
|
|
|
601
621
|
delete tileJSON['scheme'];
|
|
602
622
|
tileJSON['tilejson'] = '3.0.0';
|
|
603
623
|
|
|
624
|
+
// MBTiles metadata is a table of strings, so a `sparse` row arrives as
|
|
625
|
+
// "false" - truthy at face value. Take it as a boolean here and drop the
|
|
626
|
+
// raw key, so nothing downstream (the data decorator, the served TileJSON)
|
|
627
|
+
// ever sees the unparsed form. The resolved value is put back below.
|
|
628
|
+
const metadataSparse = parseOptionalBoolean(tileJSON.sparse);
|
|
629
|
+
delete tileJSON['sparse'];
|
|
630
|
+
|
|
604
631
|
Object.assign(tileJSON, params.tilejson || {});
|
|
605
632
|
fixTileJSONCenter(tileJSON);
|
|
606
633
|
|
|
607
634
|
if (options.dataDecoratorFunc) {
|
|
608
|
-
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
|
|
635
|
+
tileJSON = await options.dataDecoratorFunc(id, 'tilejson', tileJSON);
|
|
609
636
|
}
|
|
610
637
|
|
|
611
|
-
//
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
638
|
+
// sparse=true -> 404 (allows overzoom), sparse=false -> 204 (empty tile)
|
|
639
|
+
const sparse = resolveSparse({
|
|
640
|
+
perSource: params.sparse,
|
|
641
|
+
globalOption: options.sparse,
|
|
642
|
+
metadata: metadataSparse,
|
|
643
|
+
isVector: tileJSON.format === 'pbf',
|
|
644
|
+
});
|
|
645
|
+
// Advertise what the server will actually do, not what the archive claimed.
|
|
646
|
+
tileJSON.sparse = sparse;
|
|
617
647
|
|
|
618
648
|
// eslint-disable-next-line security/detect-object-injection -- id is from config file data source names
|
|
619
649
|
repo[id] = {
|
|
@@ -624,4 +654,29 @@ export const serve_data = {
|
|
|
624
654
|
sparse,
|
|
625
655
|
};
|
|
626
656
|
},
|
|
657
|
+
/**
|
|
658
|
+
* Removes all items from the repository and closes owned local data sources.
|
|
659
|
+
* @param {object} repo Repository object.
|
|
660
|
+
* @returns {Promise<void>}
|
|
661
|
+
*/
|
|
662
|
+
clear: async function (repo) {
|
|
663
|
+
await Promise.all(
|
|
664
|
+
Object.keys(repo).map(async (id) => {
|
|
665
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys() iteration
|
|
666
|
+
const item = repo[id];
|
|
667
|
+
try {
|
|
668
|
+
if (item && item.sourceType === 'mbtiles' && item.source) {
|
|
669
|
+
await new Promise((resolve, reject) => {
|
|
670
|
+
item.source.close((err) => (err ? reject(err) : resolve()));
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
} catch (err) {
|
|
674
|
+
console.warn(`Failed to close data source "${id}":`, err);
|
|
675
|
+
} finally {
|
|
676
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys() iteration
|
|
677
|
+
delete repo[id];
|
|
678
|
+
}
|
|
679
|
+
}),
|
|
680
|
+
);
|
|
681
|
+
},
|
|
627
682
|
};
|
package/src/serve_font.js
CHANGED
|
@@ -4,6 +4,8 @@ import express from 'express';
|
|
|
4
4
|
|
|
5
5
|
import { getFontsPbf, listFonts } from './utils.js';
|
|
6
6
|
|
|
7
|
+
let metricsModule = null;
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Initializes and returns an Express app that serves font files.
|
|
9
11
|
* @param {object} options - Configuration options for the server.
|
|
@@ -13,6 +15,11 @@ import { getFontsPbf, listFonts } from './utils.js';
|
|
|
13
15
|
*/
|
|
14
16
|
export async function serve_font(options, allowedFonts, programOpts) {
|
|
15
17
|
const { verbose } = programOpts;
|
|
18
|
+
// Cache metrics module if enabled. Safe because tests verify before production.
|
|
19
|
+
if (programOpts.metrics) {
|
|
20
|
+
const m = await import('./metrics.js');
|
|
21
|
+
metricsModule = m;
|
|
22
|
+
}
|
|
16
23
|
const app = express().disable('x-powered-by');
|
|
17
24
|
|
|
18
25
|
const lastModified = new Date().toUTCString();
|
|
@@ -64,6 +71,9 @@ export async function serve_font(options, allowedFonts, programOpts) {
|
|
|
64
71
|
);
|
|
65
72
|
res.header('Content-type', 'application/x-protobuf');
|
|
66
73
|
res.header('Last-Modified', lastModified);
|
|
74
|
+
if (metricsModule) {
|
|
75
|
+
metricsModule.tilesServedTotal.inc({ type: 'font', name: sFontStack });
|
|
76
|
+
}
|
|
67
77
|
return res.send(concatenated);
|
|
68
78
|
} catch (err) {
|
|
69
79
|
console.error(
|