tileserver-gl-light 5.0.0 → 5.1.0-pre.1
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/.github/workflows/release.yml +69 -12
- package/CHANGELOG.md +13 -0
- package/Dockerfile +1 -1
- package/PUBLISHING.md +17 -11
- package/changelog_for_version.md +5 -0
- package/docs/config.rst +21 -3
- package/docs/endpoints.rst +8 -0
- package/package.json +5 -3
- package/public/resources/elevation-control.js +51 -0
- package/public/resources/index.css +1 -1
- package/public/templates/data.tmpl +121 -42
- package/public/templates/index.tmpl +18 -7
- package/src/main.js +6 -0
- package/src/serve_data.js +335 -140
- package/src/serve_font.js +76 -25
- package/src/serve_light.js +2 -2
- package/src/serve_rendered.js +569 -411
- package/src/serve_style.js +181 -56
- package/src/server.js +189 -92
- package/src/utils.js +251 -69
- package/test/setup.js +2 -2
- package/test/static.js +2 -2
- package/test/tiles_rendered.js +7 -7
package/src/serve_data.js
CHANGED
|
@@ -7,162 +7,340 @@ import clone from 'clone';
|
|
|
7
7
|
import express from 'express';
|
|
8
8
|
import Pbf from 'pbf';
|
|
9
9
|
import { VectorTile } from '@mapbox/vector-tile';
|
|
10
|
+
import SphericalMercator from '@mapbox/sphericalmercator';
|
|
11
|
+
import { Image, createCanvas } from 'canvas';
|
|
12
|
+
import sharp from 'sharp';
|
|
10
13
|
|
|
11
|
-
import { fixTileJSONCenter, getTileUrls, isValidHttpUrl } from './utils.js';
|
|
12
14
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
fixTileJSONCenter,
|
|
16
|
+
getTileUrls,
|
|
17
|
+
isValidHttpUrl,
|
|
18
|
+
fetchTileData,
|
|
19
|
+
} from './utils.js';
|
|
20
|
+
import { getPMtilesInfo, openPMtiles } from './pmtiles_adapter.js';
|
|
17
21
|
import { gunzipP, gzipP } from './promises.js';
|
|
18
22
|
import { openMbTilesWrapper } from './mbtiles_wrapper.js';
|
|
19
23
|
|
|
20
24
|
export const serve_data = {
|
|
21
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Initializes the serve_data module.
|
|
27
|
+
* @param {object} options Configuration options.
|
|
28
|
+
* @param {object} repo Repository object.
|
|
29
|
+
* @param {object} programOpts - An object containing the program options
|
|
30
|
+
* @returns {express.Application} The initialized Express application.
|
|
31
|
+
*/
|
|
32
|
+
init: function (options, repo, programOpts) {
|
|
33
|
+
const { verbose } = programOpts;
|
|
22
34
|
const app = express().disable('x-powered-by');
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Handles requests for tile data, responding with the tile image.
|
|
38
|
+
* @param {object} req - Express request object.
|
|
39
|
+
* @param {object} res - Express response object.
|
|
40
|
+
* @param {string} req.params.id - ID of the tile.
|
|
41
|
+
* @param {string} req.params.z - Z coordinate of the tile.
|
|
42
|
+
* @param {string} req.params.x - X coordinate of the tile.
|
|
43
|
+
* @param {string} req.params.y - Y coordinate of the tile.
|
|
44
|
+
* @param {string} req.params.format - Format of the tile.
|
|
45
|
+
* @returns {Promise<void>}
|
|
46
|
+
*/
|
|
47
|
+
app.get('/:id/:z/:x/:y.:format', async (req, res) => {
|
|
48
|
+
if (verbose) {
|
|
49
|
+
console.log(
|
|
50
|
+
`Handling tile request for: /data/%s/%s/%s/%s.%s`,
|
|
51
|
+
String(req.params.id).replace(/\n|\r/g, ''),
|
|
52
|
+
String(req.params.z).replace(/\n|\r/g, ''),
|
|
53
|
+
String(req.params.x).replace(/\n|\r/g, ''),
|
|
54
|
+
String(req.params.y).replace(/\n|\r/g, ''),
|
|
55
|
+
String(req.params.format).replace(/\n|\r/g, ''),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const item = repo[req.params.id];
|
|
59
|
+
if (!item) {
|
|
60
|
+
return res.sendStatus(404);
|
|
61
|
+
}
|
|
62
|
+
const tileJSONFormat = item.tileJSON.format;
|
|
63
|
+
const z = parseInt(req.params.z, 10);
|
|
64
|
+
const x = parseInt(req.params.x, 10);
|
|
65
|
+
const y = parseInt(req.params.y, 10);
|
|
66
|
+
if (isNaN(z) || isNaN(x) || isNaN(y)) {
|
|
67
|
+
return res.status(404).send('Invalid Tile');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let format = req.params.format;
|
|
71
|
+
if (format === options.pbfAlias) {
|
|
72
|
+
format = 'pbf';
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
format !== tileJSONFormat &&
|
|
76
|
+
!(format === 'geojson' && tileJSONFormat === 'pbf')
|
|
77
|
+
) {
|
|
78
|
+
return res.status(404).send('Invalid format');
|
|
79
|
+
}
|
|
80
|
+
if (
|
|
81
|
+
z < item.tileJSON.minzoom ||
|
|
82
|
+
x < 0 ||
|
|
83
|
+
y < 0 ||
|
|
84
|
+
z > item.tileJSON.maxzoom ||
|
|
85
|
+
x >= Math.pow(2, z) ||
|
|
86
|
+
y >= Math.pow(2, z)
|
|
87
|
+
) {
|
|
88
|
+
return res.status(404).send('Out of bounds');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const fetchTile = await fetchTileData(
|
|
92
|
+
item.source,
|
|
93
|
+
item.sourceType,
|
|
94
|
+
z,
|
|
95
|
+
x,
|
|
96
|
+
y,
|
|
97
|
+
);
|
|
98
|
+
if (fetchTile == null) return res.status(204).send();
|
|
99
|
+
|
|
100
|
+
let data = fetchTile.data;
|
|
101
|
+
let headers = fetchTile.headers;
|
|
102
|
+
let isGzipped = data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
|
|
103
|
+
|
|
104
|
+
if (tileJSONFormat === 'pbf') {
|
|
105
|
+
if (options.dataDecoratorFunc) {
|
|
106
|
+
if (isGzipped) {
|
|
107
|
+
data = await gunzipP(data);
|
|
108
|
+
isGzipped = false;
|
|
109
|
+
}
|
|
110
|
+
data = options.dataDecoratorFunc(
|
|
111
|
+
req.params.id,
|
|
112
|
+
'data',
|
|
113
|
+
data,
|
|
114
|
+
z,
|
|
115
|
+
x,
|
|
116
|
+
y,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (format === 'pbf') {
|
|
122
|
+
headers['Content-Type'] = 'application/x-protobuf';
|
|
123
|
+
} else if (format === 'geojson') {
|
|
124
|
+
headers['Content-Type'] = 'application/json';
|
|
125
|
+
const tile = new VectorTile(new Pbf(data));
|
|
126
|
+
const geojson = {
|
|
127
|
+
type: 'FeatureCollection',
|
|
128
|
+
features: [],
|
|
129
|
+
};
|
|
130
|
+
for (const layerName in tile.layers) {
|
|
131
|
+
const layer = tile.layers[layerName];
|
|
132
|
+
for (let i = 0; i < layer.length; i++) {
|
|
133
|
+
const feature = layer.feature(i);
|
|
134
|
+
const featureGeoJSON = feature.toGeoJSON(x, y, z);
|
|
135
|
+
featureGeoJSON.properties.layer = layerName;
|
|
136
|
+
geojson.features.push(featureGeoJSON);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
data = JSON.stringify(geojson);
|
|
140
|
+
}
|
|
141
|
+
if (headers) {
|
|
142
|
+
delete headers['ETag'];
|
|
143
|
+
}
|
|
144
|
+
headers['Content-Encoding'] = 'gzip';
|
|
145
|
+
res.set(headers);
|
|
146
|
+
|
|
147
|
+
if (!isGzipped) {
|
|
148
|
+
data = await gzipP(data);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return res.status(200).send(data);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Handles requests for elevation data.
|
|
156
|
+
* @param {object} req - Express request object.
|
|
157
|
+
* @param {object} res - Express response object.
|
|
158
|
+
* @param {string} req.params.id - ID of the elevation data.
|
|
159
|
+
* @param {string} req.params.z - Z coordinate of the tile.
|
|
160
|
+
* @param {string} req.params.x - X coordinate of the tile (either integer or float).
|
|
161
|
+
* @param {string} req.params.y - Y coordinate of the tile (either integer or float).
|
|
162
|
+
* @returns {Promise<void>}
|
|
163
|
+
*/
|
|
164
|
+
app.get('/:id/elevation/:z/:x/:y', async (req, res, next) => {
|
|
165
|
+
try {
|
|
166
|
+
if (verbose) {
|
|
167
|
+
console.log(
|
|
168
|
+
`Handling elevation request for: /data/%s/elevation/%s/%s/%s`,
|
|
169
|
+
String(req.params.id).replace(/\n|\r/g, ''),
|
|
170
|
+
String(req.params.z).replace(/\n|\r/g, ''),
|
|
171
|
+
String(req.params.x).replace(/\n|\r/g, ''),
|
|
172
|
+
String(req.params.y).replace(/\n|\r/g, ''),
|
|
173
|
+
);
|
|
30
174
|
}
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
175
|
+
const item = repo?.[req.params.id];
|
|
176
|
+
if (!item) return res.sendStatus(404);
|
|
177
|
+
if (!item.source) return res.status(404).send('Missing source');
|
|
178
|
+
if (!item.tileJSON) return res.status(404).send('Missing tileJSON');
|
|
179
|
+
if (!item.sourceType) return res.status(404).send('Missing sourceType');
|
|
180
|
+
const { source, tileJSON, sourceType } = item;
|
|
181
|
+
if (sourceType !== 'pmtiles' && sourceType !== 'mbtiles') {
|
|
182
|
+
return res
|
|
183
|
+
.status(400)
|
|
184
|
+
.send('Invalid sourceType. Must be pmtiles or mbtiles.');
|
|
38
185
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
) {
|
|
43
|
-
return res
|
|
186
|
+
const encoding = tileJSON?.encoding;
|
|
187
|
+
if (encoding == null) {
|
|
188
|
+
return res.status(400).send('Missing tileJSON.encoding');
|
|
189
|
+
} else if (encoding !== 'terrarium' && encoding !== 'mapbox') {
|
|
190
|
+
return res
|
|
191
|
+
.status(400)
|
|
192
|
+
.send('Invalid encoding. Must be terrarium or mapbox.');
|
|
44
193
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
z > item.tileJSON.maxzoom ||
|
|
51
|
-
x >= Math.pow(2, z) ||
|
|
52
|
-
y >= Math.pow(2, z)
|
|
53
|
-
) {
|
|
54
|
-
return res.status(404).send('Out of bounds');
|
|
194
|
+
const format = tileJSON?.format;
|
|
195
|
+
if (format == null) {
|
|
196
|
+
return res.status(400).send('Missing tileJSON.format');
|
|
197
|
+
} else if (format !== 'webp' && format !== 'png') {
|
|
198
|
+
return res.status(400).send('Invalid format. Must be webp or png.');
|
|
55
199
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (format === 'pbf') {
|
|
69
|
-
headers['Content-Type'] = 'application/x-protobuf';
|
|
70
|
-
} else if (format === 'geojson') {
|
|
71
|
-
headers['Content-Type'] = 'application/json';
|
|
72
|
-
const tile = new VectorTile(new Pbf(data));
|
|
73
|
-
const geojson = {
|
|
74
|
-
type: 'FeatureCollection',
|
|
75
|
-
features: [],
|
|
76
|
-
};
|
|
77
|
-
for (const layerName in tile.layers) {
|
|
78
|
-
const layer = tile.layers[layerName];
|
|
79
|
-
for (let i = 0; i < layer.length; i++) {
|
|
80
|
-
const feature = layer.feature(i);
|
|
81
|
-
const featureGeoJSON = feature.toGeoJSON(x, y, z);
|
|
82
|
-
featureGeoJSON.properties.layer = layerName;
|
|
83
|
-
geojson.features.push(featureGeoJSON);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
data = JSON.stringify(geojson);
|
|
87
|
-
}
|
|
88
|
-
delete headers['ETag']; // do not trust the tile ETag -- regenerate
|
|
89
|
-
headers['Content-Encoding'] = 'gzip';
|
|
90
|
-
res.set(headers);
|
|
91
|
-
|
|
92
|
-
data = await gzipP(data);
|
|
200
|
+
const z = parseInt(req.params.z, 10);
|
|
201
|
+
const x = parseFloat(req.params.x);
|
|
202
|
+
const y = parseFloat(req.params.y);
|
|
203
|
+
if (tileJSON.minzoom == null || tileJSON.maxzoom == null) {
|
|
204
|
+
return res.status(404).send(JSON.stringify(tileJSON));
|
|
205
|
+
}
|
|
206
|
+
const TILE_SIZE = tileJSON.tileSize || 512;
|
|
207
|
+
let bbox;
|
|
208
|
+
let xy;
|
|
209
|
+
var zoom = z;
|
|
93
210
|
|
|
94
|
-
|
|
211
|
+
if (Number.isInteger(x) && Number.isInteger(y)) {
|
|
212
|
+
const intX = parseInt(req.params.x, 10);
|
|
213
|
+
const intY = parseInt(req.params.y, 10);
|
|
214
|
+
if (
|
|
215
|
+
zoom < tileJSON.minzoom ||
|
|
216
|
+
zoom > tileJSON.maxzoom ||
|
|
217
|
+
intX < 0 ||
|
|
218
|
+
intY < 0 ||
|
|
219
|
+
intX >= Math.pow(2, zoom) ||
|
|
220
|
+
intY >= Math.pow(2, zoom)
|
|
221
|
+
) {
|
|
222
|
+
return res.status(404).send('Out of bounds');
|
|
95
223
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
224
|
+
xy = [intX, intY];
|
|
225
|
+
bbox = new SphericalMercator().bbox(intX, intY, zoom);
|
|
226
|
+
} else {
|
|
227
|
+
//no zoom limit with coordinates
|
|
228
|
+
if (zoom < tileJSON.minzoom) {
|
|
229
|
+
zoom = tileJSON.minzoom;
|
|
230
|
+
}
|
|
231
|
+
if (zoom > tileJSON.maxzoom) {
|
|
232
|
+
zoom = tileJSON.maxzoom;
|
|
233
|
+
}
|
|
234
|
+
bbox = [x, y, x + 0.1, y + 0.1];
|
|
235
|
+
const { minX, minY } = new SphericalMercator().xyz(bbox, zoom);
|
|
236
|
+
xy = [minX, minY];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const fetchTile = await fetchTileData(
|
|
240
|
+
source,
|
|
241
|
+
sourceType,
|
|
242
|
+
zoom,
|
|
243
|
+
xy[0],
|
|
244
|
+
xy[1],
|
|
245
|
+
);
|
|
246
|
+
if (fetchTile == null) return res.status(204).send();
|
|
247
|
+
|
|
248
|
+
let data = fetchTile.data;
|
|
249
|
+
const image = new Image();
|
|
250
|
+
await new Promise(async (resolve, reject) => {
|
|
251
|
+
image.onload = async () => {
|
|
252
|
+
const canvas = createCanvas(TILE_SIZE, TILE_SIZE);
|
|
253
|
+
const context = canvas.getContext('2d');
|
|
254
|
+
context.drawImage(image, 0, 0);
|
|
255
|
+
const long = bbox[0];
|
|
256
|
+
const lat = bbox[1];
|
|
257
|
+
|
|
258
|
+
// calculate pixel coordinate of tile,
|
|
259
|
+
// see https://developers.google.com/maps/documentation/javascript/examples/map-coordinates
|
|
260
|
+
let siny = Math.sin((lat * Math.PI) / 180);
|
|
261
|
+
// Truncating to 0.9999 effectively limits latitude to 89.189. This is
|
|
262
|
+
// about a third of a tile past the edge of the world tile.
|
|
263
|
+
siny = Math.min(Math.max(siny, -0.9999), 0.9999);
|
|
264
|
+
const xWorld = TILE_SIZE * (0.5 + long / 360);
|
|
265
|
+
const yWorld =
|
|
266
|
+
TILE_SIZE *
|
|
267
|
+
(0.5 - Math.log((1 + siny) / (1 - siny)) / (4 * Math.PI));
|
|
268
|
+
|
|
269
|
+
const scale = 1 << zoom;
|
|
270
|
+
|
|
271
|
+
const xTile = Math.floor((xWorld * scale) / TILE_SIZE);
|
|
272
|
+
const yTile = Math.floor((yWorld * scale) / TILE_SIZE);
|
|
273
|
+
|
|
274
|
+
const xPixel = Math.floor(xWorld * scale) - xTile * TILE_SIZE;
|
|
275
|
+
const yPixel = Math.floor(yWorld * scale) - yTile * TILE_SIZE;
|
|
276
|
+
if (
|
|
277
|
+
xPixel < 0 ||
|
|
278
|
+
yPixel < 0 ||
|
|
279
|
+
xPixel >= TILE_SIZE ||
|
|
280
|
+
yPixel >= TILE_SIZE
|
|
281
|
+
) {
|
|
282
|
+
return reject('Out of bounds Pixel');
|
|
283
|
+
}
|
|
284
|
+
const imgdata = context.getImageData(xPixel, yPixel, 1, 1);
|
|
285
|
+
const red = imgdata.data[0];
|
|
286
|
+
const green = imgdata.data[1];
|
|
287
|
+
const blue = imgdata.data[2];
|
|
288
|
+
let elevation;
|
|
289
|
+
if (encoding === 'mapbox') {
|
|
290
|
+
elevation = -10000 + (red * 256 * 256 + green * 256 + blue) * 0.1;
|
|
291
|
+
} else if (encoding === 'terrarium') {
|
|
292
|
+
elevation = red * 256 + green + blue / 256 - 32768;
|
|
108
293
|
} else {
|
|
109
|
-
|
|
110
|
-
return res.status(404).send('Not found');
|
|
111
|
-
} else {
|
|
112
|
-
if (tileJSONFormat === 'pbf') {
|
|
113
|
-
isGzipped =
|
|
114
|
-
data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
|
|
115
|
-
if (options.dataDecoratorFunc) {
|
|
116
|
-
if (isGzipped) {
|
|
117
|
-
data = await gunzipP(data);
|
|
118
|
-
isGzipped = false;
|
|
119
|
-
}
|
|
120
|
-
data = options.dataDecoratorFunc(id, 'data', data, z, x, y);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (format === 'pbf') {
|
|
124
|
-
headers['Content-Type'] = 'application/x-protobuf';
|
|
125
|
-
} else if (format === 'geojson') {
|
|
126
|
-
headers['Content-Type'] = 'application/json';
|
|
127
|
-
|
|
128
|
-
if (isGzipped) {
|
|
129
|
-
data = await gunzipP(data);
|
|
130
|
-
isGzipped = false;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const tile = new VectorTile(new Pbf(data));
|
|
134
|
-
const geojson = {
|
|
135
|
-
type: 'FeatureCollection',
|
|
136
|
-
features: [],
|
|
137
|
-
};
|
|
138
|
-
for (const layerName in tile.layers) {
|
|
139
|
-
const layer = tile.layers[layerName];
|
|
140
|
-
for (let i = 0; i < layer.length; i++) {
|
|
141
|
-
const feature = layer.feature(i);
|
|
142
|
-
const featureGeoJSON = feature.toGeoJSON(x, y, z);
|
|
143
|
-
featureGeoJSON.properties.layer = layerName;
|
|
144
|
-
geojson.features.push(featureGeoJSON);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
data = JSON.stringify(geojson);
|
|
148
|
-
}
|
|
149
|
-
delete headers['ETag']; // do not trust the tile ETag -- regenerate
|
|
150
|
-
headers['Content-Encoding'] = 'gzip';
|
|
151
|
-
res.set(headers);
|
|
152
|
-
|
|
153
|
-
if (!isGzipped) {
|
|
154
|
-
data = await gzipP(data);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return res.status(200).send(data);
|
|
158
|
-
}
|
|
294
|
+
elevation = 'invalid encoding';
|
|
159
295
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
296
|
+
resolve(
|
|
297
|
+
res.status(200).send({
|
|
298
|
+
z: zoom,
|
|
299
|
+
x: xy[0],
|
|
300
|
+
y: xy[1],
|
|
301
|
+
red,
|
|
302
|
+
green,
|
|
303
|
+
blue,
|
|
304
|
+
latitude: lat,
|
|
305
|
+
longitude: long,
|
|
306
|
+
elevation,
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
};
|
|
310
|
+
image.onerror = (err) => reject(err);
|
|
311
|
+
if (format === 'webp') {
|
|
312
|
+
try {
|
|
313
|
+
const img = await sharp(data).toFormat('png').toBuffer();
|
|
314
|
+
image.src = img;
|
|
315
|
+
} catch (err) {
|
|
316
|
+
reject(err);
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
image.src = data;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
} catch (err) {
|
|
323
|
+
return res
|
|
324
|
+
.status(500)
|
|
325
|
+
.header('Content-Type', 'text/plain')
|
|
326
|
+
.send(err.message);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
164
329
|
|
|
165
|
-
|
|
330
|
+
/**
|
|
331
|
+
* Handles requests for tilejson for the data tiles.
|
|
332
|
+
* @param {object} req - Express request object.
|
|
333
|
+
* @param {object} res - Express response object.
|
|
334
|
+
* @param {string} req.params.id - ID of the data source.
|
|
335
|
+
* @returns {Promise<void>}
|
|
336
|
+
*/
|
|
337
|
+
app.get('/:id.json', (req, res) => {
|
|
338
|
+
if (verbose) {
|
|
339
|
+
console.log(
|
|
340
|
+
`Handling tilejson request for: /data/%s.json`,
|
|
341
|
+
String(req.params.id).replace(/\n|\r/g, ''),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
166
344
|
const item = repo[req.params.id];
|
|
167
345
|
if (!item) {
|
|
168
346
|
return res.sendStatus(404);
|
|
@@ -185,7 +363,20 @@ export const serve_data = {
|
|
|
185
363
|
|
|
186
364
|
return app;
|
|
187
365
|
},
|
|
188
|
-
|
|
366
|
+
/**
|
|
367
|
+
* Adds a new data source to the repository.
|
|
368
|
+
* @param {object} options Configuration options.
|
|
369
|
+
* @param {object} repo Repository object.
|
|
370
|
+
* @param {object} params Parameters object.
|
|
371
|
+
* @param {string} id ID of the data source.
|
|
372
|
+
* @param {object} programOpts - An object containing the program options
|
|
373
|
+
* @param {string} programOpts.publicUrl Public URL for the data.
|
|
374
|
+
* @param {boolean} programOpts.verbose Whether verbose logging should be used.
|
|
375
|
+
* @param {Function} dataResolver Function to resolve data.
|
|
376
|
+
* @returns {Promise<void>}
|
|
377
|
+
*/
|
|
378
|
+
add: async function (options, repo, params, id, programOpts) {
|
|
379
|
+
const { publicUrl } = programOpts;
|
|
189
380
|
let inputFile;
|
|
190
381
|
let inputType;
|
|
191
382
|
if (params.pmtiles) {
|
|
@@ -225,6 +416,8 @@ export const serve_data = {
|
|
|
225
416
|
sourceType = 'pmtiles';
|
|
226
417
|
const metadata = await getPMtilesInfo(source);
|
|
227
418
|
|
|
419
|
+
tileJSON['encoding'] = params['encoding'];
|
|
420
|
+
tileJSON['tileSize'] = params['tileSize'];
|
|
228
421
|
tileJSON['name'] = id;
|
|
229
422
|
tileJSON['format'] = 'pbf';
|
|
230
423
|
Object.assign(tileJSON, metadata);
|
|
@@ -245,6 +438,8 @@ export const serve_data = {
|
|
|
245
438
|
const mbw = await openMbTilesWrapper(inputFile);
|
|
246
439
|
const info = await mbw.getInfo();
|
|
247
440
|
source = mbw.getMbTiles();
|
|
441
|
+
tileJSON['encoding'] = params['encoding'];
|
|
442
|
+
tileJSON['tileSize'] = params['tileSize'];
|
|
248
443
|
tileJSON['name'] = id;
|
|
249
444
|
tileJSON['format'] = 'pbf';
|
|
250
445
|
|
package/src/serve_font.js
CHANGED
|
@@ -4,7 +4,15 @@ import express from 'express';
|
|
|
4
4
|
|
|
5
5
|
import { getFontsPbf, listFonts } from './utils.js';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Initializes and returns an Express app that serves font files.
|
|
9
|
+
* @param {object} options - Configuration options for the server.
|
|
10
|
+
* @param {object} allowedFonts - An object containing allowed fonts.
|
|
11
|
+
* @param {object} programOpts - An object containing the program options.
|
|
12
|
+
* @returns {Promise<express.Application>} - A promise that resolves to the Express app.
|
|
13
|
+
*/
|
|
14
|
+
export async function serve_font(options, allowedFonts, programOpts) {
|
|
15
|
+
const { verbose } = programOpts;
|
|
8
16
|
const app = express().disable('x-powered-by');
|
|
9
17
|
|
|
10
18
|
const lastModified = new Date().toUTCString();
|
|
@@ -13,31 +21,74 @@ export const serve_font = async (options, allowedFonts) => {
|
|
|
13
21
|
|
|
14
22
|
const existingFonts = {};
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Handles requests for a font file.
|
|
26
|
+
* @param {object} req - Express request object.
|
|
27
|
+
* @param {object} res - Express response object.
|
|
28
|
+
* @param {string} req.params.fontstack - Name of the font stack.
|
|
29
|
+
* @param {string} req.params.range - The range of the font (e.g. 0-255).
|
|
30
|
+
* @returns {Promise<void>}
|
|
31
|
+
*/
|
|
32
|
+
app.get('/fonts/:fontstack/:range.pbf', async (req, res) => {
|
|
33
|
+
const sRange = String(req.params.range).replace(/\n|\r/g, '');
|
|
34
|
+
const sFontStack = String(decodeURI(req.params.fontstack)).replace(
|
|
35
|
+
/\n|\r/g,
|
|
36
|
+
'',
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (verbose) {
|
|
40
|
+
console.log(
|
|
41
|
+
`Handling font request for: /fonts/%s/%s.pbf`,
|
|
42
|
+
sFontStack,
|
|
43
|
+
sRange,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const modifiedSince = req.get('if-modified-since');
|
|
48
|
+
const cc = req.get('cache-control');
|
|
49
|
+
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
|
|
50
|
+
if (
|
|
51
|
+
new Date(lastModified).getTime() === new Date(modifiedSince).getTime()
|
|
52
|
+
) {
|
|
53
|
+
return res.sendStatus(304);
|
|
36
54
|
}
|
|
37
|
-
}
|
|
38
|
-
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const concatenated = await getFontsPbf(
|
|
59
|
+
options.serveAllFonts ? null : allowedFonts,
|
|
60
|
+
fontPath,
|
|
61
|
+
sFontStack,
|
|
62
|
+
sRange,
|
|
63
|
+
existingFonts,
|
|
64
|
+
);
|
|
65
|
+
res.header('Content-type', 'application/x-protobuf');
|
|
66
|
+
res.header('Last-Modified', lastModified);
|
|
67
|
+
return res.send(concatenated);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(
|
|
70
|
+
`Error serving font: %s/%s.pbf, Error: %s`,
|
|
71
|
+
sFontStack,
|
|
72
|
+
sRange,
|
|
73
|
+
String(err),
|
|
74
|
+
);
|
|
75
|
+
return res
|
|
76
|
+
.status(400)
|
|
77
|
+
.header('Content-Type', 'text/plain')
|
|
78
|
+
.send('Error serving font');
|
|
79
|
+
}
|
|
80
|
+
});
|
|
39
81
|
|
|
40
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Handles requests for a list of all available fonts.
|
|
84
|
+
* @param {object} req - Express request object.
|
|
85
|
+
* @param {object} res - Express response object.
|
|
86
|
+
* @returns {void}
|
|
87
|
+
*/
|
|
88
|
+
app.get('/fonts.json', (req, res) => {
|
|
89
|
+
if (verbose) {
|
|
90
|
+
console.log('Handling list font request for /fonts.json');
|
|
91
|
+
}
|
|
41
92
|
res.header('Content-type', 'application/json');
|
|
42
93
|
return res.send(
|
|
43
94
|
Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort(),
|
|
@@ -47,4 +98,4 @@ export const serve_font = async (options, allowedFonts) => {
|
|
|
47
98
|
const fonts = await listFonts(options.paths.fonts);
|
|
48
99
|
Object.assign(existingFonts, fonts);
|
|
49
100
|
return app;
|
|
50
|
-
}
|
|
101
|
+
}
|
package/src/serve_light.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
5
|
export const serve_rendered = {
|
|
6
|
-
init: (options, repo) => {},
|
|
7
|
-
add: (options, repo, params, id,
|
|
6
|
+
init: (options, repo, programOpts) => {},
|
|
7
|
+
add: (options, repo, params, id, programOpts, dataResolver) => {},
|
|
8
8
|
remove: (repo, id) => {},
|
|
9
9
|
};
|