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

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 +14 -14
  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,513 @@
1
+ // Test terrain tiles elevation values:
2
+ // Zoom 0: tile (0,0) = 100m (entire world)
3
+ // Zoom 1: tile (0,0) = 200m (top-left, lon<0 lat>0)
4
+ // tile (1,0) = 500m (top-right, lon>0 lat>0)
5
+ // tile (0,1) = 1000m (bottom-left, lon<0 lat<0)
6
+ // tile (1,1) = 2500m (bottom-right, lon>0 lat<0)
7
+
8
+ describe('Elevation API', function () {
9
+ describe('non-existent data source', function () {
10
+ it('/data/non_existent/elevation/0/0/0 returns 404', function (done) {
11
+ supertest(app)
12
+ .get('/data/non_existent/elevation/0/0/0')
13
+ .expect(404)
14
+ .end(done);
15
+ });
16
+ });
17
+
18
+ describe('data source without encoding', function () {
19
+ it('/data/openmaptiles/elevation/0/0/0 returns 400 missing encoding', function (done) {
20
+ supertest(app)
21
+ .get('/data/openmaptiles/elevation/0/0/0')
22
+ .expect(400)
23
+ .expect('Missing tileJSON.encoding')
24
+ .end(done);
25
+ });
26
+ });
27
+
28
+ describe('terrain data source', function () {
29
+ describe('valid tile requests with correct elevation values', function () {
30
+ it('/data/terrain/elevation/0/0/0 returns elevation 100m', function (done) {
31
+ supertest(app)
32
+ .get('/data/terrain/elevation/0/0/0')
33
+ .expect(200)
34
+ .expect('Content-Type', /application\/json/)
35
+ .expect(function (res) {
36
+ expect(res.body).to.be.an('object');
37
+ expect(res.body).to.have.property('elevation', 100);
38
+ expect(res.body).to.have.property('z', 0);
39
+ expect(res.body).to.have.property('x', 0);
40
+ expect(res.body).to.have.property('y', 0);
41
+ })
42
+ .end(done);
43
+ });
44
+
45
+ it('/data/terrain/elevation/1/0/0 returns elevation 200m (top-left)', function (done) {
46
+ supertest(app)
47
+ .get('/data/terrain/elevation/1/0/0')
48
+ .expect(200)
49
+ .expect('Content-Type', /application\/json/)
50
+ .expect(function (res) {
51
+ expect(res.body).to.have.property('elevation', 200);
52
+ expect(res.body).to.have.property('z', 1);
53
+ expect(res.body).to.have.property('x', 0);
54
+ expect(res.body).to.have.property('y', 0);
55
+ })
56
+ .end(done);
57
+ });
58
+
59
+ it('/data/terrain/elevation/1/1/0 returns elevation 500m (top-right)', function (done) {
60
+ supertest(app)
61
+ .get('/data/terrain/elevation/1/1/0')
62
+ .expect(200)
63
+ .expect('Content-Type', /application\/json/)
64
+ .expect(function (res) {
65
+ expect(res.body).to.have.property('elevation', 500);
66
+ expect(res.body).to.have.property('z', 1);
67
+ expect(res.body).to.have.property('x', 1);
68
+ expect(res.body).to.have.property('y', 0);
69
+ })
70
+ .end(done);
71
+ });
72
+
73
+ it('/data/terrain/elevation/1/0/1 returns elevation 1000m (bottom-left)', function (done) {
74
+ supertest(app)
75
+ .get('/data/terrain/elevation/1/0/1')
76
+ .expect(200)
77
+ .expect('Content-Type', /application\/json/)
78
+ .expect(function (res) {
79
+ expect(res.body).to.have.property('elevation', 1000);
80
+ expect(res.body).to.have.property('z', 1);
81
+ expect(res.body).to.have.property('x', 0);
82
+ expect(res.body).to.have.property('y', 1);
83
+ })
84
+ .end(done);
85
+ });
86
+
87
+ it('/data/terrain/elevation/1/1/1 returns elevation 2500m (bottom-right)', function (done) {
88
+ supertest(app)
89
+ .get('/data/terrain/elevation/1/1/1')
90
+ .expect(200)
91
+ .expect('Content-Type', /application\/json/)
92
+ .expect(function (res) {
93
+ expect(res.body).to.have.property('elevation', 2500);
94
+ expect(res.body).to.have.property('z', 1);
95
+ expect(res.body).to.have.property('x', 1);
96
+ expect(res.body).to.have.property('y', 1);
97
+ })
98
+ .end(done);
99
+ });
100
+ });
101
+
102
+ describe('coordinate-based requests with correct elevation values', function () {
103
+ // Note: coordinates must be non-integer to be treated as lon/lat, not tile x/y
104
+ it('top-right quadrant (lon>0, lat>0) returns 500m', function (done) {
105
+ supertest(app)
106
+ .get('/data/terrain/elevation/1/45.5/45.5')
107
+ .expect(200)
108
+ .expect('Content-Type', /application\/json/)
109
+ .expect(function (res) {
110
+ expect(res.body).to.have.property('elevation', 500);
111
+ expect(res.body).to.have.property('long', 45.5);
112
+ expect(res.body).to.have.property('lat', 45.5);
113
+ expect(res.body).to.have.property('z', 1);
114
+ expect(res.body).to.have.property('x', 1);
115
+ expect(res.body).to.have.property('y', 0);
116
+ })
117
+ .end(done);
118
+ });
119
+
120
+ it('top-left quadrant (lon<0, lat>0) returns 200m', function (done) {
121
+ supertest(app)
122
+ .get('/data/terrain/elevation/1/-45.5/45.5')
123
+ .expect(200)
124
+ .expect('Content-Type', /application\/json/)
125
+ .expect(function (res) {
126
+ expect(res.body).to.have.property('elevation', 200);
127
+ expect(res.body).to.have.property('long', -45.5);
128
+ expect(res.body).to.have.property('lat', 45.5);
129
+ expect(res.body).to.have.property('z', 1);
130
+ expect(res.body).to.have.property('x', 0);
131
+ expect(res.body).to.have.property('y', 0);
132
+ })
133
+ .end(done);
134
+ });
135
+
136
+ it('bottom-left quadrant (lon<0, lat<0) returns 1000m', function (done) {
137
+ supertest(app)
138
+ .get('/data/terrain/elevation/1/-45.5/-45.5')
139
+ .expect(200)
140
+ .expect('Content-Type', /application\/json/)
141
+ .expect(function (res) {
142
+ expect(res.body).to.have.property('elevation', 1000);
143
+ expect(res.body).to.have.property('long', -45.5);
144
+ expect(res.body).to.have.property('lat', -45.5);
145
+ expect(res.body).to.have.property('z', 1);
146
+ expect(res.body).to.have.property('x', 0);
147
+ expect(res.body).to.have.property('y', 1);
148
+ })
149
+ .end(done);
150
+ });
151
+
152
+ it('bottom-right quadrant (lon>0, lat<0) returns 2500m', function (done) {
153
+ supertest(app)
154
+ .get('/data/terrain/elevation/1/45.5/-45.5')
155
+ .expect(200)
156
+ .expect('Content-Type', /application\/json/)
157
+ .expect(function (res) {
158
+ expect(res.body).to.have.property('elevation', 2500);
159
+ expect(res.body).to.have.property('long', 45.5);
160
+ expect(res.body).to.have.property('lat', -45.5);
161
+ expect(res.body).to.have.property('z', 1);
162
+ expect(res.body).to.have.property('x', 1);
163
+ expect(res.body).to.have.property('y', 1);
164
+ })
165
+ .end(done);
166
+ });
167
+ });
168
+
169
+ describe('tile boundary conditions', function () {
170
+ // At zoom 1, tiles are divided at lon=0 and lat=0
171
+ // Testing coordinates very close to these boundaries
172
+
173
+ it('just east of prime meridian (lon=0.001) returns top-right tile (500m)', function (done) {
174
+ supertest(app)
175
+ .get('/data/terrain/elevation/1/0.001/45')
176
+ .expect(200)
177
+ .expect(function (res) {
178
+ expect(res.body).to.have.property('elevation', 500);
179
+ expect(res.body).to.have.property('x', 1);
180
+ expect(res.body).to.have.property('y', 0);
181
+ })
182
+ .end(done);
183
+ });
184
+
185
+ it('just west of prime meridian (lon=-0.001) returns top-left tile (200m)', function (done) {
186
+ supertest(app)
187
+ .get('/data/terrain/elevation/1/-0.001/45')
188
+ .expect(200)
189
+ .expect(function (res) {
190
+ expect(res.body).to.have.property('elevation', 200);
191
+ expect(res.body).to.have.property('x', 0);
192
+ expect(res.body).to.have.property('y', 0);
193
+ })
194
+ .end(done);
195
+ });
196
+
197
+ it('just north of equator (lat=0.001) returns top-right tile (500m)', function (done) {
198
+ supertest(app)
199
+ .get('/data/terrain/elevation/1/45/0.001')
200
+ .expect(200)
201
+ .expect(function (res) {
202
+ expect(res.body).to.have.property('elevation', 500);
203
+ expect(res.body).to.have.property('x', 1);
204
+ expect(res.body).to.have.property('y', 0);
205
+ })
206
+ .end(done);
207
+ });
208
+
209
+ it('just south of equator (lat=-0.001) returns bottom-right tile (2500m)', function (done) {
210
+ supertest(app)
211
+ .get('/data/terrain/elevation/1/45/-0.001')
212
+ .expect(200)
213
+ .expect(function (res) {
214
+ expect(res.body).to.have.property('elevation', 2500);
215
+ expect(res.body).to.have.property('x', 1);
216
+ expect(res.body).to.have.property('y', 1);
217
+ })
218
+ .end(done);
219
+ });
220
+
221
+ it('near corner - just NE of origin (lon=0.001, lat=0.001) returns top-right (500m)', function (done) {
222
+ supertest(app)
223
+ .get('/data/terrain/elevation/1/0.001/0.001')
224
+ .expect(200)
225
+ .expect(function (res) {
226
+ expect(res.body).to.have.property('elevation', 500);
227
+ expect(res.body).to.have.property('x', 1);
228
+ expect(res.body).to.have.property('y', 0);
229
+ })
230
+ .end(done);
231
+ });
232
+
233
+ it('near corner - just NW of origin (lon=-0.001, lat=0.001) returns top-left (200m)', function (done) {
234
+ supertest(app)
235
+ .get('/data/terrain/elevation/1/-0.001/0.001')
236
+ .expect(200)
237
+ .expect(function (res) {
238
+ expect(res.body).to.have.property('elevation', 200);
239
+ expect(res.body).to.have.property('x', 0);
240
+ expect(res.body).to.have.property('y', 0);
241
+ })
242
+ .end(done);
243
+ });
244
+
245
+ it('near corner - just SE of origin (lon=0.001, lat=-0.001) returns bottom-right (2500m)', function (done) {
246
+ supertest(app)
247
+ .get('/data/terrain/elevation/1/0.001/-0.001')
248
+ .expect(200)
249
+ .expect(function (res) {
250
+ expect(res.body).to.have.property('elevation', 2500);
251
+ expect(res.body).to.have.property('x', 1);
252
+ expect(res.body).to.have.property('y', 1);
253
+ })
254
+ .end(done);
255
+ });
256
+
257
+ it('near corner - just SW of origin (lon=-0.001, lat=-0.001) returns bottom-left (1000m)', function (done) {
258
+ supertest(app)
259
+ .get('/data/terrain/elevation/1/-0.001/-0.001')
260
+ .expect(200)
261
+ .expect(function (res) {
262
+ expect(res.body).to.have.property('elevation', 1000);
263
+ expect(res.body).to.have.property('x', 0);
264
+ expect(res.body).to.have.property('y', 1);
265
+ })
266
+ .end(done);
267
+ });
268
+
269
+ it('at origin (lon=0, lat=0) returns consistent tile', function (done) {
270
+ supertest(app)
271
+ .get('/data/terrain/elevation/1/0/0')
272
+ .expect(200)
273
+ .expect(function (res) {
274
+ // At exactly 0,0 it should pick one of the tiles consistently
275
+ expect(res.body.elevation).to.be.oneOf([200, 500, 1000, 2500]);
276
+ })
277
+ .end(done);
278
+ });
279
+
280
+ it('near western edge (lon=-179.999) returns correct tile', function (done) {
281
+ supertest(app)
282
+ .get('/data/terrain/elevation/1/-179.999/45')
283
+ .expect(200)
284
+ .expect(function (res) {
285
+ expect(res.body).to.have.property('elevation', 200);
286
+ expect(res.body).to.have.property('x', 0);
287
+ })
288
+ .end(done);
289
+ });
290
+
291
+ it('near eastern edge (lon=179.999) returns correct tile', function (done) {
292
+ supertest(app)
293
+ .get('/data/terrain/elevation/1/179.999/45')
294
+ .expect(200)
295
+ .expect(function (res) {
296
+ expect(res.body).to.have.property('elevation', 500);
297
+ expect(res.body).to.have.property('x', 1);
298
+ })
299
+ .end(done);
300
+ });
301
+ });
302
+
303
+ describe('zoom clamping', function () {
304
+ it('zoom is clamped to maxzoom for coordinate requests', function (done) {
305
+ supertest(app)
306
+ .get('/data/terrain/elevation/20/45.5/45.5')
307
+ .expect(200)
308
+ .expect('Content-Type', /application\/json/)
309
+ .expect(function (res) {
310
+ expect(res.body).to.have.property('z', 1);
311
+ expect(res.body).to.have.property('elevation', 500);
312
+ })
313
+ .end(done);
314
+ });
315
+
316
+ it('zoom is clamped to minzoom for coordinate requests', function (done) {
317
+ supertest(app)
318
+ .get('/data/terrain/elevation/-5/45.5/45.5')
319
+ .expect(200)
320
+ .expect('Content-Type', /application\/json/)
321
+ .expect(function (res) {
322
+ expect(res.body).to.have.property('z', 0);
323
+ expect(res.body).to.have.property('elevation', 100);
324
+ })
325
+ .end(done);
326
+ });
327
+ });
328
+
329
+ describe('invalid tile requests', function () {
330
+ it('tile out of bounds returns 404', function (done) {
331
+ supertest(app)
332
+ .get('/data/terrain/elevation/0/1/0')
333
+ .expect(404)
334
+ .expect('Out of bounds')
335
+ .end(done);
336
+ });
337
+
338
+ it('zoom below minzoom for tile request returns 404', function (done) {
339
+ supertest(app)
340
+ .get('/data/terrain/elevation/-1/0/0')
341
+ .expect(404)
342
+ .expect('Out of bounds')
343
+ .end(done);
344
+ });
345
+
346
+ it('zoom above maxzoom for tile request returns 404', function (done) {
347
+ supertest(app)
348
+ .get('/data/terrain/elevation/2/0/0')
349
+ .expect(404)
350
+ .expect('Out of bounds')
351
+ .end(done);
352
+ });
353
+
354
+ it('negative x coordinate returns 404', function (done) {
355
+ supertest(app)
356
+ .get('/data/terrain/elevation/0/-1/0')
357
+ .expect(404)
358
+ .expect('Out of bounds')
359
+ .end(done);
360
+ });
361
+
362
+ it('negative y coordinate returns 404', function (done) {
363
+ supertest(app)
364
+ .get('/data/terrain/elevation/0/0/-1')
365
+ .expect(404)
366
+ .expect('Out of bounds')
367
+ .end(done);
368
+ });
369
+ });
370
+
371
+ describe('batch elevation requests', function () {
372
+ it('returns elevations for multiple points in different tiles', function (done) {
373
+ supertest(app)
374
+ .post('/data/terrain/elevation')
375
+ .send({
376
+ points: [
377
+ { lon: 45.5, lat: 45.5, z: 1 }, // top-right: 500m
378
+ { lon: -45.5, lat: 45.5, z: 1 }, // top-left: 200m
379
+ { lon: -45.5, lat: -45.5, z: 1 }, // bottom-left: 1000m
380
+ { lon: 45.5, lat: -45.5, z: 1 }, // bottom-right: 2500m
381
+ ],
382
+ })
383
+ .expect(200)
384
+ .expect('Content-Type', /application\/json/)
385
+ .expect(function (res) {
386
+ expect(res.body).to.be.an('array');
387
+ expect(res.body).to.have.length(4);
388
+ expect(res.body[0]).to.equal(500);
389
+ expect(res.body[1]).to.equal(200);
390
+ expect(res.body[2]).to.equal(1000);
391
+ expect(res.body[3]).to.equal(2500);
392
+ })
393
+ .end(done);
394
+ });
395
+
396
+ it('returns elevations for multiple points in the same tile', function (done) {
397
+ supertest(app)
398
+ .post('/data/terrain/elevation')
399
+ .send({
400
+ points: [
401
+ { lon: 45.5, lat: 45.5, z: 1 }, // top-right tile
402
+ { lon: 90, lat: 30, z: 1 }, // also top-right tile
403
+ { lon: 10, lat: 10, z: 1 }, // also top-right tile
404
+ ],
405
+ })
406
+ .expect(200)
407
+ .expect('Content-Type', /application\/json/)
408
+ .expect(function (res) {
409
+ expect(res.body).to.be.an('array');
410
+ expect(res.body).to.have.length(3);
411
+ // All points are in top-right tile which has 500m elevation
412
+ expect(res.body[0]).to.equal(500);
413
+ expect(res.body[1]).to.equal(500);
414
+ expect(res.body[2]).to.equal(500);
415
+ })
416
+ .end(done);
417
+ });
418
+
419
+ it('supports different zoom levels per point', function (done) {
420
+ supertest(app)
421
+ .post('/data/terrain/elevation')
422
+ .send({
423
+ points: [
424
+ { lon: 45.5, lat: 45.5, z: 0 }, // zoom 0: 100m (whole world)
425
+ { lon: 45.5, lat: 45.5, z: 1 }, // zoom 1: 500m (top-right)
426
+ ],
427
+ })
428
+ .expect(200)
429
+ .expect(function (res) {
430
+ expect(res.body).to.be.an('array');
431
+ expect(res.body).to.have.length(2);
432
+ expect(res.body[0]).to.equal(100);
433
+ expect(res.body[1]).to.equal(500);
434
+ })
435
+ .end(done);
436
+ });
437
+
438
+ it('clamps zoom to maxzoom', function (done) {
439
+ supertest(app)
440
+ .post('/data/terrain/elevation')
441
+ .send({
442
+ points: [{ lon: 45.5, lat: 45.5, z: 20 }], // maxzoom is 1
443
+ })
444
+ .expect(200)
445
+ .expect(function (res) {
446
+ expect(res.body).to.be.an('array');
447
+ expect(res.body[0]).to.equal(500);
448
+ })
449
+ .end(done);
450
+ });
451
+
452
+ it('clamps zoom to minzoom', function (done) {
453
+ supertest(app)
454
+ .post('/data/terrain/elevation')
455
+ .send({
456
+ points: [{ lon: 45.5, lat: 45.5, z: -5 }], // minzoom is 0
457
+ })
458
+ .expect(200)
459
+ .expect(function (res) {
460
+ expect(res.body).to.be.an('array');
461
+ // At zoom 0, entire world is one tile with 100m elevation
462
+ expect(res.body[0]).to.equal(100);
463
+ })
464
+ .end(done);
465
+ });
466
+
467
+ it('returns 400 for invalid point', function (done) {
468
+ supertest(app)
469
+ .post('/data/terrain/elevation')
470
+ .send({
471
+ points: [{ lon: 'invalid', lat: 45.5, z: 1 }],
472
+ })
473
+ .expect(400)
474
+ .end(done);
475
+ });
476
+
477
+ it('returns 400 for missing points array', function (done) {
478
+ supertest(app)
479
+ .post('/data/terrain/elevation')
480
+ .send({})
481
+ .expect(400)
482
+ .expect('Missing or empty points array')
483
+ .end(done);
484
+ });
485
+
486
+ it('returns 400 for empty points array', function (done) {
487
+ supertest(app)
488
+ .post('/data/terrain/elevation')
489
+ .send({ points: [] })
490
+ .expect(400)
491
+ .expect('Missing or empty points array')
492
+ .end(done);
493
+ });
494
+
495
+ it('returns 404 for non-existent data source', function (done) {
496
+ supertest(app)
497
+ .post('/data/non_existent/elevation')
498
+ .send({ points: [{ lon: 45.5, lat: 45.5, z: 1 }] })
499
+ .expect(404)
500
+ .end(done);
501
+ });
502
+
503
+ it('returns 400 for data source without encoding', function (done) {
504
+ supertest(app)
505
+ .post('/data/openmaptiles/elevation')
506
+ .send({ points: [{ lon: 45.5, lat: 45.5, z: 1 }] })
507
+ .expect(400)
508
+ .expect('Missing tileJSON.encoding')
509
+ .end(done);
510
+ });
511
+ });
512
+ });
513
+ });