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.
Files changed (36) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/docs/config.rst +43 -2
  3. package/docs/usage.rst +2 -1
  4. package/package.json +33 -28
  5. package/public/resources/elevation-control.js +8 -4
  6. package/public/resources/maplibre-gl-inspect.mjs +2286 -0
  7. package/public/resources/maplibre-gl-inspect.mjs.map +1 -0
  8. package/public/resources/maplibre-gl-shared.mjs +6 -0
  9. package/public/resources/maplibre-gl-shared.mjs.map +1 -0
  10. package/public/resources/maplibre-gl-worker.mjs +10 -0
  11. package/public/resources/maplibre-gl-worker.mjs.map +1 -0
  12. package/public/resources/maplibre-gl.css +1 -1
  13. package/public/resources/maplibre-gl.mjs +819 -0
  14. package/public/resources/maplibre-gl.mjs.map +1 -0
  15. package/public/templates/data.tmpl +12 -6
  16. package/public/templates/viewer.tmpl +12 -8
  17. package/src/main.js +53 -7
  18. package/src/render_mode.js +38 -0
  19. package/src/serve_data.js +29 -11
  20. package/src/serve_rendered.js +317 -249
  21. package/src/server.js +10 -10
  22. package/src/utils.js +51 -0
  23. package/test/data_decorator.js +156 -0
  24. package/test/fixtures/data-decorator-async.js +17 -0
  25. package/test/fixtures/data-decorator.js +29 -0
  26. package/test/fixtures/visual/static-512px-path-alignment-2x.png +0 -0
  27. package/test/fixtures/visual/static-marker-low-zoom-large-viewport.png +0 -0
  28. package/test/render_mode.js +48 -0
  29. package/test/sparse.js +411 -0
  30. package/test/static.js +20 -0
  31. package/test/static_images.js +10 -0
  32. package/test/tiles_data_compression.js +145 -0
  33. package/public/resources/maplibre-gl-inspect.js +0 -2934
  34. package/public/resources/maplibre-gl-inspect.js.map +0 -1
  35. package/public/resources/maplibre-gl.js +0 -59
  36. package/public/resources/maplibre-gl.js.map +0 -1
