Skip to content

Commit 781bbe8

Browse files
authored
feat(landing): Add terrain parameter in the url (#3292)
### Motivation We want a terrain request parameter that shareable for map with 3d terrain enabled. This will need new aerial style config to added after merge. linz/basemaps-config#916 ### Modifications - Add terrain setting in `getStyle` api when `?terrain=LINZ-Terrain` - Update landing page to lisen the terrain event to set terrain parameter. ![image](https://github.com/linz/basemaps/assets/12163920/00b09fdf-30b0-43fa-8390-8eaaf29a6d37) ### Verification - Unit tests added for tileset to style and style to style in getStyle Api - Landing page tested with terrain button to update the request parameter.
1 parent af60117 commit 781bbe8

File tree

9 files changed

+188
-39
lines changed

9 files changed

+188
-39
lines changed

packages/_infra/src/edge/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,12 @@ export class EdgeStack extends cdk.Stack {
119119
'tileMatrix',
120120
'style',
121121
'pipeline',
122+
'terrain',
122123
// Deprecated single character query params for style and projection
123124
's',
124125
'p',
125126
'i', // ?i=:imageryId is deprecated and should be removed at some point
127+
't',
126128
].map(encodeURIComponent),
127129
},
128130
lambdaFunctionAssociations: [],

packages/config/src/config/vector.style.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ export interface Layer {
4040
'source-layer'?: string;
4141
}
4242

43+
export interface Terrain {
44+
source: string;
45+
exaggeration: number;
46+
}
47+
4348
export type Source = SourceVector | SourceRaster | SourceRasterDem;
4449

4550
export type Sources = Record<string, Source>;
@@ -67,6 +72,9 @@ export interface StyleJson {
6772

6873
/** Layers will be drawn in the order of this array. */
6974
layers: Layer[];
75+
76+
/** OPTIONAL - A global modifier that elevates layers and markers based on a DEM data source */
77+
terrain?: Terrain;
7078
}
7179

7280
export interface ConfigVectorStyle extends ConfigBase {

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

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,6 @@ export const TileSetAerial: ConfigTileSetRaster = {
3030
],
3131
};
3232

33-
export const TileSetElevation: ConfigTileSetRaster = {
34-
id: 'ts_elevation',
35-
name: 'elevation',
36-
type: TileSetType.Raster,
37-
description: 'elevation__description',
38-
title: 'Elevation',
39-
category: 'Elevation',
40-
layers: [
41-
{
42-
3857: 'im_01FYWKATAEK2ZTJQ2PX44Y0XNT',
43-
title: 'New Zealand 8m DEM (2012)',
44-
name: 'new-zealand_2012_dem_8m',
45-
},
46-
],
47-
outputs: [DefaultTerrainRgbOutput, DefaultColorRampOutput],
48-
};
49-
5033
export const TileSetVector: ConfigTileSetVector = {
5134
id: 'ts_topographic',
5235
type: TileSetType.Vector,
@@ -63,6 +46,23 @@ export const TileSetVector: ConfigTileSetVector = {
6346
},
6447
],
6548
};
49+
export const TileSetElevation: ConfigTileSetRaster = {
50+
id: 'ts_elevation',
51+
name: 'elevation',
52+
type: TileSetType.Raster,
53+
description: 'elevation__description',
54+
title: 'Elevation Imagery',
55+
category: 'Elevation',
56+
layers: [
57+
{
58+
2193: 'im_01FYWKAJ86W9P7RWM1VB62KD0H',
59+
3857: 'im_01FYWKATAEK2ZTJQ2PX44Y0XNT',
60+
title: 'New Zealand 8m DEM (2012)',
61+
name: 'new-zealand_2012_dem_8m',
62+
},
63+
],
64+
outputs: [DefaultTerrainRgbOutput, DefaultColorRampOutput],
65+
};
6666

