tileserver-gl 4.6.5 → 4.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.
- package/.eslintrc.cjs +4 -0
- package/.github/workflows/ct.yml +1 -1
- package/.github/workflows/release.yml +1 -1
- package/README.md +1 -1
- package/README_light.md +2 -2
- package/docs/config.rst +20 -1
- package/docs/endpoints.rst +8 -8
- package/package.json +16 -16
- package/public/resources/images/marker-icon-2x.png +0 -0
- package/public/resources/images/marker-icon.png +0 -0
- package/public/resources/images/marker-shadow.png +0 -0
- package/public/resources/leaflet.css +6 -1
- package/public/resources/leaflet.js +3 -3
- package/public/resources/leaflet.js.map +1 -1
- package/public/resources/maplibre-gl-inspect.min.js +1 -1
- package/public/resources/maplibre-gl.js +4 -4
- package/public/resources/maplibre-gl.js.map +1 -1
- package/public/templates/data.tmpl +0 -1
- package/public/templates/index.tmpl +1 -4
- package/public/templates/viewer.tmpl +0 -1
- package/src/healthcheck.js +3 -3
- package/src/main.js +33 -19
- package/src/pmtiles_adapter.js +9 -9
- package/src/render.js +303 -0
- package/src/serve_data.js +12 -12
- package/src/serve_font.js +25 -44
- package/src/serve_rendered.js +263 -643
- package/src/serve_style.js +5 -17
- package/src/server.js +24 -29
- package/src/utils.js +22 -3
- package/test/setup.js +0 -1
- package/public/resources/L.TileLayer.NoGap.js +0 -243
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
<link rel="stylesheet" type="text/css" href="{{public_url}}leaflet.css{{&key_query}}" />
|
|
22
22
|
<script src="{{public_url}}leaflet.js{{&key_query}}"></script>
|
|
23
23
|
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
|
|
24
|
-
<script src="{{public_url}}L.TileLayer.NoGap.js{{&key_query}}"></script>
|
|
25
24
|
<style>
|
|
26
25
|
body { margin:0; padding:0; }
|
|
27
26
|
#map { position:absolute; top:0; bottom:0; width:100%; }
|
|
@@ -79,12 +79,9 @@
|
|
|
79
79
|
<div class="details">
|
|
80
80
|
<h3>{{tileJSON.name}}</h3>
|
|
81
81
|
<div class="identifier">identifier: {{@key}}{{#if formatted_filesize}} | size: {{formatted_filesize}}{{/if}}</div>
|
|
82
|
-
<div class="identifier">type: {{#is_vector}}vector{{/is_vector}}{{^is_vector}}raster{{/is_vector}} data {{#if
|
|
82
|
+
<div class="identifier">type: {{#is_vector}}vector{{/is_vector}}{{^is_vector}}raster{{/is_vector}} data {{#if sourceType}} | ext: {{sourceType}}{{/if}}</div>
|
|
83
83
|
<p class="services">
|
|
84
84
|
services: <a href="{{public_url}}data/{{@key}}.json{{&../key_query}}">TileJSON</a>
|
|
85
|
-
{{#if wmts_link}}
|
|
86
|
-
| <a href="{{&wmts_link}}">WMTS</a>
|
|
87
|
-
{{/if}}
|
|
88
85
|
{{#if xyz_link}}
|
|
89
86
|
| <a href="#" onclick="return toggle_xyz('xyz_data_{{@key}}');">XYZ</a>
|
|
90
87
|
<input id="xyz_data_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" />
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
<script src="{{public_url}}maplibre-gl-inspect.min.js{{&key_query}}"></script>
|
|
12
12
|
<script src="{{public_url}}leaflet.js{{&key_query}}"></script>
|
|
13
13
|
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
|
|
14
|
-
<script src="{{public_url}}L.TileLayer.NoGap.js{{&key_query}}"></script>
|
|
15
14
|
<style>
|
|
16
15
|
body { margin:0; padding:0; }
|
|
17
16
|
#map { position:absolute; top:0; bottom:0; width:100%; }
|
package/src/healthcheck.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as http from 'http';
|
|
2
|
-
|
|
2
|
+
const options = {
|
|
3
3
|
timeout: 2000,
|
|
4
4
|
};
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const url = 'http://localhost:8080/health';
|
|
6
|
+
const request = http.request(url, options, (res) => {
|
|
7
7
|
console.log(`STATUS: ${res.statusCode}`);
|
|
8
8
|
if (res.statusCode == 200) {
|
|
9
9
|
process.exit(0);
|
package/src/main.js
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
|
-
import
|
|
8
|
+
import axios from 'axios';
|
|
9
9
|
import { server } from './server.js';
|
|
10
10
|
import MBTiles from '@mapbox/mbtiles';
|
|
11
11
|
import { isValidHttpUrl } from './utils.js';
|
|
12
|
-
import {
|
|
12
|
+
import { openPMtiles, getPMtilesInfo } from './pmtiles_adapter.js';
|
|
13
13
|
|
|
14
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
15
|
const __dirname = path.dirname(__filename);
|
|
@@ -62,14 +62,14 @@ const opts = program.opts();
|
|
|
62
62
|
|
|
63
63
|
console.log(`Starting ${packageJson.name} v${packageJson.version}`);
|
|
64
64
|
|
|
65
|
-
const
|
|
65
|
+
const startServer = (configPath, config) => {
|
|
66
66
|
let publicUrl = opts.public_url;
|
|
67
67
|
if (publicUrl && publicUrl.lastIndexOf('/') !== publicUrl.length - 1) {
|
|
68
68
|
publicUrl += '/';
|
|
69
69
|
}
|
|
70
70
|
return server({
|
|
71
|
-
configPath
|
|
72
|
-
config
|
|
71
|
+
configPath,
|
|
72
|
+
config,
|
|
73
73
|
bind: opts.bind,
|
|
74
74
|
port: opts.port,
|
|
75
75
|
cors: opts.cors,
|
|
@@ -77,11 +77,11 @@ const StartServer = (configPath, config) => {
|
|
|
77
77
|
silent: opts.silent,
|
|
78
78
|
logFile: opts.log_file,
|
|
79
79
|
logFormat: opts.log_format,
|
|
80
|
-
publicUrl
|
|
80
|
+
publicUrl,
|
|
81
81
|
});
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
const
|
|
84
|
+
const startWithInputFile = async (inputFile) => {
|
|
85
85
|
console.log(`[INFO] Automatically creating config file for ${inputFile}`);
|
|
86
86
|
console.log(`[INFO] Only a basic preview style will be used.`);
|
|
87
87
|
console.log(
|
|
@@ -123,8 +123,8 @@ const StartWithInputFile = async (inputFile) => {
|
|
|
123
123
|
|
|
124
124
|
const extension = inputFile.split('.').pop().toLowerCase();
|
|
125
125
|
if (extension === 'pmtiles') {
|
|
126
|
-
|
|
127
|
-
const metadata = await
|
|
126
|
+
const fileOpenInfo = openPMtiles(inputFile);
|
|
127
|
+
const metadata = await getPMtilesInfo(fileOpenInfo);
|
|
128
128
|
|
|
129
129
|
if (
|
|
130
130
|
metadata.format === 'pbf' &&
|
|
@@ -174,7 +174,7 @@ const StartWithInputFile = async (inputFile) => {
|
|
|
174
174
|
console.log('Run with --verbose to see the config file here.');
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
return
|
|
177
|
+
return startServer(null, config);
|
|
178
178
|
} else {
|
|
179
179
|
if (isValidHttpUrl(inputFile)) {
|
|
180
180
|
console.log(
|
|
@@ -215,7 +215,7 @@ const StartWithInputFile = async (inputFile) => {
|
|
|
215
215
|
config['styles'][styleName] = {
|
|
216
216
|
style: styleFileRel,
|
|
217
217
|
tilejson: {
|
|
218
|
-
bounds
|
|
218
|
+
bounds,
|
|
219
219
|
},
|
|
220
220
|
};
|
|
221
221
|
}
|
|
@@ -235,13 +235,13 @@ const StartWithInputFile = async (inputFile) => {
|
|
|
235
235
|
console.log('Run with --verbose to see the config file here.');
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
return
|
|
238
|
+
return startServer(null, config);
|
|
239
239
|
});
|
|
240
240
|
});
|
|
241
241
|
}
|
|
242
242
|
};
|
|
243
243
|
|
|
244
|
-
fs.stat(path.resolve(opts.config), (err, stats) => {
|
|
244
|
+
fs.stat(path.resolve(opts.config), async (err, stats) => {
|
|
245
245
|
if (err || !stats.isFile() || stats.size === 0) {
|
|
246
246
|
let inputFile;
|
|
247
247
|
if (opts.file) {
|
|
@@ -251,7 +251,7 @@ fs.stat(path.resolve(opts.config), (err, stats) => {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
if (inputFile) {
|
|
254
|
-
return
|
|
254
|
+
return startWithInputFile(inputFile);
|
|
255
255
|
} else {
|
|
256
256
|
// try to find in the cwd
|
|
257
257
|
const files = fs.readdirSync(process.cwd());
|
|
@@ -266,20 +266,34 @@ fs.stat(path.resolve(opts.config), (err, stats) => {
|
|
|
266
266
|
}
|
|
267
267
|
if (inputFile) {
|
|
268
268
|
console.log(`No input file specified, using ${inputFile}`);
|
|
269
|
-
return
|
|
269
|
+
return startWithInputFile(inputFile);
|
|
270
270
|
} else {
|
|
271
271
|
const url =
|
|
272
272
|
'https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles';
|
|
273
273
|
const filename = 'zurich_switzerland.mbtiles';
|
|
274
|
-
const
|
|
274
|
+
const writer = fs.createWriteStream(filename);
|
|
275
275
|
console.log(`No input file found`);
|
|
276
276
|
console.log(`[DEMO] Downloading sample data (${filename}) from ${url}`);
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const response = await axios({
|
|
280
|
+
url,
|
|
281
|
+
method: 'GET',
|
|
282
|
+
responseType: 'stream',
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
response.data.pipe(writer);
|
|
286
|
+
writer.on('finish', () => startWithInputFile(filename));
|
|
287
|
+
writer.on('error', (err) =>
|
|
288
|
+
console.error(`Error writing file: ${err}`),
|
|
289
|
+
);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error(`Error downloading file: ${error}`);
|
|
292
|
+
}
|
|
279
293
|
}
|
|
280
294
|
}
|
|
281
295
|
} else {
|
|
282
296
|
console.log(`Using specified config file from ${opts.config}`);
|
|
283
|
-
return
|
|
297
|
+
return startServer(opts.config, null);
|
|
284
298
|
}
|
|
285
299
|
});
|
package/src/pmtiles_adapter.js
CHANGED
|
@@ -11,7 +11,7 @@ class PMTilesFileSource {
|
|
|
11
11
|
}
|
|
12
12
|
async getBytes(offset, length) {
|
|
13
13
|
const buffer = Buffer.alloc(length);
|
|
14
|
-
await
|
|
14
|
+
await readFileBytes(this.fd, buffer, offset);
|
|
15
15
|
const ab = buffer.buffer.slice(
|
|
16
16
|
buffer.byteOffset,
|
|
17
17
|
buffer.byteOffset + buffer.byteLength,
|
|
@@ -26,7 +26,7 @@ class PMTilesFileSource {
|
|
|
26
26
|
* @param buffer
|
|
27
27
|
* @param offset
|
|
28
28
|
*/
|
|
29
|
-
async function
|
|
29
|
+
async function readFileBytes(fd, buffer, offset) {
|
|
30
30
|
return new Promise((resolve, reject) => {
|
|
31
31
|
fs.read(fd, buffer, 0, buffer.length, offset, (err) => {
|
|
32
32
|
if (err) {
|
|
@@ -41,7 +41,7 @@ async function ReadFileBytes(fd, buffer, offset) {
|
|
|
41
41
|
*
|
|
42
42
|
* @param FilePath
|
|
43
43
|
*/
|
|
44
|
-
export function
|
|
44
|
+
export function openPMtiles(FilePath) {
|
|
45
45
|
let pmtiles = undefined;
|
|
46
46
|
|
|
47
47
|
if (isValidHttpUrl(FilePath)) {
|
|
@@ -59,12 +59,12 @@ export function PMtilesOpen(FilePath) {
|
|
|
59
59
|
*
|
|
60
60
|
* @param pmtiles
|
|
61
61
|
*/
|
|
62
|
-
export async function
|
|
62
|
+
export async function getPMtilesInfo(pmtiles) {
|
|
63
63
|
const header = await pmtiles.getHeader();
|
|
64
64
|
const metadata = await pmtiles.getMetadata();
|
|
65
65
|
|
|
66
66
|
//Add missing metadata from header
|
|
67
|
-
metadata['format'] =
|
|
67
|
+
metadata['format'] = getPmtilesTileType(header.tileType).type;
|
|
68
68
|
metadata['minzoom'] = header.minZoom;
|
|
69
69
|
metadata['maxzoom'] = header.maxZoom;
|
|
70
70
|
|
|
@@ -103,23 +103,23 @@ export async function GetPMtilesInfo(pmtiles) {
|
|
|
103
103
|
* @param x
|
|
104
104
|
* @param y
|
|
105
105
|
*/
|
|
106
|
-
export async function
|
|
106
|
+
export async function getPMtilesTile(pmtiles, z, x, y) {
|
|
107
107
|
const header = await pmtiles.getHeader();
|
|
108
|
-
const
|
|
108
|
+
const tileType = getPmtilesTileType(header.tileType);
|
|
109
109
|
let zxyTile = await pmtiles.getZxy(z, x, y);
|
|
110
110
|
if (zxyTile && zxyTile.data) {
|
|
111
111
|
zxyTile = Buffer.from(zxyTile.data);
|
|
112
112
|
} else {
|
|
113
113
|
zxyTile = undefined;
|
|
114
114
|
}
|
|
115
|
-
return { data: zxyTile, header:
|
|
115
|
+
return { data: zxyTile, header: tileType.header };
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
119
|
*
|
|
120
120
|
* @param typenum
|
|
121
121
|
*/
|
|
122
|
-
function
|
|
122
|
+
function getPmtilesTileType(typenum) {
|
|
123
123
|
let head = {};
|
|
124
124
|
let tileType;
|
|
125
125
|
switch (typenum) {
|
package/src/render.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { createCanvas, Image } from 'canvas';
|
|
4
|
+
|
|
5
|
+
import SphericalMercator from '@mapbox/sphericalmercator';
|
|
6
|
+
|
|
7
|
+
const mercator = new SphericalMercator();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Transforms coordinates to pixels.
|
|
11
|
+
* @param {List[Number]} ll Longitude/Latitude coordinate pair.
|
|
12
|
+
* @param {number} zoom Map zoom level.
|
|
13
|
+
*/
|
|
14
|
+
const precisePx = (ll, zoom) => {
|
|
15
|
+
const px = mercator.px(ll, 20);
|
|
16
|
+
const scale = Math.pow(2, zoom - 20);
|
|
17
|
+
return [px[0] * scale, px[1] * scale];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Draws a marker in canvas context.
|
|
22
|
+
* @param {object} ctx Canvas context object.
|
|
23
|
+
* @param {object} marker Marker object parsed by extractMarkersFromQuery.
|
|
24
|
+
* @param {number} z Map zoom level.
|
|
25
|
+
*/
|
|
26
|
+
const drawMarker = (ctx, marker, z) => {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const img = new Image();
|
|
29
|
+
const pixelCoords = precisePx(marker.location, z);
|
|
30
|
+
|
|
31
|
+
const getMarkerCoordinates = (imageWidth, imageHeight, scale) => {
|
|
32
|
+
// Images are placed with their top-left corner at the provided location
|
|
33
|
+
// within the canvas but we expect icons to be centered and above it.
|
|
34
|
+
|
|
35
|
+
// Substract half of the images width from the x-coordinate to center
|
|
36
|
+
// the image in relation to the provided location
|
|
37
|
+
let xCoordinate = pixelCoords[0] - imageWidth / 2;
|
|
38
|
+
// Substract the images height from the y-coordinate to place it above
|
|
39
|
+
// the provided location
|
|
40
|
+
let yCoordinate = pixelCoords[1] - imageHeight;
|
|
41
|
+
|
|
42
|
+
// Since image placement is dependent on the size offsets have to be
|
|
43
|
+
// scaled as well. Additionally offsets are provided as either positive or
|
|
44
|
+
// negative values so we always add them
|
|
45
|
+
if (marker.offsetX) {
|
|
46
|
+
xCoordinate = xCoordinate + marker.offsetX * scale;
|
|
47
|
+
}
|
|
48
|
+
if (marker.offsetY) {
|
|
49
|
+
yCoordinate = yCoordinate + marker.offsetY * scale;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
x: xCoordinate,
|
|
54
|
+
y: yCoordinate,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const drawOnCanvas = () => {
|
|
59
|
+
// Check if the images should be resized before beeing drawn
|
|
60
|
+
const defaultScale = 1;
|
|
61
|
+
const scale = marker.scale ? marker.scale : defaultScale;
|
|
62
|
+
|
|
63
|
+
// Calculate scaled image sizes
|
|
64
|
+
const imageWidth = img.width * scale;
|
|
65
|
+
const imageHeight = img.height * scale;
|
|
66
|
+
|
|
67
|
+
// Pass the desired sizes to get correlating coordinates
|
|
68
|
+
const coords = getMarkerCoordinates(imageWidth, imageHeight, scale);
|
|
69
|
+
|
|
70
|
+
// Draw the image on canvas
|
|
71
|
+
if (scale != defaultScale) {
|
|
72
|
+
ctx.drawImage(img, coords.x, coords.y, imageWidth, imageHeight);
|
|
73
|
+
} else {
|
|
74
|
+
ctx.drawImage(img, coords.x, coords.y);
|
|
75
|
+
}
|
|
76
|
+
// Resolve the promise when image has been drawn
|
|
77
|
+
resolve();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
img.onload = drawOnCanvas;
|
|
81
|
+
img.onerror = (err) => {
|
|
82
|
+
throw err;
|
|
83
|
+
};
|
|
84
|
+
img.src = marker.icon;
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Draws a list of markers onto a canvas.
|
|
90
|
+
* Wraps drawing of markers into list of promises and awaits them.
|
|
91
|
+
* It's required because images are expected to load asynchronous in canvas js
|
|
92
|
+
* even when provided from a local disk.
|
|
93
|
+
* @param {object} ctx Canvas context object.
|
|
94
|
+
* @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery.
|
|
95
|
+
* @param {number} z Map zoom level.
|
|
96
|
+
*/
|
|
97
|
+
const drawMarkers = async (ctx, markers, z) => {
|
|
98
|
+
const markerPromises = [];
|
|
99
|
+
|
|
100
|
+
for (const marker of markers) {
|
|
101
|
+
// Begin drawing marker
|
|
102
|
+
markerPromises.push(drawMarker(ctx, marker, z));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Await marker drawings before continuing
|
|
106
|
+
await Promise.all(markerPromises);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Draws a list of coordinates onto a canvas and styles the resulting path.
|
|
111
|
+
* @param {object} ctx Canvas context object.
|
|
112
|
+
* @param {List[Number]} path List of coordinates.
|
|
113
|
+
* @param {object} query Request query parameters.
|
|
114
|
+
* @param {string} pathQuery Path query parameter.
|
|
115
|
+
* @param {number} z Map zoom level.
|
|
116
|
+
*/
|
|
117
|
+
const drawPath = (ctx, path, query, pathQuery, z) => {
|
|
118
|
+
const splitPaths = pathQuery.split('|');
|
|
119
|
+
|
|
120
|
+
if (!path || path.length < 2) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ctx.beginPath();
|
|
125
|
+
|
|
126
|
+
// Transform coordinates to pixel on canvas and draw lines between points
|
|
127
|
+
for (const pair of path) {
|
|
128
|
+
const px = precisePx(pair, z);
|
|
129
|
+
ctx.lineTo(px[0], px[1]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check if first coordinate matches last coordinate
|
|
133
|
+
if (
|
|
134
|
+
path[0][0] === path[path.length - 1][0] &&
|
|
135
|
+
path[0][1] === path[path.length - 1][1]
|
|
136
|
+
) {
|
|
137
|
+
ctx.closePath();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Optionally fill drawn shape with a rgba color from query
|
|
141
|
+
const pathHasFill = splitPaths.filter((x) => x.startsWith('fill')).length > 0;
|
|
142
|
+
if (query.fill !== undefined || pathHasFill) {
|
|
143
|
+
if ('fill' in query) {
|
|
144
|
+
ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)';
|
|
145
|
+
}
|
|
146
|
+
if (pathHasFill) {
|
|
147
|
+
ctx.fillStyle = splitPaths
|
|
148
|
+
.find((x) => x.startsWith('fill:'))
|
|
149
|
+
.replace('fill:', '');
|
|
150
|
+
}
|
|
151
|
+
ctx.fill();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Get line width from query and fall back to 1 if not provided
|
|
155
|
+
const pathHasWidth =
|
|
156
|
+
splitPaths.filter((x) => x.startsWith('width')).length > 0;
|
|
157
|
+
if (query.width !== undefined || pathHasWidth) {
|
|
158
|
+
let lineWidth = 1;
|
|
159
|
+
// Get line width from query
|
|
160
|
+
if ('width' in query) {
|
|
161
|
+
lineWidth = Number(query.width);
|
|
162
|
+
}
|
|
163
|
+
// Get line width from path in query
|
|
164
|
+
if (pathHasWidth) {
|
|
165
|
+
lineWidth = Number(
|
|
166
|
+
splitPaths.find((x) => x.startsWith('width:')).replace('width:', ''),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
// Get border width from query and fall back to 10% of line width
|
|
170
|
+
const borderWidth =
|
|
171
|
+
query.borderwidth !== undefined
|
|
172
|
+
? parseFloat(query.borderwidth)
|
|
173
|
+
: lineWidth * 0.1;
|
|
174
|
+
|
|
175
|
+
// Set rendering style for the start and end points of the path
|
|
176
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
|
|
177
|
+
ctx.lineCap = query.linecap || 'butt';
|
|
178
|
+
|
|
179
|
+
// Set rendering style for overlapping segments of the path with differing directions
|
|
180
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
|
|
181
|
+
ctx.lineJoin = query.linejoin || 'miter';
|
|
182
|
+
|
|
183
|
+
// In order to simulate a border we draw the path two times with the first
|
|
184
|
+
// beeing the wider border part.
|
|
185
|
+
if (query.border !== undefined && borderWidth > 0) {
|
|
186
|
+
// We need to double the desired border width and add it to the line width
|
|
187
|
+
// in order to get the desired border on each side of the line.
|
|
188
|
+
ctx.lineWidth = lineWidth + borderWidth * 2;
|
|
189
|
+
// Set border style as rgba
|
|
190
|
+
ctx.strokeStyle = query.border;
|
|
191
|
+
ctx.stroke();
|
|
192
|
+
}
|
|
193
|
+
ctx.lineWidth = lineWidth;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const pathHasStroke =
|
|
197
|
+
splitPaths.filter((x) => x.startsWith('stroke')).length > 0;
|
|
198
|
+
if (query.stroke !== undefined || pathHasStroke) {
|
|
199
|
+
if ('stroke' in query) {
|
|
200
|
+
ctx.strokeStyle = query.stroke;
|
|
201
|
+
}
|
|
202
|
+
// Path Stroke gets higher priority
|
|
203
|
+
if (pathHasStroke) {
|
|
204
|
+
ctx.strokeStyle = splitPaths
|
|
205
|
+
.find((x) => x.startsWith('stroke:'))
|
|
206
|
+
.replace('stroke:', '');
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
ctx.strokeStyle = 'rgba(0,64,255,0.7)';
|
|
210
|
+
}
|
|
211
|
+
ctx.stroke();
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const renderOverlay = async (
|
|
215
|
+
z,
|
|
216
|
+
x,
|
|
217
|
+
y,
|
|
218
|
+
bearing,
|
|
219
|
+
pitch,
|
|
220
|
+
w,
|
|
221
|
+
h,
|
|
222
|
+
scale,
|
|
223
|
+
paths,
|
|
224
|
+
markers,
|
|
225
|
+
query,
|
|
226
|
+
) => {
|
|
227
|
+
if ((!paths || paths.length === 0) && (!markers || markers.length === 0)) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const center = precisePx([x, y], z);
|
|
232
|
+
|
|
233
|
+
const mapHeight = 512 * (1 << z);
|
|
234
|
+
const maxEdge = center[1] + h / 2;
|
|
235
|
+
const minEdge = center[1] - h / 2;
|
|
236
|
+
if (maxEdge > mapHeight) {
|
|
237
|
+
center[1] -= maxEdge - mapHeight;
|
|
238
|
+
} else if (minEdge < 0) {
|
|
239
|
+
center[1] -= minEdge;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const canvas = createCanvas(scale * w, scale * h);
|
|
243
|
+
const ctx = canvas.getContext('2d');
|
|
244
|
+
ctx.scale(scale, scale);
|
|
245
|
+
if (bearing) {
|
|
246
|
+
ctx.translate(w / 2, h / 2);
|
|
247
|
+
ctx.rotate((-bearing / 180) * Math.PI);
|
|
248
|
+
ctx.translate(-center[0], -center[1]);
|
|
249
|
+
} else {
|
|
250
|
+
// optimized path
|
|
251
|
+
ctx.translate(-center[0] + w / 2, -center[1] + h / 2);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Draw provided paths if any
|
|
255
|
+
paths.forEach((path, i) => {
|
|
256
|
+
const pathQuery = Array.isArray(query.path) ? query.path.at(i) : query.path;
|
|
257
|
+
drawPath(ctx, path, query, pathQuery, z);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Await drawing of markers before rendering the canvas
|
|
261
|
+
await drawMarkers(ctx, markers, z);
|
|
262
|
+
|
|
263
|
+
return canvas.toBuffer();
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
export const renderWatermark = (width, height, scale, text) => {
|
|
267
|
+
const canvas = createCanvas(scale * width, scale * height);
|
|
268
|
+
const ctx = canvas.getContext('2d');
|
|
269
|
+
ctx.scale(scale, scale);
|
|
270
|
+
|
|
271
|
+
ctx.font = '10px sans-serif';
|
|
272
|
+
ctx.strokeWidth = '1px';
|
|
273
|
+
ctx.strokeStyle = 'rgba(255,255,255,.4)';
|
|
274
|
+
ctx.strokeText(text, 5, height - 5);
|
|
275
|
+
ctx.fillStyle = 'rgba(0,0,0,.4)';
|
|
276
|
+
ctx.fillText(text, 5, height - 5);
|
|
277
|
+
|
|
278
|
+
return canvas;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const renderAttribution = (width, height, scale, text) => {
|
|
282
|
+
const canvas = createCanvas(scale * width, scale * height);
|
|
283
|
+
const ctx = canvas.getContext('2d');
|
|
284
|
+
ctx.scale(scale, scale);
|
|
285
|
+
|
|
286
|
+
ctx.font = '10px sans-serif';
|
|
287
|
+
const textMetrics = ctx.measureText(text);
|
|
288
|
+
const textWidth = textMetrics.width;
|
|
289
|
+
const textHeight = 14;
|
|
290
|
+
|
|
291
|
+
const padding = 6;
|
|
292
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
293
|
+
ctx.fillRect(
|
|
294
|
+
width - textWidth - padding,
|
|
295
|
+
height - textHeight - padding,
|
|
296
|
+
textWidth + padding,
|
|
297
|
+
textHeight + padding,
|
|
298
|
+
);
|
|
299
|
+
ctx.fillStyle = 'rgba(0,0,0,.8)';
|
|
300
|
+
ctx.fillText(text, width - textWidth - padding / 2, height - textHeight + 8);
|
|
301
|
+
|
|
302
|
+
return canvas;
|
|
303
|
+
};
|
package/src/serve_data.js
CHANGED
|
@@ -12,9 +12,9 @@ import { VectorTile } from '@mapbox/vector-tile';
|
|
|
12
12
|
|
|
13
13
|
import { getTileUrls, isValidHttpUrl, fixTileJSONCenter } from './utils.js';
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
openPMtiles,
|
|
16
|
+
getPMtilesInfo,
|
|
17
|
+
getPMtilesTile,
|
|
18
18
|
} from './pmtiles_adapter.js';
|
|
19
19
|
|
|
20
20
|
export const serve_data = {
|
|
@@ -53,8 +53,8 @@ export const serve_data = {
|
|
|
53
53
|
) {
|
|
54
54
|
return res.status(404).send('Out of bounds');
|
|
55
55
|
}
|
|
56
|
-
if (item.
|
|
57
|
-
let tileinfo = await
|
|
56
|
+
if (item.sourceType === 'pmtiles') {
|
|
57
|
+
let tileinfo = await getPMtilesTile(item.source, z, x, y);
|
|
58
58
|
if (tileinfo == undefined || tileinfo.data == undefined) {
|
|
59
59
|
return res.status(404).send('Not found');
|
|
60
60
|
} else {
|
|
@@ -99,7 +99,7 @@ export const serve_data = {
|
|
|
99
99
|
|
|
100
100
|
return res.status(200).send(data);
|
|
101
101
|
}
|
|
102
|
-
} else if (item.
|
|
102
|
+
} else if (item.sourceType === 'mbtiles') {
|
|
103
103
|
item.source.getTile(z, x, y, (err, data, headers) => {
|
|
104
104
|
let isGzipped;
|
|
105
105
|
if (err) {
|
|
@@ -223,11 +223,11 @@ export const serve_data = {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
let source;
|
|
226
|
-
let
|
|
226
|
+
let sourceType;
|
|
227
227
|
if (inputType === 'pmtiles') {
|
|
228
|
-
source =
|
|
229
|
-
|
|
230
|
-
const metadata = await
|
|
228
|
+
source = openPMtiles(inputFile);
|
|
229
|
+
sourceType = 'pmtiles';
|
|
230
|
+
const metadata = await getPMtilesInfo(source);
|
|
231
231
|
|
|
232
232
|
tileJSON['name'] = id;
|
|
233
233
|
tileJSON['format'] = 'pbf';
|
|
@@ -245,7 +245,7 @@ export const serve_data = {
|
|
|
245
245
|
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
|
|
246
246
|
}
|
|
247
247
|
} else if (inputType === 'mbtiles') {
|
|
248
|
-
|
|
248
|
+
sourceType = 'mbtiles';
|
|
249
249
|
const sourceInfoPromise = new Promise((resolve, reject) => {
|
|
250
250
|
source = new MBTiles(inputFile + '?mode=ro', (err) => {
|
|
251
251
|
if (err) {
|
|
@@ -285,7 +285,7 @@ export const serve_data = {
|
|
|
285
285
|
tileJSON,
|
|
286
286
|
publicUrl,
|
|
287
287
|
source,
|
|
288
|
-
|
|
288
|
+
sourceType,
|
|
289
289
|
};
|
|
290
290
|
},
|
|
291
291
|
};
|