tileserver-gl-light 4.1.1 → 4.2.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/.dockerignore +1 -0
- package/.eslintrc.cjs +32 -0
- package/.gitattributes +11 -0
- package/.husky/commit-msg +21 -0
- package/.husky/pre-push +4 -0
- package/Dockerfile +27 -6
- package/Dockerfile_test +30 -21
- package/commitlint.config.cjs +3 -0
- package/docker-entrypoint.sh +1 -1
- package/docs/config.rst +9 -0
- package/docs/endpoints.rst +27 -1
- package/docs/installation.rst +1 -1
- package/lint-staged.config.cjs +4 -0
- package/package.json +30 -12
- package/prettier.config.cjs +13 -0
- package/public/templates/wmts.tmpl +4 -4
- package/publish.js +17 -9
- package/run.sh +1 -1
- package/src/healthcheck.js +18 -0
- package/src/main.js +57 -65
- package/src/serve_data.js +99 -82
- package/src/serve_font.js +19 -10
- package/src/serve_light.js +5 -6
- package/src/serve_rendered.js +889 -291
- package/src/serve_style.js +31 -16
- package/src/server.js +268 -144
- package/src/utils.js +62 -43
- package/test/metadata.js +44 -43
- package/test/setup.js +8 -7
- package/test/static.js +127 -31
- package/test/style.js +31 -26
- package/test/tiles_data.js +5 -5
- package/test/tiles_rendered.js +7 -7
package/src/serve_rendered.js
CHANGED
|
@@ -7,21 +7,21 @@ import url from 'url';
|
|
|
7
7
|
import util from 'util';
|
|
8
8
|
import zlib from 'zlib';
|
|
9
9
|
import sharp from 'sharp'; // sharp has to be required before node-canvas. see https://github.com/lovell/sharp/issues/371
|
|
10
|
-
import
|
|
10
|
+
import { createCanvas, Image } from 'canvas';
|
|
11
11
|
import clone from 'clone';
|
|
12
12
|
import Color from 'color';
|
|
13
13
|
import express from 'express';
|
|
14
|
+
import sanitize from 'sanitize-filename';
|
|
14
15
|
import SphericalMercator from '@mapbox/sphericalmercator';
|
|
15
16
|
import mlgl from '@maplibre/maplibre-gl-native';
|
|
16
17
|
import MBTiles from '@mapbox/mbtiles';
|
|
17
18
|
import proj4 from 'proj4';
|
|
18
19
|
import request from 'request';
|
|
19
|
-
import {getFontsPbf, getTileUrls, fixTileJSONCenter} from './utils.js';
|
|
20
|
+
import { getFontsPbf, getTileUrls, fixTileJSONCenter } from './utils.js';
|
|
20
21
|
|
|
21
|
-
const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d
|
|
22
|
+
const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
|
|
22
23
|
const httpTester = /^(http(s)?:)?\/\//;
|
|
23
24
|
|
|
24
|
-
const {createCanvas} = pkg;
|
|
25
25
|
const mercator = new SphericalMercator();
|
|
26
26
|
const getScale = (scale) => (scale || '@1x').slice(1, 2) | 0;
|
|
27
27
|
|
|
@@ -38,7 +38,7 @@ const extensionToFormat = {
|
|
|
38
38
|
'.jpg': 'jpeg',
|
|
39
39
|
'.jpeg': 'jpeg',
|
|
40
40
|
'.png': 'png',
|
|
41
|
-
'.webp': 'webp'
|
|
41
|
+
'.webp': 'webp',
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -46,18 +46,19 @@ const extensionToFormat = {
|
|
|
46
46
|
* string is for unknown or unsupported formats.
|
|
47
47
|
*/
|
|
48
48
|
const cachedEmptyResponses = {
|
|
49
|
-
'': Buffer.alloc(0)
|
|
49
|
+
'': Buffer.alloc(0),
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Create an appropriate mlgl response for http errors.
|
|
54
|
+
*
|
|
54
55
|
* @param {string} format The format (a sharp format or 'pbf').
|
|
55
56
|
* @param {string} color The background color (or empty string for transparent).
|
|
56
57
|
* @param {Function} callback The mlgl callback.
|
|
57
58
|
*/
|
|
58
59
|
function createEmptyResponse(format, color, callback) {
|
|
59
60
|
if (!format || format === 'pbf') {
|
|
60
|
-
callback(null, {data: cachedEmptyResponses['']});
|
|
61
|
+
callback(null, { data: cachedEmptyResponses[''] });
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -71,7 +72,7 @@ function createEmptyResponse(format, color, callback) {
|
|
|
71
72
|
const cacheKey = `${format},${color}`;
|
|
72
73
|
const data = cachedEmptyResponses[cacheKey];
|
|
73
74
|
if (data) {
|
|
74
|
-
callback(null, {data: data});
|
|
75
|
+
callback(null, { data: data });
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -83,47 +84,422 @@ function createEmptyResponse(format, color, callback) {
|
|
|
83
84
|
raw: {
|
|
84
85
|
width: 1,
|
|
85
86
|
height: 1,
|
|
86
|
-
channels: channels
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
channels: channels,
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
.toFormat(format)
|
|
91
|
+
.toBuffer((err, buffer, info) => {
|
|
92
|
+
if (!err) {
|
|
93
|
+
cachedEmptyResponses[cacheKey] = buffer;
|
|
94
|
+
}
|
|
95
|
+
callback(null, { data: buffer });
|
|
96
|
+
});
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Parses coordinate pair provided to pair of floats and ensures the resulting
|
|
101
|
+
* pair is a longitude/latitude combination depending on lnglat query parameter.
|
|
102
|
+
*
|
|
103
|
+
* @param {List} coordinatePair Coordinate pair.
|
|
104
|
+
* @param coordinates
|
|
105
|
+
* @param {object} query Request query parameters.
|
|
106
|
+
*/
|
|
107
|
+
const parseCoordinatePair = (coordinates, query) => {
|
|
108
|
+
const firstCoordinate = parseFloat(coordinates[0]);
|
|
109
|
+
const secondCoordinate = parseFloat(coordinates[1]);
|
|
110
|
+
|
|
111
|
+
// Ensure provided coordinates could be parsed and abort if not
|
|
112
|
+
if (isNaN(firstCoordinate) || isNaN(secondCoordinate)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check if coordinates have been provided as lat/lng pair instead of the
|
|
117
|
+
// ususal lng/lat pair and ensure resulting pair is lng/lat
|
|
118
|
+
if (query.latlng === '1' || query.latlng === 'true') {
|
|
119
|
+
return [secondCoordinate, firstCoordinate];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return [firstCoordinate, secondCoordinate];
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Parses a coordinate pair from query arguments and optionally transforms it.
|
|
127
|
+
*
|
|
128
|
+
* @param {List} coordinatePair Coordinate pair.
|
|
129
|
+
* @param {object} query Request query parameters.
|
|
130
|
+
* @param {Function} transformer Optional transform function.
|
|
131
|
+
*/
|
|
132
|
+
const parseCoordinates = (coordinatePair, query, transformer) => {
|
|
133
|
+
const parsedCoordinates = parseCoordinatePair(coordinatePair, query);
|
|
134
|
+
|
|
135
|
+
// Transform coordinates
|
|
136
|
+
if (transformer) {
|
|
137
|
+
return transformer(parsedCoordinates);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return parsedCoordinates;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Parses paths provided via query into a list of path objects.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} query Request query parameters.
|
|
147
|
+
* @param {Function} transformer Optional transform function.
|
|
148
|
+
*/
|
|
149
|
+
const extractPathsFromQuery = (query, transformer) => {
|
|
150
|
+
// Return an empty list if no paths have been provided
|
|
151
|
+
if (!query.path) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const paths = [];
|
|
156
|
+
|
|
157
|
+
// Check if multiple paths have been provided and mimic a list if it's a
|
|
158
|
+
// single path.
|
|
159
|
+
const providedPaths = Array.isArray(query.path) ? query.path : [query.path];
|
|
160
|
+
|
|
161
|
+
// Iterate through paths, parse and validate them
|
|
162
|
+
for (const provided_path of providedPaths) {
|
|
163
|
+
const currentPath = [];
|
|
164
|
+
|
|
165
|
+
// Extract coordinate-list from path
|
|
166
|
+
const pathParts = (provided_path || '').split('|');
|
|
167
|
+
|
|
168
|
+
// Iterate through coordinate-list, parse the coordinates and validate them
|
|
169
|
+
for (const pair of pathParts) {
|
|
170
|
+
// Extract coordinates from coordinate pair
|
|
171
|
+
const pairParts = pair.split(',');
|
|
172
|
+
|
|
173
|
+
// Ensure we have two coordinates
|
|
174
|
+
if (pairParts.length === 2) {
|
|
175
|
+
const pair = parseCoordinates(pairParts, query, transformer);
|
|
176
|
+
|
|
177
|
+
// Ensure coordinates could be parsed and skip them if not
|
|
178
|
+
if (pair === null) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Add the coordinate-pair to the current path if they are valid
|
|
183
|
+
currentPath.push(pair);
|
|
107
184
|
}
|
|
108
|
-
|
|
109
|
-
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Extend list of paths with current path if it contains coordinates
|
|
188
|
+
if (currentPath.length) {
|
|
189
|
+
paths.push(currentPath);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return paths;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Parses marker options provided via query and sets corresponding attributes
|
|
197
|
+
* on marker object.
|
|
198
|
+
* Options adhere to the following format
|
|
199
|
+
* [optionName]:[optionValue]
|
|
200
|
+
*
|
|
201
|
+
* @param {List[String]} optionsList List of option strings.
|
|
202
|
+
* @param {object} marker Marker object to configure.
|
|
203
|
+
*/
|
|
204
|
+
const parseMarkerOptions = (optionsList, marker) => {
|
|
205
|
+
for (const options of optionsList) {
|
|
206
|
+
const optionParts = options.split(':');
|
|
207
|
+
// Ensure we got an option name and value
|
|
208
|
+
if (optionParts.length < 2) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
switch (optionParts[0]) {
|
|
213
|
+
// Scale factor to up- or downscale icon
|
|
214
|
+
case 'scale':
|
|
215
|
+
// Scale factors must not be negative
|
|
216
|
+
marker.scale = Math.abs(parseFloat(optionParts[1]));
|
|
217
|
+
break;
|
|
218
|
+
// Icon offset as positive or negative pixel value in the following
|
|
219
|
+
// format [offsetX],[offsetY] where [offsetY] is optional
|
|
220
|
+
case 'offset':
|
|
221
|
+
const providedOffset = optionParts[1].split(',');
|
|
222
|
+
// Set X-axis offset
|
|
223
|
+
marker.offsetX = parseFloat(providedOffset[0]);
|
|
224
|
+
// Check if an offset has been provided for Y-axis
|
|
225
|
+
if (providedOffset.length > 1) {
|
|
226
|
+
marker.offsetY = parseFloat(providedOffset[1]);
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Parses markers provided via query into a list of marker objects.
|
|
235
|
+
*
|
|
236
|
+
* @param {object} query Request query parameters.
|
|
237
|
+
* @param {object} options Configuration options.
|
|
238
|
+
* @param {Function} transformer Optional transform function.
|
|
239
|
+
*/
|
|
240
|
+
const extractMarkersFromQuery = (query, options, transformer) => {
|
|
241
|
+
// Return an empty list if no markers have been provided
|
|
242
|
+
if (!query.marker) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const markers = [];
|
|
247
|
+
|
|
248
|
+
// Check if multiple markers have been provided and mimic a list if it's a
|
|
249
|
+
// single maker.
|
|
250
|
+
const providedMarkers = Array.isArray(query.marker)
|
|
251
|
+
? query.marker
|
|
252
|
+
: [query.marker];
|
|
253
|
+
|
|
254
|
+
// Iterate through provided markers which can have one of the following
|
|
255
|
+
// formats
|
|
256
|
+
// [location]|[pathToFileTelativeToConfiguredIconPath]
|
|
257
|
+
// [location]|[pathToFile...]|[option]|[option]|...
|
|
258
|
+
for (const providedMarker of providedMarkers) {
|
|
259
|
+
const markerParts = providedMarker.split('|');
|
|
260
|
+
// Ensure we got at least a location and an icon uri
|
|
261
|
+
if (markerParts.length < 2) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const locationParts = markerParts[0].split(',');
|
|
266
|
+
// Ensure the locationParts contains two items
|
|
267
|
+
if (locationParts.length !== 2) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let iconURI = markerParts[1];
|
|
272
|
+
// Check if icon is served via http otherwise marker icons are expected to
|
|
273
|
+
// be provided as filepaths relative to configured icon path
|
|
274
|
+
if (!(iconURI.startsWith('http://') || iconURI.startsWith('https://'))) {
|
|
275
|
+
// Sanitize URI with sanitize-filename
|
|
276
|
+
// https://www.npmjs.com/package/sanitize-filename#details
|
|
277
|
+
iconURI = sanitize(iconURI);
|
|
278
|
+
|
|
279
|
+
// If the selected icon is not part of available icons skip it
|
|
280
|
+
if (!options.paths.availableIcons.includes(iconURI)) {
|
|
281
|
+
continue;
|
|
110
282
|
}
|
|
111
|
-
|
|
283
|
+
|
|
284
|
+
iconURI = path.resolve(options.paths.icons, iconURI);
|
|
285
|
+
|
|
286
|
+
// When we encounter a remote icon check if the configuration explicitly allows them.
|
|
287
|
+
} else if (options.allowRemoteMarkerIcons !== true) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Ensure marker location could be parsed
|
|
292
|
+
const location = parseCoordinates(locationParts, query, transformer);
|
|
293
|
+
if (location === null) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const marker = {};
|
|
298
|
+
|
|
299
|
+
marker.location = location;
|
|
300
|
+
marker.icon = iconURI;
|
|
301
|
+
|
|
302
|
+
// Check if options have been provided
|
|
303
|
+
if (markerParts.length > 2) {
|
|
304
|
+
parseMarkerOptions(markerParts.slice(2), marker);
|
|
112
305
|
}
|
|
306
|
+
|
|
307
|
+
// Add marker to list
|
|
308
|
+
markers.push(marker);
|
|
309
|
+
}
|
|
310
|
+
return markers;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Transforms coordinates to pixels.
|
|
315
|
+
*
|
|
316
|
+
* @param {List[Number]} ll Longitude/Latitude coordinate pair.
|
|
317
|
+
* @param {number} zoom Map zoom level.
|
|
318
|
+
*/
|
|
319
|
+
const precisePx = (ll, zoom) => {
|
|
320
|
+
const px = mercator.px(ll, 20);
|
|
321
|
+
const scale = Math.pow(2, zoom - 20);
|
|
322
|
+
return [px[0] * scale, px[1] * scale];
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Draws a marker in cavans context.
|
|
327
|
+
*
|
|
328
|
+
* @param {object} ctx Canvas context object.
|
|
329
|
+
* @param {object} marker Marker object parsed by extractMarkersFromQuery.
|
|
330
|
+
* @param {number} z Map zoom level.
|
|
331
|
+
*/
|
|
332
|
+
const drawMarker = (ctx, marker, z) => {
|
|
333
|
+
return new Promise((resolve) => {
|
|
334
|
+
const img = new Image();
|
|
335
|
+
const pixelCoords = precisePx(marker.location, z);
|
|
336
|
+
|
|
337
|
+
const getMarkerCoordinates = (imageWidth, imageHeight, scale) => {
|
|
338
|
+
// Images are placed with their top-left corner at the provided location
|
|
339
|
+
// within the canvas but we expect icons to be centered and above it.
|
|
340
|
+
|
|
341
|
+
// Substract half of the images width from the x-coordinate to center
|
|
342
|
+
// the image in relation to the provided location
|
|
343
|
+
let xCoordinate = pixelCoords[0] - imageWidth / 2;
|
|
344
|
+
// Substract the images height from the y-coordinate to place it above
|
|
345
|
+
// the provided location
|
|
346
|
+
let yCoordinate = pixelCoords[1] - imageHeight;
|
|
347
|
+
|
|
348
|
+
// Since image placement is dependent on the size offsets have to be
|
|
349
|
+
// scaled as well. Additionally offsets are provided as either positive or
|
|
350
|
+
// negative values so we always add them
|
|
351
|
+
if (marker.offsetX) {
|
|
352
|
+
xCoordinate = xCoordinate + marker.offsetX * scale;
|
|
353
|
+
}
|
|
354
|
+
if (marker.offsetY) {
|
|
355
|
+
yCoordinate = yCoordinate + marker.offsetY * scale;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
x: xCoordinate,
|
|
360
|
+
y: yCoordinate,
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const drawOnCanvas = () => {
|
|
365
|
+
// Check if the images should be resized before beeing drawn
|
|
366
|
+
const defaultScale = 1;
|
|
367
|
+
const scale = marker.scale ? marker.scale : defaultScale;
|
|
368
|
+
|
|
369
|
+
// Calculate scaled image sizes
|
|
370
|
+
const imageWidth = img.width * scale;
|
|
371
|
+
const imageHeight = img.height * scale;
|
|
372
|
+
|
|
373
|
+
// Pass the desired sizes to get correlating coordinates
|
|
374
|
+
const coords = getMarkerCoordinates(imageWidth, imageHeight, scale);
|
|
375
|
+
|
|
376
|
+
// Draw the image on canvas
|
|
377
|
+
if (scale != defaultScale) {
|
|
378
|
+
ctx.drawImage(img, coords.x, coords.y, imageWidth, imageHeight);
|
|
379
|
+
} else {
|
|
380
|
+
ctx.drawImage(img, coords.x, coords.y);
|
|
381
|
+
}
|
|
382
|
+
// Resolve the promise when image has been drawn
|
|
383
|
+
resolve();
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
img.onload = drawOnCanvas;
|
|
387
|
+
img.onerror = (err) => {
|
|
388
|
+
throw err;
|
|
389
|
+
};
|
|
390
|
+
img.src = marker.icon;
|
|
391
|
+
});
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Draws a list of markers onto a canvas.
|
|
396
|
+
* Wraps drawing of markers into list of promises and awaits them.
|
|
397
|
+
* It's required because images are expected to load asynchronous in canvas js
|
|
398
|
+
* even when provided from a local disk.
|
|
399
|
+
*
|
|
400
|
+
* @param {object} ctx Canvas context object.
|
|
401
|
+
* @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery.
|
|
402
|
+
* @param {number} z Map zoom level.
|
|
403
|
+
*/
|
|
404
|
+
const drawMarkers = async (ctx, markers, z) => {
|
|
405
|
+
const markerPromises = [];
|
|
406
|
+
|
|
407
|
+
for (const marker of markers) {
|
|
408
|
+
// Begin drawing marker
|
|
409
|
+
markerPromises.push(drawMarker(ctx, marker, z));
|
|
113
410
|
}
|
|
114
|
-
|
|
411
|
+
|
|
412
|
+
// Await marker drawings before continuing
|
|
413
|
+
await Promise.all(markerPromises);
|
|
115
414
|
};
|
|
116
415
|
|
|
117
|
-
|
|
118
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Draws a list of coordinates onto a canvas and styles the resulting path.
|
|
418
|
+
*
|
|
419
|
+
* @param {object} ctx Canvas context object.
|
|
420
|
+
* @param {List[Number]} path List of coordinates.
|
|
421
|
+
* @param {object} query Request query parameters.
|
|
422
|
+
* @param {number} z Map zoom level.
|
|
423
|
+
*/
|
|
424
|
+
const drawPath = (ctx, path, query, z) => {
|
|
119
425
|
if (!path || path.length < 2) {
|
|
120
426
|
return null;
|
|
121
427
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
428
|
+
|
|
429
|
+
ctx.beginPath();
|
|
430
|
+
|
|
431
|
+
// Transform coordinates to pixel on canvas and draw lines between points
|
|
432
|
+
for (const pair of path) {
|
|
433
|
+
const px = precisePx(pair, z);
|
|
434
|
+
ctx.lineTo(px[0], px[1]);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Check if first coordinate matches last coordinate
|
|
438
|
+
if (
|
|
439
|
+
path[0][0] === path[path.length - 1][0] &&
|
|
440
|
+
path[0][1] === path[path.length - 1][1]
|
|
441
|
+
) {
|
|
442
|
+
ctx.closePath();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Optionally fill drawn shape with a rgba color from query
|
|
446
|
+
if (query.fill !== undefined) {
|
|
447
|
+
ctx.fillStyle = query.fill;
|
|
448
|
+
ctx.fill();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Get line width from query and fall back to 1 if not provided
|
|
452
|
+
const lineWidth = query.width !== undefined ? parseFloat(query.width) : 1;
|
|
453
|
+
|
|
454
|
+
// Ensure line width is valid
|
|
455
|
+
if (lineWidth > 0) {
|
|
456
|
+
// Get border width from query and fall back to 10% of line width
|
|
457
|
+
const borderWidth =
|
|
458
|
+
query.borderwidth !== undefined
|
|
459
|
+
? parseFloat(query.borderwidth)
|
|
460
|
+
: lineWidth * 0.1;
|
|
461
|
+
|
|
462
|
+
// Set rendering style for the start and end points of the path
|
|
463
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
|
|
464
|
+
ctx.lineCap = query.linecap || 'butt';
|
|
465
|
+
|
|
466
|
+
// Set rendering style for overlapping segments of the path with differing directions
|
|
467
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
|
|
468
|
+
ctx.lineJoin = query.linejoin || 'miter';
|
|
469
|
+
|
|
470
|
+
// In order to simulate a border we draw the path two times with the first
|
|
471
|
+
// beeing the wider border part.
|
|
472
|
+
if (query.border !== undefined && borderWidth > 0) {
|
|
473
|
+
// We need to double the desired border width and add it to the line width
|
|
474
|
+
// in order to get the desired border on each side of the line.
|
|
475
|
+
ctx.lineWidth = lineWidth + borderWidth * 2;
|
|
476
|
+
// Set border style as rgba
|
|
477
|
+
ctx.strokeStyle = query.border;
|
|
478
|
+
ctx.stroke();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
ctx.lineWidth = lineWidth;
|
|
482
|
+
ctx.strokeStyle = query.stroke || 'rgba(0,64,255,0.7)';
|
|
483
|
+
ctx.stroke();
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const renderOverlay = async (
|
|
488
|
+
z,
|
|
489
|
+
x,
|
|
490
|
+
y,
|
|
491
|
+
bearing,
|
|
492
|
+
pitch,
|
|
493
|
+
w,
|
|
494
|
+
h,
|
|
495
|
+
scale,
|
|
496
|
+
paths,
|
|
497
|
+
markers,
|
|
498
|
+
query,
|
|
499
|
+
) => {
|
|
500
|
+
if ((!paths || paths.length === 0) && (!markers || markers.length === 0)) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
127
503
|
|
|
128
504
|
const center = precisePx([x, y], z);
|
|
129
505
|
|
|
@@ -131,7 +507,7 @@ const renderOverlay = (z, x, y, bearing, pitch, w, h, scale,
|
|
|
131
507
|
const maxEdge = center[1] + h / 2;
|
|
132
508
|
const minEdge = center[1] - h / 2;
|
|
133
509
|
if (maxEdge > mapHeight) {
|
|
134
|
-
center[1] -=
|
|
510
|
+
center[1] -= maxEdge - mapHeight;
|
|
135
511
|
} else if (minEdge < 0) {
|
|
136
512
|
center[1] -= minEdge;
|
|
137
513
|
}
|
|
@@ -141,49 +517,39 @@ const renderOverlay = (z, x, y, bearing, pitch, w, h, scale,
|
|
|
141
517
|
ctx.scale(scale, scale);
|
|
142
518
|
if (bearing) {
|
|
143
519
|
ctx.translate(w / 2, h / 2);
|
|
144
|
-
ctx.rotate(-bearing / 180 * Math.PI);
|
|
520
|
+
ctx.rotate((-bearing / 180) * Math.PI);
|
|
145
521
|
ctx.translate(-center[0], -center[1]);
|
|
146
522
|
} else {
|
|
147
523
|
// optimized path
|
|
148
524
|
ctx.translate(-center[0] + w / 2, -center[1] + h / 2);
|
|
149
525
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)';
|
|
155
|
-
ctx.beginPath();
|
|
156
|
-
for (const pair of path) {
|
|
157
|
-
const px = precisePx(pair, z);
|
|
158
|
-
ctx.lineTo(px[0], px[1]);
|
|
159
|
-
}
|
|
160
|
-
if (path[0][0] === path[path.length - 1][0] &&
|
|
161
|
-
path[0][1] === path[path.length - 1][1]) {
|
|
162
|
-
ctx.closePath();
|
|
163
|
-
}
|
|
164
|
-
ctx.fill();
|
|
165
|
-
if (lineWidth > 0) {
|
|
166
|
-
ctx.stroke();
|
|
526
|
+
|
|
527
|
+
// Draw provided paths if any
|
|
528
|
+
for (const path of paths) {
|
|
529
|
+
drawPath(ctx, path, query, z);
|
|
167
530
|
}
|
|
168
531
|
|
|
532
|
+
// Await drawing of markers before rendering the canvas
|
|
533
|
+
await drawMarkers(ctx, markers, z);
|
|
534
|
+
|
|
169
535
|
return canvas.toBuffer();
|
|
170
536
|
};
|
|
171
537
|
|
|
172
538
|
const calcZForBBox = (bbox, w, h, query) => {
|
|
173
539
|
let z = 25;
|
|
174
540
|
|
|
175
|
-
const padding = query.padding !== undefined ?
|
|
176
|
-
parseFloat(query.padding) : 0.1;
|
|
541
|
+
const padding = query.padding !== undefined ? parseFloat(query.padding) : 0.1;
|
|
177
542
|
|
|
178
543
|
const minCorner = mercator.px([bbox[0], bbox[3]], z);
|
|
179
544
|
const maxCorner = mercator.px([bbox[2], bbox[1]], z);
|
|
180
545
|
const w_ = w / (1 + 2 * padding);
|
|
181
546
|
const h_ = h / (1 + 2 * padding);
|
|
182
547
|
|
|
183
|
-
z -=
|
|
548
|
+
z -=
|
|
549
|
+
Math.max(
|
|
184
550
|
Math.log((maxCorner[0] - minCorner[0]) / w_),
|
|
185
|
-
Math.log((maxCorner[1] - minCorner[1]) / h_)
|
|
186
|
-
|
|
551
|
+
Math.log((maxCorner[1] - minCorner[1]) / h_),
|
|
552
|
+
) / Math.LN2;
|
|
187
553
|
|
|
188
554
|
z = Math.max(Math.log(Math.max(w, h) / 256) / Math.LN2, Math.min(25, z));
|
|
189
555
|
|
|
@@ -225,14 +591,36 @@ export const serve_rendered = {
|
|
|
225
591
|
|
|
226
592
|
const app = express().disable('x-powered-by');
|
|
227
593
|
|
|
228
|
-
const respondImage = (
|
|
229
|
-
|
|
230
|
-
|
|
594
|
+
const respondImage = (
|
|
595
|
+
item,
|
|
596
|
+
z,
|
|
597
|
+
lon,
|
|
598
|
+
lat,
|
|
599
|
+
bearing,
|
|
600
|
+
pitch,
|
|
601
|
+
width,
|
|
602
|
+
height,
|
|
603
|
+
scale,
|
|
604
|
+
format,
|
|
605
|
+
res,
|
|
606
|
+
next,
|
|
607
|
+
opt_overlay,
|
|
608
|
+
opt_mode = 'tile',
|
|
609
|
+
) => {
|
|
610
|
+
if (
|
|
611
|
+
Math.abs(lon) > 180 ||
|
|
612
|
+
Math.abs(lat) > 85.06 ||
|
|
613
|
+
lon !== lon ||
|
|
614
|
+
lat !== lat
|
|
615
|
+
) {
|
|
231
616
|
return res.status(400).send('Invalid center');
|
|
232
617
|
}
|
|
233
|
-
if (
|
|
618
|
+
if (
|
|
619
|
+
Math.min(width, height) <= 0 ||
|
|
234
620
|
Math.max(width, height) * scale > (options.maxSize || 2048) ||
|
|
235
|
-
width !== width ||
|
|
621
|
+
width !== width ||
|
|
622
|
+
height !== height
|
|
623
|
+
) {
|
|
236
624
|
return res.status(400).send('Invalid size');
|
|
237
625
|
}
|
|
238
626
|
if (format === 'png' || format === 'webp') {
|
|
@@ -256,7 +644,7 @@ export const serve_rendered = {
|
|
|
256
644
|
bearing: bearing,
|
|
257
645
|
pitch: pitch,
|
|
258
646
|
width: width,
|
|
259
|
-
height: height
|
|
647
|
+
height: height,
|
|
260
648
|
};
|
|
261
649
|
if (z === 0) {
|
|
262
650
|
params.width *= 2;
|
|
@@ -296,18 +684,21 @@ export const serve_rendered = {
|
|
|
296
684
|
raw: {
|
|
297
685
|
width: params.width * scale,
|
|
298
686
|
height: params.height * scale,
|
|
299
|
-
channels: 4
|
|
300
|
-
}
|
|
687
|
+
channels: 4,
|
|
688
|
+
},
|
|
301
689
|
});
|
|
302
690
|
|
|
303
691
|
if (z > 2 && tileMargin > 0) {
|
|
304
692
|
const [_, y] = mercator.px(params.center, z);
|
|
305
|
-
let yoffset = Math.max(
|
|
693
|
+
let yoffset = Math.max(
|
|
694
|
+
Math.min(0, y - 128 - tileMargin),
|
|
695
|
+
y + 128 + tileMargin - Math.pow(2, z + 8),
|
|
696
|
+
);
|
|
306
697
|
image.extract({
|
|
307
698
|
left: tileMargin * scale,
|
|
308
699
|
top: (tileMargin + yoffset) * scale,
|
|
309
700
|
width: width * scale,
|
|
310
|
-
height: height * scale
|
|
701
|
+
height: height * scale,
|
|
311
702
|
});
|
|
312
703
|
}
|
|
313
704
|
|
|
@@ -317,7 +708,7 @@ export const serve_rendered = {
|
|
|
317
708
|
}
|
|
318
709
|
|
|
319
710
|
if (opt_overlay) {
|
|
320
|
-
image.composite([{input: opt_overlay}]);
|
|
711
|
+
image.composite([{ input: opt_overlay }]);
|
|
321
712
|
}
|
|
322
713
|
if (item.watermark) {
|
|
323
714
|
const canvas = createCanvas(scale * width, scale * height);
|
|
@@ -330,17 +721,17 @@ export const serve_rendered = {
|
|
|
330
721
|
ctx.fillStyle = 'rgba(0,0,0,.4)';
|
|
331
722
|
ctx.fillText(item.watermark, 5, height - 5);
|
|
332
723
|
|
|
333
|
-
image.composite([{input: canvas.toBuffer()}]);
|
|
724
|
+
image.composite([{ input: canvas.toBuffer() }]);
|
|
334
725
|
}
|
|
335
726
|
|
|
336
727
|
const formatQuality = (options.formatQuality || {})[format];
|
|
337
728
|
|
|
338
729
|
if (format === 'png') {
|
|
339
|
-
image.png({adaptiveFiltering: false});
|
|
730
|
+
image.png({ adaptiveFiltering: false });
|
|
340
731
|
} else if (format === 'jpeg') {
|
|
341
|
-
image.jpeg({quality: formatQuality || 80});
|
|
732
|
+
image.jpeg({ quality: formatQuality || 80 });
|
|
342
733
|
} else if (format === 'webp') {
|
|
343
|
-
image.webp({quality: formatQuality || 90});
|
|
734
|
+
image.webp({ quality: formatQuality || 90 });
|
|
344
735
|
}
|
|
345
736
|
image.toBuffer((err, buffer, info) => {
|
|
346
737
|
if (!buffer) {
|
|
@@ -349,7 +740,7 @@ export const serve_rendered = {
|
|
|
349
740
|
|
|
350
741
|
res.set({
|
|
351
742
|
'Last-Modified': item.lastModified,
|
|
352
|
-
'Content-Type': `image/${format}
|
|
743
|
+
'Content-Type': `image/${format}`,
|
|
353
744
|
});
|
|
354
745
|
return res.status(200).send(buffer);
|
|
355
746
|
});
|
|
@@ -357,91 +748,162 @@ export const serve_rendered = {
|
|
|
357
748
|
});
|
|
358
749
|
};
|
|
359
750
|
|
|
360
|
-
app.get(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return res.sendStatus(404);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const modifiedSince = req.get('if-modified-since'); const cc = req.get('cache-control');
|
|
367
|
-
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
|
|
368
|
-
if (new Date(item.lastModified) <= new Date(modifiedSince)) {
|
|
369
|
-
return res.sendStatus(304);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const z = req.params.z | 0;
|
|
374
|
-
const x = req.params.x | 0;
|
|
375
|
-
const y = req.params.y | 0;
|
|
376
|
-
const scale = getScale(req.params.scale);
|
|
377
|
-
const format = req.params.format;
|
|
378
|
-
if (z < 0 || x < 0 || y < 0 ||
|
|
379
|
-
z > 22 || x >= Math.pow(2, z) || y >= Math.pow(2, z)) {
|
|
380
|
-
return res.status(404).send('Out of bounds');
|
|
381
|
-
}
|
|
382
|
-
const tileSize = 256;
|
|
383
|
-
const tileCenter = mercator.ll([
|
|
384
|
-
((x + 0.5) / (1 << z)) * (256 << z),
|
|
385
|
-
((y + 0.5) / (1 << z)) * (256 << z)
|
|
386
|
-
], z);
|
|
387
|
-
return respondImage(item, z, tileCenter[0], tileCenter[1], 0, 0, tileSize, tileSize, scale, format, res, next);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
if (options.serveStaticMaps !== false) {
|
|
391
|
-
const staticPattern =
|
|
392
|
-
`/:id/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+):scale(${scalePattern})?.:format([\\w]+)`;
|
|
393
|
-
|
|
394
|
-
const centerPattern =
|
|
395
|
-
util.format(':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?',
|
|
396
|
-
FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN,
|
|
397
|
-
FLOAT_PATTERN, FLOAT_PATTERN);
|
|
398
|
-
|
|
399
|
-
app.get(util.format(staticPattern, centerPattern), (req, res, next) => {
|
|
751
|
+
app.get(
|
|
752
|
+
`/:id/:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.:format([\\w]+)`,
|
|
753
|
+
(req, res, next) => {
|
|
400
754
|
const item = repo[req.params.id];
|
|
401
755
|
if (!item) {
|
|
402
756
|
return res.sendStatus(404);
|
|
403
757
|
}
|
|
404
|
-
const raw = req.params.raw;
|
|
405
|
-
const z = +req.params.z;
|
|
406
|
-
let x = +req.params.x;
|
|
407
|
-
let y = +req.params.y;
|
|
408
|
-
const bearing = +(req.params.bearing || '0');
|
|
409
|
-
const pitch = +(req.params.pitch || '0');
|
|
410
|
-
const w = req.params.width | 0;
|
|
411
|
-
const h = req.params.height | 0;
|
|
412
|
-
const scale = getScale(req.params.scale);
|
|
413
|
-
const format = req.params.format;
|
|
414
758
|
|
|
415
|
-
|
|
416
|
-
|
|
759
|
+
const modifiedSince = req.get('if-modified-since');
|
|
760
|
+
const cc = req.get('cache-control');
|
|
761
|
+
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
|
|
762
|
+
if (new Date(item.lastModified) <= new Date(modifiedSince)) {
|
|
763
|
+
return res.sendStatus(304);
|
|
764
|
+
}
|
|
417
765
|
}
|
|
418
766
|
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
767
|
+
const z = req.params.z | 0;
|
|
768
|
+
const x = req.params.x | 0;
|
|
769
|
+
const y = req.params.y | 0;
|
|
770
|
+
const scale = getScale(req.params.scale);
|
|
771
|
+
const format = req.params.format;
|
|
772
|
+
if (
|
|
773
|
+
z < 0 ||
|
|
774
|
+
x < 0 ||
|
|
775
|
+
y < 0 ||
|
|
776
|
+
z > 22 ||
|
|
777
|
+
x >= Math.pow(2, z) ||
|
|
778
|
+
y >= Math.pow(2, z)
|
|
779
|
+
) {
|
|
780
|
+
return res.status(404).send('Out of bounds');
|
|
426
781
|
}
|
|
782
|
+
const tileSize = 256;
|
|
783
|
+
const tileCenter = mercator.ll(
|
|
784
|
+
[
|
|
785
|
+
((x + 0.5) / (1 << z)) * (256 << z),
|
|
786
|
+
((y + 0.5) / (1 << z)) * (256 << z),
|
|
787
|
+
],
|
|
788
|
+
z,
|
|
789
|
+
);
|
|
790
|
+
return respondImage(
|
|
791
|
+
item,
|
|
792
|
+
z,
|
|
793
|
+
tileCenter[0],
|
|
794
|
+
tileCenter[1],
|
|
795
|
+
0,
|
|
796
|
+
0,
|
|
797
|
+
tileSize,
|
|
798
|
+
tileSize,
|
|
799
|
+
scale,
|
|
800
|
+
format,
|
|
801
|
+
res,
|
|
802
|
+
next,
|
|
803
|
+
);
|
|
804
|
+
},
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
if (options.serveStaticMaps !== false) {
|
|
808
|
+
const staticPattern = `/:id/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+):scale(${scalePattern})?.:format([\\w]+)`;
|
|
809
|
+
|
|
810
|
+
const centerPattern = util.format(
|
|
811
|
+
':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?',
|
|
812
|
+
FLOAT_PATTERN,
|
|
813
|
+
FLOAT_PATTERN,
|
|
814
|
+
FLOAT_PATTERN,
|
|
815
|
+
FLOAT_PATTERN,
|
|
816
|
+
FLOAT_PATTERN,
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
app.get(
|
|
820
|
+
util.format(staticPattern, centerPattern),
|
|
821
|
+
async (req, res, next) => {
|
|
822
|
+
const item = repo[req.params.id];
|
|
823
|
+
if (!item) {
|
|
824
|
+
return res.sendStatus(404);
|
|
825
|
+
}
|
|
826
|
+
const raw = req.params.raw;
|
|
827
|
+
const z = +req.params.z;
|
|
828
|
+
let x = +req.params.x;
|
|
829
|
+
let y = +req.params.y;
|
|
830
|
+
const bearing = +(req.params.bearing || '0');
|
|
831
|
+
const pitch = +(req.params.pitch || '0');
|
|
832
|
+
const w = req.params.width | 0;
|
|
833
|
+
const h = req.params.height | 0;
|
|
834
|
+
const scale = getScale(req.params.scale);
|
|
835
|
+
const format = req.params.format;
|
|
836
|
+
|
|
837
|
+
if (z < 0) {
|
|
838
|
+
return res.status(404).send('Invalid zoom');
|
|
839
|
+
}
|
|
427
840
|
|
|
428
|
-
|
|
429
|
-
|
|
841
|
+
const transformer = raw
|
|
842
|
+
? mercator.inverse.bind(mercator)
|
|
843
|
+
: item.dataProjWGStoInternalWGS;
|
|
430
844
|
|
|
431
|
-
|
|
432
|
-
|
|
845
|
+
if (transformer) {
|
|
846
|
+
const ll = transformer([x, y]);
|
|
847
|
+
x = ll[0];
|
|
848
|
+
y = ll[1];
|
|
849
|
+
}
|
|
433
850
|
|
|
434
|
-
|
|
851
|
+
const paths = extractPathsFromQuery(req.query, transformer);
|
|
852
|
+
const markers = extractMarkersFromQuery(
|
|
853
|
+
req.query,
|
|
854
|
+
options,
|
|
855
|
+
transformer,
|
|
856
|
+
);
|
|
857
|
+
const overlay = await renderOverlay(
|
|
858
|
+
z,
|
|
859
|
+
x,
|
|
860
|
+
y,
|
|
861
|
+
bearing,
|
|
862
|
+
pitch,
|
|
863
|
+
w,
|
|
864
|
+
h,
|
|
865
|
+
scale,
|
|
866
|
+
paths,
|
|
867
|
+
markers,
|
|
868
|
+
req.query,
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
return respondImage(
|
|
872
|
+
item,
|
|
873
|
+
z,
|
|
874
|
+
x,
|
|
875
|
+
y,
|
|
876
|
+
bearing,
|
|
877
|
+
pitch,
|
|
878
|
+
w,
|
|
879
|
+
h,
|
|
880
|
+
scale,
|
|
881
|
+
format,
|
|
882
|
+
res,
|
|
883
|
+
next,
|
|
884
|
+
overlay,
|
|
885
|
+
'static',
|
|
886
|
+
);
|
|
887
|
+
},
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
const serveBounds = async (req, res, next) => {
|
|
435
891
|
const item = repo[req.params.id];
|
|
436
892
|
if (!item) {
|
|
437
893
|
return res.sendStatus(404);
|
|
438
894
|
}
|
|
439
895
|
const raw = req.params.raw;
|
|
440
|
-
const bbox = [
|
|
896
|
+
const bbox = [
|
|
897
|
+
+req.params.minx,
|
|
898
|
+
+req.params.miny,
|
|
899
|
+
+req.params.maxx,
|
|
900
|
+
+req.params.maxy,
|
|
901
|
+
];
|
|
441
902
|
let center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];
|
|
442
903
|
|
|
443
|
-
const transformer = raw
|
|
444
|
-
mercator.inverse.bind(mercator)
|
|
904
|
+
const transformer = raw
|
|
905
|
+
? mercator.inverse.bind(mercator)
|
|
906
|
+
: item.dataProjWGStoInternalWGS;
|
|
445
907
|
|
|
446
908
|
if (transformer) {
|
|
447
909
|
const minCorner = transformer(bbox.slice(0, 2));
|
|
@@ -464,15 +926,50 @@ export const serve_rendered = {
|
|
|
464
926
|
const bearing = 0;
|
|
465
927
|
const pitch = 0;
|
|
466
928
|
|
|
467
|
-
const
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
929
|
+
const paths = extractPathsFromQuery(req.query, transformer);
|
|
930
|
+
const markers = extractMarkersFromQuery(
|
|
931
|
+
req.query,
|
|
932
|
+
options,
|
|
933
|
+
transformer,
|
|
934
|
+
);
|
|
935
|
+
const overlay = await renderOverlay(
|
|
936
|
+
z,
|
|
937
|
+
x,
|
|
938
|
+
y,
|
|
939
|
+
bearing,
|
|
940
|
+
pitch,
|
|
941
|
+
w,
|
|
942
|
+
h,
|
|
943
|
+
scale,
|
|
944
|
+
paths,
|
|
945
|
+
markers,
|
|
946
|
+
req.query,
|
|
947
|
+
);
|
|
948
|
+
return respondImage(
|
|
949
|
+
item,
|
|
950
|
+
z,
|
|
951
|
+
x,
|
|
952
|
+
y,
|
|
953
|
+
bearing,
|
|
954
|
+
pitch,
|
|
955
|
+
w,
|
|
956
|
+
h,
|
|
957
|
+
scale,
|
|
958
|
+
format,
|
|
959
|
+
res,
|
|
960
|
+
next,
|
|
961
|
+
overlay,
|
|
962
|
+
'static',
|
|
963
|
+
);
|
|
471
964
|
};
|
|
472
965
|
|
|
473
|
-
const boundsPattern =
|
|
474
|
-
|
|
475
|
-
|
|
966
|
+
const boundsPattern = util.format(
|
|
967
|
+
':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)',
|
|
968
|
+
FLOAT_PATTERN,
|
|
969
|
+
FLOAT_PATTERN,
|
|
970
|
+
FLOAT_PATTERN,
|
|
971
|
+
FLOAT_PATTERN,
|
|
972
|
+
);
|
|
476
973
|
|
|
477
974
|
app.get(util.format(staticPattern, boundsPattern), serveBounds);
|
|
478
975
|
|
|
@@ -500,48 +997,102 @@ export const serve_rendered = {
|
|
|
500
997
|
|
|
501
998
|
const autoPattern = 'auto';
|
|
502
999
|
|
|
503
|
-
app.get(
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
1000
|
+
app.get(
|
|
1001
|
+
util.format(staticPattern, autoPattern),
|
|
1002
|
+
async (req, res, next) => {
|
|
1003
|
+
const item = repo[req.params.id];
|
|
1004
|
+
if (!item) {
|
|
1005
|
+
return res.sendStatus(404);
|
|
1006
|
+
}
|
|
1007
|
+
const raw = req.params.raw;
|
|
1008
|
+
const w = req.params.width | 0;
|
|
1009
|
+
const h = req.params.height | 0;
|
|
1010
|
+
const bearing = 0;
|
|
1011
|
+
const pitch = 0;
|
|
1012
|
+
const scale = getScale(req.params.scale);
|
|
1013
|
+
const format = req.params.format;
|
|
1014
|
+
|
|
1015
|
+
const transformer = raw
|
|
1016
|
+
? mercator.inverse.bind(mercator)
|
|
1017
|
+
: item.dataProjWGStoInternalWGS;
|
|
1018
|
+
|
|
1019
|
+
const paths = extractPathsFromQuery(req.query, transformer);
|
|
1020
|
+
const markers = extractMarkersFromQuery(
|
|
1021
|
+
req.query,
|
|
1022
|
+
options,
|
|
1023
|
+
transformer,
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
// Extract coordinates from markers
|
|
1027
|
+
const markerCoordinates = [];
|
|
1028
|
+
for (const marker of markers) {
|
|
1029
|
+
markerCoordinates.push(marker.location);
|
|
1030
|
+
}
|
|
523
1031
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
bbox[0] = Math.min(bbox[0], pair[0]);
|
|
527
|
-
bbox[1] = Math.min(bbox[1], pair[1]);
|
|
528
|
-
bbox[2] = Math.max(bbox[2], pair[0]);
|
|
529
|
-
bbox[3] = Math.max(bbox[3], pair[1]);
|
|
530
|
-
}
|
|
1032
|
+
// Create array with coordinates from markers and path
|
|
1033
|
+
const coords = [].concat(paths.flat()).concat(markerCoordinates);
|
|
531
1034
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1035
|
+
// Check if we have at least one coordinate to calculate a bounding box
|
|
1036
|
+
if (coords.length < 1) {
|
|
1037
|
+
return res.status(400).send('No coordinates provided');
|
|
1038
|
+
}
|
|
536
1039
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1040
|
+
const bbox = [Infinity, Infinity, -Infinity, -Infinity];
|
|
1041
|
+
for (const pair of coords) {
|
|
1042
|
+
bbox[0] = Math.min(bbox[0], pair[0]);
|
|
1043
|
+
bbox[1] = Math.min(bbox[1], pair[1]);
|
|
1044
|
+
bbox[2] = Math.max(bbox[2], pair[0]);
|
|
1045
|
+
bbox[3] = Math.max(bbox[3], pair[1]);
|
|
1046
|
+
}
|
|
540
1047
|
|
|
541
|
-
|
|
1048
|
+
const bbox_ = mercator.convert(bbox, '900913');
|
|
1049
|
+
const center = mercator.inverse([
|
|
1050
|
+
(bbox_[0] + bbox_[2]) / 2,
|
|
1051
|
+
(bbox_[1] + bbox_[3]) / 2,
|
|
1052
|
+
]);
|
|
1053
|
+
|
|
1054
|
+
// Calculate zoom level
|
|
1055
|
+
const maxZoom = parseFloat(req.query.maxzoom);
|
|
1056
|
+
let z = calcZForBBox(bbox, w, h, req.query);
|
|
1057
|
+
if (maxZoom > 0) {
|
|
1058
|
+
z = Math.min(z, maxZoom);
|
|
1059
|
+
}
|
|
542
1060
|
|
|
543
|
-
|
|
544
|
-
|
|
1061
|
+
const x = center[0];
|
|
1062
|
+
const y = center[1];
|
|
1063
|
+
|
|
1064
|
+
const overlay = await renderOverlay(
|
|
1065
|
+
z,
|
|
1066
|
+
x,
|
|
1067
|
+
y,
|
|
1068
|
+
bearing,
|
|
1069
|
+
pitch,
|
|
1070
|
+
w,
|
|
1071
|
+
h,
|
|
1072
|
+
scale,
|
|
1073
|
+
paths,
|
|
1074
|
+
markers,
|
|
1075
|
+
req.query,
|
|
1076
|
+
);
|
|
1077
|
+
|
|
1078
|
+
return respondImage(
|
|
1079
|
+
item,
|
|
1080
|
+
z,
|
|
1081
|
+
x,
|
|
1082
|
+
y,
|
|
1083
|
+
bearing,
|
|
1084
|
+
pitch,
|
|
1085
|
+
w,
|
|
1086
|
+
h,
|
|
1087
|
+
scale,
|
|
1088
|
+
format,
|
|
1089
|
+
res,
|
|
1090
|
+
next,
|
|
1091
|
+
overlay,
|
|
1092
|
+
'static',
|
|
1093
|
+
);
|
|
1094
|
+
},
|
|
1095
|
+
);
|
|
545
1096
|
}
|
|
546
1097
|
|
|
547
1098
|
app.get('/:id.json', (req, res, next) => {
|
|
@@ -550,8 +1101,13 @@ export const serve_rendered = {
|
|
|
550
1101
|
return res.sendStatus(404);
|
|
551
1102
|
}
|
|
552
1103
|
const info = clone(item.tileJSON);
|
|
553
|
-
info.tiles = getTileUrls(
|
|
554
|
-
|
|
1104
|
+
info.tiles = getTileUrls(
|
|
1105
|
+
req,
|
|
1106
|
+
info.tiles,
|
|
1107
|
+
`styles/${req.params.id}`,
|
|
1108
|
+
info.format,
|
|
1109
|
+
item.publicUrl,
|
|
1110
|
+
);
|
|
555
1111
|
return res.send(info);
|
|
556
1112
|
});
|
|
557
1113
|
|
|
@@ -561,7 +1117,7 @@ export const serve_rendered = {
|
|
|
561
1117
|
const map = {
|
|
562
1118
|
renderers: [],
|
|
563
1119
|
renderers_static: [],
|
|
564
|
-
sources: {}
|
|
1120
|
+
sources: {},
|
|
565
1121
|
};
|
|
566
1122
|
|
|
567
1123
|
let styleJSON;
|
|
@@ -577,19 +1133,26 @@ export const serve_rendered = {
|
|
|
577
1133
|
const dir = options.paths[protocol];
|
|
578
1134
|
const file = unescape(req.url).substring(protocol.length + 3);
|
|
579
1135
|
fs.readFile(path.join(dir, file), (err, data) => {
|
|
580
|
-
callback(err, {data: data});
|
|
1136
|
+
callback(err, { data: data });
|
|
581
1137
|
});
|
|
582
1138
|
} else if (protocol === 'fonts') {
|
|
583
1139
|
const parts = req.url.split('/');
|
|
584
1140
|
const fontstack = unescape(parts[2]);
|
|
585
1141
|
const range = parts[3].split('.')[0];
|
|
586
1142
|
getFontsPbf(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
1143
|
+
null,
|
|
1144
|
+
options.paths[protocol],
|
|
1145
|
+
fontstack,
|
|
1146
|
+
range,
|
|
1147
|
+
existingFonts,
|
|
1148
|
+
).then(
|
|
1149
|
+
(concated) => {
|
|
1150
|
+
callback(null, { data: concated });
|
|
1151
|
+
},
|
|
1152
|
+
(err) => {
|
|
1153
|
+
callback(err, { data: null });
|
|
1154
|
+
},
|
|
1155
|
+
);
|
|
593
1156
|
} else if (protocol === 'mbtiles') {
|
|
594
1157
|
const parts = req.url.split('/');
|
|
595
1158
|
const sourceId = parts[2];
|
|
@@ -601,8 +1164,13 @@ export const serve_rendered = {
|
|
|
601
1164
|
const format = parts[5].split('.')[1];
|
|
602
1165
|
source.getTile(z, x, y, (err, data, headers) => {
|
|
603
1166
|
if (err) {
|
|
604
|
-
if (options.verbose)
|
|
605
|
-
|
|
1167
|
+
if (options.verbose)
|
|
1168
|
+
console.log('MBTiles error, serving empty', err);
|
|
1169
|
+
createEmptyResponse(
|
|
1170
|
+
sourceInfo.format,
|
|
1171
|
+
sourceInfo.color,
|
|
1172
|
+
callback,
|
|
1173
|
+
);
|
|
606
1174
|
return;
|
|
607
1175
|
}
|
|
608
1176
|
|
|
@@ -615,11 +1183,23 @@ export const serve_rendered = {
|
|
|
615
1183
|
try {
|
|
616
1184
|
response.data = zlib.unzipSync(data);
|
|
617
1185
|
} catch (err) {
|
|
618
|
-
console.log(
|
|
1186
|
+
console.log(
|
|
1187
|
+
'Skipping incorrect header for tile mbtiles://%s/%s/%s/%s.pbf',
|
|
1188
|
+
id,
|
|
1189
|
+
z,
|
|
1190
|
+
x,
|
|
1191
|
+
y,
|
|
1192
|
+
);
|
|
619
1193
|
}
|
|
620
1194
|
if (options.dataDecoratorFunc) {
|
|
621
1195
|
response.data = options.dataDecoratorFunc(
|
|
622
|
-
|
|
1196
|
+
sourceId,
|
|
1197
|
+
'data',
|
|
1198
|
+
response.data,
|
|
1199
|
+
z,
|
|
1200
|
+
x,
|
|
1201
|
+
y,
|
|
1202
|
+
);
|
|
623
1203
|
}
|
|
624
1204
|
} else {
|
|
625
1205
|
response.data = data;
|
|
@@ -628,36 +1208,39 @@ export const serve_rendered = {
|
|
|
628
1208
|
callback(null, response);
|
|
629
1209
|
});
|
|
630
1210
|
} else if (protocol === 'http' || protocol === 'https') {
|
|
631
|
-
request(
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
1211
|
+
request(
|
|
1212
|
+
{
|
|
1213
|
+
url: req.url,
|
|
1214
|
+
encoding: null,
|
|
1215
|
+
gzip: true,
|
|
1216
|
+
},
|
|
1217
|
+
(err, res, body) => {
|
|
1218
|
+
const parts = url.parse(req.url);
|
|
1219
|
+
const extension = path.extname(parts.pathname).toLowerCase();
|
|
1220
|
+
const format = extensionToFormat[extension] || '';
|
|
1221
|
+
if (err || res.statusCode < 200 || res.statusCode >= 300) {
|
|
1222
|
+
// console.log('HTTP error', err || res.statusCode);
|
|
1223
|
+
createEmptyResponse(format, '', callback);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
644
1226
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1227
|
+
const response = {};
|
|
1228
|
+
if (res.headers.modified) {
|
|
1229
|
+
response.modified = new Date(res.headers.modified);
|
|
1230
|
+
}
|
|
1231
|
+
if (res.headers.expires) {
|
|
1232
|
+
response.expires = new Date(res.headers.expires);
|
|
1233
|
+
}
|
|
1234
|
+
if (res.headers.etag) {
|
|
1235
|
+
response.etag = res.headers.etag;
|
|
1236
|
+
}
|
|
655
1237
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1238
|
+
response.data = body;
|
|
1239
|
+
callback(null, response);
|
|
1240
|
+
},
|
|
1241
|
+
);
|
|
659
1242
|
}
|
|
660
|
-
}
|
|
1243
|
+
},
|
|
661
1244
|
});
|
|
662
1245
|
renderer.load(styleJSON);
|
|
663
1246
|
createCallback(null, renderer);
|
|
@@ -668,7 +1251,7 @@ export const serve_rendered = {
|
|
|
668
1251
|
create: createRenderer.bind(null, ratio),
|
|
669
1252
|
destroy: (renderer) => {
|
|
670
1253
|
renderer.release();
|
|
671
|
-
}
|
|
1254
|
+
},
|
|
672
1255
|
});
|
|
673
1256
|
};
|
|
674
1257
|
|
|
@@ -682,16 +1265,20 @@ export const serve_rendered = {
|
|
|
682
1265
|
}
|
|
683
1266
|
|
|
684
1267
|
if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) {
|
|
685
|
-
styleJSON.sprite =
|
|
1268
|
+
styleJSON.sprite =
|
|
1269
|
+
'sprites://' +
|
|
686
1270
|
styleJSON.sprite
|
|
687
|
-
|
|
688
|
-
|
|
1271
|
+
.replace('{style}', path.basename(styleFile, '.json'))
|
|
1272
|
+
.replace(
|
|
1273
|
+
'{styleJsonFolder}',
|
|
1274
|
+
path.relative(options.paths.sprites, path.dirname(styleJSONPath)),
|
|
1275
|
+
);
|
|
689
1276
|
}
|
|
690
1277
|
if (styleJSON.glyphs && !httpTester.test(styleJSON.glyphs)) {
|
|
691
1278
|
styleJSON.glyphs = `fonts://${styleJSON.glyphs}`;
|
|
692
1279
|
}
|
|
693
1280
|
|
|
694
|
-
for (const layer of
|
|
1281
|
+
for (const layer of styleJSON.layers || []) {
|
|
695
1282
|
if (layer && layer.paint) {
|
|
696
1283
|
// Remove (flatten) 3D buildings
|
|
697
1284
|
if (layer.paint['fill-extrusion-height']) {
|
|
@@ -704,14 +1291,14 @@ export const serve_rendered = {
|
|
|
704
1291
|
}
|
|
705
1292
|
|
|
706
1293
|
const tileJSON = {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1294
|
+
tilejson: '2.0.0',
|
|
1295
|
+
name: styleJSON.name,
|
|
1296
|
+
attribution: '',
|
|
1297
|
+
minzoom: 0,
|
|
1298
|
+
maxzoom: 20,
|
|
1299
|
+
bounds: [-180, -85.0511, 180, 85.0511],
|
|
1300
|
+
format: 'png',
|
|
1301
|
+
type: 'baselayer',
|
|
715
1302
|
};
|
|
716
1303
|
const attributionOverride = params.tilejson && params.tilejson.attribution;
|
|
717
1304
|
Object.assign(tileJSON, params.tilejson || {});
|
|
@@ -724,7 +1311,7 @@ export const serve_rendered = {
|
|
|
724
1311
|
map,
|
|
725
1312
|
dataProjWGStoInternalWGS: null,
|
|
726
1313
|
lastModified: new Date().toUTCString(),
|
|
727
|
-
watermark: params.watermark || options.watermark
|
|
1314
|
+
watermark: params.watermark || options.watermark,
|
|
728
1315
|
};
|
|
729
1316
|
repo[id] = repoobj;
|
|
730
1317
|
|
|
@@ -738,8 +1325,8 @@ export const serve_rendered = {
|
|
|
738
1325
|
delete source.url;
|
|
739
1326
|
|
|
740
1327
|
let mbtilesFile = url.substring('mbtiles://'.length);
|
|
741
|
-
const fromData =
|
|
742
|
-
mbtilesFile[mbtilesFile.length - 1] === '}';
|
|
1328
|
+
const fromData =
|
|
1329
|
+
mbtilesFile[0] === '{' && mbtilesFile[mbtilesFile.length - 1] === '}';
|
|
743
1330
|
|
|
744
1331
|
if (fromData) {
|
|
745
1332
|
mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2);
|
|
@@ -754,52 +1341,58 @@ export const serve_rendered = {
|
|
|
754
1341
|
}
|
|
755
1342
|
}
|
|
756
1343
|
|
|
757
|
-
queue.push(
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
map.sources[name]
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
if (
|
|
793
|
-
|
|
794
|
-
|
|
1344
|
+
queue.push(
|
|
1345
|
+
new Promise((resolve, reject) => {
|
|
1346
|
+
mbtilesFile = path.resolve(options.paths.mbtiles, mbtilesFile);
|
|
1347
|
+
const mbtilesFileStats = fs.statSync(mbtilesFile);
|
|
1348
|
+
if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) {
|
|
1349
|
+
throw Error(`Not valid MBTiles file: ${mbtilesFile}`);
|
|
1350
|
+
}
|
|
1351
|
+
map.sources[name] = new MBTiles(mbtilesFile + '?mode=ro', (err) => {
|
|
1352
|
+
map.sources[name].getInfo((err, info) => {
|
|
1353
|
+
if (err) {
|
|
1354
|
+
console.error(err);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (!repoobj.dataProjWGStoInternalWGS && info.proj4) {
|
|
1359
|
+
// how to do this for multiple sources with different proj4 defs?
|
|
1360
|
+
const to3857 = proj4('EPSG:3857');
|
|
1361
|
+
const toDataProj = proj4(info.proj4);
|
|
1362
|
+
repoobj.dataProjWGStoInternalWGS = (xy) =>
|
|
1363
|
+
to3857.inverse(toDataProj.forward(xy));
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const type = source.type;
|
|
1367
|
+
Object.assign(source, info);
|
|
1368
|
+
source.type = type;
|
|
1369
|
+
source.tiles = [
|
|
1370
|
+
// meta url which will be detected when requested
|
|
1371
|
+
`mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`,
|
|
1372
|
+
];
|
|
1373
|
+
delete source.scheme;
|
|
1374
|
+
|
|
1375
|
+
if (options.dataDecoratorFunc) {
|
|
1376
|
+
source = options.dataDecoratorFunc(name, 'tilejson', source);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
if (
|
|
1380
|
+
!attributionOverride &&
|
|
1381
|
+
source.attribution &&
|
|
1382
|
+
source.attribution.length > 0
|
|
1383
|
+
) {
|
|
1384
|
+
if (!tileJSON.attribution.includes(source.attribution)) {
|
|
1385
|
+
if (tileJSON.attribution.length > 0) {
|
|
1386
|
+
tileJSON.attribution += ' | ';
|
|
1387
|
+
}
|
|
1388
|
+
tileJSON.attribution += source.attribution;
|
|
795
1389
|
}
|
|
796
|
-
tileJSON.attribution += source.attribution;
|
|
797
1390
|
}
|
|
798
|
-
|
|
799
|
-
|
|
1391
|
+
resolve();
|
|
1392
|
+
});
|
|
800
1393
|
});
|
|
801
|
-
})
|
|
802
|
-
|
|
1394
|
+
}),
|
|
1395
|
+
);
|
|
803
1396
|
}
|
|
804
1397
|
}
|
|
805
1398
|
|
|
@@ -813,7 +1406,12 @@ export const serve_rendered = {
|
|
|
813
1406
|
const minPoolSize = minPoolSizes[i];
|
|
814
1407
|
const maxPoolSize = Math.max(minPoolSize, maxPoolSizes[j]);
|
|
815
1408
|
map.renderers[s] = createPool(s, 'tile', minPoolSize, maxPoolSize);
|
|
816
|
-
map.renderers_static[s] = createPool(
|
|
1409
|
+
map.renderers_static[s] = createPool(
|
|
1410
|
+
s,
|
|
1411
|
+
'static',
|
|
1412
|
+
minPoolSize,
|
|
1413
|
+
maxPoolSize,
|
|
1414
|
+
);
|
|
817
1415
|
}
|
|
818
1416
|
});
|
|
819
1417
|
|
|
@@ -830,5 +1428,5 @@ export const serve_rendered = {
|
|
|
830
1428
|
});
|
|
831
1429
|
}
|
|
832
1430
|
delete repo[id];
|
|
833
|
-
}
|
|
1431
|
+
},
|
|
834
1432
|
};
|