tileserver-gl-light 5.5.0-pre.1 → 5.5.0-pre.11

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +51 -34
  2. package/docs/config.rst +52 -11
  3. package/docs/endpoints.rst +12 -2
  4. package/docs/installation.rst +6 -6
  5. package/docs/usage.rst +26 -0
  6. package/package.json +15 -15
  7. package/public/resources/elevation-control.js +92 -21
  8. package/public/resources/maplibre-gl-inspect.js +2827 -2770
  9. package/public/resources/maplibre-gl-inspect.js.map +1 -1
  10. package/public/resources/maplibre-gl.css +1 -1
  11. package/public/resources/maplibre-gl.js +4 -4
  12. package/public/resources/maplibre-gl.js.map +1 -1
  13. package/src/main.js +31 -20
  14. package/src/pmtiles_adapter.js +104 -45
  15. package/src/promises.js +1 -1
  16. package/src/render.js +270 -93
  17. package/src/serve_data.js +266 -90
  18. package/src/serve_font.js +2 -2
  19. package/src/serve_light.js +2 -4
  20. package/src/serve_rendered.js +445 -236
  21. package/src/serve_style.js +29 -8
  22. package/src/server.js +115 -60
  23. package/src/utils.js +47 -20
  24. package/test/elevation.js +513 -0
  25. package/test/fixtures/visual/encoded-path-auto.png +0 -0
  26. package/test/fixtures/visual/linecap-linejoin-bevel-square.png +0 -0
  27. package/test/fixtures/visual/linecap-linejoin-round-round.png +0 -0
  28. package/test/fixtures/visual/path-auto.png +0 -0
  29. package/test/fixtures/visual/static-bbox.png +0 -0
  30. package/test/fixtures/visual/static-bearing-pitch.png +0 -0
  31. package/test/fixtures/visual/static-bearing.png +0 -0
  32. package/test/fixtures/visual/static-border-global.png +0 -0
  33. package/test/fixtures/visual/static-lat-lng.png +0 -0
  34. package/test/fixtures/visual/static-markers.png +0 -0
  35. package/test/fixtures/visual/static-multiple-paths.png +0 -0
  36. package/test/fixtures/visual/static-path-border-isolated.png +0 -0
  37. package/test/fixtures/visual/static-path-border-stroke.png +0 -0
  38. package/test/fixtures/visual/static-path-latlng.png +0 -0
  39. package/test/fixtures/visual/static-pixel-ratio-2x.png +0 -0
  40. package/test/static_images.js +241 -0
  41. package/test/tiles_data.js +1 -1
  42. package/test/utils/create_terrain_mbtiles.js +124 -0
