tileserver-gl 4.5.1 → 4.6.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.
@@ -6,7 +6,7 @@ import path from 'path';
6
6
  import url from 'url';
7
7
  import util from 'util';
8
8
  import zlib from 'zlib';
9
- import sharp from 'sharp'; // sharp has to be required before node-canvas. see https://github.com/lovell/sharp/issues/371
9
+ import sharp from 'sharp'; // sharp has to be required before node-canvas on linux but after it on windows. see https://github.com/lovell/sharp/issues/371
10
10
  import { createCanvas, Image } from 'canvas';
11
11
  import clone from 'clone';
12
12
  import Color from 'color';
@@ -18,12 +18,22 @@ import MBTiles from '@mapbox/mbtiles';
18
18
  import polyline from '@mapbox/polyline';
19
19
  import proj4 from 'proj4';
20
20
  import request from 'request';
21
- import { getFontsPbf, getTileUrls, fixTileJSONCenter } from './utils.js';
21
+ import {
22
+ getFontsPbf,
23
+ getTileUrls,
24
+ isValidHttpUrl,
25
+ fixTileJSONCenter,
26
+ } from './utils.js';
27
+ import {
28
+ PMtilesOpen,
29
+ GetPMtilesInfo,
30
+ GetPMtilesTile,
31
+ } from './pmtiles_adapter.js';
22
32
 
23
33
  const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
24
34
  const PATH_PATTERN =
25
- /^((fill|stroke|width)\:[^\|]+\|)*((enc:.+)|((-?\d+\.?\d*,-?\d+\.?\d*\|)+(-?\d+\.?\d*,-?\d+\.?\d*)))/;
26
- const httpTester = /^(http(s)?:)?\/\//;
35
+ /^((fill|stroke|width)\:[^\|]+\|)*(enc:.+|-?\d+(\.\d*)?,-?\d+(\.\d*)?(\|-?\d+(\.\d*)?,-?\d+(\.\d*)?)+)/;
36
+ const httpTester = /^\/\//;
27
37
 
28
38
  const mercator = new SphericalMercator();
29
39
  const getScale = (scale) => (scale || '@1x').slice(1, 2) | 0;
