Skip to content

Commit 1fc339c

Browse files
authored
feat(lambda-tiler): expose all pipelines in WMTSCapabilities BM-1455 (#3586)
### Motivation Layers can now have quite a few pipelines, it would be useful to expose all the pipelines to WMTS consumers ### Modifications If no pipeline is provided expose all pipelines to the WMTS consumers ### Verification Unit tests
1 parent a3f49c2 commit 1fc339c

File tree

5 files changed

+168
-34
lines changed

5 files changed

+168
-34
lines changed

package-lock.json

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/lambda-tiler/src/__tests__/config.data.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ export const TileSetElevation: ConfigTileSetRaster = {
5555
category: 'Elevation',
5656
layers: [
5757
{
58-
2193: 'im_01FYWKAJ86W9P7RWM1VB62KD0H',
59-
3857: 'im_01FYWKATAEK2ZTJQ2PX44Y0XNT',
58+
2193: 'im_01FYWKAJ86W9P7RWM1VB62KDEM',
59+
3857: 'im_01FYWKATAEK2ZTJQ2PX44Y0DEM',
6060
title: 'New Zealand 8m DEM (2012)',
6161
name: 'new-zealand_2012_dem_8m',
6262
},
@@ -82,6 +82,44 @@ export const TileSetHillshadeElevation: ConfigTileSetRaster = {
8282
outputs: [DefaultColorRampOutput],
8383
};
8484

85+
export const Imagery2193Elevation: ConfigImagery = {
86+
v: 2,
87+
id: 'im_01FYWKAJ86W9P7RWM1VB62KDEM',
88+
name: 'ōtorohanga_urban_2021_0-1m_DEM',
89+
title: 'Ōtorohanga 0.1m DEM (2021)',
90+
category: 'Elevation',
91+
projection: 2193,
92+
tileMatrix: 'NZTM2000Quad',
93+
uri: 's3://linz-basemaps/2193/ōtorohanga_urban_2021_0-1m_DEM/im_01FYWKAJ86W9P7RWM1VB62KDEM',
94+
bounds: {
95+
x: 1757351.3044652338,
96+
y: 5766358.996410044,
97+
width: 40970.247160854284,
98+
height: 26905.833956381306,
99+
},
100+
files: [],
101+
bands: [{ type: 'float32' }],
102+
};
103+
104+
export const Imagery3857Elevation: ConfigImagery = {
105+
v: 2,
106+
id: 'im_01FYWKATAEK2ZTJQ2PX44Y0DEM',
107+
name: 'ōtorohanga_urban_2021_0-1m_DEM',
108+
title: 'Ōtorohanga 0.1m Urban DEM (2021)',
109+
category: 'Elevation',
110+
projection: 3857,
111+
tileMatrix: 'WebMercatorQuad',
112+
uri: 's3://linz-basemaps/3857/ōtorohanga_urban_2021_0-1m_DEM/im_01FYWKATAEK2ZTJQ2PX44Y0DEM',
113+
bounds: {
114+
x: 1757351.3044652338,
115+
y: 5766358.996410044,
116+
width: 40970.247160854284,
117+
height: 26905.833956381306,
118+
},
119+
files: [],
120+
bands: [{ type: 'float32' }],
121+
};
122+
85123
export const Imagery2193: ConfigImagery = {
86124
v: 2,
87125
id: 'im_01FYWKAJ86W9P7RWM1VB62KD0H',

packages/lambda-tiler/src/__tests__/wmts.capability.test.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@ import { V, VNodeElement } from '@basemaps/shared';
77
import { roundNumbersInString } from '@basemaps/test/build/rounding.js';
88

99
import { WmtsCapabilities } from '../wmts.capability.js';
10-
import { Imagery2193, Imagery3857, Provider, TileSetAerial } from './config.data.js';
10+
import {
11+
Imagery2193,
12+
Imagery2193Elevation,
13+
Imagery3857,
14+
Imagery3857Elevation,
15+
Provider,
16+
TileSetAerial,
17+
TileSetElevation,
18+
} from './config.data.js';
1119

1220
function tags(node: VNodeElement | null | undefined, tag: string): VNodeElement[] {
1321
if (node == null) return [];
@@ -88,7 +96,7 @@ describe('WmtsCapabilities', () => {
8896
});
8997

9098
it('should support unicorns and rainbows', () => {
91-
const tileSet = { ...TileSetAerial };
99+
const tileSet = structuredClone(TileSetAerial);
92100
tileSet.name = '🦄_🌈_2022_0-5m';
93101
tileSet.title = '🦄 🌈 Imagery (2022)';
94102
tileSet.description = '🦄 🌈 Description';
@@ -512,6 +520,14 @@ describe('WmtsCapabilities', () => {
512520
const rawB = wmtsB.toVNode();
513521
const layersB = tags(rawB, 'Layer');
514522
assert.equal(layersB.length, 1);
523+
assert.deepEqual(
524+
layersB.map((l) => l.find('ows:Identifier')?.textContent),
525+
['aerial'],
526+
);
527+
assert.deepEqual(
528+
layersB.map((l) => l.find('ows:Title')?.textContent),
529+
['Aerial Imagery'],
530+
);
515531
});
516532

517533
it('should cover the entire WebMercatorBounds', () => {
@@ -527,7 +543,7 @@ describe('WmtsCapabilities', () => {
527543
imageBottomRight.bounds = { x: 0, y: -halfSize, width: halfSize, height: halfSize };
528544
imagery.set(imageBottomRight.id, imageBottomRight);
529545

530-
const tileSet = { ...TileSetAerial };
546+
const tileSet = structuredClone(TileSetAerial);
531547
tileSet.layers = [
532548
{ 3857: imageTopLeft.id, name: 'a_top_left', title: 'A Top Left' },
533549
{ 3857: imageBottomRight.id, name: 'b_bottom_right', title: 'B Bottom Right' },
@@ -573,7 +589,7 @@ describe('WmtsCapabilities', () => {
573589
imageBottomRight.bounds = { x: halfSize / 2, y: -halfSize, width: halfSize, height: halfSize };
574590
imagery.set(imageBottomRight.id, imageBottomRight);
575591

576-
const tileSet = { ...TileSetAerial };
592+
const tileSet = structuredClone(TileSetAerial);
577593
tileSet.layers = [{ 3857: imageBottomRight.id, name: 'b_bottom_right', title: 'B Bottom Right' }];
578594

579595
const wmts = new WmtsCapabilities({
@@ -628,4 +644,41 @@ describe('WmtsCapabilities', () => {
628644
assert.equal(bboxC.children[0].textContent, '-180 -49.929855');
629645
assert.equal(bboxC.children[1].textContent, '180 2.938603');
630646
});
647+
648+
it('should export a layer per pipeline', () => {
649+
const tileSet = structuredClone(TileSetElevation);
650+
651+
const wmts = new WmtsCapabilities({ httpBase: '' });
652+
wmts.fromParams({
653+
provider: Provider,
654+
tileMatrix: [GoogleTms],
655+
tileSet,
656+
imagery: new Map([
657+
[Imagery2193Elevation.id, Imagery2193Elevation],
658+
[Imagery3857Elevation.id, Imagery3857Elevation],
659+
]),
660+
formats: ['webp'],
661+
});
662+
663+
const raw = wmts.toVNode();
664+
const layers = [...(raw.find('Contents')?.tags('Layer') ?? [])];
665+
666+
assert.equal(layers.length, 2);
667+
assert.deepEqual(
668+
layers.map((l) => l.find('ows:Identifier')?.textContent),
669+
['elevation_terrain-rgb', 'elevation_color-ramp'],
670+
);
671+
assert.deepEqual(
672+
layers.map((l) => l.find('ows:Title')?.textContent),
673+
['Elevation Imagery TerrainRGB', 'Elevation Imagery Color ramp'],
674+
);
675+
676+
assert.deepEqual(
677+
layers.map((l) => tags(l, 'Format').map((m) => m.textContent)),
678+
[
679+
['image/png'], // Terrain RGB can only be used as PNG
680+
['image/webp'],
681+
],
682+
);
683+
});
631684
});

packages/lambda-tiler/src/routes/__tests__/wmts.test.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@ import { afterEach, beforeEach, describe, it } from 'node:test';
44
import { ConfigProviderMemory, ConfigTileSetRaster } from '@basemaps/config';
55
import { Env } from '@basemaps/shared';
66

7-
import { Imagery2193, Imagery3857, Provider, TileSetAerial, TileSetElevation } from '../../__tests__/config.data.js';
7+
import {
8+
Imagery2193,
9+
Imagery2193Elevation,
10+
Imagery3857,
11+
Imagery3857Elevation,
12+
Provider,
13+
TileSetAerial,
14+
TileSetElevation,
15+
} from '../../__tests__/config.data.js';
816
import { Api, mockUrlRequest } from '../../__tests__/xyz.util.js';
917
import { handler } from '../../index.js';
1018
import { ConfigLoader } from '../../util/config.loader.js';
@@ -33,6 +41,8 @@ describe('WMTSRouting', () => {
3341
return process.env[arg];
3442
});
3543
config.put(TileSetElevation);
44+
config.put(Imagery2193Elevation);
45+
config.put(Imagery3857Elevation);
3646
t.mock.method(ConfigLoader, 'load', () => Promise.resolve(config));
3747
const req = mockUrlRequest(
3848
'/v1/tiles/elevation/WebMercatorQuad/WMTSCapabilities.xml',
@@ -42,9 +52,10 @@ describe('WMTSRouting', () => {
4252

4353
assert.equal(res.status, 200);
4454
const lines = Buffer.from(res.body, 'base64').toString().split('\n');
45-
const resourceUrl = lines.find((f) => f.includes('ResourceURL'))?.trim();
55+
const resourceUrls = lines.filter((f) => f.includes('ResourceURL'));
4656

47-
assert.ok(resourceUrl);
57+
const resourceUrl = resourceUrls[0].trim();
58+
assert.equal(resourceUrls.length, 1);
4859
assert.ok(resourceUrl.includes('amp;pipeline=terrain-rgb'), `includes pipeline=terrain-rgb in ${resourceUrl}`);
4960
assert.ok(resourceUrl.includes('.png'), `includes .png in ${resourceUrl}`);
5061
});

packages/lambda-tiler/src/wmts.capability.ts

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { ConfigImagery, ConfigLayer, ConfigTileSet, standardizeLayerName } from '@basemaps/config';
1+
import {
2+
ConfigImagery,
3+
ConfigLayer,
4+
ConfigTileSet,
5+
ConfigTileSetRasterOutput,
6+
standardizeLayerName,
7+
TileSetType,
8+
} from '@basemaps/config';
29
import { BoundingBox, Bounds, GoogleTms, ImageFormat, Projection, TileMatrixSet, WmtsProvider } from '@basemaps/geo';
310
import { toQueryString, V, VNodeElement } from '@basemaps/shared';
411
import { ImageFormatOrder } from '@basemaps/tiler';
@@ -74,7 +81,13 @@ export class WmtsBuilder {
7481
for (const format of formats) this.formats.push(format);
7582
}
7683

77-
getFormats(): ImageFormat[] {
84+
getFormats(restrictTo?: ImageFormat[]): ImageFormat[] {
85+
if (restrictTo) {
86+
if (this.formats.length === 0) return restrictTo;
87+
const filtered = this.formats.filter((f) => restrictTo.includes(f));
88+
if (filtered.length === 0) return restrictTo;
89+
return filtered;
90+
}
7891
if (this.formats.length) return this.formats;
7992
return ImageFormatOrder;
8093
}
@@ -154,17 +167,17 @@ export class WmtsBuilder {
154167
return V('Style', { isDefault: 'true' }, [V('ows:Title', 'Default Style'), V('ows:Identifier', 'default')]);
155168
}
156169

157-
buildResourceUrl(tileSetId: string, suffix: string): VNodeElement {
170+
buildResourceUrl(tileSetId: string, suffix: string, pipeline?: string): VNodeElement {
158171
return V('ResourceURL', {
159172
format: 'image/' + suffix,
160173
resourceType: 'tile',
161-
template: this.buildTileUrl(tileSetId, suffix),
174+
template: this.buildTileUrl(tileSetId, suffix, pipeline),
162175
});
163176
}
164177

165-
buildTileUrl(tileSetId: string, suffix: string): string {
178+
buildTileUrl(tileSetId: string, suffix: string, pipeline?: string): string {
166179
// TODO this should restrict the output formats to supported formats in pipelines
167-
const query = { api: this.apiKey, config: this.config, pipeline: this.pipeline };
180+
const query = { api: this.apiKey, config: this.config, pipeline };
168181

169182
return [
170183
this.httpBase,
@@ -178,10 +191,6 @@ export class WmtsBuilder {
178191
].join('/');
179192
}
180193

181-
buildFormats(): VNodeElement[] {
182-
return this.getFormats().map((fmt) => V('Format', 'image/' + fmt));
183-
}
184-
185194
buildTileMatrixLink(tileSet: ConfigTileSet): VNodeElement[] {
186195
const matrixSetNodes: VNodeElement[] = [];
187196
for (const tms of this.tileMatrixSets.values()) {
@@ -319,7 +328,7 @@ export class WmtsCapabilities extends WmtsBuilder {
319328
];
320329
}
321330

322-
toLayerVNode(tileSet: ConfigTileSet): VNodeElement {
331+
toLayerVNode(tileSet: ConfigTileSet): VNodeElement[] {
323332
const matrixSets = this.getMatrixSets(tileSet);
324333
const matrixSetList = [...matrixSets.values()];
325334
const firstMatrix = matrixSetList[0];
@@ -334,19 +343,43 @@ export class WmtsCapabilities extends WmtsBuilder {
334343
bounds.push(Bounds.fromJson(img.bounds));
335344
}
336345

346+
const layers: VNodeElement[] = [];
347+
348+
const pipelines = this.getPipelines(tileSet, this.pipeline);
349+
337350
const layerNameId = standardizeLayerName(tileSet.name);
338-
return V('Layer', [
339-
V('ows:Title', tileSet.title),
340-
V('ows:Abstract', tileSet.description ?? ''),
341-
V('ows:Identifier', layerNameId),
342-
this.buildKeywords(tileSet),
343-
...[...matrixSets.values()].map((tms) => this.buildBoundingBoxFromImagery(tms, tileSet.layers)),
344-
this.buildWgs84BoundingBox(webMercatorOrFirst, bounds),
345-
this.buildStyle(),
346-
...this.buildFormats(),
347-
...this.buildTileMatrixLink(tileSet),
348-
...this.getFormats().map((fmt) => this.buildResourceUrl(layerNameId, fmt)),
349-
]);
351+
352+
for (const pipeline of pipelines) {
353+
const formats = this.getFormats(pipeline.format);
354+
const layerId = pipeline.default ? layerNameId : `${layerNameId}_${pipeline.name}`;
355+
const layerTitle = pipeline.default ? tileSet.title : `${tileSet.title} ${pipeline.title}`;
356+
357+
const layer = V('Layer', [
358+
V('ows:Title', layerTitle),
359+
V('ows:Abstract', tileSet.description ?? ''),
360+
V('ows:Identifier', layerId),
361+
this.buildKeywords(tileSet),
362+
...[...matrixSets.values()].map((tms) => this.buildBoundingBoxFromImagery(tms, tileSet.layers)),
363+
this.buildWgs84BoundingBox(webMercatorOrFirst, bounds),
364+
this.buildStyle(),
365+
...formats.map((fmt) => V('Format', 'image/' + fmt)),
366+
...this.buildTileMatrixLink(tileSet),
367+
...formats.map((fmt) => this.buildResourceUrl(layerNameId, fmt, pipeline.default ? undefined : pipeline.name)),
368+
]);
369+
layers.push(layer);
370+
}
371+
372+
return layers;
373+
}
374+
375+
getPipelines(tileSet: ConfigTileSet, pipeline?: string): ConfigTileSetRasterOutput[] {
376+
if (tileSet.type !== TileSetType.Raster) return [];
377+
378+
if (tileSet.outputs == null) return [{ name: 'rgba', title: 'RGBA', default: true }];
379+
380+
if (pipeline) return tileSet.outputs.filter((f) => f.name === pipeline);
381+
382+
return tileSet.outputs;
350383
}
351384

352385
toAllImageryLayersVNode(configLayers?: ConfigLayer[]): VNodeElement[] {
@@ -393,7 +426,7 @@ export class WmtsCapabilities extends WmtsBuilder {
393426

394427
// Build TileSet Layer VNodes
395428
const layers: VNodeElement[] = [];
396-
layers.push(this.toLayerVNode(this.tileSet));
429+
layers.push(...this.toLayerVNode(this.tileSet));
397430
const contents = layers.concat(this.toAllImageryLayersVNode(this.configLayers));
398431

399432
// Build TileMatrix Sets vNodes

0 commit comments

Comments
 (0)