package/test/sparse.js ADDED
@@ -0,0 +1,411 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import util from 'node:util';
4
+ import MBTiles from '@mapbox/mbtiles';
5
+ import sharp from 'sharp';
6
+ import { server } from '../src/server.js';
7
+ import { serve_data } from '../src/serve_data.js';
8
+ import { parseOptionalBoolean, resolveSparse } from '../src/utils.js';
9
+
10
+ const SIGNALS = ['SIGHUP', 'SIGINT', 'SIGTERM'];
11
+
12
+ // A vector tile inside the declared zoom range that the Zurich extract does not
13
+ // contain. The same hole is used by test/tiles_data.js.
14
+ const MISSING_VECTOR_TILE = '14/0/0.pbf';
15
+ // Every z1 tile of the raster fixture below is a hole.
16
+ const MISSING_RASTER_TILE = '1/0/0.png';
17
+
18
+ const RASTER_FIXTURE = 'sparse-raster-fixture.mbtiles';
19
+ // Same archive, but its own metadata asks not to be sparse.
20
+ const META_FIXTURE = 'sparse-metadata-fixture.mbtiles';
21
+
22
+ /**
23
+ * Writes a raster mbtiles containing only its z0 tile, leaving every z1 tile
24
+ * missing while still inside the archive's declared zoom range.
25
+ * @param {string} file - Path of the mbtiles file to create.
26
+ * @param {object} [extraMetadata] - Extra keys to merge into the archive metadata.
27
+ * @returns {Promise<void>}
28
+ */
29
+ async function createRasterMbtilesWithHole(file, extraMetadata = {}) {
30
+ const tile = await sharp({
31
+ create: {
32
+ width: 256,
33
+ height: 256,
34
+ channels: 4,
35
+ background: { r: 0, g: 0, b: 0, alpha: 1 },
36
+ },
37
+ })
38
+ .png()
39
+ .toBuffer();
40
+
41
+ const mbtiles = await util.promisify((uri, cb) => new MBTiles(uri, cb))(
42
+ `${file}?mode=rwc`,
43
+ );
44
+ const startWriting = util.promisify(mbtiles.startWriting.bind(mbtiles));
45
+ const putInfo = util.promisify(mbtiles.putInfo.bind(mbtiles));
46
+ const putTile = util.promisify(mbtiles.putTile.bind(mbtiles));
47
+ const stopWriting = util.promisify(mbtiles.stopWriting.bind(mbtiles));
48
+ const close = util.promisify(mbtiles.close.bind(mbtiles));
49
+
50
+ await startWriting();
51
+ await putInfo({
52
+ name: 'sparse-raster-fixture',
53
+ format: 'png',
54
+ bounds: [-180, -85.0511, 180, 85.0511],
55
+ center: [0, 0, 0],
56
+ minzoom: 0,
57
+ maxzoom: 1,
58
+ ...extraMetadata,
59
+ });
60
+ await putTile(0, 0, 0, tile);
61
+ await stopWriting();
62
+ await close();
63
+ }
64
+
65
+ /**
66
+ * Boots a server from an in-memory config on an ephemeral port.
67
+ * @param {object} config - The tileserver config object.
68
+ * @returns {Promise<{app: object, stop: () => Promise<void>}>} The running app
69
+ * and a function that shuts it down and releases its data sources.
70
+ */
71
+ async function startServer(config) {
72
+ const before = SIGNALS.map((eventName) => ({
73
+ eventName,
74
+ count: process.listeners(eventName).length,
75
+ }));
76
+
77
+ const running = await server({ config, port: 0, publicUrl: '/test/' });
78
+ await running.startupPromise;
79
+
80
+ return {
81
+ app: running.app,
82
+ async stop() {
83
+ await running.cleanup();
84
+ // Closes the mbtiles handles; without this the fixture file stays
85
+ // locked on Windows and cannot be removed.
86
+ await serve_data.clear(running.serving.data);
87
+ if (running.server.listening) {
88
+ await new Promise((resolve) => running.server.close(resolve));
89
+ }
90
+ // server() registers a signal handler per generation; drop ours so
91
+ // repeated boots do not pile up listeners on the test process.
92
+ for (const { eventName, count } of before) {
93
+ for (const listener of process.listeners(eventName).slice(count)) {
94
+ process.removeListener(eventName, listener);
95
+ }
96
+ }
97
+ },
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Builds a config serving the given data sources out of the test_data dir.
103
+ * @param {object} data - The `data` section of the config.
104
+ * @param {boolean} [globalSparse] - Value for the top-level `sparse` option.
105
+ * @returns {object} A tileserver config object.
106
+ */
107
+ function configFor(data, globalSparse) {
108
+ const options = { paths: { fonts: 'fonts', styles: 'styles' } };
109
+ if (globalSparse !== undefined) {
110
+ options.sparse = globalSparse;
111
+ }
112
+ return { options, data };
113
+ }
114
+
115
+ describe('Sparse tile responses', function () {
116
+ let fixturePath;
117
+ let metaFixturePath;
118
+
119
+ before(async function () {
120
+ // The global setup hook has already chdir'd into test_data.
121
+ fixturePath = path.join(process.cwd(), RASTER_FIXTURE);
122
+ metaFixturePath = path.join(process.cwd(), META_FIXTURE);
123
+ fs.rmSync(fixturePath, { force: true });
124
+ fs.rmSync(metaFixturePath, { force: true });
125
+ await createRasterMbtilesWithHole(fixturePath);
126
+ // mbtiles metadata is a table of strings, so this round-trips as "false"
127
+ // and exercises the coercion on the way back out.
128
+ await createRasterMbtilesWithHole(metaFixturePath, { sparse: false });
129
+ });
130
+
131
+ after(function () {
132
+ fs.rmSync(fixturePath, { force: true });
133
+ fs.rmSync(metaFixturePath, { force: true });
134
+ });
135
+
136
+ /**
137
+ * Asserts the status a missing tile returns from a given source.
138
+ * @param {() => object} getApp - Returns the express app under test.
139
+ * @param {string} source - Data source id.
140
+ * @param {string} tile - Tile path suffix, e.g. '14/0/0.pbf'.
141
+ * @param {number} status - Expected HTTP status.
142
+ * @returns {void}
143
+ */
144
+ const expectMissingTile = (getApp, source, tile, status) => {
145
+ const url = `/data/${source}/${tile}`;
146
+ it(`${url} returns ${status}`, function (done) {
147
+ supertest(getApp()).get(url).expect(status).end(done);
148
+ });
149
+ };
150
+
151
+ describe('format-based default, with per-source overrides', function () {
152
+ let running;
153
+
154
+ before(async function () {
155
+ running = await startServer(
156
+ configFor({
157
+ 'vector-default': { mbtiles: 'zurich_switzerland.mbtiles' },
158
+ 'vector-sparse': {
159
+ mbtiles: 'zurich_switzerland.mbtiles',
160
+ sparse: true,
161
+ },
162
+ 'raster-default': { mbtiles: RASTER_FIXTURE },
163
+ 'raster-dense': { mbtiles: RASTER_FIXTURE, sparse: false },
164
+ }),
165
+ );
166
+ });
167
+
168
+ after(async function () {
169
+ await running.stop();
170
+ });
171
+
172
+ it('serves a tile the raster fixture does contain', function (done) {
173
+ supertest(running.app)
174
+ .get('/data/raster-default/0/0/0.png')
175
+ .expect(200)
176
+ .expect('Content-Type', /image\/png/)
177
+ .end(done);
178
+ });
179
+
180
+ // Vector defaults to non-sparse: 204, so MapLibre does not overzoom.
181
+ expectMissingTile(
182
+ () => running.app,
183
+ 'vector-default',
184
+ MISSING_VECTOR_TILE,
185
+ 204,
186
+ );
187
+ // Raster defaults to sparse: 404, so MapLibre overzooms from the parent.
188
+ expectMissingTile(
189
+ () => running.app,
190
+ 'raster-default',
191
+ MISSING_RASTER_TILE,
192
+ 404,
193
+ );
194
+ // A per-source setting overrides the format default in both directions.
195
+ expectMissingTile(
196
+ () => running.app,
197
+ 'vector-sparse',
198
+ MISSING_VECTOR_TILE,
199
+ 404,
200
+ );
201
+ expectMissingTile(
202
+ () => running.app,
203
+ 'raster-dense',
204
+ MISSING_RASTER_TILE,
205
+ 204,
206
+ );
207
+ });
208
+
209
+ describe('global sparse: true', function () {
210
+ let running;
211
+
212
+ before(async function () {
213
+ running = await startServer(
214
+ configFor(
215
+ {
216
+ 'vector-global': { mbtiles: 'zurich_switzerland.mbtiles' },
217
+ 'vector-override': {
218
+ mbtiles: 'zurich_switzerland.mbtiles',
219
+ sparse: false,
220
+ },
221
+ },
222
+ true,
223
+ ),
224
+ );
225
+ });
226
+
227
+ after(async function () {
228
+ await running.stop();
229
+ });
230
+
231
+ // The global option overrides the format default...
232
+ expectMissingTile(
233
+ () => running.app,
234
+ 'vector-global',
235
+ MISSING_VECTOR_TILE,
236
+ 404,
237
+ );
238
+ // ...and a per-source `false` still overrides the global.
239
+ expectMissingTile(
240
+ () => running.app,
241
+ 'vector-override',
242
+ MISSING_VECTOR_TILE,
243
+ 204,
244
+ );
245
+ });
246
+
247
+ describe('global sparse: false', function () {
248
+ let running;
249
+
250
+ before(async function () {
251
+ running = await startServer(
252
+ configFor(
253
+ {
254
+ 'raster-global': { mbtiles: RASTER_FIXTURE },
255
+ 'raster-override': { mbtiles: RASTER_FIXTURE, sparse: true },
256
+ },
257
+ false,
258
+ ),
259
+ );
260
+ });
261
+
262
+ after(async function () {
263
+ await running.stop();
264
+ });
265
+
266
+ // A global `false` turns off sparse behaviour for raster too...
267
+ expectMissingTile(
268
+ () => running.app,
269
+ 'raster-global',
270
+ MISSING_RASTER_TILE,
271
+ 204,
272
+ );
273
+ // ...and a per-source `true` still overrides the global.
274
+ expectMissingTile(
275
+ () => running.app,
276
+ 'raster-override',
277
+ MISSING_RASTER_TILE,
278
+ 404,
279
+ );
280
+ });
281
+ describe('sparse from archive metadata', function () {
282
+ let running;
283
+ let globalRunning;
284
+
285
+ before(async function () {
286
+ running = await startServer(
287
+ configFor({
288
+ 'meta-dense': { mbtiles: META_FIXTURE },
289
+ 'meta-overridden': { mbtiles: META_FIXTURE, sparse: true },
290
+ }),
291
+ );
292
+ globalRunning = await startServer(
293
+ configFor({ 'meta-vs-global': { mbtiles: META_FIXTURE } }, true),
294
+ );
295
+ });
296
+
297
+ after(async function () {
298
+ await running.stop();
299
+ await globalRunning.stop();
300
+ });
301
+
302
+ // A raster archive that declares sparse=false in its own metadata.
303
+ expectMissingTile(
304
+ () => running.app,
305
+ 'meta-dense',
306
+ MISSING_RASTER_TILE,
307
+ 204,
308
+ );
309
+ // Per-source config outranks the archive.
310
+ expectMissingTile(
311
+ () => running.app,
312
+ 'meta-overridden',
313
+ MISSING_RASTER_TILE,
314
+ 404,
315
+ );
316
+ // So does the global option.
317
+ expectMissingTile(
318
+ () => globalRunning.app,
319
+ 'meta-vs-global',
320
+ MISSING_RASTER_TILE,
321
+ 404,
322
+ );
323
+
324
+ it('reports the resolved value in the TileJSON, not the raw metadata', function (done) {
325
+ supertest(running.app)
326
+ .get('/data/meta-dense.json')
327
+ .expect(200)
328
+ .expect((res) => {
329
+ expect(res.body.sparse).to.equal(false);
330
+ })
331
+ .end(done);
332
+ });
333
+ });
334
+ });
335
+
336
+ describe('resolveSparse precedence', function () {
337
+ // perSource, globalOption, metadata, isVector, expected
338
+ const cases = [
339
+ // Nothing set: the format decides.
340
+ [undefined, undefined, undefined, true, false],
341
+ [undefined, undefined, undefined, false, true],
342
+ // The global option overrides the format default, both ways.
343
+ [undefined, true, undefined, true, true],
344
+ [undefined, false, undefined, false, false],
345
+ // A per-source value overrides the format default, both ways.
346
+ [true, undefined, undefined, true, true],
347
+ [false, undefined, undefined, false, false],
348
+ // A per-source value also overrides the global, both ways. These break if
349
+ // the chain ever uses `||` instead of `??`.
350
+ [true, false, undefined, true, true],
351
+ [false, true, undefined, false, false],
352
+ // Archive metadata overrides the format default, both ways.
353
+ [undefined, undefined, true, true, true],
354
+ [undefined, undefined, false, false, false],
355
+ // ...but loses to a per-source value, both ways.
356
+ [true, undefined, false, true, true],
357
+ [false, undefined, true, false, false],
358
+ // ...and loses to the global option, both ways.
359
+ [undefined, true, false, true, true],
360
+ [undefined, false, true, false, false],
361
+ // Per-source still wins when all three disagree.
362
+ [true, false, false, true, true],
363
+ [false, true, true, false, false],
364
+ // Agreement between levels changes nothing.
365
+ [true, true, true, true, true],
366
+ [false, false, false, false, false],
367
+ ];
368
+
369
+ for (const [perSource, globalOption, metadata, isVector, expected] of cases) {
370
+ const label =
371
+ `source=${perSource} global=${globalOption} meta=${metadata} ` +
372
+ `${isVector ? 'vector' : 'raster'} -> ${expected}`;
373
+ it(label, function () {
374
+ expect(
375
+ resolveSparse({ perSource, globalOption, metadata, isVector }),
376
+ ).to.equal(expected);
377
+ });
378
+ }
379
+ });
380
+
381
+ describe('parseOptionalBoolean', function () {
382
+ // mbtiles metadata is a table of strings, so `sparse` arrives as text. Taken
383
+ // at face value "false" is truthy, which would invert the setting.
384
+ const cases = [
385
+ [true, true],
386
+ [false, false],
387
+ ['true', true],
388
+ ['false', false],
389
+ ['TRUE', true],
390
+ ['False', false],
391
+ [' true ', true],
392
+ ['1', true],
393
+ ['0', false],
394
+ // Anything unrecognised is absent, so it falls through to the next level
395
+ // of precedence rather than being guessed at.
396
+ [undefined, undefined],
397
+ [null, undefined],
398
+ ['', undefined],
399
+ ['yes', undefined],
400
+ ['maybe', undefined],
401
+ [1, undefined],
402
+ [0, undefined],
403
+ [{}, undefined],
404
+ ];
405
+
406
+ for (const [input, expected] of cases) {
407
+ it(`${JSON.stringify(input)} -> ${expected}`, function () {
408
+ expect(parseOptionalBoolean(input)).to.equal(expected);
409
+ });
410
+ }
411
+ });
package/test/static.js CHANGED
@@ -70,6 +70,26 @@ describe('Static endpoints', function () {
70
70
  testStatic(prefix, '8,40,2@10/300x150', 'png', 200, 3);
71
71
  testStatic(prefix, '8,40,2@10.3,20.4/300x300', 'png', 200);
72
72
  testStatic(prefix, '0,0,2@390,120/300x300', 'png', 200);
73
+
74
+ it('keeps overlays aligned when a large viewport raises the effective zoom', async function () {
75
+ const marker = '8.5432,47.3669|marker-icon.png|scale:0.1';
76
+ const requestedZoom = await supertest(app)
77
+ .get(`/styles/${prefix}/static/8.5432,47.3669,1/600x600.png`)
78
+ .query({ marker })
79
+ .expect(200);
80
+ const minimumZoom = Math.log2(600 / 256);
81
+ const effectiveZoom = await supertest(app)
82
+ .get(
83
+ `/styles/${prefix}/static/8.5432,47.3669,${minimumZoom}/600x600.png`,
84
+ )
85
+ .query({ marker })
86
+ .expect(200);
87
+
88
+ assert.ok(
89
+ requestedZoom.body.equals(effectiveZoom.body),
90
+ 'expected requested and effective zoom renders to match',
91
+ );
92
+ });
73
93
  });
74
94
  });
