Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/_infra/src/edge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,12 @@ export class EdgeStack extends cdk.Stack {
'tileMatrix',
'style',
'pipeline',
'terrain',
// Deprecated single character query params for style and projection
's',
'p',
'i', // ?i=:imageryId is deprecated and should be removed at some point
't',
].map(encodeURIComponent),
},
lambdaFunctionAssociations: [],
Expand Down
8 changes: 8 additions & 0 deletions packages/config/src/config/vector.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export interface Layer {
'source-layer'?: string;
}

export interface Terrain {
source: string;
exaggeration: number;
}

export type Source = SourceVector | SourceRaster | SourceRasterDem;

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

/** Layers will be drawn in the order of this array. */
layers: Layer[];

/** OPTIONAL - A global modifier that elevates layers and markers based on a DEM data source */
terrain?: Terrain;
}

export interface ConfigVectorStyle extends ConfigBase {
Expand Down
43 changes: 26 additions & 17 deletions packages/lambda-tiler/src/__tests__/config.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,6 @@ export const TileSetAerial: ConfigTileSetRaster = {
],
};

export const TileSetElevation: ConfigTileSetRaster = {
id: 'ts_elevation',
name: 'elevation',
type: TileSetType.Raster,
description: 'elevation__description',
title: 'Elevation',
category: 'Elevation',
layers: [
{
3857: 'im_01FYWKATAEK2ZTJQ2PX44Y0XNT',
title: 'New Zealand 8m DEM (2012)',
name: 'new-zealand_2012_dem_8m',
},
],
outputs: [DefaultTerrainRgbOutput, DefaultColorRampOutput],
};

export const TileSetVector: ConfigTileSetVector = {
id: 'ts_topographic',
type: TileSetType.Vector,
Expand All @@ -63,6 +46,23 @@ export const TileSetVector: ConfigTileSetVector = {
},
],
};
export const TileSetElevation: ConfigTileSetRaster = {
id: 'ts_elevation',
name: 'elevation',
type: TileSetType.Raster,
description: 'elevation__description',
title: 'Elevation Imagery',
category: 'Elevation',
layers: [
{
2193: 'im_01FYWKAJ86W9P7RWM1VB62KD0H',
3857: 'im_01FYWKATAEK2ZTJQ2PX44Y0XNT',
title: 'New Zealand 8m DEM (2012)',
name: 'new-zealand_2012_dem_8m',
},
],
outputs: [DefaultTerrainRgbOutput, DefaultColorRampOutput],
};

