Skip to content

Commit

Permalink
feat(config): serve tilejson 3.0.0 and allow raster imagery (#2173)
Browse files Browse the repository at this point in the history
* feat(config): serve tilejson 3.0.0 and allow raster imagery

* refactor: maxzoom of 30 like tilejson

* refactor: remove caching on tile.json
  • Loading branch information
blacha authored May 5, 2022
1 parent 4ad2d37 commit 29f5313
Show file tree
Hide file tree
Showing 20 changed files with 272 additions and 115 deletions.
20 changes: 14 additions & 6 deletions packages/config/src/config/tile.set.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EpsgCode } from '@basemaps/geo';
import { EpsgCode, ImageFormat, VectorFormat } from '@basemaps/geo';
import { BaseConfig } from './base.js';

export enum TileSetType {
Expand All @@ -13,7 +13,7 @@ export interface ConfigLayer extends Partial<Record<EpsgCode, string>> {
/** Minimal zoom to show the layer @default 0 */
minZoom?: number;

/** Max zoom to show the layer @default 32 */
/** Max zoom to show the layer @default 30 */
maxZoom?: number;
}

Expand All @@ -31,19 +31,27 @@ export interface ConfigTileSetBase extends BaseConfig {
*/
layers: ConfigLayer[];

/** Minimum zoom level for this tileSet @default 0 */
minZoom?: number;
/** Maximum zoom level for this tileSet @default 30 */
maxZoom?: number;
}

export interface ConfigTileSetRaster extends ConfigTileSetBase {
type: TileSetType.Raster;
/** Preferred imagery format to use */
format: ImageFormat;

/** Background to render for areas where there is no data */
background?: { r: number; g: number; b: number; alpha: number };

/** When scaling tiles in the rendering process what kernel to use */
resizeKernel?: { in: TileResizeKernel; out: TileResizeKernel };
}

export interface ConfigTileSetRaster extends ConfigTileSetBase {
type: TileSetType.Raster;
}

export interface ConfigTileSetVector extends ConfigTileSetBase {
type: TileSetType.Vector;
format: VectorFormat;
}

export type ConfigTileSet = ConfigTileSetVector | ConfigTileSetRaster;
12 changes: 12 additions & 0 deletions packages/geo/src/formats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/** Image formats supported by basemaps */
export enum ImageFormat {
Png = 'png',
Jpeg = 'jpeg',
Webp = 'webp',
Avif = 'avif',
}

/** Vector tile formats supported by basemaps */
export enum VectorFormat {
MapboxVectorTiles = 'pbf',
}
2 changes: 2 additions & 0 deletions packages/geo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export { AttributionCollection, AttributionItem, AttributionStac } from './stac/
export { TileMatrixSets } from './tms/index.js';
export { Nztm2000Tms, Nztm2000QuadTms } from './tms/nztm2000.js';
export { GoogleTms } from './tms/google.js';
export { ImageFormat, VectorFormat } from './formats.js';
export { TileJson, TileJsonV3, TileJsonVectorLayer } from './tile.json/tile.json.js';
94 changes: 94 additions & 0 deletions packages/geo/src/tile.json/tile.json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
export type TileJson = TileJsonV3;

export interface TileJsonV3 {
/** Specification: https://github.com/mapbox/tilejson-spec/tree/master/3.0.0 */
tilejson: '3.0.0';

/**
* A semver.org style version number of the tiles.
* @default '1.0.0'
*/
version?: string;

/**
* An array of tile endpoints. {z}, {x} and {y}, if present, are replaced with the corresponding integers.
*
* If multiple endpoints are specified, clients may use any combination of endpoints.
*
* All endpoint urls MUST be absolute.
* All endpoints MUST return the same content for the same URL.
* The array MUST contain at least one endpoint.
*/
tiles: string[];

/** A name describing the set of tiles. */
name?: string;
/** A text description of the set of tiles */
description?: string;

/**
* Either "xyz" or "tms". Influences the y direction of the tile coordinates.
* @default 'xyz'
*/
scheme?: 'xyz' | 'tms';
/**
* An integer specifying the minimum zoom level.
*
* MUST be in range: 0 <= minzoom <= maxzoom <= 30.
*
* @default 0
*/
minzoom?: number;
/**
* An integer specifying the maximum zoom level.
*
* MUST be in range: 0 <= minzoom <= maxzoom <= 30
*
* @default 30
*/
maxzoom?: number;

/**
* The maximum extent of available map tiles. Bounds MUST define an area covered by all zoom levels.
*
* The bounds are represented in WGS 84 latitude and longitude values, in the order left, bottom, right, top
*/
bounds?: number[] | [number, number, number, number];
/**
* The first value is the longitude, the second is latitude (both in WGS:84 values), the third value is the zoom level as an integer.
* Longitude and latitude MUST be within the specified bounds
*/
center?: number[] | [number, number] | [number, number, number];

/** An array of objects. Each object describes one layer of vector tile data. */
vector_layers: TileJsonVectorLayer[];
}

export interface TileJsonVectorLayer {
/** A string value representing the the layer id. For added context, this is referred to as the name of the layer in the Mapbox Vector Tile spec. */
id: string;
/** A string representing a human-readable description of the entire layer's contents. */
description?: string;
/**
* An integer representing the lowest level whose tiles this layer appears in
*
* minzoom MUST be greater than or equal to the set of tiles' minzoom.
*/
minzoom?: number;

/**
* An integer representing the highest level whose tiles this layer appears in.
*
* maxzoom MUST be less than or equal to the set of tiles' maxzoom.
*/
maxzoom?: number;

/**
* An object whose keys and values are the names and descriptions of attributes available in this layer.
*
* Each value (description) MUST be a string that describes the underlying data.
*
* If no fields are present, the fields key MUST be an empty object.
*/
fields: Record<string, string>;
}
6 changes: 3 additions & 3 deletions packages/lambda-tiler/src/__test__/tile.cache.key.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { GoogleTms, Nztm2000Tms } from '@basemaps/geo';
import { GoogleTms, Nztm2000Tms, ImageFormat } from '@basemaps/geo';
import { TileDataXyz, TileType } from '@basemaps/shared';
import { TestTiff } from '@basemaps/test';
import { Composition, ImageFormat } from '@basemaps/tiler';
import { Composition } from '@basemaps/tiler';
import o from 'ospec';
import { TileEtag } from '../routes/tile.etag.js';

Expand All @@ -14,7 +14,7 @@ o.spec('TileCacheKey', () => {
z: 0,
tileMatrix: GoogleTms,
name: 'foo',
ext: ImageFormat.PNG,
ext: ImageFormat.Png,
type: TileType.Tile,
};

Expand Down
87 changes: 59 additions & 28 deletions packages/lambda-tiler/src/__test__/xyz.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ConfigProvider, StyleJson } from '@basemaps/config';
import { TileMatrixSets } from '@basemaps/geo';
import { GoogleTms, Nztm2000QuadTms, TileMatrixSets } from '@basemaps/geo';
import { Config, Env, LogConfig, VNodeParser } from '@basemaps/shared';
import { round } from '@basemaps/test/build/rounding.js';
import o from 'ospec';
Expand All @@ -8,7 +8,7 @@ import { handleRequest } from '../index.js';
import { TileEtag } from '../routes/tile.etag.js';
import { TileSets } from '../tile.set.cache.js';
import { TileComposer } from '../tile.set.raster.js';
import { FakeTileSet, mockRequest, Provider } from './xyz.util.js';
import { FakeTileSet, FakeTileSetVector, mockRequest, Provider } from './xyz.util.js';

const sandbox = sinon.createSandbox();

Expand Down Expand Up @@ -53,6 +53,8 @@ o.spec('LambdaXyz', () => {
}
}

TileSets.add(new FakeTileSetVector('topographic', GoogleTms));

(Config.Provider as any).get = async (): Promise<ConfigProvider> => Provider;
});