75
95
 
@@ -39,6 +39,11 @@ const tests = [
39
39
  // Test high-DPI rendering using @2x scale
40
40
  url: '/styles/test-style/static/8.5375,47.379,11/200x150@2x.png',
41
41
  },
42
+ {
43
+ name: 'static-512px-path-alignment-2x',
44
+ // Test a real route overlay on an auto-fitted 512px high-DPI static map
45
+ url: '/styles/test-style/static/auto/512x512@2x.png?stroke=red&width=5&padding=0.1&path=8.53324,47.36674|8.53310,47.36661|8.53352,47.36603|8.53530,47.36666|8.53456,47.36770|8.53398,47.36926|8.53367,47.36963|8.53608,47.37132|8.53502,47.37209|8.53459,47.37256|8.53685,47.37315|8.53712,47.37375|8.53835,47.37392|8.53881,47.37450|8.54169,47.37443|8.54198,47.37582|8.54174,47.37669|8.54189,47.37759|8.54171,47.37841|8.54277,47.37867|8.54302,47.37893|8.54265,47.37987|8.54290,47.38031|8.54195,47.38155|8.54149,47.38272|8.54174,47.38329',
46
+ },
42
47
  {
43
48
  name: 'path-auto',
44
49
  // Test path rendering with simple coordinates and auto-centering
@@ -64,6 +69,11 @@ const tests = [
64
69
  // Test multiple markers with scale and offset options
65
70
  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
71
  },
72
+ {
73
+ name: 'static-marker-low-zoom-large-viewport',
74
+ // Test marker alignment when the viewport raises the effective low zoom
75
+ url: '/styles/test-style/static/8.5432,47.3669,1/1000x1000.png?marker=8.5432,47.3669|marker-icon.png|scale:0.8|offset:0,30',
76
+ },
67
77
  {
68
78
  name: 'static-bbox',
69
79
  // Test area-based map rendering using a bounding box (bbox)
@@ -0,0 +1,145 @@
1
+ import { createHook } from 'node:async_hooks';
2
+ import { once } from 'node:events';
3
+ import http from 'node:http';
4
+ import { gzipSync, gunzipSync } from 'node:zlib';
5
+ import { expect } from 'chai';
6
+ import { PbfWriter } from 'pbf';
7
+ import { serve_data } from '../src/serve_data.js';
8
+
9
+ /**
10
+ * Creates an MVT with one point at the center of the tile.
11
+ * @param {number} id Feature ID.
12
+ * @returns {Buffer} Encoded vector tile.
13
+ */
14
+ function createTile(id) {
15
+ const feature = new PbfWriter();
16
+ feature.writeVarintField(1, id);
17
+ feature.writeVarintField(3, 1);
18
+ // MoveTo one point at (2048, 2048), zigzag-encoded for a 4096-unit tile.
19
+ feature.writePackedVarint(4, [9, 4096, 4096]);
20
+ const layer = new PbfWriter();
21
+ layer.writeStringField(1, 'test');
22
+ layer.writeBytesField(2, feature.finish());
23
+ layer.writeVarintField(5, 4096);
24
+ layer.writeVarintField(15, 2);
25
+ const tile = new PbfWriter();
26
+ tile.writeBytesField(3, layer.finish());
27
+ return Buffer.from(tile.finish());
28
+ }
29
+
30
+ const tile = createTile(1);
31
+ const gzippedTile = gzipSync(tile, { level: 0 });
32
+
33
+ /**
34
+ * Requests a tile without decompressing the HTTP response.
35
+ * @param {Buffer} data Bytes returned by the MBTiles source.
36
+ * @param {object} [options] Tile server options.
37
+ * @param {string} [format] Requested output format.
38
+ * @returns {Promise<object>} Raw response body and headers.
39
+ */
40
+
41
+ async function requestTile(data, options = {}, format = 'pbf') {
42
+ const repo = {
43
+ test: {
44
+ sourceType: 'mbtiles',
45
+ source: { getTile: (_z, _x, _y, callback) => callback(null, data, {}) },
46
+ tileJSON: { format: 'pbf', minzoom: 0, maxzoom: 0 },
47
+ },
48
+ };
49
+
50
+ const app = serve_data.init(options, repo, {});
51
+ // Use a free loopback port to inspect the raw gzip response bytes.
52
+ const server = app.listen(0, '127.0.0.1');
53
+
54
+ try {
55
+ await once(server, 'listening');
56
+ return await new Promise((resolve, reject) => {
57
+ const request = http.get(
58
+ {
59
+ hostname: '127.0.0.1',
60
+ port: server.address().port,
61
+ path: `/test/0/0/0.${format}`,
62
+ agent: false,
63
+ },
64
+ (res) => {
65
+ const chunks = [];
66
+ res.on('data', (chunk) => chunks.push(chunk));
67
+ res.on('error', reject);
68
+ res.on('end', () =>
69
+ resolve({
70
+ status: res.statusCode,
71
+ headers: res.headers,
72
+ body: Buffer.concat(chunks),
73
+ }),
74
+ );
75
+ },
76
+ );
77
+ request.on('error', reject);
78
+ });
79
+ } finally {
80
+ if (server.listening) {
81
+ await new Promise((resolve) => server.close(resolve));
82
+ }
83
+ }
84
+ }
85
+
86
+ describe('Vector tile compression', function () {
87
+ it('returns an existing gzip buffer without decompressing or recompressing it', async function () {
88
+ let zlibOperations = 0;
89
+ const hook = createHook({
90
+ init(_asyncId, type) {
91
+ if (type === 'ZLIB') zlibOperations += 1;
92
+ },
93
+ });
94
+ hook.enable();
95
+ let response;
96
+ try {
97
+ response = await requestTile(gzippedTile);
98
+ } finally {
99
+ hook.disable();
100
+ }
101
+
102
+ expect(response.status).to.equal(200);
103
+ expect(response.headers['content-type']).to.equal('application/x-protobuf');
104
+ expect(response.headers['content-encoding']).to.equal('gzip');
105
+ expect(zlibOperations).to.equal(0);
106
+ expect(response.body).to.deep.equal(gzippedTile);
107
+ });
108
+
109
+ it('still decompresses, decorates and recompresses gzip tiles', async function () {
110
+ const decoratedTile = createTile(2);
111
+ const response = await requestTile(gzippedTile, {
112
+ dataDecoratorFunc: async (id, type, data) => {
113
+ expect([id, type]).to.deep.equal(['test', 'data']);
114
+ expect(data).to.deep.equal(tile);
115
+ return decoratedTile;
116
+ },
117
+ });
118
+
119
+ expect(response.status).to.equal(200);
120
+ expect(response.headers['content-encoding']).to.equal('gzip');
121
+ expect(gunzipSync(response.body)).to.deep.equal(decoratedTile);
122
+ });
123
+
124
+ it('still compresses uncompressed PBF tiles', async function () {
125
+ const response = await requestTile(tile);
126
+
127
+ expect(response.status).to.equal(200);
128
+ expect(response.headers['content-encoding']).to.equal('gzip');
129
+ expect(gunzipSync(response.body)).to.deep.equal(tile);
130
+ });
131
+
132
+ it('still converts gzip PBF tiles to GeoJSON', async function () {
133
+ const response = await requestTile(gzippedTile, {}, 'geojson');
134
+
135
+ expect(response.status).to.equal(200);
136
+ expect(response.headers['content-type']).to.match(/^application\/json/);
137
+ expect(response.headers['content-encoding']).to.equal('gzip');
138
+
139
+ const geojson = JSON.parse(gunzipSync(response.body).toString());
140
+
141
+ expect(geojson.type).to.equal('FeatureCollection');
142
+ expect(geojson.features).to.have.lengthOf(1);
143
+ expect(geojson.features[0].id).to.equal(1);
144
+ });
145
+ });