tileserver-gl-light 5.5.0-pre.6 → 5.5.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 +52 -41
- package/docs/endpoints.rst +12 -2
- package/docs/installation.rst +6 -6
- package/package.json +10 -8
- package/public/resources/elevation-control.js +84 -22
- package/public/resources/maplibre-gl.js +4 -4
- package/public/resources/maplibre-gl.js.map +1 -1
- package/src/serve_data.js +239 -70
- package/src/serve_light.js +2 -3
- package/src/serve_rendered.js +39 -56
- package/src/utils.js +29 -0
- package/test/elevation.js +513 -0
- 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
|
+
});
|
|
@@ -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);
|