Expand Down Expand Up @@ -186,51 +188,80 @@ o.spec('LambdaXyz', () => {
});

o.spec('tileJson', () => {
o('should 304 if a json is not modified', async () => {
// delete process.env[Env.PublicUrlBase];
o('should 404 if invalid url is given', async () => {
const request = mockRequest('/v1/tiles/tile.json', 'get', apiKeyHeader);

const key = 'BBfQpNXA3Q90jlFrLSOZhxbvfOh7OpN1OEE+BylpbHw=';
const request = mockRequest('/v1/tiles/tile.json', 'get', { 'if-none-match': key, ...apiKeyHeader });
const res = await handleRequest(request);
o(res.status).equals(404);
});

o('should serve tile json for tile_set', async () => {
const request = mockRequest('/v1/tiles/aerial/NZTM2000Quad/tile.json', 'get', apiKeyHeader);

const res = await handleRequest(request);
if (res.status === 200) {
o(res.header('eTaG')).equals(key); // this line is useful for discovering the new etag
return;
}
o(res.status).equals(200);
o(res.header('cache-control')).equals('no-store');

o(res.status).equals(304);
o(rasterMock.calls.length).equals(0);
const body = Buffer.from(res.body ?? '', 'base64').toString();
o(JSON.parse(body)).deepEquals({
tiles: [`https://tiles.test/v1/tiles/aerial/NZTM2000Quad/{z}/{x}/{y}.webp?api=${apiKey}`],
vector_layers: [],
tilejson: '3.0.0',
});
});

o(request.logContext['cache']).deepEquals({ key, match: key, hit: true });
o('should serve vector tiles', async () => {
const request = mockRequest('/v1/tiles/topographic/WebMercatorQuad/tile.json', 'get', apiKeyHeader);

const res = await handleRequest(request);
o(res.status).equals(200);

const body = Buffer.from(res.body ?? '', 'base64').toString();
o(JSON.parse(body)).deepEquals({
tiles: [`https://tiles.test/v1/tiles/topographic/EPSG:3857/{z}/{x}/{y}.pbf?api=${apiKey}`],
vector_layers: [],
tilejson: '3.0.0',
});
});

o('should 200 if a invalid etag is given', async () => {
const key = 'ABCXecTdbcdjCyzB1MHOOQbrOkD2TTJ0ORh4JuXqhxXEE0=';
const request = mockRequest('/v1/tiles/tile.json', 'get', { 'if-none-match': key, ...apiKeyHeader });
o('should serve vector tiles with min/max zoom', async () => {
const fakeTileSet = new FakeTileSetVector('fake-vector', GoogleTms);
fakeTileSet.tileSet.maxZoom = 15;
fakeTileSet.tileSet.minZoom = 3;
TileSets.add(fakeTileSet);
const request = mockRequest('/v1/tiles/fake-vector/WebMercatorQuad/tile.json', 'get', apiKeyHeader);

const res = await handleRequest(request);
o(res.status).equals(200);
o(res.header('etag')).equals('BBfQpNXA3Q90jlFrLSOZhxbvfOh7OpN1OEE+BylpbHw=');
const out = JSON.parse(Buffer.from(res.body ?? '', 'base64').toString());
o(out.tiles[0].startsWith('https://tiles.test/v1/tiles/tile.json/undefined/{z}/{x}/{y}.pbf?api=')).equals(true);
o(request.logContext['cache']).deepEquals(undefined);

const body = Buffer.from(res.body ?? '', 'base64').toString();
o(JSON.parse(body)).deepEquals({
tiles: [`https://tiles.test/v1/tiles/fake-vector/EPSG:3857/{z}/{x}/{y}.pbf?api=${apiKey}`],
vector_layers: [],
maxzoom: 15,
minzoom: 3,
tilejson: '3.0.0',
});
});

o('should serve tile json for tile_set', async () => {
const request = mockRequest('/v1/tiles/topographic/Google/tile.json', 'get', apiKeyHeader);
o('should serve convert zoom to tile matrix', async () => {
const fakeTileSet = new FakeTileSetVector('fake-vector', Nztm2000QuadTms);
fakeTileSet.tileSet.maxZoom = 15;
fakeTileSet.tileSet.minZoom = 1;
TileSets.add(fakeTileSet);

const request = mockRequest('/v1/tiles/fake-vector/NZTM2000Quad/tile.json', 'get', apiKeyHeader);

const res = await handleRequest(request);
o(res.status).equals(200);
o(res.header('content-type')).equals('application/json');
o(res.header('cache-control')).equals('max-age=120');

const body = Buffer.from(res.body ?? '', 'base64').toString();
o(JSON.parse(body)).deepEquals({
tiles: [`https://tiles.test/v1/tiles/topographic/Google/{z}/{x}/{y}.pbf?api=${apiKey}`],
tiles: [`https://tiles.test/v1/tiles/fake-vector/NZTM2000Quad/{z}/{x}/{y}.pbf?api=${apiKey}`],
vector_layers: [],
maxzoom: 13,
minzoom: 0,
maxzoom: 15,
format: 'pbf',
tilejson: '2.0.0',
tilejson: '3.0.0',
});
});
});
Expand Down
12 changes: 10 additions & 2 deletions packages/lambda-tiler/src/__test__/xyz.util.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ConfigProvider } from '@basemaps/config';
import { TileMatrixSet } from '@basemaps/geo';
import { LambdaHttpRequest, LambdaAlbRequest } from '@linzjs/lambda';
import { LogConfig } from '@basemaps/shared';
import { TileSetRaster } from '../tile.set.raster.js';
import { LambdaAlbRequest, LambdaHttpRequest } from '@linzjs/lambda';
import { Context } from 'aws-lambda';
import { TileSetRaster } from '../tile.set.raster.js';
import { TileSetVector } from '../tile.set.vector.js';

export function mockRequest(path: string, method = 'get', headers: Record<string, string> = {}): LambdaHttpRequest {
return new LambdaAlbRequest(
Expand All @@ -27,6 +28,13 @@ export class FakeTileSet extends TileSetRaster {
}
}

export class FakeTileSetVector extends TileSetVector {
constructor(name: string, tileMatrix: TileMatrixSet) {
super(name, tileMatrix);
this.tileSet = {} as any;
}
}

export const Provider: ConfigProvider = {
createdAt: Date.now(),
name: 'main',
Expand Down
5 changes: 2 additions & 3 deletions packages/lambda-tiler/src/cli/dump.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Nztm2000Tms } from '@basemaps/geo';
import { Nztm2000Tms, ImageFormat } from '@basemaps/geo';
import { LogConfig } from '@basemaps/shared';
import { ImageFormat } from '@basemaps/tiler';
import { LambdaAlbRequest } from '@linzjs/lambda';
import { Context } from 'aws-lambda';
import { promises as fs } from 'fs';
Expand All @@ -12,7 +11,7 @@ import { TileSetLocal } from './tile.set.local.js';
const xyz = { x: 0, y: 0, z: 0 };
const tileMatrix = Nztm2000Tms;
const tileSetName = 'aerial';
const ext = ImageFormat.PNG;
const ext = ImageFormat.Png;

/** Load a tileset form a file path otherwise default to the hard coded one from AWS */
async function getTileSet(filePath?: string): Promise<TileSet> {
Expand Down
7 changes: 3 additions & 4 deletions packages/lambda-tiler/src/routes/health.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { GoogleTms, Nztm2000QuadTms } from '@basemaps/geo';
import { GoogleTms, Nztm2000QuadTms, ImageFormat } from '@basemaps/geo';
import { TileDataXyz, TileType } from '@basemaps/shared';
import { ImageFormat } from '@basemaps/tiler';
import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
import * as fs from 'fs';
import * as path from 'path';
Expand All @@ -14,8 +13,8 @@ interface TestTile extends TileDataXyz {
}

export const TestTiles: TestTile[] = [
{ type: TileType.Tile, name: 'health', tileMatrix: GoogleTms, ext: ImageFormat.PNG, x: 252, y: 156, z: 8 },
{ type: TileType.Tile, name: 'health', tileMatrix: Nztm2000QuadTms, ext: ImageFormat.PNG, x: 30, y: 33, z: 6 },
{ type: TileType.Tile, name: 'health', tileMatrix: GoogleTms, ext: ImageFormat.Png, x: 252, y: 156, z: 8 },
{ type: TileType.Tile, name: 'health', tileMatrix: Nztm2000QuadTms, ext: ImageFormat.Png, x: 30, y: 33, z: 6 },
];
const TileSize = 256;

Expand Down
Loading

0 comments on commit 29f5313

Please sign in to comment.