tileserver-gl-light 4.1.1 → 4.1.2
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/Dockerfile +23 -5
- package/Dockerfile_test +30 -21
- package/docs/config.rst +9 -0
- package/docs/endpoints.rst +27 -1
- package/package.json +3 -2
- package/public/templates/wmts.tmpl +4 -4
- package/src/serve_rendered.js +412 -55
- package/src/server.js +39 -7
- package/test/static.js +1 -1
package/.dockerignore
CHANGED
package/Dockerfile
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
|
-
FROM
|
|
1
|
+
FROM ubuntu:focal
|
|
2
|
+
|
|
3
|
+
ENV \
|
|
4
|
+
NODE_ENV="production" \
|
|
5
|
+
CHOKIDAR_USEPOLLING=1 \
|
|
6
|
+
CHOKIDAR_INTERVAL=500
|
|
7
|
+
|
|
8
|
+
RUN set -ex; \
|
|
9
|
+
export DEBIAN_FRONTEND=noninteractive; \
|
|
10
|
+
groupadd -r node; \
|
|
11
|
+
useradd -r -g node node; \
|
|
12
|
+
apt-get -qq update; \
|
|
13
|
+
apt-get -y --no-install-recommends install \
|
|
14
|
+
ca-certificates \
|
|
15
|
+
wget; \
|
|
16
|
+
wget -qO- https://deb.nodesource.com/setup_16.x | bash; \
|
|
17
|
+
apt-get install -y nodejs; \
|
|
18
|
+
apt-get -y remove wget; \
|
|
19
|
+
apt-get -y --purge autoremove; \
|
|
20
|
+
apt-get clean; \
|
|
21
|
+
rm -rf /var/lib/apt/lists/*;
|
|
2
22
|
|
|
3
|
-
ENV NODE_ENV="production"
|
|
4
|
-
ENV CHOKIDAR_USEPOLLING=1
|
|
5
|
-
ENV CHOKIDAR_INTERVAL=500
|
|
6
23
|
EXPOSE 80
|
|
7
24
|
VOLUME /data
|
|
8
25
|
WORKDIR /data
|
|
@@ -10,5 +27,6 @@ ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"]
|
|
|
10
27
|
|
|
11
28
|
RUN mkdir -p /usr/src/app
|
|
12
29
|
COPY / /usr/src/app
|
|
13
|
-
RUN cd /usr/src/app && npm install --
|
|
30
|
+
RUN cd /usr/src/app && npm install --omit=dev
|
|
14
31
|
RUN ["chmod", "+x", "/usr/src/app/docker-entrypoint.sh"]
|
|
32
|
+
USER node:node
|
package/Dockerfile_test
CHANGED
|
@@ -2,34 +2,43 @@
|
|
|
2
2
|
# Simply run "docker build -f Dockerfile_test ."
|
|
3
3
|
# WARNING: sometimes it fails with a core dumped exception
|
|
4
4
|
|
|
5
|
-
FROM
|
|
5
|
+
FROM ubuntu:focal
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
ENV NODE_ENV="development"
|
|
8
|
+
|
|
9
|
+
RUN set -ex; \
|
|
10
|
+
export DEBIAN_FRONTEND=noninteractive; \
|
|
11
|
+
apt-get -qq update; \
|
|
12
|
+
apt-get -y --no-install-recommends install \
|
|
13
|
+
unzip \
|
|
14
|
+
build-essential \
|
|
15
|
+
ca-certificates \
|
|
16
|
+
wget \
|
|
17
|
+
pkg-config \
|
|
18
|
+
xvfb \
|
|
19
|
+
libglfw3-dev \
|
|
20
|
+
libuv1-dev \
|
|
21
|
+
libjpeg-turbo8 \
|
|
22
|
+
libicu66 \
|
|
23
|
+
libcairo2-dev \
|
|
24
|
+
libpango1.0-dev \
|
|
25
|
+
libjpeg-dev \
|
|
26
|
+
libgif-dev \
|
|
27
|
+
librsvg2-dev \
|
|
28
|
+
libcurl4-openssl-dev \
|
|
29
|
+
libpixman-1-dev; \
|
|
30
|
+
wget -qO- https://deb.nodesource.com/setup_16.x | bash; \
|
|
31
|
+
apt-get install -y nodejs; \
|
|
32
|
+
apt-get clean;
|
|
23
33
|
|
|
24
34
|
RUN mkdir -p /usr/src/app
|
|
25
35
|
WORKDIR /usr/src/app
|
|
26
36
|
|
|
27
|
-
RUN wget -O test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
ENV NODE_ENV="test"
|
|
37
|
+
RUN wget -O test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip; \
|
|
38
|
+
unzip -q test_data.zip -d test_data
|
|
31
39
|
|
|
32
40
|
COPY package.json .
|
|
33
41
|
RUN npm install
|
|
34
42
|
COPY / .
|
|
43
|
+
|
|
35
44
|
RUN xvfb-run --server-args="-screen 0 1024x768x24" npm test
|
package/docs/config.rst
CHANGED
|
@@ -14,6 +14,7 @@ Example:
|
|
|
14
14
|
"root": "",
|
|
15
15
|
"fonts": "fonts",
|
|
16
16
|
"sprites": "sprites",
|
|
17
|
+
"icons": "icons",
|
|
17
18
|
"styles": "styles",
|
|
18
19
|
"mbtiles": ""
|
|
19
20
|
},
|
|
@@ -31,6 +32,7 @@ Example:
|
|
|
31
32
|
"serveAllFonts": false,
|
|
32
33
|
"serveAllStyles": false,
|
|
33
34
|
"serveStaticMaps": true,
|
|
35
|
+
"allowRemoteMarkerIcons": true,
|
|
34
36
|
"tileMargin": 0
|
|
35
37
|
},
|
|
36
38
|
"styles": {
|
|
@@ -141,6 +143,13 @@ Optional string to be rendered into the raster tiles (and static maps) as waterm
|
|
|
141
143
|
Can be used for hard-coding attributions etc. (can also be specified per-style).
|
|
142
144
|
Not used by default.
|
|
143
145
|
|
|
146
|
+
``allowRemoteMarkerIcons``
|
|
147
|
+
--------------
|
|
148
|
+
|
|
149
|
+
Allows the rendering of marker icons fetched via http(s) hyperlinks.
|
|
150
|
+
For security reasons only allow this if you can control the origins from where the markers are fetched!
|
|
151
|
+
Default is to disallow fetching of icons from remote sources.
|
|
152
|
+
|
|
144
153
|
``styles``
|
|
145
154
|
==========
|
|
146
155
|
|
package/docs/endpoints.rst
CHANGED
|
@@ -38,15 +38,41 @@ Static images
|
|
|
38
38
|
* ``path`` - comma-separated ``lng,lat``, pipe-separated pairs
|
|
39
39
|
|
|
40
40
|
* e.g. ``5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8``
|
|
41
|
+
* can be provided multiple times
|
|
41
42
|
|
|
42
|
-
* ``latlng`` - indicates
|
|
43
|
+
* ``latlng`` - indicates coordinates are in ``lat,lng`` order rather than the usual ``lng,lat``
|
|
43
44
|
* ``fill`` - color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``)
|
|
44
45
|
* ``stroke`` - color of the path stroke
|
|
45
46
|
* ``width`` - width of the stroke
|
|
47
|
+
* ``linecap`` - rendering style for the start and end points of the path
|
|
48
|
+
* ``linejoin`` - rendering style for overlapping segments of the path with differing directions
|
|
49
|
+
* ``border`` - color of the optional border path stroke
|
|
50
|
+
* ``borderwidth`` - width of the border stroke (default 10% of width)
|
|
51
|
+
* ``marker`` - Marker in format ``lng,lat|iconPath|option|option|...``
|
|
52
|
+
|
|
53
|
+
* Will be rendered with the bottom center at the provided location
|
|
54
|
+
* ``lng,lat`` and ``iconPath`` are mandatory and icons won't be rendered without them
|
|
55
|
+
* ``iconPath`` is either a link to an image served via http(s) or a path to a file relative to the configured icon path
|
|
56
|
+
* ``option`` must adhere to the format ``optionName:optionValue`` and supports the following names
|
|
57
|
+
|
|
58
|
+
* ``scale`` - Factor to scale image by
|
|
59
|
+
|
|
60
|
+
* e.g. ``0.5`` - Scales the image to half it's original size
|
|
61
|
+
|
|
62
|
+
* ``offset`` - Image offset as positive or negative pixel value in format ``[offsetX],[offsetY]``
|
|
63
|
+
|
|
64
|
+
* scales with ``scale`` parameter since image placement is relative to it's size
|
|
65
|
+
* e.g. ``2,-4`` - Image will be moved 2 pixel to the right and 4 pixel in the upwards direction from the provided location
|
|
66
|
+
|
|
67
|
+
* e.g. ``5.9,45.8|marker-start.svg|scale:0.5|offset:2,-4``
|
|
68
|
+
* can be provided multiple times
|
|
69
|
+
|
|
46
70
|
* ``padding`` - "percentage" padding for fitted endpoints (area-based and path autofit)
|
|
47
71
|
|
|
48
72
|
* value of ``0.1`` means "add 10% size to each side to make sure the area of interest is nicely visible"
|
|
49
73
|
|
|
74
|
+
* ``maxzoom`` - Maximum zoom level (only for auto endpoint where zoom level is calculated and not provided)
|
|
75
|
+
|
|
50
76
|
* You can also use (experimental) ``/styles/{id}/static/raw/...`` endpoints with raw spherical mercator coordinates (EPSG:3857) instead of WGS84.
|
|
51
77
|
|
|
52
78
|
* The static images are not available in the ``tileserver-gl-light`` version.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tileserver-gl-light",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.2",
|
|
4
4
|
"description": "Map tile server for JSON GL styles - serving vector tiles",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"bin": "src/main.js",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"pbf": "3.2.1",
|
|
37
37
|
"proj4": "2.8.0",
|
|
38
38
|
"request": "2.88.2",
|
|
39
|
-
"tileserver-gl-styles": "2.0.0"
|
|
39
|
+
"tileserver-gl-styles": "2.0.0",
|
|
40
|
+
"sanitize-filename": "1.6.3"
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<ows:Operation name="GetCapabilities">
|
|
12
12
|
<ows:DCP>
|
|
13
13
|
<ows:HTTP>
|
|
14
|
-
<ows:Get xlink:href="{{baseUrl}}
|
|
14
|
+
<ows:Get xlink:href="{{baseUrl}}wmts/{{id}}/">
|
|
15
15
|
<ows:Constraint name="GetEncoding">
|
|
16
16
|
<ows:AllowedValues>
|
|
17
17
|
<ows:Value>RESTful</ows:Value>
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<ows:Operation name="GetTile">
|
|
25
25
|
<ows:DCP>
|
|
26
26
|
<ows:HTTP>
|
|
27
|
-
<ows:Get xlink:href="{{baseUrl}}
|
|
27
|
+
<ows:Get xlink:href="{{baseUrl}}styles/">
|
|
28
28
|
<ows:Constraint name="GetEncoding">
|
|
29
29
|
<ows:AllowedValues>
|
|
30
30
|
<ows:Value>RESTful</ows:Value>
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
<TileMatrixSetLink>
|
|
51
51
|
<TileMatrixSet>GoogleMapsCompatible</TileMatrixSet>
|
|
52
52
|
</TileMatrixSetLink>
|
|
53
|
-
<ResourceURL format="image/png" resourceType="tile" template="{{baseUrl}}
|
|
53
|
+
<ResourceURL format="image/png" resourceType="tile" template="{{baseUrl}}styles/{{id}}/{TileMatrix}/{TileCol}/{TileRow}.png{{key_query}}"/>
|
|
54
54
|
</Layer><TileMatrixSet>
|
|
55
55
|
<ows:Title>GoogleMapsCompatible</ows:Title>
|
|
56
56
|
<ows:Abstract>GoogleMapsCompatible EPSG:3857</ows:Abstract>
|
|
@@ -403,5 +403,5 @@
|
|
|
403
403
|
<MatrixHeight>262144</MatrixHeight>
|
|
404
404
|
</TileMatrix></TileMatrixSet>
|
|
405
405
|
</Contents>
|
|
406
|
-
<ServiceMetadataURL xlink:href="{{baseUrl}}
|
|
406
|
+
<ServiceMetadataURL xlink:href="{{baseUrl}}wmts/{{id}}/"/>
|
|
407
407
|
</Capabilities>
|
package/src/serve_rendered.js
CHANGED
|
@@ -7,10 +7,11 @@ 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';
|
|
@@ -21,7 +22,6 @@ import {getFontsPbf, getTileUrls, fixTileJSONCenter} from './utils.js';
|
|
|
21
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
|
|
|
@@ -93,37 +93,385 @@ function createEmptyResponse(format, color, callback) {
|
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Parses coordinate pair provided to pair of floats and ensures the resulting
|
|
98
|
+
* pair is a longitude/latitude combination depending on lnglat query parameter.
|
|
99
|
+
* @param {List} coordinatePair Coordinate pair.
|
|
100
|
+
* @param {Object} query Request query parameters.
|
|
101
|
+
*/
|
|
102
|
+
const parseCoordinatePair = (coordinates, query) => {
|
|
103
|
+
const firstCoordinate = parseFloat(coordinates[0]);
|
|
104
|
+
const secondCoordinate = parseFloat(coordinates[1]);
|
|
105
|
+
|
|
106
|
+
// Ensure provided coordinates could be parsed and abort if not
|
|
107
|
+
if (isNaN(firstCoordinate) || isNaN(secondCoordinate)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check if coordinates have been provided as lat/lng pair instead of the
|
|
112
|
+
// ususal lng/lat pair and ensure resulting pair is lng/lat
|
|
113
|
+
if (query.latlng === '1' || query.latlng === 'true') {
|
|
114
|
+
return [secondCoordinate, firstCoordinate];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return [firstCoordinate, secondCoordinate];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parses a coordinate pair from query arguments and optionally transforms it.
|
|
122
|
+
* @param {List} coordinatePair Coordinate pair.
|
|
123
|
+
* @param {Object} query Request query parameters.
|
|
124
|
+
* @param {Function} transformer Optional transform function.
|
|
125
|
+
*/
|
|
126
|
+
const parseCoordinates = (coordinatePair, query, transformer) => {
|
|
127
|
+
const parsedCoordinates = parseCoordinatePair(coordinatePair, query);
|
|
128
|
+
|
|
129
|
+
// Transform coordinates
|
|
130
|
+
if (transformer) {
|
|
131
|
+
return transformer(parsedCoordinates);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return parsedCoordinates;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Parses paths provided via query into a list of path objects.
|
|
140
|
+
* @param {Object} query Request query parameters.
|
|
141
|
+
* @param {Function} transformer Optional transform function.
|
|
142
|
+
*/
|
|
143
|
+
const extractPathsFromQuery = (query, transformer) => {
|
|
144
|
+
// Return an empty list if no paths have been provided
|
|
145
|
+
if (!query.path) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const paths = [];
|
|
150
|
+
|
|
151
|
+
// Check if multiple paths have been provided and mimic a list if it's a
|
|
152
|
+
// single path.
|
|
153
|
+
const providedPaths = Array.isArray(query.path) ? query.path : [query.path];
|
|
154
|
+
|
|
155
|
+
// Iterate through paths, parse and validate them
|
|
156
|
+
for (const provided_path of providedPaths) {
|
|
157
|
+
const currentPath = [];
|
|
158
|
+
|
|
159
|
+
// Extract coordinate-list from path
|
|
160
|
+
const pathParts = (provided_path || '').split('|');
|
|
161
|
+
|
|
162
|
+
// Iterate through coordinate-list, parse the coordinates and validate them
|
|
163
|
+
for (const pair of pathParts) {
|
|
164
|
+
// Extract coordinates from coordinate pair
|
|
165
|
+
const pairParts = pair.split(',');
|
|
166
|
+
|
|
167
|
+
// Ensure we have two coordinates
|
|
168
|
+
if (pairParts.length === 2) {
|
|
169
|
+
const pair = parseCoordinates(pairParts, query, transformer);
|
|
170
|
+
|
|
171
|
+
// Ensure coordinates could be parsed and skip them if not
|
|
172
|
+
if (pair === null) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Add the coordinate-pair to the current path if they are valid
|
|
177
|
+
currentPath.push(pair);
|
|
107
178
|
}
|
|
108
|
-
|
|
109
|
-
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Extend list of paths with current path if it contains coordinates
|
|
182
|
+
if (currentPath.length) {
|
|
183
|
+
paths.push(currentPath)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
}
|
|
187
|
+
return paths;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Parses marker options provided via query and sets corresponding attributes
|
|
192
|
+
* on marker object.
|
|
193
|
+
* Options adhere to the following format
|
|
194
|
+
* [optionName]:[optionValue]
|
|
195
|
+
* @param {List[String]} optionsList List of option strings.
|
|
196
|
+
* @param {Object} marker Marker object to configure.
|
|
197
|
+
*/
|
|
198
|
+
const parseMarkerOptions = (optionsList, marker) => {
|
|
199
|
+
for (const options of optionsList) {
|
|
200
|
+
const optionParts = options.split(':');
|
|
201
|
+
// Ensure we got an option name and value
|
|
202
|
+
if (optionParts.length < 2) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
switch (optionParts[0]) {
|
|
207
|
+
// Scale factor to up- or downscale icon
|
|
208
|
+
case 'scale':
|
|
209
|
+
// Scale factors must not be negative
|
|
210
|
+
marker.scale = Math.abs(parseFloat(optionParts[1]))
|
|
211
|
+
break;
|
|
212
|
+
// Icon offset as positive or negative pixel value in the following
|
|
213
|
+
// format [offsetX],[offsetY] where [offsetY] is optional
|
|
214
|
+
case 'offset':
|
|
215
|
+
const providedOffset = optionParts[1].split(',');
|
|
216
|
+
// Set X-axis offset
|
|
217
|
+
marker.offsetX = parseFloat(providedOffset[0]);
|
|
218
|
+
// Check if an offset has been provided for Y-axis
|
|
219
|
+
if (providedOffset.length > 1) {
|
|
220
|
+
marker.offsetY = parseFloat(providedOffset[1]);
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Parses markers provided via query into a list of marker objects.
|
|
229
|
+
* @param {Object} query Request query parameters.
|
|
230
|
+
* @param {Object} options Configuration options.
|
|
231
|
+
* @param {Function} transformer Optional transform function.
|
|
232
|
+
*/
|
|
233
|
+
const extractMarkersFromQuery = (query, options, transformer) => {
|
|
234
|
+
// Return an empty list if no markers have been provided
|
|
235
|
+
if (!query.marker) {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const markers = [];
|
|
240
|
+
|
|
241
|
+
// Check if multiple markers have been provided and mimic a list if it's a
|
|
242
|
+
// single maker.
|
|
243
|
+
const providedMarkers = Array.isArray(query.marker) ?
|
|
244
|
+
query.marker : [query.marker];
|
|
245
|
+
|
|
246
|
+
// Iterate through provided markers which can have one of the following
|
|
247
|
+
// formats
|
|
248
|
+
// [location]|[pathToFileTelativeToConfiguredIconPath]
|
|
249
|
+
// [location]|[pathToFile...]|[option]|[option]|...
|
|
250
|
+
for (const providedMarker of providedMarkers) {
|
|
251
|
+
const markerParts = providedMarker.split('|');
|
|
252
|
+
// Ensure we got at least a location and an icon uri
|
|
253
|
+
if (markerParts.length < 2) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const locationParts = markerParts[0].split(',');
|
|
258
|
+
// Ensure the locationParts contains two items
|
|
259
|
+
if (locationParts.length !== 2) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let iconURI = markerParts[1];
|
|
264
|
+
// Check if icon is served via http otherwise marker icons are expected to
|
|
265
|
+
// be provided as filepaths relative to configured icon path
|
|
266
|
+
if (!(iconURI.startsWith('http://') || iconURI.startsWith('https://'))) {
|
|
267
|
+
// Sanitize URI with sanitize-filename
|
|
268
|
+
// https://www.npmjs.com/package/sanitize-filename#details
|
|
269
|
+
iconURI = sanitize(iconURI)
|
|
270
|
+
|
|
271
|
+
// If the selected icon is not part of available icons skip it
|
|
272
|
+
if (!options.paths.availableIcons.includes(iconURI)) {
|
|
273
|
+
continue;
|
|
110
274
|
}
|
|
111
|
-
|
|
275
|
+
|
|
276
|
+
iconURI = path.resolve(options.paths.icons, iconURI);
|
|
277
|
+
|
|
278
|
+
// When we encounter a remote icon check if the configuration explicitly allows them.
|
|
279
|
+
} else if (options.allowRemoteMarkerIcons !== true) {
|
|
280
|
+
continue;
|
|
112
281
|
}
|
|
282
|
+
|
|
283
|
+
// Ensure marker location could be parsed
|
|
284
|
+
const location = parseCoordinates(locationParts, query, transformer);
|
|
285
|
+
if (location === null) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const marker = {};
|
|
290
|
+
|
|
291
|
+
marker.location = location;
|
|
292
|
+
marker.icon = iconURI;
|
|
293
|
+
|
|
294
|
+
// Check if options have been provided
|
|
295
|
+
if (markerParts.length > 2) {
|
|
296
|
+
parseMarkerOptions(markerParts.slice(2), marker);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Add marker to list
|
|
300
|
+
markers.push(marker);
|
|
301
|
+
|
|
113
302
|
}
|
|
114
|
-
return
|
|
303
|
+
return markers;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Transforms coordinates to pixels.
|
|
308
|
+
* @param {List[Number]} ll Longitude/Latitude coordinate pair.
|
|
309
|
+
* @param {Number} zoom Map zoom level.
|
|
310
|
+
*/
|
|
311
|
+
const precisePx = (ll, zoom) => {
|
|
312
|
+
const px = mercator.px(ll, 20);
|
|
313
|
+
const scale = Math.pow(2, zoom - 20);
|
|
314
|
+
return [px[0] * scale, px[1] * scale];
|
|
115
315
|
};
|
|
116
316
|
|
|
117
|
-
|
|
118
|
-
|
|
317
|
+
/**
|
|
318
|
+
* Draws a marker in cavans context.
|
|
319
|
+
* @param {Object} ctx Canvas context object.
|
|
320
|
+
* @param {Object} marker Marker object parsed by extractMarkersFromQuery.
|
|
321
|
+
* @param {Number} z Map zoom level.
|
|
322
|
+
*/
|
|
323
|
+
const drawMarker = (ctx, marker, z) => {
|
|
324
|
+
return new Promise(resolve => {
|
|
325
|
+
const img = new Image();
|
|
326
|
+
const pixelCoords = precisePx(marker.location, z);
|
|
327
|
+
|
|
328
|
+
const getMarkerCoordinates = (imageWidth, imageHeight, scale) => {
|
|
329
|
+
// Images are placed with their top-left corner at the provided location
|
|
330
|
+
// within the canvas but we expect icons to be centered and above it.
|
|
331
|
+
|
|
332
|
+
// Substract half of the images width from the x-coordinate to center
|
|
333
|
+
// the image in relation to the provided location
|
|
334
|
+
let xCoordinate = pixelCoords[0] - imageWidth / 2;
|
|
335
|
+
// Substract the images height from the y-coordinate to place it above
|
|
336
|
+
// the provided location
|
|
337
|
+
let yCoordinate = pixelCoords[1] - imageHeight;
|
|
338
|
+
|
|
339
|
+
// Since image placement is dependent on the size offsets have to be
|
|
340
|
+
// scaled as well. Additionally offsets are provided as either positive or
|
|
341
|
+
// negative values so we always add them
|
|
342
|
+
if (marker.offsetX) {
|
|
343
|
+
xCoordinate = xCoordinate + (marker.offsetX * scale);
|
|
344
|
+
}
|
|
345
|
+
if (marker.offsetY) {
|
|
346
|
+
yCoordinate = yCoordinate + (marker.offsetY * scale);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
'x': xCoordinate,
|
|
351
|
+
'y': yCoordinate
|
|
352
|
+
};
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const drawOnCanvas = () => {
|
|
356
|
+
// Check if the images should be resized before beeing drawn
|
|
357
|
+
const defaultScale = 1;
|
|
358
|
+
const scale = marker.scale ? marker.scale : defaultScale;
|
|
359
|
+
|
|
360
|
+
// Calculate scaled image sizes
|
|
361
|
+
const imageWidth = img.width * scale;
|
|
362
|
+
const imageHeight = img.height * scale;
|
|
363
|
+
|
|
364
|
+
// Pass the desired sizes to get correlating coordinates
|
|
365
|
+
const coords = getMarkerCoordinates(imageWidth, imageHeight, scale);
|
|
366
|
+
|
|
367
|
+
// Draw the image on canvas
|
|
368
|
+
if (scale != defaultScale) {
|
|
369
|
+
ctx.drawImage(img, coords.x, coords.y, imageWidth, imageHeight);
|
|
370
|
+
} else {
|
|
371
|
+
ctx.drawImage(img, coords.x, coords.y);
|
|
372
|
+
}
|
|
373
|
+
// Resolve the promise when image has been drawn
|
|
374
|
+
resolve();
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
img.onload = drawOnCanvas;
|
|
378
|
+
img.onerror = err => { throw err };
|
|
379
|
+
img.src = marker.icon;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Draws a list of markers onto a canvas.
|
|
385
|
+
* Wraps drawing of markers into list of promises and awaits them.
|
|
386
|
+
* It's required because images are expected to load asynchronous in canvas js
|
|
387
|
+
* even when provided from a local disk.
|
|
388
|
+
* @param {Object} ctx Canvas context object.
|
|
389
|
+
* @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery.
|
|
390
|
+
* @param {Number} z Map zoom level.
|
|
391
|
+
*/
|
|
392
|
+
const drawMarkers = async (ctx, markers, z) => {
|
|
393
|
+
const markerPromises = [];
|
|
394
|
+
|
|
395
|
+
for (const marker of markers) {
|
|
396
|
+
// Begin drawing marker
|
|
397
|
+
markerPromises.push(drawMarker(ctx, marker, z));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Await marker drawings before continuing
|
|
401
|
+
await Promise.all(markerPromises);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Draws a list of coordinates onto a canvas and styles the resulting path.
|
|
406
|
+
* @param {Object} ctx Canvas context object.
|
|
407
|
+
* @param {List[Number]} path List of coordinates.
|
|
408
|
+
* @param {Object} query Request query parameters.
|
|
409
|
+
* @param {Number} z Map zoom level.
|
|
410
|
+
*/
|
|
411
|
+
const drawPath = (ctx, path, query, z) => {
|
|
119
412
|
if (!path || path.length < 2) {
|
|
120
413
|
return null;
|
|
121
414
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
415
|
+
|
|
416
|
+
ctx.beginPath();
|
|
417
|
+
|
|
418
|
+
// Transform coordinates to pixel on canvas and draw lines between points
|
|
419
|
+
for (const pair of path) {
|
|
420
|
+
const px = precisePx(pair, z);
|
|
421
|
+
ctx.lineTo(px[0], px[1]);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check if first coordinate matches last coordinate
|
|
425
|
+
if (path[0][0] === path[path.length - 1][0] &&
|
|
426
|
+
path[0][1] === path[path.length - 1][1]) {
|
|
427
|
+
ctx.closePath();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Optionally fill drawn shape with a rgba color from query
|
|
431
|
+
if (query.fill !== undefined) {
|
|
432
|
+
ctx.fillStyle = query.fill;
|
|
433
|
+
ctx.fill();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Get line width from query and fall back to 1 if not provided
|
|
437
|
+
const lineWidth = query.width !== undefined ?
|
|
438
|
+
parseFloat(query.width) : 1;
|
|
439
|
+
|
|
440
|
+
// Ensure line width is valid
|
|
441
|
+
if (lineWidth > 0) {
|
|
442
|
+
// Get border width from query and fall back to 10% of line width
|
|
443
|
+
const borderWidth = query.borderwidth !== undefined ?
|
|
444
|
+
parseFloat(query.borderwidth) : lineWidth * 0.1;
|
|
445
|
+
|
|
446
|
+
// Set rendering style for the start and end points of the path
|
|
447
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
|
|
448
|
+
ctx.lineCap = query.linecap || 'butt';
|
|
449
|
+
|
|
450
|
+
// Set rendering style for overlapping segments of the path with differing directions
|
|
451
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
|
|
452
|
+
ctx.lineJoin = query.linejoin || 'miter';
|
|
453
|
+
|
|
454
|
+
// In order to simulate a border we draw the path two times with the first
|
|
455
|
+
// beeing the wider border part.
|
|
456
|
+
if (query.border !== undefined && borderWidth > 0) {
|
|
457
|
+
// We need to double the desired border width and add it to the line width
|
|
458
|
+
// in order to get the desired border on each side of the line.
|
|
459
|
+
ctx.lineWidth = lineWidth + (borderWidth * 2);
|
|
460
|
+
// Set border style as rgba
|
|
461
|
+
ctx.strokeStyle = query.border;
|
|
462
|
+
ctx.stroke();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
ctx.lineWidth = lineWidth;
|
|
466
|
+
ctx.strokeStyle = query.stroke || 'rgba(0,64,255,0.7)';
|
|
467
|
+
ctx.stroke();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const renderOverlay = async (z, x, y, bearing, pitch, w, h, scale, paths, markers, query) => {
|
|
472
|
+
if ((!paths || paths.length === 0) && (!markers || markers.length === 0)) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
127
475
|
|
|
128
476
|
const center = precisePx([x, y], z);
|
|
129
477
|
|
|
@@ -147,25 +495,15 @@ const renderOverlay = (z, x, y, bearing, pitch, w, h, scale,
|
|
|
147
495
|
// optimized path
|
|
148
496
|
ctx.translate(-center[0] + w / 2, -center[1] + h / 2);
|
|
149
497
|
}
|
|
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();
|
|
498
|
+
|
|
499
|
+
// Draw provided paths if any
|
|
500
|
+
for (const path of paths) {
|
|
501
|
+
drawPath(ctx, path, query, z);
|
|
167
502
|
}
|
|
168
503
|
|
|
504
|
+
// Await drawing of markers before rendering the canvas
|
|
505
|
+
await drawMarkers(ctx, markers, z);
|
|
506
|
+
|
|
169
507
|
return canvas.toBuffer();
|
|
170
508
|
};
|
|
171
509
|
|
|
@@ -396,7 +734,7 @@ export const serve_rendered = {
|
|
|
396
734
|
FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN,
|
|
397
735
|
FLOAT_PATTERN, FLOAT_PATTERN);
|
|
398
736
|
|
|
399
|
-
app.get(util.format(staticPattern, centerPattern), (req, res, next) => {
|
|
737
|
+
app.get(util.format(staticPattern, centerPattern), async (req, res, next) => {
|
|
400
738
|
const item = repo[req.params.id];
|
|
401
739
|
if (!item) {
|
|
402
740
|
return res.sendStatus(404);
|
|
@@ -425,13 +763,14 @@ export const serve_rendered = {
|
|
|
425
763
|
y = ll[1];
|
|
426
764
|
}
|
|
427
765
|
|
|
428
|
-
const
|
|
429
|
-
const
|
|
766
|
+
const paths = extractPathsFromQuery(req.query, transformer);
|
|
767
|
+
const markers = extractMarkersFromQuery(req.query, options, transformer);
|
|
768
|
+
const overlay = await renderOverlay(z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query);
|
|
430
769
|
|
|
431
770
|
return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format, res, next, overlay, 'static');
|
|
432
771
|
});
|
|
433
772
|
|
|
434
|
-
const serveBounds = (req, res, next) => {
|
|
773
|
+
const serveBounds = async (req, res, next) => {
|
|
435
774
|
const item = repo[req.params.id];
|
|
436
775
|
if (!item) {
|
|
437
776
|
return res.sendStatus(404);
|
|
@@ -464,9 +803,9 @@ export const serve_rendered = {
|
|
|
464
803
|
const bearing = 0;
|
|
465
804
|
const pitch = 0;
|
|
466
805
|
|
|
467
|
-
const
|
|
468
|
-
const
|
|
469
|
-
|
|
806
|
+
const paths = extractPathsFromQuery(req.query, transformer);
|
|
807
|
+
const markers = extractMarkersFromQuery(req.query, options, transformer);
|
|
808
|
+
const overlay = await renderOverlay(z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query);
|
|
470
809
|
return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format, res, next, overlay, 'static');
|
|
471
810
|
};
|
|
472
811
|
|
|
@@ -500,7 +839,7 @@ export const serve_rendered = {
|
|
|
500
839
|
|
|
501
840
|
const autoPattern = 'auto';
|
|
502
841
|
|
|
503
|
-
app.get(util.format(staticPattern, autoPattern), (req, res, next) => {
|
|
842
|
+
app.get(util.format(staticPattern, autoPattern), async (req, res, next) => {
|
|
504
843
|
const item = repo[req.params.id];
|
|
505
844
|
if (!item) {
|
|
506
845
|
return res.sendStatus(404);
|
|
@@ -516,13 +855,25 @@ export const serve_rendered = {
|
|
|
516
855
|
const transformer = raw ?
|
|
517
856
|
mercator.inverse.bind(mercator) : item.dataProjWGStoInternalWGS;
|
|
518
857
|
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
858
|
+
const paths = extractPathsFromQuery(req.query, transformer);
|
|
859
|
+
const markers = extractMarkersFromQuery(req.query, options, transformer);
|
|
860
|
+
|
|
861
|
+
// Extract coordinates from markers
|
|
862
|
+
const markerCoordinates = [];
|
|
863
|
+
for (const marker of markers) {
|
|
864
|
+
markerCoordinates.push(marker.location);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Create array with coordinates from markers and path
|
|
868
|
+
const coords = new Array().concat(paths.flat()).concat(markerCoordinates);
|
|
869
|
+
|
|
870
|
+
// Check if we have at least one coordinate to calculate a bounding box
|
|
871
|
+
if (coords.length < 1) {
|
|
872
|
+
return res.status(400).send('No coordinates provided');
|
|
522
873
|
}
|
|
523
874
|
|
|
524
875
|
const bbox = [Infinity, Infinity, -Infinity, -Infinity];
|
|
525
|
-
for (const pair of
|
|
876
|
+
for (const pair of coords) {
|
|
526
877
|
bbox[0] = Math.min(bbox[0], pair[0]);
|
|
527
878
|
bbox[1] = Math.min(bbox[1], pair[1]);
|
|
528
879
|
bbox[2] = Math.max(bbox[2], pair[0]);
|
|
@@ -534,11 +885,17 @@ export const serve_rendered = {
|
|
|
534
885
|
[(bbox_[0] + bbox_[2]) / 2, (bbox_[1] + bbox_[3]) / 2]
|
|
535
886
|
);
|
|
536
887
|
|
|
537
|
-
|
|
888
|
+
// Calculate zoom level
|
|
889
|
+
const maxZoom = parseFloat(req.query.maxzoom);
|
|
890
|
+
let z = calcZForBBox(bbox, w, h, req.query);
|
|
891
|
+
if (maxZoom > 0) {
|
|
892
|
+
z = Math.min(z, maxZoom);
|
|
893
|
+
}
|
|
894
|
+
|
|
538
895
|
const x = center[0];
|
|
539
896
|
const y = center[1];
|
|
540
897
|
|
|
541
|
-
const overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale,
|
|
898
|
+
const overlay = await renderOverlay(z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query);
|
|
542
899
|
|
|
543
900
|
return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format, res, next, overlay, 'static');
|
|
544
901
|
});
|
package/src/server.js
CHANGED
|
@@ -30,7 +30,7 @@ const packageJson = JSON.parse(fs.readFileSync(__dirname + '/../package.json', '
|
|
|
30
30
|
const isLight = packageJson.name.slice(-6) === '-light';
|
|
31
31
|
const serve_rendered = (await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)).serve_rendered;
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
function start(opts) {
|
|
34
34
|
console.log('Starting server');
|
|
35
35
|
|
|
36
36
|
const app = express().disable('x-powered-by');
|
|
@@ -79,6 +79,7 @@ export function server(opts) {
|
|
|
79
79
|
paths.fonts = path.resolve(paths.root, paths.fonts || '');
|
|
80
80
|
paths.sprites = path.resolve(paths.root, paths.sprites || '');
|
|
81
81
|
paths.mbtiles = path.resolve(paths.root, paths.mbtiles || '');
|
|
82
|
+
paths.icons = path.resolve(paths.root, paths.icons || '');
|
|
82
83
|
|
|
83
84
|
const startupPromises = [];
|
|
84
85
|
|
|
@@ -92,6 +93,36 @@ export function server(opts) {
|
|
|
92
93
|
checkPath('fonts');
|
|
93
94
|
checkPath('sprites');
|
|
94
95
|
checkPath('mbtiles');
|
|
96
|
+
checkPath('icons');
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Recursively get all files within a directory.
|
|
100
|
+
* Inspired by https://stackoverflow.com/a/45130990/10133863
|
|
101
|
+
* @param {String} directory Absolute path to a directory to get files from.
|
|
102
|
+
*/
|
|
103
|
+
const getFiles = async (directory) => {
|
|
104
|
+
// Fetch all entries of the directory and attach type information
|
|
105
|
+
const dirEntries = await fs.promises.readdir(directory, { withFileTypes: true });
|
|
106
|
+
|
|
107
|
+
// Iterate through entries and return the relative file-path to the icon directory if it is not a directory
|
|
108
|
+
// otherwise initiate a recursive call
|
|
109
|
+
const files = await Promise.all(dirEntries.map((dirEntry) => {
|
|
110
|
+
const entryPath = path.resolve(directory, dirEntry.name);
|
|
111
|
+
return dirEntry.isDirectory() ?
|
|
112
|
+
getFiles(entryPath) : entryPath.replace(paths.icons + path.sep, "");
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
// Flatten the list of files to a single array
|
|
116
|
+
return files.flat();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Load all available icons into a settings object
|
|
120
|
+
startupPromises.push(new Promise(resolve => {
|
|
121
|
+
getFiles(paths.icons).then((files) => {
|
|
122
|
+
paths.availableIcons = files;
|
|
123
|
+
resolve();
|
|
124
|
+
});
|
|
125
|
+
}));
|
|
95
126
|
|
|
96
127
|
if (options.dataDecorator) {
|
|
97
128
|
try {
|
|
@@ -420,7 +451,12 @@ export function server(opts) {
|
|
|
420
451
|
}
|
|
421
452
|
wmts.id = id;
|
|
422
453
|
wmts.name = (serving.styles[id] || serving.rendered[id]).name;
|
|
423
|
-
|
|
454
|
+
if (opts.publicUrl) {
|
|
455
|
+
wmts.baseUrl = opts.publicUrl;
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
wmts.baseUrl = `${req.get('X-Forwarded-Protocol') ? req.get('X-Forwarded-Protocol') : req.protocol}://${req.get('host')}/`;
|
|
459
|
+
}
|
|
424
460
|
return wmts;
|
|
425
461
|
});
|
|
426
462
|
|
|
@@ -466,7 +502,7 @@ export function server(opts) {
|
|
|
466
502
|
};
|
|
467
503
|
}
|
|
468
504
|
|
|
469
|
-
export
|
|
505
|
+
export function server(opts) {
|
|
470
506
|
const running = start(opts);
|
|
471
507
|
|
|
472
508
|
running.startupPromise.catch((err) => {
|
|
@@ -482,10 +518,6 @@ export const exports = (opts) => {
|
|
|
482
518
|
console.log('Stopping server and reloading config');
|
|
483
519
|
|
|
484
520
|
running.server.shutdown(() => {
|
|
485
|
-
for (const key in require.cache) {
|
|
486
|
-
delete require.cache[key];
|
|
487
|
-
}
|
|
488
|
-
|
|
489
521
|
const restarted = start(opts);
|
|
490
522
|
running.server = restarted.server;
|
|
491
523
|
running.app = restarted.app;
|
package/test/static.js
CHANGED
|
@@ -95,7 +95,7 @@ describe('Static endpoints', function() {
|
|
|
95
95
|
|
|
96
96
|
describe('invalid requests return 4xx', function() {
|
|
97
97
|
testStatic(prefix, 'auto/256x256', 'png', 400);
|
|
98
|
-
testStatic(prefix, 'auto/256x256', 'png', 400, undefined, undefined, '?path=
|
|
98
|
+
testStatic(prefix, 'auto/256x256', 'png', 400, undefined, undefined, '?path=invalid');
|
|
99
99
|
testStatic(prefix, 'auto/2560x2560', 'png', 400, undefined, undefined, '?path=10,10|20,20');
|
|
100
100
|
});
|
|
101
101
|
});
|