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
package/src/serve_data.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
getTileUrls,
|
|
15
15
|
isValidRemoteUrl,
|
|
16
16
|
fetchTileData,
|
|
17
|
+
lonLatToTilePixel,
|
|
17
18
|
} from './utils.js';
|
|
18
19
|
import { getPMtilesInfo, openPMtiles } from './pmtiles_adapter.js';
|
|
19
20
|
import { gunzipP, gzipP } from './promises.js';
|
|
@@ -45,6 +46,7 @@ export const serve_data = {
|
|
|
45
46
|
init: function (options, repo, programOpts) {
|
|
46
47
|
const { verbose } = programOpts;
|
|
47
48
|
const app = express().disable('x-powered-by');
|
|
49
|
+
app.use(express.json());
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
52
|
* Handles requests for tile data, responding with the tile image.
|
|
@@ -168,6 +170,164 @@ export const serve_data = {
|
|
|
168
170
|
return res.status(200).send(data);
|
|
169
171
|
});
|
|
170
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Validates elevation data source and returns source info or sends error response.
|
|
175
|
+
* @param {string} id - ID of the data source.
|
|
176
|
+
* @param {object} res - Express response object.
|
|
177
|
+
* @returns {object|null} Source info object or null if validation failed.
|
|
178
|
+
*/
|
|
179
|
+
const validateElevationSource = (id, res) => {
|
|
180
|
+
// eslint-disable-next-line security/detect-object-injection -- id is route parameter for data source lookup
|
|
181
|
+
const item = repo?.[id];
|
|
182
|
+
if (!item) {
|
|
183
|
+
res.sendStatus(404);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (!item.source) {
|
|
187
|
+
res.status(404).send('Missing source');
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
if (!item.tileJSON) {
|
|
191
|
+
res.status(404).send('Missing tileJSON');
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
if (!item.sourceType) {
|
|
195
|
+
res.status(404).send('Missing sourceType');
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const { source, tileJSON, sourceType } = item;
|
|
199
|
+
if (sourceType !== 'pmtiles' && sourceType !== 'mbtiles') {
|
|
200
|
+
res.status(400).send('Invalid sourceType. Must be pmtiles or mbtiles.');
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const encoding = tileJSON?.encoding;
|
|
204
|
+
if (encoding == null) {
|
|
205
|
+
res.status(400).send('Missing tileJSON.encoding');
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (encoding !== 'terrarium' && encoding !== 'mapbox') {
|
|
209
|
+
res.status(400).send('Invalid encoding. Must be terrarium or mapbox.');
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const format = tileJSON?.format;
|
|
213
|
+
if (format == null) {
|
|
214
|
+
res.status(400).send('Missing tileJSON.format');
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
if (format !== 'webp' && format !== 'png') {
|
|
218
|
+
res.status(400).send('Invalid format. Must be webp or png.');
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
if (tileJSON.minzoom == null || tileJSON.maxzoom == null) {
|
|
222
|
+
res.status(400).send('Missing tileJSON zoom bounds');
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
source,
|
|
227
|
+
sourceType,
|
|
228
|
+
encoding,
|
|
229
|
+
format,
|
|
230
|
+
tileSize: tileJSON.tileSize || 512,
|
|
231
|
+
minzoom: tileJSON.minzoom,
|
|
232
|
+
maxzoom: tileJSON.maxzoom,
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Validates that a point has valid lon, lat, and z properties.
|
|
238
|
+
* @param {object} point - Point to validate.
|
|
239
|
+
* @param {number} index - Index of the point in the array.
|
|
240
|
+
* @returns {string|null} Error message if invalid, null if valid.
|
|
241
|
+
*/
|
|
242
|
+
const validatePoint = (point, index) => {
|
|
243
|
+
if (point == null || typeof point !== 'object') {
|
|
244
|
+
return `Invalid point at index ${index}: point must be an object`;
|
|
245
|
+
}
|
|
246
|
+
if (typeof point.lon !== 'number' || !isFinite(point.lon)) {
|
|
247
|
+
return `Invalid point at index ${index}: lon must be a finite number`;
|
|
248
|
+
}
|
|
249
|
+
if (typeof point.lat !== 'number' || !isFinite(point.lat)) {
|
|
250
|
+
return `Invalid point at index ${index}: lat must be a finite number`;
|
|
251
|
+
}
|
|
252
|
+
if (typeof point.z !== 'number' || !isFinite(point.z)) {
|
|
253
|
+
return `Invalid point at index ${index}: z must be a finite number`;
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Gets batch elevations for an array of points.
|
|
260
|
+
* @param {object} sourceInfo - Validated source info from validateElevationSource.
|
|
261
|
+
* @param {Array<{lon: number, lat: number, z: number}>} points - Array of validated points.
|
|
262
|
+
* @returns {Promise<Array<number|null>>} Array of elevations in same order as input.
|
|
263
|
+
*/
|
|
264
|
+
const getBatchElevations = async (sourceInfo, points) => {
|
|
265
|
+
const {
|
|
266
|
+
source,
|
|
267
|
+
sourceType,
|
|
268
|
+
encoding,
|
|
269
|
+
format,
|
|
270
|
+
tileSize,
|
|
271
|
+
minzoom,
|
|
272
|
+
maxzoom,
|
|
273
|
+
} = sourceInfo;
|
|
274
|
+
|
|
275
|
+
// Group points by tile (including zoom level in the key)
|
|
276
|
+
const tileGroups = new Map();
|
|
277
|
+
for (let i = 0; i < points.length; i++) {
|
|
278
|
+
// eslint-disable-next-line security/detect-object-injection -- i is loop counter
|
|
279
|
+
const point = points[i];
|
|
280
|
+
let zoom = point.z;
|
|
281
|
+
if (zoom < minzoom) {
|
|
282
|
+
zoom = minzoom;
|
|
283
|
+
}
|
|
284
|
+
if (zoom > maxzoom) {
|
|
285
|
+
zoom = maxzoom;
|
|
286
|
+
}
|
|
287
|
+
const { tileX, tileY, pixelX, pixelY } = lonLatToTilePixel(
|
|
288
|
+
point.lon,
|
|
289
|
+
point.lat,
|
|
290
|
+
zoom,
|
|
291
|
+
tileSize,
|
|
292
|
+
);
|
|
293
|
+
const tileKey = `${zoom},${tileX},${tileY}`;
|
|
294
|
+
if (!tileGroups.has(tileKey)) {
|
|
295
|
+
tileGroups.set(tileKey, { zoom, tileX, tileY, pixels: [] });
|
|
296
|
+
}
|
|
297
|
+
tileGroups.get(tileKey).pixels.push({ pixelX, pixelY, index: i });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Initialize results array with nulls
|
|
301
|
+
const results = new Array(points.length).fill(null);
|
|
302
|
+
|
|
303
|
+
// Process each tile and extract elevations
|
|
304
|
+
for (const [, tileData] of tileGroups) {
|
|
305
|
+
const { zoom, tileX, tileY, pixels } = tileData;
|
|
306
|
+
const fetchTile = await fetchTileData(
|
|
307
|
+
source,
|
|
308
|
+
sourceType,
|
|
309
|
+
zoom,
|
|
310
|
+
tileX,
|
|
311
|
+
tileY,
|
|
312
|
+
);
|
|
313
|
+
if (fetchTile == null) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const elevations = await serve_rendered.getBatchElevationsFromTile(
|
|
318
|
+
fetchTile.data,
|
|
319
|
+
{ encoding, format, tile_size: tileSize },
|
|
320
|
+
pixels,
|
|
321
|
+
);
|
|
322
|
+
for (const { index, elevation } of elevations) {
|
|
323
|
+
// eslint-disable-next-line security/detect-object-injection -- index is from internal elevation processing
|
|
324
|
+
results[index] = elevation;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return results;
|
|
329
|
+
};
|
|
330
|
+
|
|
171
331
|
/**
|
|
172
332
|
* Handles requests for elevation data.
|
|
173
333
|
* @param {object} req - Express request object.
|
|
@@ -190,48 +350,23 @@ export const serve_data = {
|
|
|
190
350
|
);
|
|
191
351
|
}
|
|
192
352
|
|
|
193
|
-
const
|
|
194
|
-
if (!
|
|
195
|
-
|
|
196
|
-
if (!item.tileJSON) return res.status(404).send('Missing tileJSON');
|
|
197
|
-
if (!item.sourceType) return res.status(404).send('Missing sourceType');
|
|
198
|
-
const { source, tileJSON, sourceType } = item;
|
|
199
|
-
if (sourceType !== 'pmtiles' && sourceType !== 'mbtiles') {
|
|
200
|
-
return res
|
|
201
|
-
.status(400)
|
|
202
|
-
.send('Invalid sourceType. Must be pmtiles or mbtiles.');
|
|
203
|
-
}
|
|
204
|
-
const encoding = tileJSON?.encoding;
|
|
205
|
-
if (encoding == null) {
|
|
206
|
-
return res.status(400).send('Missing tileJSON.encoding');
|
|
207
|
-
} else if (encoding !== 'terrarium' && encoding !== 'mapbox') {
|
|
208
|
-
return res
|
|
209
|
-
.status(400)
|
|
210
|
-
.send('Invalid encoding. Must be terrarium or mapbox.');
|
|
211
|
-
}
|
|
212
|
-
const format = tileJSON?.format;
|
|
213
|
-
if (format == null) {
|
|
214
|
-
return res.status(400).send('Missing tileJSON.format');
|
|
215
|
-
} else if (format !== 'webp' && format !== 'png') {
|
|
216
|
-
return res.status(400).send('Invalid format. Must be webp or png.');
|
|
217
|
-
}
|
|
353
|
+
const sourceInfo = validateElevationSource(req.params.id, res);
|
|
354
|
+
if (!sourceInfo) return;
|
|
355
|
+
|
|
218
356
|
const z = parseInt(req.params.z, 10);
|
|
219
357
|
const x = parseFloat(req.params.x);
|
|
220
358
|
const y = parseFloat(req.params.y);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const TILE_SIZE = tileJSON.tileSize || 512;
|
|
225
|
-
let bbox;
|
|
226
|
-
let xy;
|
|
227
|
-
var zoom = z;
|
|
359
|
+
|
|
360
|
+
let lon, lat;
|
|
361
|
+
let zoom = z;
|
|
228
362
|
|
|
229
363
|
if (Number.isInteger(x) && Number.isInteger(y)) {
|
|
364
|
+
// Tile coordinates mode - strict bounds checking
|
|
230
365
|
const intX = parseInt(req.params.x, 10);
|
|
231
366
|
const intY = parseInt(req.params.y, 10);
|
|
232
367
|
if (
|
|
233
|
-
zoom <
|
|
234
|
-
zoom >
|
|
368
|
+
zoom < sourceInfo.minzoom ||
|
|
369
|
+
zoom > sourceInfo.maxzoom ||
|
|
235
370
|
intX < 0 ||
|
|
236
371
|
intY < 0 ||
|
|
237
372
|
intX >= Math.pow(2, zoom) ||
|
|
@@ -239,49 +374,83 @@ export const serve_data = {
|
|
|
239
374
|
) {
|
|
240
375
|
return res.status(404).send('Out of bounds');
|
|
241
376
|
}
|
|
242
|
-
|
|
243
|
-
|
|
377
|
+
const bbox = new SphericalMercator().bbox(intX, intY, zoom);
|
|
378
|
+
lon = (bbox[0] + bbox[2]) / 2;
|
|
379
|
+
lat = (bbox[1] + bbox[3]) / 2;
|
|
244
380
|
} else {
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
if (zoom > tileJSON.maxzoom) {
|
|
250
|
-
zoom = tileJSON.maxzoom;
|
|
251
|
-
}
|
|
252
|
-
bbox = [x, y, x + 0.1, y + 0.1];
|
|
253
|
-
const { minX, minY } = new SphericalMercator().xyz(bbox, zoom);
|
|
254
|
-
xy = [minX, minY];
|
|
381
|
+
// Coordinate mode
|
|
382
|
+
lon = x;
|
|
383
|
+
lat = y;
|
|
255
384
|
}
|
|
256
385
|
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
386
|
+
const results = await getBatchElevations(sourceInfo, [
|
|
387
|
+
{ lon, lat, z: zoom },
|
|
388
|
+
]);
|
|
389
|
+
|
|
390
|
+
if (results[0] == null) {
|
|
391
|
+
return res.status(204).send();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Build response matching original format
|
|
395
|
+
const clampedZoom = Math.min(
|
|
396
|
+
Math.max(zoom, sourceInfo.minzoom),
|
|
397
|
+
sourceInfo.maxzoom,
|
|
263
398
|
);
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
399
|
+
const { tileX, tileY, pixelX, pixelY } = lonLatToTilePixel(
|
|
400
|
+
lon,
|
|
401
|
+
lat,
|
|
402
|
+
clampedZoom,
|
|
403
|
+
sourceInfo.tileSize,
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
res.status(200).json({
|
|
407
|
+
long: lon,
|
|
408
|
+
lat: lat,
|
|
409
|
+
elevation: results[0],
|
|
410
|
+
z: clampedZoom,
|
|
411
|
+
x: tileX,
|
|
412
|
+
y: tileY,
|
|
413
|
+
pixelX,
|
|
414
|
+
pixelY,
|
|
415
|
+
});
|
|
416
|
+
} catch (err) {
|
|
417
|
+
return res
|
|
418
|
+
.status(500)
|
|
419
|
+
.header('Content-Type', 'text/plain')
|
|
420
|
+
.send(err.message);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Handles batch elevation requests.
|
|
426
|
+
* Accepts a POST request with JSON body containing:
|
|
427
|
+
* - points: Array of {lon, lat, z} coordinates with zoom level
|
|
428
|
+
* Returns an array of elevations (or null for points with no data) in the same order as input.
|
|
429
|
+
* @param {object} req - Express request object.
|
|
430
|
+
* @param {object} res - Express response object.
|
|
431
|
+
* @param {string} req.params.id - ID of the data source.
|
|
432
|
+
* @returns {Promise<void>}
|
|
433
|
+
*/
|
|
434
|
+
app.post('/:id/elevation', async (req, res, next) => {
|
|
435
|
+
try {
|
|
436
|
+
const sourceInfo = validateElevationSource(req.params.id, res);
|
|
437
|
+
if (!sourceInfo) return;
|
|
438
|
+
|
|
439
|
+
const { points } = req.body;
|
|
440
|
+
if (!Array.isArray(points) || points.length === 0) {
|
|
441
|
+
return res.status(400).send('Missing or empty points array');
|
|
268
442
|
}
|
|
269
443
|
|
|
270
|
-
let
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
z: zoom,
|
|
278
|
-
x: xy[0],
|
|
279
|
-
y: xy[1],
|
|
280
|
-
};
|
|
444
|
+
for (let i = 0; i < points.length; i++) {
|
|
445
|
+
// eslint-disable-next-line security/detect-object-injection -- i is loop counter
|
|
446
|
+
const error = validatePoint(points[i], i);
|
|
447
|
+
if (error) {
|
|
448
|
+
return res.status(400).send(error);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
281
451
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
.send(await serve_rendered.getTerrainElevation(data, param));
|
|
452
|
+
const results = await getBatchElevations(sourceInfo, points);
|
|
453
|
+
res.status(200).json(results);
|
|
285
454
|
} catch (err) {
|
|
286
455
|
return res
|
|
287
456
|
.status(500)
|
package/src/serve_light.js
CHANGED
|
@@ -6,8 +6,7 @@ export const serve_rendered = {
|
|
|
6
6
|
add: (options, repo, params, id, programOpts, dataResolver) => {},
|
|
7
7
|
remove: (repo, id) => {},
|
|
8
8
|
clear: (repo) => {},
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return param;
|
|
9
|
+
getBatchElevationsFromTile: (data, param, pixels) => {
|
|
10
|
+
return pixels.map(({ index }) => ({ index, elevation: null }));
|
|
12
11
|
},
|
|
13
12
|
};
|
package/src/serve_rendered.js
CHANGED
|
@@ -1832,83 +1832,66 @@ export const serve_rendered = {
|
|
|
1832
1832
|
delete repo[id];
|
|
1833
1833
|
});
|
|
1834
1834
|
},
|
|
1835
|
+
|
|
1835
1836
|
/**
|
|
1836
|
-
*
|
|
1837
|
-
* @param {Buffer} data
|
|
1838
|
-
* @param {object} param
|
|
1839
|
-
* @
|
|
1837
|
+
* Gets multiple elevation values from a single decoded tile image.
|
|
1838
|
+
* @param {Buffer} data - Raw tile image data
|
|
1839
|
+
* @param {object} param - Parameters object containing encoding, format, and tile_size
|
|
1840
|
+
* @param {Array<{pixelX: number, pixelY: number, index: number}>} pixels - Array of pixel coordinates to sample
|
|
1841
|
+
* @returns {Promise<Array<{index: number, elevation: number}>>} Promise resolving to array of elevation results
|
|
1840
1842
|
*/
|
|
1841
|
-
|
|
1843
|
+
getBatchElevationsFromTile: async function (data, param, pixels) {
|
|
1842
1844
|
return new Promise((resolve, reject) => {
|
|
1843
|
-
const image = new Image();
|
|
1845
|
+
const image = new Image();
|
|
1844
1846
|
image.onload = () => {
|
|
1845
1847
|
try {
|
|
1846
|
-
const canvas = createCanvas(param
|
|
1848
|
+
const canvas = createCanvas(param.tile_size, param.tile_size);
|
|
1847
1849
|
const context = canvas.getContext('2d');
|
|
1848
1850
|
context.drawImage(image, 0, 0);
|
|
1849
1851
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
) {
|
|
1876
|
-
return reject('Out of bounds Pixel');
|
|
1877
|
-
}
|
|
1878
|
-
const imgdata = context.getImageData(xPixel, yPixel, 1, 1);
|
|
1879
|
-
const red = imgdata.data[0];
|
|
1880
|
-
const green = imgdata.data[1];
|
|
1881
|
-
const blue = imgdata.data[2];
|
|
1882
|
-
let elevation;
|
|
1883
|
-
if (param['encoding'] === 'mapbox') {
|
|
1884
|
-
elevation = -10000 + (red * 256 * 256 + green * 256 + blue) * 0.1;
|
|
1885
|
-
} else if (param['encoding'] === 'terrarium') {
|
|
1886
|
-
elevation = red * 256 + green + blue / 256 - 32768;
|
|
1887
|
-
} else {
|
|
1888
|
-
elevation = 'invalid encoding';
|
|
1852
|
+
const results = [];
|
|
1853
|
+
for (const pixel of pixels) {
|
|
1854
|
+
const { pixelX, pixelY, index } = pixel;
|
|
1855
|
+
if (
|
|
1856
|
+
pixelX < 0 ||
|
|
1857
|
+
pixelY < 0 ||
|
|
1858
|
+
pixelX >= param.tile_size ||
|
|
1859
|
+
pixelY >= param.tile_size
|
|
1860
|
+
) {
|
|
1861
|
+
results.push({ index, elevation: null });
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
const imgdata = context.getImageData(pixelX, pixelY, 1, 1);
|
|
1865
|
+
const red = imgdata.data[0];
|
|
1866
|
+
const green = imgdata.data[1];
|
|
1867
|
+
const blue = imgdata.data[2];
|
|
1868
|
+
let elevation;
|
|
1869
|
+
if (param.encoding === 'mapbox') {
|
|
1870
|
+
elevation = -10000 + (red * 256 * 256 + green * 256 + blue) * 0.1;
|
|
1871
|
+
} else if (param.encoding === 'terrarium') {
|
|
1872
|
+
elevation = red * 256 + green + blue / 256 - 32768;
|
|
1873
|
+
} else {
|
|
1874
|
+
elevation = null;
|
|
1875
|
+
}
|
|
1876
|
+
results.push({ index, elevation });
|
|
1889
1877
|
}
|
|
1890
|
-
|
|
1891
|
-
param['red'] = red;
|
|
1892
|
-
param['green'] = green;
|
|
1893
|
-
param['blue'] = blue;
|
|
1894
|
-
resolve(param);
|
|
1878
|
+
resolve(results);
|
|
1895
1879
|
} catch (error) {
|
|
1896
|
-
reject(error);
|
|
1880
|
+
reject(error);
|
|
1897
1881
|
}
|
|
1898
1882
|
};
|
|
1899
1883
|
image.onerror = (err) => reject(err);
|
|
1900
1884
|
|
|
1901
|
-
// Load the image data - handle the sharp conversion outside the Promise
|
|
1902
1885
|
(async () => {
|
|
1903
1886
|
try {
|
|
1904
|
-
if (param
|
|
1887
|
+
if (param.format === 'webp') {
|
|
1905
1888
|
const img = await sharp(data).toFormat('png').toBuffer();
|
|
1906
|
-
image.src = `data:image/png;base64,${img.toString('base64')}`;
|
|
1889
|
+
image.src = `data:image/png;base64,${img.toString('base64')}`;
|
|
1907
1890
|
} else {
|
|
1908
1891
|
image.src = data;
|
|
1909
1892
|
}
|
|
1910
1893
|
} catch (err) {
|
|
1911
|
-
reject(err);
|
|
1894
|
+
reject(err);
|
|
1912
1895
|
}
|
|
1913
1896
|
})();
|
|
1914
1897
|
});
|
package/src/utils.js
CHANGED
|
@@ -471,6 +471,35 @@ export function isMBTilesProtocol(string) {
|
|
|
471
471
|
}
|
|
472
472
|
}
|
|
473
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Converts a longitude/latitude point to tile and pixel coordinates at a given zoom level.
|
|
476
|
+
* @param {number} lon - Longitude in degrees.
|
|
477
|
+
* @param {number} lat - Latitude in degrees.
|
|
478
|
+
* @param {number} zoom - Zoom level.
|
|
479
|
+
* @param {number} tileSize - Size of the tile in pixels (e.g., 256 or 512).
|
|
480
|
+
* @returns {{tileX: number, tileY: number, pixelX: number, pixelY: number}} - Tile and pixel coordinates.
|
|
481
|
+
*/
|
|
482
|
+
export function lonLatToTilePixel(lon, lat, zoom, tileSize) {
|
|
483
|
+
let siny = Math.sin((lat * Math.PI) / 180);
|
|
484
|
+
// Truncating to 0.9999 effectively limits latitude to 89.189. This is
|
|
485
|
+
// about a third of a tile past the edge of the world tile.
|
|
486
|
+
siny = Math.min(Math.max(siny, -0.9999), 0.9999);
|
|
487
|
+
|
|
488
|
+
const xWorld = tileSize * (0.5 + lon / 360);
|
|
489
|
+
const yWorld =
|
|
490
|
+
tileSize * (0.5 - Math.log((1 + siny) / (1 - siny)) / (4 * Math.PI));
|
|
491
|
+
|
|
492
|
+
const scale = 1 << zoom;
|
|
493
|
+
|
|
494
|
+
const tileX = Math.floor((xWorld * scale) / tileSize);
|
|
495
|
+
const tileY = Math.floor((yWorld * scale) / tileSize);
|
|
496
|
+
|
|
497
|
+
const pixelX = Math.floor(xWorld * scale) - tileX * tileSize;
|
|
498
|
+
const pixelY = Math.floor(yWorld * scale) - tileY * tileSize;
|
|
499
|
+
|
|
500
|
+
return { tileX, tileY, pixelX, pixelY };
|
|
501
|
+
}
|
|
502
|
+
|
|
474
503
|
/**
|
|
475
504
|
* Fetches tile data from either PMTiles or MBTiles source.
|
|
476
505
|
* @param {object} source - The source object, which may contain a mbtiles object, or pmtiles object.
|