@@ -158,10 +168,7 @@ const extractPathsFromQuery = (query, transformer) => {
158
168
  // Iterate through paths, parse and validate them
159
169
  for (const providedPath of providedPaths) {
160
170
  // Logic for pushing coords to path when path includes google polyline
161
- if (
162
- providedPath.includes('enc:') &&
163
- PATH_PATTERN.test(decodeURIComponent(providedPath))
164
- ) {
171
+ if (providedPath.includes('enc:') && PATH_PATTERN.test(providedPath)) {
165
172
  // +4 because 'enc:' is 4 characters, everything after 'enc:' is considered to be part of the polyline
166
173
  const encIndex = providedPath.indexOf('enc:') + 4;
167
174
  const coords = polyline
@@ -279,7 +286,10 @@ const extractMarkersFromQuery = (query, options, transformer) => {
279
286
  let iconURI = markerParts[1];
280
287
  // Check if icon is served via http otherwise marker icons are expected to
281
288
  // be provided as filepaths relative to configured icon path
282
- if (!(iconURI.startsWith('http://') || iconURI.startsWith('https://'))) {
289
+ const isRemoteURL =
290
+ iconURI.startsWith('http://') || iconURI.startsWith('https://');
291
+ const isDataURL = iconURI.startsWith('data:');
292
+ if (!(isRemoteURL || isDataURL)) {
283
293
  // Sanitize URI with sanitize-filename
284
294
  // https://www.npmjs.com/package/sanitize-filename#details
285
295
  iconURI = sanitize(iconURI);
@@ -292,7 +302,9 @@ const extractMarkersFromQuery = (query, options, transformer) => {
292
302
  iconURI = path.resolve(options.paths.icons, iconURI);
293
303
 
294
304
  // When we encounter a remote icon check if the configuration explicitly allows them.
295
- } else if (options.allowRemoteMarkerIcons !== true) {
305
+ } else if (isRemoteURL && options.allowRemoteMarkerIcons !== true) {
306
+ continue;
307
+ } else if (isDataURL && options.allowInlineMarkerImages !== true) {
296
308
  continue;
297
309
  }
298
310
 
@@ -427,7 +439,7 @@ const drawMarkers = async (ctx, markers, z) => {
427
439
  * @param {number} z Map zoom level.
428
440
  */
429
441
  const drawPath = (ctx, path, query, pathQuery, z) => {
430
- const splitPaths = decodeURIComponent(pathQuery).split('|');
442
+ const splitPaths = pathQuery.split('|');
431
443
 
432
444
  if (!path || path.length < 2) {
433
445
  return null;
@@ -771,6 +783,35 @@ export const serve_rendered = {
771
783
  composite_array.push({ input: canvas.toBuffer() });
772
784
  }
773
785
 
786
+ if (opt_mode === 'static' && item.staticAttributionText) {
787
+ const canvas = createCanvas(scale * width, scale * height);
788
+ const ctx = canvas.getContext('2d');
789
+ ctx.scale(scale, scale);
790
+
791
+ ctx.font = '10px sans-serif';
792
+ const text = item.staticAttributionText;
793
+ const textMetrics = ctx.measureText(text);
794
+ const textWidth = textMetrics.width;
795
+ const textHeight = 14;
796
+
797
+ const padding = 6;
798
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
799
+ ctx.fillRect(
800
+ width - textWidth - padding,
801
+ height - textHeight - padding,
802
+ textWidth + padding,
803
+ textHeight + padding,
804
+ );
805
+ ctx.fillStyle = 'rgba(0,0,0,.8)';
806
+ ctx.fillText(
807
+ item.staticAttributionText,
808
+ width - textWidth - padding / 2,
809
+ height - textHeight + 8,
810
+ );
811
+
812
+ composite_array.push({ input: canvas.toBuffer() });
813
+ }
814
+
774
815
  if (composite_array.length > 0) {
775
816
  image.composite(composite_array);
776
817
  }
@@ -1176,11 +1217,12 @@ export const serve_rendered = {
1176
1217
 
1177
1218
  return Promise.all([fontListingPromise]).then(() => app);
1178
1219
  },
1179
- add: (options, repo, params, id, publicUrl, dataResolver) => {
1220
+ add: async (options, repo, params, id, publicUrl, dataResolver) => {
1180
1221
  const map = {
1181
1222
  renderers: [],
1182
1223
  renderers_static: [],
1183
1224
  sources: {},
1225
+ source_types: {},
1184
1226
  };
1185
1227
 
1186
1228
  let styleJSON;
@@ -1189,7 +1231,7 @@ export const serve_rendered = {
1189
1231
  const renderer = new mlgl.Map({
1190
1232
  mode: mode,
1191
1233
  ratio: ratio,
1192
- request: (req, callback) => {
1234
+ request: async (req, callback) => {
1193
1235
  const protocol = req.url.split(':')[0];
1194
1236
  // console.log('Handling request:', req);
1195
1237
  if (protocol === 'sprites') {
@@ -1216,17 +1258,23 @@ export const serve_rendered = {
1216
1258
  callback(err, { data: null });
1217
1259
  },
1218
1260
  );
1219
- } else if (protocol === 'mbtiles') {
1261
+ } else if (protocol === 'mbtiles' || protocol === 'pmtiles') {
1220
1262
  const parts = req.url.split('/');
1221
1263
  const sourceId = parts[2];
1222
1264
  const source = map.sources[sourceId];
1265
+ const source_type = map.source_types[sourceId];
1223
1266
  const sourceInfo = styleJSON.sources[sourceId];
1267
+
1224
1268
  const z = parts[3] | 0;
1225
1269
  const x = parts[4] | 0;
1226
1270
  const y = parts[5].split('.')[0] | 0;
1227
1271
  const format = parts[5].split('.')[1];
1228
- source.getTile(z, x, y, (err, data, headers) => {
1229
- if (err) {
1272
+
1273
+ if (source_type === 'pmtiles') {
1274
+ let tileinfo = await GetPMtilesTile(source, z, x, y);
1275
+ let data = tileinfo.data;
1276
+ let headers = tileinfo.header;
1277
+ if (data == undefined) {
1230
1278
  if (options.verbose)
1231
1279
  console.log('MBTiles error, serving empty', err);
1232
1280
  createEmptyResponse(
@@ -1235,41 +1283,75 @@ export const serve_rendered = {
1235
1283
  callback,
1236
1284
  );
1237
1285
  return;
1238
- }
1286
+ } else {
1287
+ const response = {};
1288
+ response.data = data;
1289
+ if (headers['Last-Modified']) {
1290
+ response.modified = new Date(headers['Last-Modified']);
1291
+ }
1239
1292
 
1240
- const response = {};
1241
- if (headers['Last-Modified']) {
1242
- response.modified = new Date(headers['Last-Modified']);
1243
- }
1293
+ if (format === 'pbf') {
1294
+ if (options.dataDecoratorFunc) {
1295
+ response.data = options.dataDecoratorFunc(
1296
+ sourceId,
1297
+ 'data',
1298
+ response.data,
1299
+ z,
1300
+ x,
1301
+ y,
1302
+ );
1303
+ }
1304
+ }
1244
1305
 
1245
- if (format === 'pbf') {
1246
- try {
1247
- response.data = zlib.unzipSync(data);
1248
- } catch (err) {
1249
- console.log(
1250
- 'Skipping incorrect header for tile mbtiles://%s/%s/%s/%s.pbf',
1251
- id,
1252
- z,
1253
- x,
1254
- y,
1306
+ callback(null, response);
1307
+ }
1308
+ } else if (source_type === 'mbtiles') {
1309
+ source.getTile(z, x, y, (err, data, headers) => {
1310
+ if (err) {
1311
+ if (options.verbose)
1312
+ console.log('MBTiles error, serving empty', err);
1313
+ createEmptyResponse(
1314
+ sourceInfo.format,
1315
+ sourceInfo.color,
1316
+ callback,
1255
1317
  );
1318
+ return;
1256
1319
  }
1257
- if (options.dataDecoratorFunc) {
1258
- response.data = options.dataDecoratorFunc(
1259
- sourceId,
1260
- 'data',
1261
- response.data,
1262
- z,
1263
- x,
1264
- y,
1265
- );
1320
+
1321
+ const response = {};
1322
+ if (headers['Last-Modified']) {
1323
+ response.modified = new Date(headers['Last-Modified']);
1266
1324
  }
1267
- } else {
1268
- response.data = data;
1269
- }
1270
1325
 
1271
- callback(null, response);
1272
- });
1326
+ if (format === 'pbf') {
1327
+ try {
1328
+ response.data = zlib.unzipSync(data);
1329
+ } catch (err) {
1330
+ console.log(
1331
+ 'Skipping incorrect header for tile mbtiles://%s/%s/%s/%s.pbf',
1332
+ id,
1333
+ z,
1334
+ x,
1335
+ y,
1336
+ );
1337
+ }
1338
+ if (options.dataDecoratorFunc) {
1339
+ response.data = options.dataDecoratorFunc(
1340
+ sourceId,
1341
+ 'data',
1342
+ response.data,
1343
+ z,
1344
+ x,
1345
+ y,
1346
+ );
1347
+ }
1348
+ } else {
1349
+ response.data = data;
1350
+ }
1351
+
1352
+ callback(null, response);
1353
+ });
1354
+ }
1273
1355
  } else if (protocol === 'http' || protocol === 'https') {
1274
1356
  request(
1275
1357
  {
@@ -1378,87 +1460,143 @@ export const serve_rendered = {
1378
1460
  dataProjWGStoInternalWGS: null,
1379
1461
  lastModified: new Date().toUTCString(),
1380
1462
  watermark: params.watermark || options.watermark,
1463
+ staticAttributionText:
1464
+ params.staticAttributionText || options.staticAttributionText,
1381
1465
  };
1382
1466
  repo[id] = repoobj;
1383
1467
 
1384
1468
  const queue = [];
1385
1469
  for (const name of Object.keys(styleJSON.sources)) {
1470
+ let source_type;
1386
1471
  let source = styleJSON.sources[name];
1387
- const url = source.url;
1388
-
1389
- if (url && url.lastIndexOf('mbtiles:', 0) === 0) {
1390
- // found mbtiles source, replace with info from local file
1472
+ let url = source.url;
1473
+ if (
1474
+ url &&
1475
+ (url.startsWith('pmtiles://') || url.startsWith('mbtiles://'))
1476
+ ) {
1477
+ // found pmtiles or mbtiles source, replace with info from local file
1391
1478
  delete source.url;
1392
1479
 
1393
- let mbtilesFile = url.substring('mbtiles://'.length);
1394
- const fromData =
1395
- mbtilesFile[0] === '{' && mbtilesFile[mbtilesFile.length - 1] === '}';
1480
+ let dataId = url.replace('pmtiles://', '').replace('mbtiles://', '');
1481
+ if (dataId.startsWith('{') && dataId.endsWith('}')) {
1482
+ dataId = dataId.slice(1, -1);
1483
+ }
1396
1484
 
1397
- if (fromData) {
1398
- mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2);
1399
- const mapsTo = (params.mapping || {})[mbtilesFile];
1400
- if (mapsTo) {
1401
- mbtilesFile = mapsTo;
1402
- }
1403
- mbtilesFile = dataResolver(mbtilesFile);
1404
- if (!mbtilesFile) {
1405
- console.error(`ERROR: data "${mbtilesFile}" not found!`);
1406
- process.exit(1);
1485
+ const mapsTo = (params.mapping || {})[dataId];
1486
+ if (mapsTo) {
1487
+ dataId = mapsTo;
1488
+ }
1489
+
1490
+ let inputFile;
1491
+ const DataInfo = dataResolver(dataId);
1492
+ if (DataInfo.inputfile) {
1493
+ inputFile = DataInfo.inputfile;
1494
+ source_type = DataInfo.filetype;
1495
+ } else {
1496
+ console.error(`ERROR: data "${inputFile}" not found!`);
1497
+ process.exit(1);
1498
+ }
1499
+
1500
+ if (!isValidHttpUrl(inputFile)) {
1501
+ const inputFileStats = fs.statSync(inputFile);
1502
+ if (!inputFileStats.isFile() || inputFileStats.size === 0) {
1503
+ throw Error(`Not valid PMTiles file: "${inputFile}"`);
1407
1504
  }
1408
1505
  }
1409
1506
 
1410
- queue.push(
1411
- new Promise((resolve, reject) => {
1412
- mbtilesFile = path.resolve(options.paths.mbtiles, mbtilesFile);
1413
- const mbtilesFileStats = fs.statSync(mbtilesFile);
1414
- if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) {
1415
- throw Error(`Not valid MBTiles file: ${mbtilesFile}`);
1507
+ if (source_type === 'pmtiles') {
1508
+ map.sources[name] = PMtilesOpen(inputFile);
1509
+ map.source_types[name] = 'pmtiles';
1510
+ const metadata = await GetPMtilesInfo(map.sources[name]);
1511
+
1512
+ if (!repoobj.dataProjWGStoInternalWGS && metadata.proj4) {
1513
+ // how to do this for multiple sources with different proj4 defs?
1514
+ const to3857 = proj4('EPSG:3857');
1515
+ const toDataProj = proj4(metadata.proj4);
1516
+ repoobj.dataProjWGStoInternalWGS = (xy) =>
1517
+ to3857.inverse(toDataProj.forward(xy));
1518
+ }
1519
+
1520
+ const type = source.type;
1521
+ Object.assign(source, metadata);
1522
+ source.type = type;
1523
+ source.tiles = [
1524
+ // meta url which will be detected when requested
1525
+ `pmtiles://${name}/{z}/{x}/{y}.${metadata.format || 'pbf'}`,
1526
+ ];
1527
+ delete source.scheme;
1528
+
1529
+ if (
1530
+ !attributionOverride &&
1531
+ source.attribution &&
1532
+ source.attribution.length > 0
1533
+ ) {
1534
+ if (!tileJSON.attribution.includes(source.attribution)) {
1535
+ if (tileJSON.attribution.length > 0) {
1536
+ tileJSON.attribution += ' | ';
1537
+ }
1538
+ tileJSON.attribution += source.attribution;
1416
1539
  }
1417
- map.sources[name] = new MBTiles(mbtilesFile + '?mode=ro', (err) => {
1418
- map.sources[name].getInfo((err, info) => {
1419
- if (err) {
1420
- console.error(err);
1421
- return;
1422
- }
1540
+ }
1541
+ } else {
1542
+ queue.push(
1543
+ new Promise((resolve, reject) => {
1544
+ inputFile = path.resolve(options.paths.mbtiles, inputFile);
1545
+ const inputFileStats = fs.statSync(inputFile);
1546
+ if (!inputFileStats.isFile() || inputFileStats.size === 0) {
1547
+ throw Error(`Not valid MBTiles file: "${inputFile}"`);
1548
+ }
1549
+ map.sources[name] = new MBTiles(inputFile + '?mode=ro', (err) => {
1550
+ map.sources[name].getInfo((err, info) => {
1551
+ if (err) {
1552
+ console.error(err);
1553
+ return;
1554
+ }
1555
+ map.source_types[name] = 'mbtiles';
1556
+
1557
+ if (!repoobj.dataProjWGStoInternalWGS && info.proj4) {
1558
+ // how to do this for multiple sources with different proj4 defs?
1559
+ const to3857 = proj4('EPSG:3857');
1560
+ const toDataProj = proj4(info.proj4);
1561
+ repoobj.dataProjWGStoInternalWGS = (xy) =>
1562
+ to3857.inverse(toDataProj.forward(xy));
1563
+ }
1423
1564
 
1424
- if (!repoobj.dataProjWGStoInternalWGS && info.proj4) {
1425
- // how to do this for multiple sources with different proj4 defs?
1426
- const to3857 = proj4('EPSG:3857');
1427
- const toDataProj = proj4(info.proj4);
1428
- repoobj.dataProjWGStoInternalWGS = (xy) =>
1429
- to3857.inverse(toDataProj.forward(xy));
1430
- }
1565
+ const type = source.type;
1566
+ Object.assign(source, info);
1567
+ source.type = type;
1568
+ source.tiles = [
1569
+ // meta url which will be detected when requested
1570
+ `mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`,
1571
+ ];
1572
+ delete source.scheme;
1431
1573
 
1432
- const type = source.type;
1433
- Object.assign(source, info);
1434
- source.type = type;
1435
- source.tiles = [
1436
- // meta url which will be detected when requested
1437
- `mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`,
1438
- ];
1439
- delete source.scheme;
1440
-
1441
- if (options.dataDecoratorFunc) {
1442
- source = options.dataDecoratorFunc(name, 'tilejson', source);
1443
- }
1574
+ if (options.dataDecoratorFunc) {
1575
+ source = options.dataDecoratorFunc(
1576
+ name,
1577
+ 'tilejson',
1578
+ source,
1579
+ );
1580
+ }
1444
1581
 
1445
- if (
1446
- !attributionOverride &&
1447
- source.attribution &&
1448
- source.attribution.length > 0
1449
- ) {
1450
- if (!tileJSON.attribution.includes(source.attribution)) {
1451
- if (tileJSON.attribution.length > 0) {
1452
- tileJSON.attribution += ' | ';
1582
+ if (
1583
+ !attributionOverride &&
1584
+ source.attribution &&
1585
+ source.attribution.length > 0
1586
+ ) {
1587
+ if (!tileJSON.attribution.includes(source.attribution)) {
1588
+ if (tileJSON.attribution.length > 0) {
1589
+ tileJSON.attribution += ' | ';
1590
+ }
1591
+ tileJSON.attribution += source.attribution;
1453
1592
  }
1454
- tileJSON.attribution += source.attribution;
1455
1593
  }
1456
- }
1457
- resolve();
1594
+ resolve();
1595
+ });
1458
1596
  });
1459
- });
1460
- }),
1461
- );
1597
+ }),
1598
+ );
1599
+ }
1462
1600
  }
1463
1601
  }
1464
1602
 
@@ -9,7 +9,7 @@ import { validate } from '@maplibre/maplibre-gl-style-spec';
9
9
 
10
10
  import { getPublicUrl } from './utils.js';
11
11
 
12
- const httpTester = /^(http(s)?:)?\/\//;
12
+ const httpTester = /^\/\//;
13
13
 
14
14
  const fixUrl = (req, url, publicUrl, opt_nokey) => {
15
15
  if (!url || typeof url !== 'string' || url.indexOf('local://') !== 0) {
@@ -110,20 +110,24 @@ export const serve_style = {
110
110
 
111
111
  for (const name of Object.keys(styleJSON.sources)) {
112
112
  const source = styleJSON.sources[name];
113
- const url = source.url;
114
- if (url && url.lastIndexOf('mbtiles:', 0) === 0) {
115
- let mbtilesFile = url.substring('mbtiles://'.length);
116
- const fromData =
117
- mbtilesFile[0] === '{' && mbtilesFile[mbtilesFile.length - 1] === '}';
118
-
119
- if (fromData) {
120
- mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2);
121
- const mapsTo = (params.mapping || {})[mbtilesFile];
122
- if (mapsTo) {
123
- mbtilesFile = mapsTo;
124
- }
113
+ let url = source.url;
114
+ if (
115
+ url &&
116
+ (url.startsWith('pmtiles://') || url.startsWith('mbtiles://'))
117
+ ) {
118
+ const protocol = url.split(':')[0];
119
+
120
+ let dataId = url.replace('pmtiles://', '').replace('mbtiles://', '');
121
+ if (dataId.startsWith('{') && dataId.endsWith('}')) {
122
+ dataId = dataId.slice(1, -1);
125
123
  }
126
- const identifier = reportTiles(mbtilesFile, fromData);
124
+
125
+ const mapsTo = (params.mapping || {})[dataId];
126
+ if (mapsTo) {
127
+ dataId = mapsTo;
128
+ }
129
+
130
+ const identifier = reportTiles(dataId, protocol);
127
131
  if (!identifier) {
128
132
  return false;
129
133
  }