export const Imagery2193: ConfigImagery = {
id: 'im_01FYWKAJ86W9P7RWM1VB62KD0H',
Expand Down Expand Up @@ -286,6 +286,15 @@ export class FakeData {
return tileSet;
}

static tileSetElevation(name: string): ConfigTileSetRaster {
const tileSet = JSON.parse(JSON.stringify(TileSetElevation)) as ConfigTileSetRaster;

tileSet.name = name;
tileSet.id = `ts_${name}`;

return tileSet;
}

static bundle(configs: BaseConfig[]): string {
const cfg = new ConfigProviderMemory();
for (const rec of configs) cfg.put(rec);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import assert from 'node:assert';
import { afterEach, before, beforeEach, describe, it } from 'node:test';

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

import { FakeData, TileSetElevation } from '../../__tests__/config.data.js';
import { FakeData, TileSetAerial, TileSetElevation } from '../../__tests__/config.data.js';
import { Api, mockRequest, mockUrlRequest } from '../../__tests__/xyz.util.js';
import { handler } from '../../index.js';
import { ConfigLoader } from '../../util/config.loader.js';
Expand Down Expand Up @@ -48,6 +49,10 @@ describe('/v1/styles', () => {
type: 'raster',
tiles: [`/raster/{z}/{x}/{y}.webp`], // Shouldn't encode the {}
},
basemaps_terrain: {
type: 'raster-dem',
tiles: [`/elevation/{z}/{x}/{y}.png?pipeline=terrain-rgb`],
},
test_vector: {
type: 'vector',
url: 'vector.url.co.nz',
Expand Down Expand Up @@ -127,6 +132,11 @@ describe('/v1/styles', () => {
tiles: [`${host}/raster/{z}/{x}/{y}.webp?api=${Api.key}`],
};

fakeStyle.sources['basemaps_terrain'] = {
type: 'raster-dem',
tiles: [`${host}/elevation/{z}/{x}/{y}.png?pipeline=terrain-rgb&api=${Api.key}`],
};

fakeStyle.sprite = `${host}/sprite`;
fakeStyle.glyphs = `${host}/glyphs`;

Expand Down Expand Up @@ -257,11 +267,70 @@ describe('/v1/styles', () => {
},
]);

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

assert.deepEqual(rasterDemSource.type, 'raster-dem');
assert.deepEqual(rasterDemSource.tiles, [
`https://tiles.test/v1/tiles/elevation/WebMercatorQuad/{z}/{x}/{y}.png?api=${Api.key}&config=${configId}&pipeline=terrain-rgb`,
]);
});

const fakeStyleConfig = {
id: 'test',
name: 'test',
sources: {
basemaps_raster: {
type: 'raster',
tiles: [`/raster/{z}/{x}/{y}.webp`],
},
basemaps_terrain: {
type: 'raster-dem',
tiles: [`/elevation/{z}/{x}/{y}.png?pipeline=terrain-rgb`],
},
},
layers: [
{
layout: {
visibility: 'visible',
},
paint: {
'background-color': 'rgba(206, 229, 242, 1)',
},
id: 'Background1',
type: 'background',
minzoom: 0,
},
],
};

const fakeAerialRecord = {
id: 'st_aerial',
name: 'aerial',
style: fakeStyleConfig,
};

it('should set terrain via parameter for style config', async () => {
const request = mockUrlRequest('/v1/styles/aerial.json', '?terrain=basemaps_terrain', Api.header);
config.put(fakeAerialRecord);
const res = await handler.router.handle(request);
assert.equal(res.status, 200, res.statusDescription);

const body = JSON.parse(Buffer.from(res.body, 'base64').toString()) as StyleJson;
const terrain = body.terrain as unknown as Terrain;
assert.deepEqual(terrain.source, 'basemaps_terrain');
assert.deepEqual(terrain.exaggeration, 1.2);
});

it('should set terrain via parameter for tileSet config', async () => {
config.put(TileSetAerial);
config.put(TileSetElevation);
const request = mockUrlRequest('/v1/styles/aerial.json', `?terrain=LINZ-Terrain`, Api.header);
const res = await handler.router.handle(request);
assert.equal(res.status, 200, res.statusDescription);

const body = JSON.parse(Buffer.from(res.body, 'base64').toString()) as StyleJson;
const terrain = body.terrain as unknown as Terrain;
assert.deepEqual(terrain.source, 'LINZ-Terrain');
assert.deepEqual(terrain.exaggeration, 1.2);
});
});
76 changes: 57 additions & 19 deletions packages/lambda-tiler/src/routes/tile.style.json.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConfigTileSetRaster, Layer, Sources, StyleJson, TileSetType } from '@basemaps/config';
import { ConfigId, ConfigPrefix, ConfigTileSetRaster, Layer, Sources, StyleJson, TileSetType } from '@basemaps/config';
import { GoogleTms, TileMatrixSet, TileMatrixSets } from '@basemaps/geo';
import { Env, toQueryString } from '@basemaps/shared';
import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
Expand Down Expand Up @@ -80,11 +80,41 @@ export interface StyleGet {
};
}

function setStyleTerrain(style: StyleJson, terrain: string): void {
const source = Object.keys(style.sources).find((s) => s === terrain);
if (source == null) throw new LambdaHttpResponse(400, `Terrain: ${terrain} is not exists in the style source.`);
style.terrain = {
source,
exaggeration: 1.2,
};
}

async function ensureTerrain(
req: LambdaHttpRequest<StyleGet>,
tileMatrix: TileMatrixSet,
apiKey: string,
style: StyleJson,
): Promise<void> {
const config = await ConfigLoader.load(req);
const terrain = await config.TileSet.get('ts_elevation');
if (terrain) {
const configLocation = ConfigLoader.extract(req);
const elevationQuery = toQueryString({ config: configLocation, api: apiKey, pipeline: 'terrain-rgb' });
style.sources['LINZ-Terrain'] = {
type: 'raster-dem',
tileSize: 256,
maxzoom: 18,
tiles: [convertRelativeUrl(`/v1/tiles/elevation/${tileMatrix.identifier}/{z}/{x}/{y}.png${elevationQuery}`)],
};
}
}

