@basemaps/lambda-tiler 7.7.0 → 7.10.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/CHANGELOG.md +32 -0
- package/build/__tests__/config.data.js +3 -3
- package/build/__tests__/config.data.js.map +1 -1
- package/build/__tests__/tile.style.json.test.js +13 -12
- package/build/__tests__/tile.style.json.test.js.map +1 -1
- package/build/routes/__tests__/health.test.js +40 -20
- package/build/routes/__tests__/health.test.js.map +1 -1
- package/build/routes/__tests__/tile.style.json.test.js +81 -0
- package/build/routes/__tests__/tile.style.json.test.js.map +1 -1
- package/build/routes/__tests__/xyz.test.js +13 -0
- package/build/routes/__tests__/xyz.test.js.map +1 -1
- package/build/routes/health.d.ts +17 -0
- package/build/routes/health.js +119 -21
- package/build/routes/health.js.map +1 -1
- package/build/routes/tile.style.json.d.ts +36 -8
- package/build/routes/tile.style.json.js +144 -128
- package/build/routes/tile.style.json.js.map +1 -1
- package/build/routes/tile.xyz.raster.js.map +1 -1
- package/build/routes/tile.xyz.vector.js +9 -9
- package/build/routes/tile.xyz.vector.js.map +1 -1
- package/build/util/__test__/cache.test.d.ts +1 -0
- package/build/util/__test__/cache.test.js +29 -0
- package/build/util/__test__/cache.test.js.map +1 -0
- package/build/util/__test__/nztm.style.test.d.ts +1 -0
- package/build/util/__test__/nztm.style.test.js +87 -0
- package/build/util/__test__/nztm.style.test.js.map +1 -0
- package/build/util/nztm.style.d.ts +12 -0
- package/build/util/nztm.style.js +45 -0
- package/build/util/nztm.style.js.map +1 -0
- package/build/util/source.cache.d.ts +2 -8
- package/build/util/source.cache.js +6 -24
- package/build/util/source.cache.js.map +1 -1
- package/build/util/swapping.lru.d.ts +1 -0
- package/build/util/swapping.lru.js +4 -0
- package/build/util/swapping.lru.js.map +1 -1
- package/package.json +7 -6
- package/src/__tests__/config.data.ts +3 -3
- package/src/__tests__/tile.style.json.test.ts +16 -14
- package/src/routes/__tests__/health.test.ts +46 -22
- package/src/routes/__tests__/tile.style.json.test.ts +91 -0
- package/src/routes/__tests__/xyz.test.ts +18 -0
- package/src/routes/health.ts +129 -21
- package/src/routes/tile.style.json.ts +172 -149
- package/src/routes/tile.xyz.raster.ts +0 -1
- package/src/routes/tile.xyz.vector.ts +10 -6
- package/src/util/__test__/cache.test.ts +36 -0
- package/src/util/__test__/nztm.style.test.ts +100 -0
- package/src/util/nztm.style.ts +44 -0
- package/src/util/source.cache.ts +10 -20
- package/src/util/swapping.lru.ts +5 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,20 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BasemapsConfigProvider,
|
|
3
|
+
ConfigId,
|
|
4
|
+
ConfigPrefix,
|
|
5
|
+
ConfigTileSetRaster,
|
|
6
|
+
Layer,
|
|
7
|
+
Sources,
|
|
8
|
+
StyleJson,
|
|
9
|
+
TileSetType,
|
|
10
|
+
} from '@basemaps/config';
|
|
2
11
|
import { DefaultExaggeration } from '@basemaps/config/build/config/vector.style.js';
|
|
3
|
-
import { GoogleTms, TileMatrixSet, TileMatrixSets } from '@basemaps/geo';
|
|
12
|
+
import { GoogleTms, Nztm2000QuadTms, TileMatrixSet, TileMatrixSets } from '@basemaps/geo';
|
|
4
13
|
import { Env, toQueryString } from '@basemaps/shared';
|
|
5
14
|
import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
6
15
|
import { URL } from 'url';
|
|
7
16
|
|
|
8
17
|
import { ConfigLoader } from '../util/config.loader.js';
|
|
9
18
|
import { Etag } from '../util/etag.js';
|
|
19
|
+
import { convertStyleToNztmStyle } from '../util/nztm.style.js';
|
|
10
20
|
import { NotFound, NotModified } from '../util/response.js';
|
|
11
21
|
import { Validate } from '../util/validate.js';
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
|
-
* Convert relative
|
|
24
|
+
* Convert relative URL into a full hostname URL, converting {tileMatrix} into the provided tileMatrix
|
|
25
|
+
*
|
|
26
|
+
* Will also add query parameters of apiKey and configuration if provided
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* convertRelativeUrl("/v1/tiles/aerial/{tileMatrix}/{z}/{x}/{y}.webp", NZTM2000Quad)
|
|
31
|
+
* "https://basemaps.linz.govt.nz/v1/tiles/aerial/NZTM2000Quad/{z}/{x}/{y}.webp?api=c..."
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
15
34
|
* @param url possible url to update
|
|
16
35
|
* @param apiKey ApiKey to append with ?api= if required
|
|
17
|
-
* @
|
|
36
|
+
* @param tileMatrix replace {tileMatrix} with the tile matrix
|
|
37
|
+
*
|
|
38
|
+
* @returns Updated URL or empty string if url is empty
|
|
18
39
|
*/
|
|
19
40
|
export function convertRelativeUrl(
|
|
20
41
|
url?: string,
|
|
@@ -33,64 +54,82 @@ export function convertRelativeUrl(
|
|
|
33
54
|
}
|
|
34
55
|
|
|
35
56
|
/**
|
|
36
|
-
*
|
|
57
|
+
* Update the style JSON to have absolute urls to the current host and API Keys where required
|
|
58
|
+
*
|
|
37
59
|
* @param style style to update
|
|
60
|
+
* @param tileMatrix convert the tile matrix to the target tile matrix
|
|
38
61
|
* @param apiKey api key to inject
|
|
39
|
-
* @
|
|
62
|
+
* @param config optional configuration url to use
|
|
63
|
+
* @param layers replace the layers in the style json
|
|
64
|
+
* @returns new style JSON
|
|
40
65
|
*/
|
|
41
|
-
export function
|
|
42
|
-
style
|
|
43
|
-
tileMatrix: TileMatrixSet,
|
|
44
|
-
apiKey: string,
|
|
45
|
-
config: string | null,
|
|
46
|
-
layers?: Layer[],
|
|
47
|
-
): StyleJson {
|
|
48
|
-
const sources = JSON.parse(JSON.stringify(style.sources)) as Sources;
|
|
49
|
-
for (const [key, value] of Object.entries(sources)) {
|
|
66
|
+
export function setStyleUrls(style: StyleJson, tileMatrix: TileMatrixSet, apiKey: string, config: string | null): void {
|
|
67
|
+
for (const [key, value] of Object.entries(style.sources ?? {})) {
|
|
50
68
|
if (value.type === 'vector') {
|
|
51
|
-
if (tileMatrix !== GoogleTms) {
|
|
52
|
-
throw new LambdaHttpResponse(400, `TileMatrix is not supported for the vector source ${value.url}.`);
|
|
53
|
-
}
|
|
54
69
|
value.url = convertRelativeUrl(value.url, tileMatrix, apiKey, config);
|
|
55
70
|
} else if ((value.type === 'raster' || value.type === 'raster-dem') && Array.isArray(value.tiles)) {
|
|
56
71
|
for (let i = 0; i < value.tiles.length; i++) {
|
|
57
72
|
value.tiles[i] = convertRelativeUrl(value.tiles[i], tileMatrix, apiKey, config);
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
|
-
sources[key] = value;
|
|
75
|
+
style.sources[key] = value;
|
|
61
76
|
}
|
|
62
77
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
id: style.id,
|
|
66
|
-
name: style.name,
|
|
67
|
-
sources,
|
|
68
|
-
layers: layers ? layers : style.layers,
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
if (style.metadata) styleJson.metadata = style.metadata;
|
|
72
|
-
if (style.glyphs) styleJson.glyphs = convertRelativeUrl(style.glyphs, undefined, undefined, config);
|
|
73
|
-
if (style.sprite) styleJson.sprite = convertRelativeUrl(style.sprite, undefined, undefined, config);
|
|
74
|
-
if (style.sky) styleJson.sky = style.sky;
|
|
75
|
-
|
|
76
|
-
return styleJson;
|
|
78
|
+
if (style.glyphs) style.glyphs = convertRelativeUrl(style.glyphs, undefined, undefined, config);
|
|
79
|
+
if (style.sprite) style.sprite = convertRelativeUrl(style.sprite, undefined, undefined, config);
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
export interface
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
export interface StyleConfig {
|
|
83
|
+
/** Name of the terrain layer */
|
|
84
|
+
terrain?: string | null;
|
|
85
|
+
/** Combine layer with the labels layer */
|
|
86
|
+
labels: boolean;
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Turn on the terrain setting in the style json
|
|
91
|
+
*/
|
|
85
92
|
function setStyleTerrain(style: StyleJson, terrain: string, tileMatrix: TileMatrixSet): void {
|
|
86
93
|
const source = Object.keys(style.sources).find((s) => s === terrain);
|
|
87
|
-
if (source == null) throw new LambdaHttpResponse(400, `Terrain: ${terrain}
|
|
94
|
+
if (source == null) throw new LambdaHttpResponse(400, `Terrain: ${terrain} does not exists in the style source.`);
|
|
88
95
|
style.terrain = {
|
|
89
96
|
source,
|
|
90
97
|
exaggeration: DefaultExaggeration[tileMatrix.identifier] ?? DefaultExaggeration[GoogleTms.identifier],
|
|
91
98
|
};
|
|
92
99
|
}
|
|
93
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Merge the "labels" layer into the style json
|
|
103
|
+
*/
|
|
104
|
+
async function setStyleLabels(req: LambdaHttpRequest<StyleGet>, style: StyleJson): Promise<void> {
|
|
105
|
+
const config = await ConfigLoader.load(req);
|
|
106
|
+
const labels = await config.Style.get('labels');
|
|
107
|
+
|
|
108
|
+
if (labels == null) {
|
|
109
|
+
req.log.warn('LabelsStyle:Missing');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const layerId = new Set<string>();
|
|
114
|
+
for (const l of style.layers) layerId.add(l.id);
|
|
115
|
+
|
|
116
|
+
for (const newLayers of labels.style.layers) {
|
|
117
|
+
if (layerId.has(newLayers.id)) {
|
|
118
|
+
throw new LambdaHttpResponse(400, 'Cannot merge styles with duplicate layerIds: ' + newLayers.id);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (style.glyphs == null) style.glyphs = labels.style.glyphs;
|
|
123
|
+
if (style.sprite == null) style.sprite = labels.style.sprite;
|
|
124
|
+
if (style.sky == null) style.sky = labels.style.sky;
|
|
125
|
+
|
|
126
|
+
Object.assign(style.sources, labels.style.sources);
|
|
127
|
+
style.layers = style.layers.concat(labels.style.layers);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Ensure that a "LINZ-Terrain" layer is force added into the output styleJSON source
|
|
132
|
+
*/
|
|
94
133
|
async function ensureTerrain(
|
|
95
134
|
req: LambdaHttpRequest<StyleGet>,
|
|
96
135
|
tileMatrix: TileMatrixSet,
|
|
@@ -98,28 +137,33 @@ async function ensureTerrain(
|
|
|
98
137
|
style: StyleJson,
|
|
99
138
|
): Promise<void> {
|
|
100
139
|
const config = await ConfigLoader.load(req);
|
|
101
|
-
const terrain = await config.TileSet.get('
|
|
102
|
-
if (terrain)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
140
|
+
const terrain = await config.TileSet.get('elevation');
|
|
141
|
+
if (terrain == null) return;
|
|
142
|
+
const configLocation = ConfigLoader.extract(req);
|
|
143
|
+
const elevationQuery = toQueryString({ config: configLocation, api: apiKey, pipeline: 'terrain-rgb' });
|
|
144
|
+
style.sources['LINZ-Terrain'] = {
|
|
145
|
+
type: 'raster-dem',
|
|
146
|
+
tileSize: 256,
|
|
147
|
+
maxzoom: 18, // TODO: this should be configurable based on the elevation layer
|
|
148
|
+
tiles: [convertRelativeUrl(`/v1/tiles/elevation/${tileMatrix.identifier}/{z}/{x}/{y}.png${elevationQuery}`)],
|
|
149
|
+
};
|
|
112
150
|
}
|
|
113
151
|
|
|
114
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Generate a StyleJSON from a tileset
|
|
154
|
+
* @returns
|
|
155
|
+
*/
|
|
156
|
+
export function tileSetToStyle(
|
|
115
157
|
req: LambdaHttpRequest<StyleGet>,
|
|
116
158
|
tileSet: ConfigTileSetRaster,
|
|
117
159
|
tileMatrix: TileMatrixSet,
|
|
118
160
|
apiKey: string,
|
|
119
|
-
|
|
120
|
-
|
|
161
|
+
): StyleJson {
|
|
162
|
+
// If the style has outputs defined it has a different process for generating the stylejson
|
|
163
|
+
if (tileSet.outputs) return tileSetOutputToStyle(req, tileSet, tileMatrix, apiKey);
|
|
164
|
+
|
|
121
165
|
const [tileFormat] = Validate.getRequestedFormats(req) ?? ['webp'];
|
|
122
|
-
if (tileFormat == null)
|
|
166
|
+
if (tileFormat == null) throw new LambdaHttpResponse(400, 'Invalid image format');
|
|
123
167
|
|
|
124
168
|
const pipeline = Validate.pipeline(tileSet, tileFormat, req.query.get('pipeline'));
|
|
125
169
|
const pipelineName = pipeline?.name === 'rgba' ? undefined : pipeline?.name;
|
|
@@ -132,155 +176,134 @@ export async function tileSetToStyle(
|
|
|
132
176
|
`/v1/tiles/${tileSet.name}/${tileMatrix.identifier}/{z}/{x}/{y}.${tileFormat}${query}`;
|
|
133
177
|
|
|
134
178
|
const styleId = `basemaps-${tileSet.name}`;
|
|
135
|
-
|
|
179
|
+
return {
|
|
136
180
|
id: ConfigId.prefix(ConfigPrefix.Style, tileSet.name),
|
|
137
181
|
name: tileSet.name,
|
|
138
182
|
version: 8,
|
|
139
183
|
sources: { [styleId]: { type: 'raster', tiles: [tileUrl], tileSize: 256 } },
|
|
140
184
|
layers: [{ id: styleId, type: 'raster', source: styleId }],
|
|
141
185
|
};
|
|
142
|
-
|
|
143
|
-
// Ensure elevation for individual tilesets
|
|
144
|
-
await ensureTerrain(req, tileMatrix, apiKey, style);
|
|
145
|
-
|
|
146
|
-
// Add terrain in style
|
|
147
|
-
if (terrain) setStyleTerrain(style, terrain, tileMatrix);
|
|
148
|
-
|
|
149
|
-
const data = Buffer.from(JSON.stringify(style));
|
|
150
|
-
|
|
151
|
-
const cacheKey = Etag.key(data);
|
|
152
|
-
if (Etag.isNotModified(req, cacheKey)) return NotModified();
|
|
153
|
-
|
|
154
|
-
const response = new LambdaHttpResponse(200, 'ok');
|
|
155
|
-
response.header(HttpHeader.ETag, cacheKey);
|
|
156
|
-
response.header(HttpHeader.CacheControl, 'no-store');
|
|
157
|
-
response.buffer(data, 'application/json');
|
|
158
|
-
req.set('bytes', data.byteLength);
|
|
159
|
-
return response;
|
|
160
186
|
}
|
|
161
187
|
|
|
162
|
-
|
|
188
|
+
/**
|
|
189
|
+
* generate a style from a tile set which has a output
|
|
190
|
+
*/
|
|
191
|
+
export function tileSetOutputToStyle(
|
|
163
192
|
req: LambdaHttpRequest<StyleGet>,
|
|
164
193
|
tileSet: ConfigTileSetRaster,
|
|
165
194
|
tileMatrix: TileMatrixSet,
|
|
166
195
|
apiKey: string,
|
|
167
|
-
|
|
168
|
-
)
|
|
196
|
+
): StyleJson {
|
|
197
|
+
if (tileSet.outputs == null) throw new LambdaHttpResponse(400, 'TileSet does not have any outputs to generate');
|
|
169
198
|
const configLocation = ConfigLoader.extract(req);
|
|
170
|
-
const query = toQueryString({ config: configLocation, api: apiKey });
|
|
171
199
|
|
|
172
200
|
const styleId = `basemaps-${tileSet.name}`;
|
|
173
201
|
const sources: Sources = {};
|
|
174
202
|
const layers: Layer[] = [];
|
|
175
203
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
sources[`${styleId}-${output.name}-dem`] = {
|
|
191
|
-
type: 'raster-dem',
|
|
192
|
-
tiles: [tileUrl + `&pipeline=${output.name}`],
|
|
193
|
-
tileSize: 256,
|
|
194
|
-
};
|
|
195
|
-
} else {
|
|
196
|
-
// Add raster source other outputs
|
|
197
|
-
sources[`${styleId}-${output.name}`] = {
|
|
198
|
-
type: 'raster',
|
|
199
|
-
tiles: [tileUrl + `&pipeline=${output.name}`],
|
|
200
|
-
tileSize: 256,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
204
|
+
for (const output of tileSet.outputs) {
|
|
205
|
+
const format = output.format?.[0] ?? 'webp';
|
|
206
|
+
const urlBase = Env.get(Env.PublicUrlBase) ?? '';
|
|
207
|
+
const query = toQueryString({ config: configLocation, api: apiKey, pipeline: output.name });
|
|
208
|
+
|
|
209
|
+
const tileUrl = `${urlBase}/v1/tiles/${tileSet.name}/${tileMatrix.identifier}/{z}/{x}/{y}.${format}${query}`;
|
|
210
|
+
|
|
211
|
+
if (output.name === 'terrain-rgb') {
|
|
212
|
+
// Add both raster source and dem raster source for terrain-rgb output
|
|
213
|
+
sources[`${styleId}-${output.name}`] = { type: 'raster', tiles: [tileUrl], tileSize: 256 };
|
|
214
|
+
sources[`${styleId}-${output.name}-dem`] = { type: 'raster-dem', tiles: [tileUrl], tileSize: 256 };
|
|
215
|
+
} else {
|
|
216
|
+
// Add raster source other outputs
|
|
217
|
+
sources[`${styleId}-${output.name}`] = { type: 'raster', tiles: [tileUrl], tileSize: 256 };
|
|
203
218
|
}
|
|
204
219
|
}
|
|
205
220
|
|
|
206
221
|
// Add first raster source as default layer
|
|
207
222
|
for (const source of Object.keys(sources)) {
|
|
208
223
|
if (sources[source].type === 'raster') {
|
|
209
|
-
layers.push({
|
|
210
|
-
id: styleId,
|
|
211
|
-
type: 'raster',
|
|
212
|
-
source,
|
|
213
|
-
});
|
|
224
|
+
layers.push({ id: styleId, type: 'raster', source });
|
|
214
225
|
break;
|
|
215
226
|
}
|
|
216
227
|
}
|
|
217
228
|
|
|
218
|
-
|
|
229
|
+
return {
|
|
219
230
|
id: ConfigId.prefix(ConfigPrefix.Style, tileSet.name),
|
|
220
231
|
name: tileSet.name,
|
|
221
232
|
version: 8,
|
|
222
233
|
sources,
|
|
223
234
|
layers,
|
|
224
235
|
};
|
|
236
|
+
}
|
|
225
237
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (
|
|
238
|
+
async function generateStyleFromTileSet(
|
|
239
|
+
req: LambdaHttpRequest<StyleGet>,
|
|
240
|
+
config: BasemapsConfigProvider,
|
|
241
|
+
tileSetName: string,
|
|
242
|
+
tileMatrix: TileMatrixSet,
|
|
243
|
+
apiKey: string,
|
|
244
|
+
): Promise<StyleJson> {
|
|
245
|
+
const tileSet = await config.TileSet.get(tileSetName);
|
|
246
|
+
if (tileSet == null) throw NotFound();
|
|
247
|
+
if (tileSet.type !== TileSetType.Raster) {
|
|
248
|
+
throw new LambdaHttpResponse(400, 'Only raster tile sets can generate style JSON');
|
|
249
|
+
}
|
|
250
|
+
if (tileSet.outputs) return tileSetOutputToStyle(req, tileSet, tileMatrix, apiKey);
|
|
251
|
+
else return tileSetToStyle(req, tileSet, tileMatrix, apiKey);
|
|
252
|
+
}
|
|
236
253
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
req.set('bytes', data.byteLength);
|
|
242
|
-
return Promise.resolve(response);
|
|
254
|
+
export interface StyleGet {
|
|
255
|
+
Params: {
|
|
256
|
+
styleName: string;
|
|
257
|
+
};
|
|
243
258
|
}
|
|
244
259
|
|
|
245
260
|
export async function styleJsonGet(req: LambdaHttpRequest<StyleGet>): Promise<LambdaHttpResponse> {
|
|
246
261
|
const apiKey = Validate.apiKey(req);
|
|
247
262
|
const styleName = req.params.styleName;
|
|
248
|
-
|
|
249
|
-
const excluded = new Set(excludeLayers.map((l) => l.toLowerCase()));
|
|
263
|
+
|
|
250
264
|
const tileMatrix = TileMatrixSets.find(req.query.get('tileMatrix') ?? GoogleTms.identifier);
|
|
251
265
|
if (tileMatrix == null) return new LambdaHttpResponse(400, 'Invalid tile matrix');
|
|
266
|
+
|
|
267
|
+
// Remove layers from the output style json
|
|
268
|
+
const excludeLayers = req.query.getAll('exclude');
|
|
269
|
+
const excluded = new Set(excludeLayers.map((l) => l.toLowerCase()));
|
|
270
|
+
if (excluded.size > 0) req.set('excludedLayers', [...excluded]);
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Configuration options used for the landing page:
|
|
274
|
+
* "terrain" - force add a terrain layer
|
|
275
|
+
* "labels" - merge the labels style with the current style
|
|
276
|
+
*
|
|
277
|
+
* TODO: (2024-08) this is not a very scalable way of configuring styles, it would be good to provide a styleJSON merge
|
|
278
|
+
*/
|
|
252
279
|
const terrain = req.query.get('terrain') ?? undefined;
|
|
280
|
+
const labels = Boolean(req.query.get('labels') ?? false);
|
|
281
|
+
req.set('styleConfig', { terrain, labels });
|
|
253
282
|
|
|
254
283
|
// Get style Config from db
|
|
255
284
|
const config = await ConfigLoader.load(req);
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
// Were we given a tileset name instead, generated
|
|
260
|
-
const tileSet = await config.TileSet.get(config.TileSet.id(styleName));
|
|
261
|
-
if (tileSet == null) return NotFound();
|
|
262
|
-
if (tileSet.type !== TileSetType.Raster) return NotFound();
|
|
263
|
-
if (tileSet.outputs) return await tileSetOutputToStyle(req, tileSet, tileMatrix, apiKey, terrain);
|
|
264
|
-
else return await tileSetToStyle(req, tileSet, tileMatrix, apiKey, terrain);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Prepare sources and add linz source
|
|
268
|
-
const style = convertStyleJson(
|
|
269
|
-
styleConfig.style,
|
|
270
|
-
tileMatrix,
|
|
271
|
-
apiKey,
|
|
272
|
-
ConfigLoader.extract(req),
|
|
273
|
-
styleConfig.style.layers.filter((f) => !excluded.has(f.id.toLowerCase())),
|
|
274
|
-
);
|
|
285
|
+
const styleConfig = await config.Style.get(styleName);
|
|
286
|
+
const styleSource =
|
|
287
|
+
styleConfig?.style ?? (await generateStyleFromTileSet(req, config, styleName, tileMatrix, apiKey));
|
|
275
288
|
|
|
289
|
+
const targetStyle = structuredClone(styleSource);
|
|
276
290
|
// Ensure elevation for style json config
|
|
277
291
|
// TODO: We should remove this after adding terrain source into style configs. PR-916
|
|
278
|
-
await ensureTerrain(req, tileMatrix, apiKey,
|
|
292
|
+
await ensureTerrain(req, tileMatrix, apiKey, targetStyle);
|
|
279
293
|
|
|
280
294
|
// Add terrain in style
|
|
281
|
-
if (terrain) setStyleTerrain(
|
|
295
|
+
if (terrain) setStyleTerrain(targetStyle, terrain, tileMatrix);
|
|
296
|
+
if (labels) await setStyleLabels(req, targetStyle);
|
|
297
|
+
|
|
298
|
+
// convert sources to full URLS and convert style between projections
|
|
299
|
+
setStyleUrls(targetStyle, tileMatrix, apiKey, ConfigLoader.extract(req));
|
|
300
|
+
|
|
301
|
+
if (tileMatrix.identifier === Nztm2000QuadTms.identifier) convertStyleToNztmStyle(targetStyle, false);
|
|
302
|
+
|
|
303
|
+
// filter out any excluded layers
|
|
304
|
+
if (excluded.size > 0) targetStyle.layers = targetStyle.layers.filter((f) => !excluded.has(f.id.toLowerCase()));
|
|
282
305
|
|
|
283
|
-
const data = Buffer.from(JSON.stringify(
|
|
306
|
+
const data = Buffer.from(JSON.stringify(targetStyle));
|
|
284
307
|
|
|
285
308
|
const cacheKey = Etag.key(data);
|
|
286
309
|
if (Etag.isNotModified(req, cacheKey)) return NotModified();
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ConfigTileSetVector } from '@basemaps/config';
|
|
2
|
-
import { GoogleTms } from '@basemaps/geo';
|
|
3
2
|
import { fsa } from '@basemaps/shared';
|
|
4
3
|
import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
5
4
|
|
|
@@ -13,12 +12,17 @@ export const tileXyzVector = {
|
|
|
13
12
|
/** Serve a MVT vector tile */
|
|
14
13
|
async tile(req: LambdaHttpRequest, tileSet: ConfigTileSetVector, xyz: TileXyz): Promise<LambdaHttpResponse> {
|
|
15
14
|
if (xyz.tileType !== 'pbf') return NotFound();
|
|
16
|
-
if (xyz.tileMatrix.identifier !== GoogleTms.identifier) return NotFound();
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
// Vector tiles cannot be merged (yet!)
|
|
17
|
+
if (tileSet.layers.length > 1) {
|
|
18
|
+
return new LambdaHttpResponse(500, `Too many layers in vector tileset ${tileSet.layers.length}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const epsgCode = xyz.tileMatrix.projection.code;
|
|
22
|
+
const layerId = tileSet.layers[0][epsgCode];
|
|
23
|
+
if (layerId == null) {
|
|
24
|
+
return new LambdaHttpResponse(404, `No data found for tile matrix: ${xyz.tileMatrix.identifier}`);
|
|
25
|
+
}
|
|
22
26
|
|
|
23
27
|
// Flip Y coordinate because MBTiles files are TMS.
|
|
24
28
|
const y = (1 << xyz.tile.z) - 1 - xyz.tile.y;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { fsa, FsMemory } from '@chunkd/fs';
|
|
5
|
+
|
|
6
|
+
import { SourceCache } from '../source.cache.js';
|
|
7
|
+
|
|
8
|
+
describe('CoSourceCache', () => {
|
|
9
|
+
it('should not exit if a promise rejection happens for tiff', async () => {
|
|
10
|
+
const cache = new SourceCache(5);
|
|
11
|
+
|
|
12
|
+
const mem = new FsMemory();
|
|
13
|
+
const tiffLoc = new URL('memory://foo/bar.tiff');
|
|
14
|
+
await mem.write(tiffLoc, Buffer.from('ABC123'));
|
|
15
|
+
fsa.register('memory://', mem);
|
|
16
|
+
|
|
17
|
+
let failCount = 0;
|
|
18
|
+
await cache.getCog(tiffLoc).catch(() => failCount++);
|
|
19
|
+
assert.equal(cache.cache.currentSize, 0);
|
|
20
|
+
assert.equal(failCount, 1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should not exit if a promise rejection happens for tar', async () => {
|
|
24
|
+
const cache = new SourceCache(5);
|
|
25
|
+
|
|
26
|
+
const mem = new FsMemory();
|
|
27
|
+
const tiffLoc = new URL('memory://foo/bar.tar');
|
|
28
|
+
await mem.write(tiffLoc, Buffer.from('ABC123'));
|
|
29
|
+
fsa.register('memory://', mem);
|
|
30
|
+
|
|
31
|
+
let failCount = 0;
|
|
32
|
+
await cache.getCotar(tiffLoc).catch(() => failCount++);
|
|
33
|
+
assert.equal(cache.cache.currentSize, 0);
|
|
34
|
+
assert.equal(failCount, 1);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { StyleJson } from '@basemaps/config';
|
|
5
|
+
|
|
6
|
+
import { convertStyleToNztmStyle } from '../nztm.style.js';
|
|
7
|
+
|
|
8
|
+
describe('NZTM2000QuadStyle', () => {
|
|
9
|
+
const fakeStyle: StyleJson = {
|
|
10
|
+
version: 8,
|
|
11
|
+
id: 'test',
|
|
12
|
+
name: 'topographic',
|
|
13
|
+
sources: {},
|
|
14
|
+
layers: [],
|
|
15
|
+
glyphs: '/glyphs',
|
|
16
|
+
sprite: '/sprite',
|
|
17
|
+
metadata: { id: 'test' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
it('should not modify the source style', () => {
|
|
21
|
+
const baseStyle = {
|
|
22
|
+
...fakeStyle,
|
|
23
|
+
terrain: { exaggeration: 1.1, source: 'abc' },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
convertStyleToNztmStyle(baseStyle);
|
|
27
|
+
assert.equal(baseStyle.terrain?.exaggeration, 1.1);
|
|
28
|
+
|
|
29
|
+
convertStyleToNztmStyle(baseStyle, false);
|
|
30
|
+
assert.equal(baseStyle.terrain?.exaggeration, 4.4);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should convert min/maxzooms', () => {
|
|
34
|
+
const newStyle = convertStyleToNztmStyle({
|
|
35
|
+
...fakeStyle,
|
|
36
|
+
layers: [{ minzoom: 5, maxzoom: 10, id: 'something', type: '' }],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
assert.deepEqual(newStyle.layers[0], { minzoom: 3, maxzoom: 8, id: 'something', type: '' });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should offset terrain', () => {
|
|
43
|
+
const newStyle = convertStyleToNztmStyle({
|
|
44
|
+
...fakeStyle,
|
|
45
|
+
terrain: { exaggeration: 1.1, source: 'abc' },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
assert.deepEqual(newStyle.terrain, { exaggeration: 4.4, source: 'abc' });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should convert stops inside of paint and layout', () => {
|
|
52
|
+
const newStyle = convertStyleToNztmStyle({
|
|
53
|
+
...fakeStyle,
|
|
54
|
+
layers: [
|
|
55
|
+
{
|
|
56
|
+
layout: {
|
|
57
|
+
'line-width': {
|
|
58
|
+
stops: [
|
|
59
|
+
[16, 0.75],
|
|
60
|
+
[24, 1.5],
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
paint: {
|
|
66
|
+
'line-width': {
|
|
67
|
+
stops: [
|
|
68
|
+
[16, 0.75],
|
|
69
|
+
[24, 1.5],
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
id: 'something',
|
|
74
|
+
type: '',
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
assert.deepEqual(newStyle.layers[0], {
|
|
80
|
+
layout: {
|
|
81
|
+
'line-width': {
|
|
82
|
+
stops: [
|
|
83
|
+
[14, 0.75],
|
|
84
|
+
[22, 1.5],
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
paint: {
|
|
89
|
+
'line-width': {
|
|
90
|
+
stops: [
|
|
91
|
+
[14, 0.75],
|
|
92
|
+
[22, 1.5],
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
id: 'something',
|
|
97
|
+
type: '',
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { StyleJson } from '@basemaps/config';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* limited checking to cast a unknown paint/layout into one with stops
|
|
5
|
+
*/
|
|
6
|
+
function hasStops(x: unknown): x is { stops: [number, unknown][] } {
|
|
7
|
+
if (x == null) return false;
|
|
8
|
+
return Array.isArray((x as { stops: unknown })['stops']);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert a style json from a WebMercatorQuad style into a NZTM2000Quad style,
|
|
13
|
+
* This creates a clone of the source style and does not modify the source
|
|
14
|
+
*
|
|
15
|
+
* NZTM2000Quad is offset from WebMercatorQuad by two zoom levels
|
|
16
|
+
*
|
|
17
|
+
* @param inputStyle style to convert
|
|
18
|
+
* @param clone Should the input style be cloned or modified
|
|
19
|
+
* @returns a new style converted into NZTM2000Quad zooms
|
|
20
|
+
*/
|
|
21
|
+
export function convertStyleToNztmStyle(inputStyle: StyleJson, clone: boolean = true): StyleJson {
|
|
22
|
+
const style = clone ? structuredClone(inputStyle) : inputStyle;
|
|
23
|
+
|
|
24
|
+
for (const layer of style.layers) {
|
|
25
|
+
// Adjust the min/max zoom
|
|
26
|
+
if (layer.minzoom) layer.minzoom = Math.max(0, layer.minzoom - 2);
|
|
27
|
+
if (layer.maxzoom) layer.maxzoom = Math.max(0, layer.maxzoom - 2);
|
|
28
|
+
|
|
29
|
+
// Check all the pain and layout for "stops" then adjust the stops by two
|
|
30
|
+
const stylesToCheck = [layer.paint, layer.layout];
|
|
31
|
+
for (const obj of stylesToCheck) {
|
|
32
|
+
if (obj == null) continue;
|
|
33
|
+
for (const val of Object.values(obj)) {
|
|
34
|
+
if (!hasStops(val)) continue;
|
|
35
|
+
for (const stop of val.stops) stop[0] = Math.max(0, stop[0] - 2);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Based on {@link DefaultExaggeration} offsetting by 2 two levels changes the exaggeration needed by approx 4x */
|
|
41
|
+
if (style.terrain) style.terrain.exaggeration = style.terrain.exaggeration * 4;
|
|
42
|
+
|
|
43
|
+
return style;
|
|
44
|
+
}
|