tileserver-gl 5.7.0-pre.0 → 5.7.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/CHANGELOG.md +12 -1
- package/docs/config.rst +43 -2
- package/docs/usage.rst +2 -1
- package/package.json +33 -28
- package/public/resources/elevation-control.js +8 -4
- package/public/resources/maplibre-gl-inspect.mjs +2286 -0
- package/public/resources/maplibre-gl-inspect.mjs.map +1 -0
- package/public/resources/maplibre-gl-shared.mjs +6 -0
- package/public/resources/maplibre-gl-shared.mjs.map +1 -0
- package/public/resources/maplibre-gl-worker.mjs +10 -0
- package/public/resources/maplibre-gl-worker.mjs.map +1 -0
- package/public/resources/maplibre-gl.css +1 -1
- package/public/resources/maplibre-gl.mjs +819 -0
- package/public/resources/maplibre-gl.mjs.map +1 -0
- package/public/templates/data.tmpl +12 -6
- package/public/templates/viewer.tmpl +12 -8
- package/src/main.js +53 -7
- package/src/render_mode.js +38 -0
- package/src/serve_data.js +29 -11
- package/src/serve_rendered.js +317 -249
- package/src/server.js +10 -10
- 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/visual/static-512px-path-alignment-2x.png +0 -0
- package/test/fixtures/visual/static-marker-low-zoom-large-viewport.png +0 -0
- package/test/render_mode.js +48 -0
- package/test/sparse.js +411 -0
- package/test/static.js +20 -0
- package/test/static_images.js +10 -0
- package/test/tiles_data_compression.js +145 -0
- package/public/resources/maplibre-gl-inspect.js +0 -2934
- package/public/resources/maplibre-gl-inspect.js.map +0 -1
- package/public/resources/maplibre-gl.js +0 -59
- package/public/resources/maplibre-gl.js.map +0 -1
|
@@ -7,11 +7,9 @@
|
|
|
7
7
|
{{#use_maplibre}}
|
|
8
8
|
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl.css{{&key_query}}" />
|
|
9
9
|
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" />
|
|
10
|
-
<script
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<script src="{{public_url}}elevation-control.js{{&key_query}}"></script>
|
|
14
|
-
{{/is_light}}
|
|
10
|
+
<script type="importmap">
|
|
11
|
+
{ "imports": { "maplibre-gl": "{{public_url}}maplibre-gl.mjs{{&key_query}}" } }
|
|
12
|
+
</script>
|
|
15
13
|
<style>
|
|
16
14
|
body {background:#fff;color:#333;font-family:Arial, sans-serif;}
|
|
17
15
|
{{^is_terrain}}
|
|
@@ -58,7 +56,15 @@
|
|
|
58
56
|
<div id="layerList"></div>
|
|
59
57
|
<pre id="propertyList"></pre>
|
|
60
58
|
{{/is_terrain}}
|
|
61
|
-
<script>
|
|
59
|
+
<script type="module">
|
|
60
|
+
import * as maplibregl from 'maplibre-gl';
|
|
61
|
+
{{^is_terrain}}
|
|
62
|
+
import MaplibreInspect from '{{public_url}}maplibre-gl-inspect.mjs{{&key_query}}';
|
|
63
|
+
{{/is_terrain}}
|
|
64
|
+
{{^is_light}}
|
|
65
|
+
import ElevationInfoControl from '{{public_url}}elevation-control.js{{&key_query}}';
|
|
66
|
+
{{/is_light}}
|
|
67
|
+
|
|
62
68
|
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
|
|
63
69
|
var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
|
|
64
70
|
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl.css{{&key_query}}" />
|
|
8
8
|
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" />
|
|
9
9
|
<link rel="stylesheet" type="text/css" href="{{public_url}}leaflet.css{{&key_query}}" />
|
|
10
|
-
<script
|
|
11
|
-
|
|
10
|
+
<script type="importmap">
|
|
11
|
+
{ "imports": { "maplibre-gl": "{{public_url}}maplibre-gl.mjs{{&key_query}}" } }
|
|
12
|
+
</script>
|
|
12
13
|
<script src="{{public_url}}leaflet.js{{&key_query}}"></script>
|
|
13
14
|
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
|
|
14
15
|
<style>
|
|
@@ -31,12 +32,15 @@
|
|
|
31
32
|
<body>
|
|
32
33
|
<h1 style="display:none;">{{name}}</h1>
|
|
33
34
|
<div id='map'></div>
|
|
34
|
-
<script>
|
|
35
|
+
<script type="module">
|
|
36
|
+
import * as maplibregl from 'maplibre-gl';
|
|
37
|
+
import MaplibreInspect from '{{public_url}}maplibre-gl-inspect.mjs{{&key_query}}';
|
|
38
|
+
|
|
35
39
|
function isWebglSupported() {
|
|
36
|
-
if (window.
|
|
40
|
+
if (window.WebGL2RenderingContext) {
|
|
37
41
|
const canvas = document.createElement('canvas');
|
|
38
42
|
try {
|
|
39
|
-
const context = canvas.getContext('webgl2')
|
|
43
|
+
const context = canvas.getContext('webgl2');
|
|
40
44
|
if (context && typeof context.getParameter == 'function') {
|
|
41
45
|
return true;
|
|
42
46
|
}
|
|
@@ -45,7 +49,7 @@
|
|
|
45
49
|
}
|
|
46
50
|
return false;
|
|
47
51
|
}
|
|
48
|
-
//
|
|
52
|
+
// WebGL2 not supported (required by maplibre-gl v6+)
|
|
49
53
|
return false;
|
|
50
54
|
}
|
|
51
55
|
var q = (location.search || '').substr(1).split('&');
|
|
@@ -107,7 +111,7 @@
|
|
|
107
111
|
req.open('GET', url, true);
|
|
108
112
|
req.onload = function() {
|
|
109
113
|
var jsonResponse = JSON.parse(req.responseText);
|
|
110
|
-
for (key in jsonResponse) {
|
|
114
|
+
for (var key in jsonResponse) {
|
|
111
115
|
var keyl = key.toLowerCase();
|
|
112
116
|
switch(keyl) {
|
|
113
117
|
case "tiles":
|
|
@@ -125,7 +129,7 @@
|
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
for (tile_url in tile_urls) {
|
|
132
|
+
for (var tile_url in tile_urls) {
|
|
129
133
|
var leafletUrl = tile_urls[tile_url];
|
|
130
134
|
{{#leaflet_retina}}
|
|
131
135
|
// Inject {r} for Leaflet retina support without modifying TileJSON directly.
|
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
|
// ============================================================================
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Calculates the TileServer zoom MapLibre Native effectively uses for a
|
|
24
|
+
* static-map overlay. Horizontal world copies can wrap, but the world must
|
|
25
|
+
* fill the viewport vertically. The 256px zoom-zero render has a dedicated
|
|
26
|
+
* resize path in serve_rendered.
|
|
27
|
+
* @param {number} zoom Requested TileServer zoom level.
|
|
28
|
+
* @param {number} logicalWidth Unscaled request width in pixels.
|
|
29
|
+
* @param {number} logicalHeight Unscaled request height in pixels.
|
|
30
|
+
* @returns {number} Effective TileServer zoom level.
|
|
31
|
+
*/
|
|
32
|
+
export function getStaticOverlayZoom(zoom, logicalWidth, logicalHeight) {
|
|
33
|
+
const minimumViewportZoom = Math.log2(logicalHeight / 256);
|
|
34
|
+
if (zoom === 0 && logicalWidth === 256) {
|
|
35
|
+
return Math.max(0, minimumViewportZoom);
|
|
36
|
+
}
|
|
37
|
+
return Math.max(1, zoom, minimumViewportZoom);
|
|
38
|
+
}
|
package/src/serve_data.js
CHANGED
|
@@ -15,6 +15,8 @@ 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';
|
|
@@ -131,15 +133,19 @@ export const serve_data = {
|
|
|
131
133
|
|
|
132
134
|
let data = fetchTile.data;
|
|
133
135
|
let headers = fetchTile.headers;
|
|
134
|
-
let isGzipped = data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
|
|
135
136
|
|
|
136
|
-
|
|
137
|
+
const isGzipped =
|
|
138
|
+
data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
|
|
139
|
+
const serveGzipped =
|
|
140
|
+
isGzipped && format === 'pbf' && !options.dataDecoratorFunc;
|
|
141
|
+
|
|
142
|
+
if (isGzipped && !serveGzipped) {
|
|
137
143
|
data = await gunzipP(data);
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
if (tileJSONFormat === 'pbf') {
|
|
141
147
|
if (options.dataDecoratorFunc) {
|
|
142
|
-
data = options.dataDecoratorFunc(
|
|
148
|
+
data = await options.dataDecoratorFunc(
|
|
143
149
|
req.params.id,
|
|
144
150
|
'data',
|
|
145
151
|
data,
|
|
@@ -177,7 +183,9 @@ export const serve_data = {
|
|
|
177
183
|
headers['Content-Encoding'] = 'gzip';
|
|
178
184
|
res.set(headers);
|
|
179
185
|
|
|
180
|
-
|
|
186
|
+
if (!serveGzipped) {
|
|
187
|
+
data = await gzipP(data);
|
|
188
|
+
}
|
|
181
189
|
|
|
182
190
|
if (metricsModule) {
|
|
183
191
|
metricsModule.tilesServedTotal.inc({
|
|
@@ -619,19 +627,29 @@ export const serve_data = {
|
|
|
619
627
|
delete tileJSON['scheme'];
|
|
620
628
|
tileJSON['tilejson'] = '3.0.0';
|
|
621
629
|
|
|
630
|
+
// MBTiles metadata is a table of strings, so a `sparse` row arrives as
|
|
631
|
+
// "false" - truthy at face value. Take it as a boolean here and drop the
|
|
632
|
+
// raw key, so nothing downstream (the data decorator, the served TileJSON)
|
|
633
|
+
// ever sees the unparsed form. The resolved value is put back below.
|
|
634
|
+
const metadataSparse = parseOptionalBoolean(tileJSON.sparse);
|
|
635
|
+
delete tileJSON['sparse'];
|
|
636
|
+
|
|
622
637
|
Object.assign(tileJSON, params.tilejson || {});
|
|
623
638
|
fixTileJSONCenter(tileJSON);
|
|
624
639
|
|
|
625
640
|
if (options.dataDecoratorFunc) {
|
|
626
|
-
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
|
|
641
|
+
tileJSON = await options.dataDecoratorFunc(id, 'tilejson', tileJSON);
|
|
627
642
|
}
|
|
628
643
|
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
644
|
+
// sparse=true -> 404 (allows overzoom), sparse=false -> 204 (empty tile)
|
|
645
|
+
const sparse = resolveSparse({
|
|
646
|
+
perSource: params.sparse,
|
|
647
|
+
globalOption: options.sparse,
|
|
648
|
+
metadata: metadataSparse,
|
|
649
|
+
isVector: tileJSON.format === 'pbf',
|
|
650
|
+
});
|
|
651
|
+
// Advertise what the server will actually do, not what the archive claimed.
|
|
652
|
+
tileJSON.sparse = sparse;
|
|
635
653
|
|
|
636
654
|
// eslint-disable-next-line security/detect-object-injection -- id is from config file data source names
|
|
637
655
|
repo[id] = {
|