6767
export const Imagery2193: ConfigImagery = {
6868
id: 'im_01FYWKAJ86W9P7RWM1VB62KD0H',
@@ -286,6 +286,15 @@ export class FakeData {
286286
return tileSet;
287287
}
288288

289+
static tileSetElevation(name: string): ConfigTileSetRaster {
290+
const tileSet = JSON.parse(JSON.stringify(TileSetElevation)) as ConfigTileSetRaster;
291+
292+
tileSet.name = name;
293+
tileSet.id = `ts_${name}`;
294+
295+
return tileSet;
296+
}
297+
289298
static bundle(configs: BaseConfig[]): string {
290299
const cfg = new ConfigProviderMemory();
291300
for (const rec of configs) cfg.put(rec);

packages/lambda-tiler/src/routes/__tests__/tile.style.json.test.ts

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import assert from 'node:assert';
22
import { afterEach, before, beforeEach, describe, it } from 'node:test';
33

44
import { ConfigProviderMemory, SourceRaster, StyleJson } from '@basemaps/config';
5+
import { Terrain } from '@basemaps/config/src/config/vector.style.js';
56
import { Env } from '@basemaps/shared';
67
import { createSandbox } from 'sinon';
78

8-
import { FakeData, TileSetElevation } from '../../__tests__/config.data.js';
9+
import { FakeData, TileSetAerial, TileSetElevation } from '../../__tests__/config.data.js';
910
import { Api, mockRequest, mockUrlRequest } from '../../__tests__/xyz.util.js';
1011
import { handler } from '../../index.js';
1112
import { ConfigLoader } from '../../util/config.loader.js';
@@ -48,6 +49,10 @@ describe('/v1/styles', () => {
4849
type: 'raster',
4950
tiles: [`/raster/{z}/{x}/{y}.webp`], // Shouldn't encode the {}
5051
},
52+
basemaps_terrain: {
53+
type: 'raster-dem',
54+
tiles: [`/elevation/{z}/{x}/{y}.png?pipeline=terrain-rgb`],
55+
},
5156
test_vector: {
5257
type: 'vector',
5358
url: 'vector.url.co.nz',
@@ -127,6 +132,11 @@ describe('/v1/styles', () => {
127132
tiles: [`${host}/raster/{z}/{x}/{y}.webp?api=${Api.key}`],
128133
};
129134

135+
fakeStyle.sources['basemaps_terrain'] = {
136+
type: 'raster-dem',
137+
tiles: [`${host}/elevation/{z}/{x}/{y}.png?pipeline=terrain-rgb&api=${Api.key}`],
138+
};
139+
130140
fakeStyle.sprite = `${host}/sprite`;
131141
fakeStyle.glyphs = `${host}/glyphs`;
132142

@@ -257,11 +267,70 @@ describe('/v1/styles', () => {
257267
},
258268
]);
259269

260-
const rasterDemSource = body.sources['basemaps-elevation'] as unknown as SourceRaster;
270+
const rasterDemSource = body.sources['LINZ-Terrain'] as unknown as SourceRaster;
261271

262272
assert.deepEqual(rasterDemSource.type, 'raster-dem');
263273
assert.deepEqual(rasterDemSource.tiles, [
264274
`https://tiles.test/v1/tiles/elevation/WebMercatorQuad/{z}/{x}/{y}.png?api=${Api.key}&config=${configId}&pipeline=terrain-rgb`,
265275
]);
266276
});
277+
278+
const fakeStyleConfig = {
279+
id: 'test',
280+
name: 'test',
281+
sources: {
282+
basemaps_raster: {
283+
type: 'raster',
284+
tiles: [`/raster/{z}/{x}/{y}.webp`],
285+
},
286+
basemaps_terrain: {
287+
type: 'raster-dem',
288+
tiles: [`/elevation/{z}/{x}/{y}.png?pipeline=terrain-rgb`],
289+
},
290+
},
291+
layers: [
292+
{
293+
layout: {
294+
visibility: 'visible',
295+
},
296+
paint: {
297+
'background-color': 'rgba(206, 229, 242, 1)',
298+
},
299+
id: 'Background1',
300+
type: 'background',
301+
minzoom: 0,
302+
},
303+
],
304+
};
305+
306+
const fakeAerialRecord = {
307+
id: 'st_aerial',
308+
name: 'aerial',
309+
style: fakeStyleConfig,
310+
};
311+
312+
it('should set terrain via parameter for style config', async () => {
313+
const request = mockUrlRequest('/v1/styles/aerial.json', '?terrain=basemaps_terrain', Api.header);
314+
config.put(fakeAerialRecord);
315+
const res = await handler.router.handle(request);
316+
assert.equal(res.status, 200, res.statusDescription);
317+
318+
const body = JSON.parse(Buffer.from(res.body, 'base64').toString()) as StyleJson;
319+
const terrain = body.terrain as unknown as Terrain;
320+
assert.deepEqual(terrain.source, 'basemaps_terrain');
321+
assert.deepEqual(terrain.exaggeration, 1.2);
322+
});
323+
324+
it('should set terrain via parameter for tileSet config', async () => {
325+
config.put(TileSetAerial);
326+
config.put(TileSetElevation);
327+
const request = mockUrlRequest('/v1/styles/aerial.json', `?terrain=LINZ-Terrain`, Api.header);
328+
const res = await handler.router.handle(request);
329+
assert.equal(res.status, 200, res.statusDescription);
330+
331+
const body = JSON.parse(Buffer.from(res.body, 'base64').toString()) as StyleJson;
332+
const terrain = body.terrain as unknown as Terrain;
333+
assert.deepEqual(terrain.source, 'LINZ-Terrain');
334+
assert.deepEqual(terrain.exaggeration, 1.2);
335+
});
267336
});