@@ -0,0 +1,241 @@
1
+ // test/static_images.js
2
+ import { describe, it } from 'mocha';
3
+ import { expect } from 'chai';
4
+ import supertest from 'supertest';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import sharp from 'sharp';
8
+ import pixelmatch from 'pixelmatch';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ const FIXTURES_DIR = path.join(__dirname, 'fixtures', 'visual');
14
+ const THRESHOLD = 0.1;
15
+ const MAX_DIFF_PIXELS = 100;
16
+
17
+ // Check for the environment variable to conditionally generate fixtures
18
+ const shouldGenerateFixtures = process.env.GENERATE_FIXTURES === 'true';
19
+
20
+ // --- Test Definitions ---
21
+ const tests = [
22
+ {
23
+ name: 'static-lat-lng',
24
+ // Test default center format (lng,lat,zoom)
25
+ url: '/styles/test-style/static/8.5375,47.379,12/400x300.png',
26
+ },
27
+ {
28
+ name: 'static-bearing',
29
+ // Test map bearing (rotation) at 180 degrees
30
+ url: '/styles/test-style/static/8.5375,47.379,12@180/400x300.png',
31
+ },
32
+ {
33
+ name: 'static-bearing-pitch',
34
+ // Test map bearing and pitch (3D tilt)
35
+ url: '/styles/test-style/static/8.5375,47.379,12@15,80/400x300.png',
36
+ },
37
+ {
38
+ name: 'static-pixel-ratio-2x',
39
+ // Test high-DPI rendering using @2x scale
40
+ url: '/styles/test-style/static/8.5375,47.379,11/200x150@2x.png',
41
+ },
42
+ {
43
+ name: 'path-auto',
44
+ // Test path rendering with simple coordinates and auto-centering
45
+ url: '/styles/test-style/static/auto/400x300.png?fill=%23ff000080&path=8.53180,47.38713|8.53841,47.38248|8.53320,47.37457',
46
+ },
47
+ {
48
+ name: 'encoded-path-auto',
49
+ // Test path rendering using encoded polyline and auto-centering
50
+ url: '/styles/test-style/static/auto/400x300.png?stroke=red&width=5&path=enc:wwg`Hyu}r@fNgn@hKyh@rR{ZlP{YrJmM`PJhNbH`P`VjUbNfJ|LzM~TtLnKxQZ',
51
+ },
52
+ {
53
+ name: 'linecap-linejoin-round-round',
54
+ // Test custom line styling: round linejoin and round linecap
55
+ url: '/styles/test-style/static/8.5375,47.379,12/400x300.png?width=30&linejoin=round&linecap=round&path=enc:uhd`Hqk_s@kiA}nAnfAqpA',
56
+ },
57
+ {
58
+ name: 'linecap-linejoin-bevel-square',
59
+ // Test custom line styling: bevel linejoin and square linecap
60
+ url: '/styles/test-style/static/8.5375,47.379,12/400x300.png?width=30&linejoin=bevel&linecap=square&path=enc:uhd`Hqk_s@kiA}nAnfAqpA',
61
+ },
62
+ {
63
+ name: 'static-markers',
64
+ // Test multiple markers with scale and offset options
65
+ url: '/styles/test-style/static/8.5375,47.379,12/400x300.png?marker=8.531,47.38|marker-icon.png|scale:0.8&marker=8.545,47.375|marker-icon-2x.png|offset:5,-10',
66
+ },
67
+ {
68
+ name: 'static-bbox',
69
+ // Test area-based map rendering using a bounding box (bbox)
70
+ url: '/styles/test-style/static/8.5,47.35,8.6,47.4/400x300.png',
71
+ },
72
+ {
73
+ name: 'static-multiple-paths',
74
+ // Test rendering of multiple, individually styled path parameters
75
+ url: '/styles/test-style/static/8.5375,47.379,12/400x300.png?path=stroke:blue|width:8|fill:none|8.53,47.38|8.54,47.385&path=stroke:red|width:3|fill:yellow|8.53,47.37|8.54,47.375',
76
+ },
77
+ {
78
+ name: 'static-path-latlng',
79
+ // Test path rendering when the 'latlng' parameter reverses coordinate order
80
+ url: '/styles/test-style/static/auto/400x300.png?latlng=true&path=47.38,8.53|47.385,8.54&fill=rgba(0,0,255,0.5)',
81
+ },
82
+ {
83
+ name: 'static-path-border-stroke',
84
+ // Test path border/halo functionality (line stroke with border halo)
85
+ url: '/styles/test-style/static/8.5375,47.379,12/400x300.png?path=stroke:yellow|width:10|border:black|borderwidth:2|8.53,47.37|8.54,47.38|8.53,47.39',
86
+ },
87
+ {
88
+ name: 'static-path-border-isolated',
89
+ // Test path border/halo in isolation (only border, no stroke)
90
+ url: '/styles/test-style/static/8.5375,47.379,12/400x300.png?path=border:black|borderwidth:10|8.53,47.37|8.54,47.38|8.53,47.39',
91
+ },
92
+ {
93
+ name: 'static-border-global',
94
+ // Test border functionality using global query parameters (less common, but valid)
95
+ url: '/styles/test-style/static/8.5375,47.379,12/400x300.png?stroke=yellow&width=10&border=black&borderwidth=2&path=8.53,47.37|8.54,47.38|8.53,47.39',
96
+ },
97
+ ];
98
+
99
+ /**
100
+ * Loads an image buffer and extracts its raw pixel data.
101
+ * @param {Buffer} buffer The raw image data buffer (e.g., from an HTTP response).
102
+ * @returns {Promise<{data: Buffer, width: number, height: number}>} An object containing the raw RGBA pixel data, width, and height.
103
+ */
104
+ async function loadImageData(buffer) {
105
+ const image = sharp(buffer);
106
+ const { width, height } = await image.metadata();
107
+
108
+ // Get raw RGBA pixel data
109
+ const data = await image.ensureAlpha().raw().toBuffer();
110
+
111
+ return { data, width, height };
112
+ }
113
+
114
+ /**
115
+ * Fetches an image from the test server URL.
116
+ * @param {string} url The URL of the static image endpoint to fetch.
117
+ * @returns {Promise<Buffer>} A promise that resolves with the image buffer.
118
+ */
119
+ async function fetchImage(url) {
120
+ return new Promise((resolve, reject) => {
121
+ supertest(global.app)
122
+ .get(url)
123
+ .expect(200)
124
+ .expect('Content-Type', /image\/png/)
125
+ .end((err, res) => {
126
+ if (err) {
127
+ reject(err);
128
+ } else {
129
+ resolve(res.body);
130
+ }
131
+ });
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Compares two images (actual result vs. expected fixture) and counts the differing pixels.
137
+ * @param {Buffer} actualBuffer The buffer of the image rendered by the server.
138
+ * @param {string} expectedPath The file path to the expected fixture image.
139
+ * @returns {Promise<{numDiffPixels: number, diffBuffer: Buffer, width: number, height: number}>} Comparison results.
140
+ */
141
+ async function compareImages(actualBuffer, expectedPath) {
142
+ const actual = await loadImageData(actualBuffer);
143
+ const expectedBuffer = fs.readFileSync(expectedPath);
144
+ const expected = await loadImageData(expectedBuffer);
145
+
146
+ if (actual.width !== expected.width || actual.height !== expected.height) {
147
+ throw new Error(
148
+ `Image dimensions don't match: ${actual.width}x${actual.height} vs ${expected.width}x${expected.height}`,
149
+ );
150
+ }
151
+
152
+ const diffBuffer = Buffer.alloc(actual.width * actual.height * 4);
153
+ const numDiffPixels = pixelmatch(
154
+ actual.data,
155
+ expected.data,
156
+ diffBuffer,
157
+ actual.width,
158
+ actual.height,
159
+ { threshold: THRESHOLD },
160
+ );
161
+
162
+ return {
163
+ numDiffPixels,
164
+ diffBuffer,
165
+ width: actual.width,
166
+ height: actual.height,
167
+ };
168
+ }
169
+
170
+ // Conditional definition: Only define this suite if the GENERATE_FIXTURES environment variable is true
171
+ if (shouldGenerateFixtures) {
172
+ describe('GENERATE Visual Fixtures', function () {
173
+ this.timeout(10000);
174
+
175
+ it('should generate all fixture images', async function () {
176
+ fs.mkdirSync(FIXTURES_DIR, { recursive: true });
177
+ console.log(`\nGenerating fixtures to ${FIXTURES_DIR}\n`);
178
+
179
+ for (const { name, url } of tests) {
180
+ try {
181
+ const actualBuffer = await fetchImage(url);
182
+ const fixturePath = path.join(FIXTURES_DIR, `${name}.png`);
183
+ fs.writeFileSync(fixturePath, actualBuffer);
184
+ console.log(
185
+ `✓ Generated: ${name}.png (${actualBuffer.length} bytes)`,
186
+ );
187
+ } catch (error) {
188
+ console.error(`❌ Failed to generate ${name}:`, error.message);
189
+ throw error;
190
+ }
191
+ }
192
+
193
+ console.log(
194
+ `\n✓ Successfully generated ${tests.length} fixture images!\n`,
195
+ );
196
+ });
197
+ });
198
+ }
199
+
200
+ describe('Static Image Visual Regression Tests', function () {
201
+ this.timeout(10000);
202
+
203
+ tests.forEach(({ name, url }) => {
204
+ it(`should match expected output: ${name}`, async function () {
205
+ const expectedPath = path.join(FIXTURES_DIR, `${name}.png`);
206
+
207
+ if (!fs.existsSync(expectedPath)) {
208
+ this.skip();
209
+ return;
210
+ }
211
+
212
+ const actualBuffer = await fetchImage(url);
213
+ const { numDiffPixels, diffBuffer, width, height } = await compareImages(
214
+ actualBuffer,
215
+ expectedPath,
216
+ );
217
+
218
+ if (numDiffPixels > MAX_DIFF_PIXELS) {
219
+ const diffPath = path.join(FIXTURES_DIR, 'diffs', `${name}-diff.png`);
220
+ fs.mkdirSync(path.dirname(diffPath), { recursive: true });
221
+
222
+ await sharp(diffBuffer, {
223
+ raw: {
224
+ width,
225
+ height,
226
+ channels: 4,
227
+ },
228
+ })
229
+ .png()
230
+ .toFile(diffPath);
231
+
232
+ console.log(`Diff image saved to: ${diffPath}`);
233
+ }
234
+
235
+ expect(numDiffPixels).to.be.at.most(
236
+ MAX_DIFF_PIXELS,
237
+ `Expected at most ${MAX_DIFF_PIXELS} different pixels, but got ${numDiffPixels}`,
238
+ );
239
+ });
240
+ });
241
+ });
@@ -23,6 +23,6 @@ describe('Vector tiles', function () {
23
23
  testTile(prefix, 0, 1, 0, 404);
24
24
  testTile(prefix, 0, 0, 1, 404);
25
25
 
26
- testTile(prefix, 14, 0, 0, 204); // non existent tile
26
+ testTile(prefix, 14, 0, 0, 204); // non existent tile (vector tiles default to 204)
27
27
  });
28
28
  });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Creates a simple terrain mbtiles file for testing the elevation API.
3
+ * Uses mapbox encoding: elevation = -10000 + (R * 256 * 256 + G * 256 + B) * 0.1
4
+ */
5
+
6
+ import sqlite3 from 'sqlite3';
7
+ import { createCanvas } from 'canvas';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ function elevationToMapboxRGB(elevation) {
15
+ // elevation = -10000 + (R * 65536 + G * 256 + B) * 0.1
16
+ // (R * 65536 + G * 256 + B) = (elevation + 10000) / 0.1
17
+ const value = Math.round((elevation + 10000) / 0.1);
18
+ const r = Math.floor(value / 65536);
19
+ const g = Math.floor((value % 65536) / 256);
20
+ const b = value % 256;
21
+ return { r, g, b };
22
+ }
23
+
24
+ function createTerrainTile(tileSize, elevation) {
25
+ const canvas = createCanvas(tileSize, tileSize);
26
+ const ctx = canvas.getContext('2d');
27
+ const { r, g, b } = elevationToMapboxRGB(elevation);
28
+
29
+ // Fill with solid color representing the elevation
30
+ ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
31
+ ctx.fillRect(0, 0, tileSize, tileSize);
32
+
33
+ return canvas.toBuffer('image/png');
34
+ }
35
+
36
+ function runDb(db, sql, params = []) {
37
+ return new Promise((resolve, reject) => {
38
+ db.run(sql, params, function (err) {
39
+ if (err) reject(err);
40
+ else resolve(this);
41
+ });
42
+ });
43
+ }
44
+
45
+ async function createTerrainMbtiles(outputPath) {
46
+ const db = new sqlite3.Database(outputPath);
47
+
48
+ // Create mbtiles schema
49
+ await runDb(
50
+ db,
51
+ `
52
+ CREATE TABLE IF NOT EXISTS metadata (name TEXT, value TEXT)
53
+ `,
54
+ );
55
+ await runDb(
56
+ db,
57
+ `
58
+ CREATE TABLE IF NOT EXISTS tiles (zoom_level INTEGER, tile_column INTEGER, tile_row INTEGER, tile_data BLOB)
59
+ `,
60
+ );
61
+ await runDb(
62
+ db,
63
+ `
64
+ CREATE UNIQUE INDEX IF NOT EXISTS tile_index ON tiles (zoom_level, tile_column, tile_row)
65
+ `,
66
+ );
67
+
68
+ // Insert metadata
69
+ const metadata = [
70
+ ['name', 'test-terrain'],
71
+ ['format', 'png'],
72
+ ['encoding', 'mapbox'],
73
+ ['minzoom', '0'],
74
+ ['maxzoom', '1'],
75
+ ['bounds', '-180,-85.051129,180,85.051129'],
76
+ ['center', '0,0,0'],
77
+ ['type', 'baselayer'],
78
+ ['description', 'Test terrain tiles for elevation API testing'],
79
+ ];
80
+
81
+ for (const [name, value] of metadata) {
82
+ await runDb(db, 'INSERT INTO metadata (name, value) VALUES (?, ?)', [
83
+ name,
84
+ value,
85
+ ]);
86
+ }
87
+
88
+ const tileSize = 512;
89
+
90
+ // Zoom 0: single tile covering the world at elevation 100m
91
+ const tile0 = createTerrainTile(tileSize, 100);
92
+ await runDb(
93
+ db,
94
+ 'INSERT INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES (?, ?, ?, ?)',
95
+ [0, 0, 0, tile0],
96
+ );
97
+
98
+ // Zoom 1: 4 tiles with different elevations
99
+ const elevations = [
100
+ [0, 0, 200], // top-left
101
+ [1, 0, 500], // top-right
102
+ [0, 1, 1000], // bottom-left
103
+ [1, 1, 2500], // bottom-right
104
+ ];
105
+
106
+ for (const [x, y, elevation] of elevations) {
107
+ const tile = createTerrainTile(tileSize, elevation);
108
+ // MBTiles uses TMS scheme where y is flipped
109
+ const tmsY = (1 << 1) - 1 - y;
110
+ await runDb(
111
+ db,
112
+ 'INSERT INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES (?, ?, ?, ?)',
113
+ [1, x, tmsY, tile],
114
+ );
115
+ }
116
+
117
+ db.close();
118
+ console.log(`Created terrain mbtiles at: ${outputPath}`);
119
+ }
120
+
121
+ // Get output path from command line or use default
122
+ const outputPath =
123
+ process.argv[2] || path.join(__dirname, '../../test_data/terrain.mbtiles');
124
+ createTerrainMbtiles(outputPath);