export async function tileSetToStyle(
req: LambdaHttpRequest<StyleGet>,
tileSet: ConfigTileSetRaster,
tileMatrix: TileMatrixSet,
apiKey: string,
terrain?: string,
): Promise<LambdaHttpResponse> {
const [tileFormat] = Validate.getRequestedFormats(req) ?? ['webp'];
if (tileFormat == null) return new LambdaHttpResponse(400, 'Invalid image format');
Expand All @@ -100,26 +130,19 @@ export async function tileSetToStyle(
`/v1/tiles/${tileSet.name}/${tileMatrix.identifier}/{z}/{x}/{y}.${tileFormat}${query}`;

const styleId = `basemaps-${tileSet.name}`;
const style = {
const style: StyleJson = {
id: ConfigId.prefix(ConfigPrefix.Style, tileSet.name),
name: tileSet.name,
version: 8,
sources: { [styleId]: { type: 'raster', tiles: [tileUrl], tileSize: 256 } },
layers: [{ id: styleId, type: 'raster', source: styleId }],
};

// Add terrain source if elevation tileset exists in the config.
const config = await ConfigLoader.load(req);
const tsElevation = await config.TileSet.get('ts_elevation');
if (tsElevation) {
const elevationQuery = toQueryString({ config: configLocation, api: apiKey, pipeline: 'terrain-rgb' });
const elevationUrl =
(Env.get(Env.PublicUrlBase) ?? '') +
`/v1/tiles/${tsElevation.name}/${tileMatrix.identifier}/{z}/{x}/{y}.png${elevationQuery}`;
style.sources[`basemaps-${tsElevation.name}`] = {
type: 'raster-dem',
tiles: [elevationUrl],
tileSize: 256,
};
}
// Ensure elevation for individual tilesets
await ensureTerrain(req, tileMatrix, apiKey, style);

// Add terrain in style
if (terrain) setStyleTerrain(style, terrain);

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

Expand All @@ -139,6 +162,7 @@ export function tileSetOutputToStyle(
tileSet: ConfigTileSetRaster,
tileMatrix: TileMatrixSet,
apiKey: string,
terrain?: string,
): Promise<LambdaHttpResponse> {
const configLocation = ConfigLoader.extract(req);
const query = toQueryString({ config: configLocation, api: apiKey });
Expand Down Expand Up @@ -189,7 +213,16 @@ export function tileSetOutputToStyle(
}
}

const style = { version: 8, sources, layers };
const style: StyleJson = {
id: ConfigId.prefix(ConfigPrefix.Style, tileSet.name),
name: tileSet.name,
version: 8,
sources,
layers,
};

// Add terrain in style
if (terrain) setStyleTerrain(style, terrain);

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

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

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

// Prepare sources and add linz source
Expand All @@ -233,6 +267,10 @@ export async function styleJsonGet(req: LambdaHttpRequest<StyleGet>): Promise<La
ConfigLoader.extract(req),
styleConfig.style.layers.filter((f) => !excluded.has(f.id.toLowerCase())),
);

// Add terrain in style
if (terrain) setStyleTerrain(style, terrain);

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

const cacheKey = Etag.key(data);
Expand Down
7 changes: 7 additions & 0 deletions packages/landing/src/components/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export class Basemaps extends Component<unknown, { isLayerSwitcherEnabled: boole
if (location.pitch != null) this.map.setPitch(location.pitch);
};

updateTerrainFromEvent = (): void => {
const terrain = this.map.getTerrain();
Config.map.setTerrain(terrain?.source ?? null);
};

updateBounds = (bounds: maplibregl.LngLatBoundsLike): void => {
if (Config.map.tileMatrix !== GoogleTms) {
// Transform bounds to current tileMatrix
Expand Down Expand Up @@ -110,6 +115,7 @@ export class Basemaps extends Component<unknown, { isLayerSwitcherEnabled: boole
this.controlTerrain = null;
}
}

/**
* Only show the scale on GoogleTMS
* As it does not work with the projection logic we are currently using
Expand Down Expand Up @@ -238,6 +244,7 @@ export class Basemaps extends Component<unknown, { isLayerSwitcherEnabled: boole
// TODO: Disable updateVisibleLayers for now before we need implement date range slider
// Config.map.on('visibleLayers', this.updateVisibleLayers),
);
this.map.on('terrain', this.updateTerrainFromEvent);

this.updateStyle();
// Need to ensure the debug layer has access to the map
Expand Down
Loading