Skip to content

Commit

Permalink
Add terrain into style json
Browse files Browse the repository at this point in the history
  • Loading branch information
Wentao-Kuang committed Jun 26, 2024
1 parent 17eaf50 commit 4c5e1f3
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 50 deletions.
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
44 changes: 25 additions & 19 deletions packages/lambda-tiler/src/__tests__/config.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import {
ConfigProviderMemory,
ConfigTileSetRaster,
ConfigTileSetVector,
DefaultColorRampOutput,
DefaultTerrainRgbOutput,
TileSetType,
} from '@basemaps/config';
import { fsa, FsMemory } from '@basemaps/shared';
Expand All @@ -30,23 +28,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 +44,22 @@ 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',
},
],
};

export const Imagery2193: ConfigImagery = {
id: 'im_01FYWKAJ86W9P7RWM1VB62KD0H',
Expand Down Expand Up @@ -286,6 +283,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
73 changes: 71 additions & 2 deletions packages/lambda-tiler/src/routes/__tests__/tile.style.json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { ConfigProviderMemory, SourceRaster, StyleJson } from '@basemaps/config'
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';
import { Terrain } from '@basemaps/config/src/config/vector.style.js';

describe('/v1/styles', () => {
const host = 'https://tiles.test';
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
Loading

0 comments on commit 4c5e1f3

Please sign in to comment.