@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/build/__tests__/config.data.js +3 -3
  3. package/build/__tests__/config.data.js.map +1 -1
  4. package/build/__tests__/tile.style.json.test.js +13 -12
  5. package/build/__tests__/tile.style.json.test.js.map +1 -1
  6. package/build/routes/__tests__/health.test.js +40 -20
  7. package/build/routes/__tests__/health.test.js.map +1 -1
  8. package/build/routes/__tests__/tile.style.json.test.js +81 -0
  9. package/build/routes/__tests__/tile.style.json.test.js.map +1 -1
  10. package/build/routes/__tests__/xyz.test.js +13 -0
  11. package/build/routes/__tests__/xyz.test.js.map +1 -1
  12. package/build/routes/health.d.ts +17 -0
  13. package/build/routes/health.js +119 -21
  14. package/build/routes/health.js.map +1 -1
  15. package/build/routes/tile.style.json.d.ts +36 -8
  16. package/build/routes/tile.style.json.js +144 -128
  17. package/build/routes/tile.style.json.js.map +1 -1
  18. package/build/routes/tile.xyz.raster.js.map +1 -1
  19. package/build/routes/tile.xyz.vector.js +9 -9
  20. package/build/routes/tile.xyz.vector.js.map +1 -1
  21. package/build/util/__test__/cache.test.d.ts +1 -0
  22. package/build/util/__test__/cache.test.js +29 -0
  23. package/build/util/__test__/cache.test.js.map +1 -0
  24. package/build/util/__test__/nztm.style.test.d.ts +1 -0
  25. package/build/util/__test__/nztm.style.test.js +87 -0
  26. package/build/util/__test__/nztm.style.test.js.map +1 -0
  27. package/build/util/nztm.style.d.ts +12 -0
  28. package/build/util/nztm.style.js +45 -0
  29. package/build/util/nztm.style.js.map +1 -0
  30. package/build/util/source.cache.d.ts +2 -8
  31. package/build/util/source.cache.js +6 -24
  32. package/build/util/source.cache.js.map +1 -1
  33. package/build/util/swapping.lru.d.ts +1 -0
  34. package/build/util/swapping.lru.js +4 -0
  35. package/build/util/swapping.lru.js.map +1 -1
  36. package/package.json +7 -6
  37. package/src/__tests__/config.data.ts +3 -3
  38. package/src/__tests__/tile.style.json.test.ts +16 -14
  39. package/src/routes/__tests__/health.test.ts +46 -22
  40. package/src/routes/__tests__/tile.style.json.test.ts +91 -0
  41. package/src/routes/__tests__/xyz.test.ts +18 -0
  42. package/src/routes/health.ts +129 -21
  43. package/src/routes/tile.style.json.ts +172 -149
  44. package/src/routes/tile.xyz.raster.ts +0 -1
  45. package/src/routes/tile.xyz.vector.ts +10 -6
  46. package/src/util/__test__/cache.test.ts +36 -0
  47. package/src/util/__test__/nztm.style.test.ts +100 -0
  48. package/src/util/nztm.style.ts +44 -0
  49. package/src/util/source.cache.ts +10 -20
  50. package/src/util/swapping.lru.ts +5 -0
  51. package/tsconfig.tsbuildinfo +1 -1
@@ -1,20 +1,41 @@
1
- import { ConfigId, ConfigPrefix, ConfigTileSetRaster, Layer, Sources, StyleJson, TileSetType } from '@basemaps/config';
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 URLS into a full hostname url
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
- * @returns Updated Url or empty string if url is empty
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
- * Create a new style json that has absolute urls to the current host and API Keys where required
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
- * @returns new stylejson
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 convertStyleJson(
42
- style: StyleJson,
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
- const styleJson: StyleJson = {
64
- version: 8,
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 StyleGet {
80
- Params: {
81
- styleName: string;
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} is not exists in the style source.`);
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('ts_elevation');
102
- if (terrain) {
103
- const configLocation = ConfigLoader.extract(req);
104
- const elevationQuery = toQueryString({ config: configLocation, api: apiKey, pipeline: 'terrain-rgb' });
105
- style.sources['LINZ-Terrain'] = {
106
- type: 'raster-dem',
107
- tileSize: 256,
108
- maxzoom: 18,
109
- tiles: [convertRelativeUrl(`/v1/tiles/elevation/${tileMatrix.identifier}/{z}/{x}/{y}.png${elevationQuery}`)],
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
- export async function tileSetToStyle(
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
- terrain?: string,
120
- ): Promise<LambdaHttpResponse> {
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) return new LambdaHttpResponse(400, 'Invalid image format');
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
- const style: StyleJson = {
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
- export async function tileSetOutputToStyle(
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
- terrain?: string,
168
- ): Promise<LambdaHttpResponse> {
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
- if (tileSet.outputs) {
177
- //for loop output.
178
- for (const output of tileSet.outputs) {
179
- const format = output.format?.[0] ?? 'webp';
180
- const urlBase = Env.get(Env.PublicUrlBase) ?? '';
181
- const tileUrl = `${urlBase}/v1/tiles/${tileSet.name}/${tileMatrix.identifier}/{z}/{x}/{y}.${format}${query}`;
182
-
183
- if (output.name === 'terrain-rgb') {
184
- // Add both raster source and dem raster source for terrain-rgb output
185
- sources[`${styleId}-${output.name}`] = {
186
- type: 'raster',
187
- tiles: [tileUrl + `&pipeline=${output.name}`],
188
- tileSize: 256,
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
- const style: StyleJson = {
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
- // Ensure elevation for style json config
227
- await ensureTerrain(req, tileMatrix, apiKey, style);
228
-
229
- // Add terrain in style
230
- if (terrain) setStyleTerrain(style, terrain, tileMatrix);
231
-
232
- const data = Buffer.from(JSON.stringify(style));
233
-
234
- const cacheKey = Etag.key(data);
235
- if (Etag.isNotModified(req, cacheKey)) return Promise.resolve(NotModified());
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
- const response = new LambdaHttpResponse(200, 'ok');
238
- response.header(HttpHeader.ETag, cacheKey);
239
- response.header(HttpHeader.CacheControl, 'no-store');
240
- response.buffer(data, 'application/json');
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
- const excludeLayers = req.query.getAll('exclude');
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 dbId = config.Style.id(styleName);
257
- const styleConfig = await config.Style.get(dbId);
258
- if (styleConfig == null) {
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, style);
292
+ await ensureTerrain(req, tileMatrix, apiKey, targetStyle);
279
293
 
280
294
  // Add terrain in style
281
- if (terrain) setStyleTerrain(style, terrain, tileMatrix);
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(style));
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();
@@ -109,7 +109,6 @@ export const TileXyzRaster = {
109
109
  }
110
110
 
111
111
  // Remove with typescript >=5.5.0
112
-
113
112
  return (await Promise.all(toLoad)).filter((f) => f != null);
114
113
  },
115
114
 
@@ -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
- if (tileSet.layers.length > 1) return new LambdaHttpResponse(500, 'Too many layers in tileset');
19
- const [layer] = tileSet.layers;
20
- const layerId = layer[3857];
21
- if (layerId == null) return new LambdaHttpResponse(500, 'Layer url not found from tileset Config');
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
+ }