packages/lambda-tiler/src/routes/tile.style.json.ts

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ConfigTileSetRaster, Layer, Sources, StyleJson, TileSetType } from '@basemaps/config';
1+
import { ConfigId, ConfigPrefix, ConfigTileSetRaster, Layer, Sources, StyleJson, TileSetType } from '@basemaps/config';
22
import { GoogleTms, TileMatrixSet, TileMatrixSets } from '@basemaps/geo';
33
import { Env, toQueryString } from '@basemaps/shared';
44
import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
@@ -80,11 +80,41 @@ export interface StyleGet {
8080
};
8181
}
8282

83+
function setStyleTerrain(style: StyleJson, terrain: string): void {
84+
const source = Object.keys(style.sources).find((s) => s === terrain);
85+
if (source == null) throw new LambdaHttpResponse(400, `Terrain: ${terrain} is not exists in the style source.`);
86+
style.terrain = {
87+
source,
88+
exaggeration: 1.2,
89+
};
90+
}
91+
92+
async function ensureTerrain(
93+
req: LambdaHttpRequest<StyleGet>,
94+
tileMatrix: TileMatrixSet,
95+
apiKey: string,
96+
style: StyleJson,
97+
): Promise<void> {
98+
const config = await ConfigLoader.load(req);
99+
const terrain = await config.TileSet.get('ts_elevation');
100+
if (terrain) {
101+
const configLocation = ConfigLoader.extract(req);
102+
const elevationQuery = toQueryString({ config: configLocation, api: apiKey, pipeline: 'terrain-rgb' });
103+
style.sources['LINZ-Terrain'] = {
104+
type: 'raster-dem',
105+
tileSize: 256,
106+
maxzoom: 18,
107+
tiles: [convertRelativeUrl(`/v1/tiles/elevation/${tileMatrix.identifier}/{z}/{x}/{y}.png${elevationQuery}`)],
108+
};
109+
}
110+
}
111+
83112
export async function tileSetToStyle(
84113
req: LambdaHttpRequest<StyleGet>,
85114
tileSet: ConfigTileSetRaster,
86115
tileMatrix: TileMatrixSet,
87116
apiKey: string,
117+
terrain?: string,
88118
): Promise<LambdaHttpResponse> {
89119
const [tileFormat] = Validate.getRequestedFormats(req) ?? ['webp'];
90120
if (tileFormat == null) return new LambdaHttpResponse(400, 'Invalid image format');
@@ -100,26 +130,19 @@ export async function tileSetToStyle(
100130
`/v1/tiles/${tileSet.name}/${tileMatrix.identifier}/{z}/{x}/{y}.${tileFormat}${query}`;
101131

102132
const styleId = `basemaps-${tileSet.name}`;
103-
const style = {
133+
const style: StyleJson = {
134+
id: ConfigId.prefix(ConfigPrefix.Style, tileSet.name),
135+
name: tileSet.name,
104136
version: 8,
105137
sources: { [styleId]: { type: 'raster', tiles: [tileUrl], tileSize: 256 } },
106138
layers: [{ id: styleId, type: 'raster', source: styleId }],
107139
};
108140

109-
// Add terrain source if elevation tileset exists in the config.
110-
const config = await ConfigLoader.load(req);
111-
const tsElevation = await config.TileSet.get('ts_elevation');
112-
if (tsElevation) {
113-
const elevationQuery = toQueryString({ config: configLocation, api: apiKey, pipeline: 'terrain-rgb' });
114-
const elevationUrl =
115-
(Env.get(Env.PublicUrlBase) ?? '') +
116-
`/v1/tiles/${tsElevation.name}/${tileMatrix.identifier}/{z}/{x}/{y}.png${elevationQuery}`;
117-
style.sources[`basemaps-${tsElevation.name}`] = {
118-
type: 'raster-dem',
119-
tiles: [elevationUrl],
120-
tileSize: 256,
121-
};
122-
}
141+
// Ensure elevation for individual tilesets
142+
await ensureTerrain(req, tileMatrix, apiKey, style);
143+
144+
// Add terrain in style
145+
if (terrain) setStyleTerrain(style, terrain);
123146

124147
const data = Buffer.from(JSON.stringify(style));
125148

@@ -139,6 +162,7 @@ export function tileSetOutputToStyle(
139162
tileSet: ConfigTileSetRaster,
140163
tileMatrix: TileMatrixSet,
141164
apiKey: string,
165+
terrain?: string,
142166
): Promise<LambdaHttpResponse> {
143167
const configLocation = ConfigLoader.extract(req);
144168
const query = toQueryString({ config: configLocation, api: apiKey });
@@ -189,7 +213,16 @@ export function tileSetOutputToStyle(
189213
}
190214
}
191215

192-
const style = { version: 8, sources, layers };
216+
const style: StyleJson = {
217+
id: ConfigId.prefix(ConfigPrefix.Style, tileSet.name),
218+
name: tileSet.name,
219+
version: 8,
220+
sources,
221+
layers,
222+
};
223+
224+
// Add terrain in style
225+
if (terrain) setStyleTerrain(style, terrain);
193226

194227
const data = Buffer.from(JSON.stringify(style));
195228

@@ -211,6 +244,7 @@ export async function styleJsonGet(req: LambdaHttpRequest<StyleGet>): Promise<La
211244
const excluded = new Set(excludeLayers.map((l) => l.toLowerCase()));
212245
const tileMatrix = TileMatrixSets.find(req.query.get('tileMatrix') ?? GoogleTms.identifier);
213246
if (tileMatrix == null) return new LambdaHttpResponse(400, 'Invalid tile matrix');
247+
const terrain = req.query.get('terrain') ?? undefined;
214248

215249
// Get style Config from db
216250
const config = await ConfigLoader.load(req);
@@ -221,8 +255,8 @@ export async function styleJsonGet(req: LambdaHttpRequest<StyleGet>): Promise<La
221255
const tileSet = await config.TileSet.get(config.TileSet.id(styleName));
222256
if (tileSet == null) return NotFound();
223257
if (tileSet.type !== TileSetType.Raster) return NotFound();
224-
if (tileSet.outputs) return tileSetOutputToStyle(req, tileSet, tileMatrix, apiKey);
225-
else return tileSetToStyle(req, tileSet, tileMatrix, apiKey);
258+
if (tileSet.outputs) return tileSetOutputToStyle(req, tileSet, tileMatrix, apiKey, terrain);
259+
else return tileSetToStyle(req, tileSet, tileMatrix, apiKey, terrain);
226260
}
227261

228262
// Prepare sources and add linz source
@@ -233,6 +267,10 @@ export async function styleJsonGet(req: LambdaHttpRequest<StyleGet>): Promise<La
233267
ConfigLoader.extract(req),
234268
styleConfig.style.layers.filter((f) => !excluded.has(f.id.toLowerCase())),
235269
);
270+
271+
// Add terrain in style
272+
if (terrain) setStyleTerrain(style, terrain);
273+
236274
const data = Buffer.from(JSON.stringify(style));
237275

238276
const cacheKey = Etag.key(data);

packages/landing/src/components/map.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export class Basemaps extends Component<unknown, { isLayerSwitcherEnabled: boole
4444
if (location.pitch != null) this.map.setPitch(location.pitch);
4545
};
4646

47+
updateTerrainFromEvent = (): void => {
48+
const terrain = this.map.getTerrain();
49+
Config.map.setTerrain(terrain?.source ?? null);
50+
};
51+
4752
updateBounds = (bounds: maplibregl.LngLatBoundsLike): void => {
4853
if (Config.map.tileMatrix !== GoogleTms) {
4954
// Transform bounds to current tileMatrix
@@ -110,6 +115,7 @@ export class Basemaps extends Component<unknown, { isLayerSwitcherEnabled: boole
110115
this.controlTerrain = null;
111116
}
112117
}
118+
113119
/**
114120
* Only show the scale on GoogleTMS
115121
* As it does not work with the projection logic we are currently using
@@ -238,6 +244,7 @@ export class Basemaps extends Component<unknown, { isLayerSwitcherEnabled: boole
238244
// TODO: Disable updateVisibleLayers for now before we need implement date range slider
239245
// Config.map.on('visibleLayers', this.updateVisibleLayers),
240246
);
247+
this.map.on('terrain', this.updateTerrainFromEvent);
241248

242249
this.updateStyle();
243250
// Need to ensure the debug layer has access to the map

0 commit comments

Comments
 (0)