Skip to content

Commit

Permalink
feat: Allow alternative TileMatrixSet definitions (#1321)
Browse files Browse the repository at this point in the history
  • Loading branch information
jacott committed Dec 3, 2020
1 parent 0b35646 commit b7cfa7b
Show file tree
Hide file tree
Showing 23 changed files with 683 additions and 89 deletions.
2 changes: 1 addition & 1 deletion packages/cli/src/cli/cogify/__test__/action.batch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Aws, NamedBounds } from '@basemaps/shared';
import { qkToNamedBounds } from '@basemaps/shared/build/proj/__test__/test.util';
import { round } from '@basemaps/test/build/rounding';
import o from 'ospec';
import { CogJobJson } from 'packages/cli/src/cog/types';
import { CogJobJson } from '../../../cog/types';
import { CogStacJob } from '../../../cog/cog.stac.job';
import { createImageryRecordFromJob, createMetadataFromJob, extractResolutionFromName } from '../action.batch';

Expand Down
1 change: 1 addition & 0 deletions packages/geo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { Epsg, EpsgCode } from './epsg';
export { QuadKey } from './quad.key';
export { Tile, TileMatrixSet } from './tile.matrix.set';
export { WmtsLayer, WmtsProvider } from './wmts/wmts';
export * from './tms/tile.matrix.set.type';
45 changes: 45 additions & 0 deletions packages/geo/src/tile.matrix.set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function compareMatrix(a: TileMatrixSetTypeMatrix, b: TileMatrixSetTypeMatrix):
export class TileMatrixSet {
/** Projection of the matrix set */
projection: Epsg;

/** Number of pixels for a tile */
tileSize: number;
/**
Expand Down Expand Up @@ -75,6 +76,15 @@ export class TileMatrixSet {
return this.zooms.length - 1;
}

get id(): string {
return TileMatrixSet.getId(this.projection);
}

static getId(epsg: Epsg, altName?: string): string {
if (altName == null) return epsg.toEpsgString();
return epsg.toEpsgString() + ':' + altName;
}

/** Get the pixels / meter at a specified zoom level */
pixelScale(zoom: number): number {
const z = this.zooms[zoom];
Expand Down Expand Up @@ -221,4 +231,39 @@ export class TileMatrixSet {
}
throw new Error(`Invalid tile name '${name}'`);
}

/**
* Convert the tile z value from the Tile Matrix Set to match the TileSet rule filter.
* Override this function to change it
*/
getParentZoom(z: number): number {
return z;
}

/**
* Calculate the scale mapping between two TileMatrixSets based from child to parent
* @param parentTmx
* @param childTmx
*
*/
public static scaleMapping(parentTmx: TileMatrixSet, childTmx: TileMatrixSet): Map<number, number> {
const scaleMap: Map<number, number> = new Map();
for (let i = 0; i < childTmx.zooms.length; i++) {
const child = childTmx.zooms[i];
const index = findBestMatch(child.scaleDenominator, parentTmx.zooms);
scaleMap.set(i, index);
}
return scaleMap;
}
}

/**
* Find the best matching scales from the parent zooms.
*
*/
function findBestMatch(scaleDenominator: number, zooms: TileMatrixSetTypeMatrix[]): number {
for (let i = 0; i < zooms.length; i++) {
if (zooms[i].scaleDenominator < scaleDenominator) return i;
}
return zooms.length - 1;
}
21 changes: 21 additions & 0 deletions packages/lambda-tiler/src/__test__/tiler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Epsg } from '@basemaps/geo';
import { Tilers } from '../tiler';
import o from 'ospec';

o.spec('tiler', () => {
o.spec('getParentZoom', () => {
o('nztm2000', () => {
const tiler = Tilers.get(Epsg.Nztm2000);
o(tiler?.tms?.getParentZoom(10)).equals(10);
});
o('agol', () => {
const tiler = Tilers.get(Epsg.Nztm2000, 'agol');
o(tiler?.tms?.getParentZoom(0)).equals(0);
o(tiler?.tms?.getParentZoom(5)).equals(1);
o(tiler?.tms?.getParentZoom(10)).equals(6);
o(tiler?.tms?.getParentZoom(15)).equals(11);
o(tiler?.tms?.getParentZoom(20)).equals(15);
o(tiler?.tms?.getParentZoom(23)).equals(16);
});
});
});
47 changes: 35 additions & 12 deletions packages/lambda-tiler/src/__test__/wmts.capability.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Epsg, Bounds } from '@basemaps/geo';
import { V, VNodeElement, TileSetName } from '@basemaps/shared';
import { Bounds, Epsg, TileMatrixSet } from '@basemaps/geo';
import { GoogleTms } from '@basemaps/geo/build/tms/google';
import { Nztm2000Tms } from '@basemaps/geo/build/tms/nztm2000';
import { TileSetName, V, VNodeElement } from '@basemaps/shared';
import { roundNumbersInString } from '@basemaps/test/build/rounding';
import { createHash } from 'crypto';
import o from 'ospec';
import { WmtsCapabilities } from '../wmts.capability';
import { Provider, FakeTileSet } from './xyz.util';
import { roundNumbersInString } from '@basemaps/test/build/rounding';
import { FakeTileSet, Provider } from './xyz.util';

function tags(node: VNodeElement | null | undefined, tag: string): VNodeElement[] {
if (node == null) return [];
Expand All @@ -19,8 +21,20 @@ o.spec('WmtsCapabilities', () => {
const tileSet = new FakeTileSet(TileSetName.aerial, Epsg.Google);
const tileSetImagery = new FakeTileSet('01E7PJFR9AMQFJ05X9G7FQ3XMW', Epsg.Google);

const tileMatrixSetMap = new Map<Epsg, TileMatrixSet>([
[Epsg.Google, GoogleTms],
[Epsg.Nztm2000, Nztm2000Tms],
]);

o('should build capability xml for tileset and projection', () => {
const wmts = new WmtsCapabilities('https://basemaps.test', Provider, [tileSet], apiKey);
const wmts = new WmtsCapabilities(
'https://basemaps.test',
Provider,
[tileSet],
tileMatrixSetMap,
undefined,
apiKey,
);

const raw = wmts.toVNode();
const serviceId = raw.find('ows:ServiceIdentification');
Expand Down Expand Up @@ -103,22 +117,31 @@ o.spec('WmtsCapabilities', () => {
compareMatrix(tileMatrices[0], '0', 1, 559082264.028717);
compareMatrix(tileMatrices[10], '10', 1024, 545978.773465544);

const xml = WmtsCapabilities.toXml('https://basemaps.test', Provider, [tileSet], apiKey) ?? '';
const xml =
WmtsCapabilities.toXml('https://basemaps.test', Provider, [tileSet], tileMatrixSetMap, undefined, apiKey) ??
'';

o(xml).deepEquals('<?xml version="1.0"?>\n' + raw?.toString());

o(createHash('sha256').update(Buffer.from(xml)).digest('base64')).equals(
'TRDSi9zHUjylAsdj/8WnnOlP3ocRLSS3JzheGoJ9oQg=',
'T/Ht5RdGQyxQxkVgyZO0hb018OmuUUP7olFMZXvmGUY=',
);
});

o('should return null if not found', () => {
const ts = new FakeTileSet(TileSetName.aerial, { code: 9999 } as Epsg);
o(() => WmtsCapabilities.toXml('basemaps.test', Provider, [ts])).throws('Invalid projection: 9999');
o(() => WmtsCapabilities.toXml('basemaps.test', Provider, [ts], tileMatrixSetMap)).throws(
'Invalid projection: 9999',
);
});

o('should allow individual imagery sets', () => {
const raw = new WmtsCapabilities('https://basemaps.test', Provider, [tileSetImagery]).toVNode();
const raw = new WmtsCapabilities(
'https://basemaps.test',
Provider,
[tileSetImagery],
tileMatrixSetMap,
).toVNode();

const tms = raw?.find('TileMatrixSet', 'ows:Identifier');

Expand All @@ -137,7 +160,7 @@ o.spec('WmtsCapabilities', () => {
new FakeTileSet(TileSetName.aerial, Epsg.Nztm2000),
new FakeTileSet(TileSetName.aerial, Epsg.Google),
];
const xml = new WmtsCapabilities('basemaps.test', Provider, ts);
const xml = new WmtsCapabilities('basemaps.test', Provider, ts, tileMatrixSetMap);
const nodes = xml.toVNode();

const layers = tags(nodes, 'Layer');
Expand Down Expand Up @@ -184,7 +207,7 @@ o.spec('WmtsCapabilities', () => {
new FakeTileSet(TileSetName.aerial, Epsg.Nztm2000, 'aerial-title'),
new FakeTileSet('01E7PJFR9AMQFJ05X9G7FQ3XMW', Epsg.Nztm2000, 'imagery-title'),
];
const nodes = new WmtsCapabilities('basemaps.test', Provider, ts).toVNode();
const nodes = new WmtsCapabilities('basemaps.test', Provider, ts, tileMatrixSetMap).toVNode();
const layers = tags(nodes, 'Layer');
o(layers.length).equals(2);

Expand All @@ -205,7 +228,7 @@ o.spec('WmtsCapabilities', () => {
ts[1].titleOverride = 'override sub tileset 1';

ts[2].tileSet.title = 'aerial_dunedin_urban';
const nodes = new WmtsCapabilities('basemaps.test', Provider, ts).toVNode();
const nodes = new WmtsCapabilities('basemaps.test', Provider, ts, tileMatrixSetMap).toVNode();

const layers = tags(nodes, 'Layer');
o(layers.length).equals(3);
Expand Down
56 changes: 53 additions & 3 deletions packages/lambda-tiler/src/__test__/xyz.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Epsg } from '@basemaps/geo';
import { Epsg, TileMatrixSet } from '@basemaps/geo';
import { GoogleTms } from '@basemaps/geo/build/tms/google';
import {
Aws,
Expand Down Expand Up @@ -83,7 +83,16 @@ o.spec('LambdaXyz', () => {
o(res.getBody()).equals(rasterMockBuffer.toString('base64'));
o(generateMock.args).deepEquals([
tileMockData,
{ type: 'image', name: tileSetName, projection: Epsg.Google, x: 0, y: 0, z: 0, ext: 'png' },
{
type: 'image',
name: tileSetName,
projection: Epsg.Google,
x: 0,
y: 0,
z: 0,
ext: 'png',
altTms: undefined,
},
] as any);

o(tileMock.calls.length).equals(1);
Expand All @@ -100,6 +109,47 @@ o.spec('LambdaXyz', () => {
});
});

o(`should generate a tile 0,0,0 for alternate tms`, async () => {
tiler = Object.create(Tilers.get(Epsg.Nztm2000, 'agol')!);
Tilers.map.set(TileMatrixSet.getId(Epsg.Nztm2000, 'agol'), tiler);
tiler.tile = tileMock as any;
const tileSet = new FakeTileSet('aerial', Epsg.Nztm2000);
TileSets.set(tileSet.id, tileSet);
tileSet.load = () => Promise.resolve(true);
tileSet.getTiffsForTile = (): [] => [];
const request = mockRequest(`/v1/tiles/aerial/EPSG:2193:agol/0/0/0.png`);
const res = await handleRequest(request);
o(res.status).equals(200);
o(res.header('content-type')).equals('image/png');
o(res.header('eTaG')).equals('foo');
o(res.getBody()).equals(rasterMockBuffer.toString('base64'));
o(generateMock.args).deepEquals([
tileMockData,
{
type: 'image',
name: 'aerial',
projection: Epsg.Nztm2000,
x: 0,
y: 0,
z: 0,
ext: 'png',
altTms: 'agol',
},
] as any);

o(tileMock.calls.length).equals(1);
const [tiffs, x, y, z] = tileMock.args;
o(tiffs).deepEquals([]);
o(x).equals(0);
o(y).equals(0);
o(z).equals(0);

// Validate the session information has been set correctly
o(request.logContext['tileSet']).equals('aerial');
o(request.logContext['xyz']).deepEquals({ x: 0, y: 0, z: 0 });
o(round(request.logContext['location'])).deepEquals({ lat: -90, lon: 0 });
});

o('should generate a tile 0,0,0 for webp', async () => {
const request = mockRequest('/v1/tiles/aerial/3857/0/0/0.webp');
const res = await handleRequest(request);
Expand Down Expand Up @@ -165,7 +215,7 @@ o.spec('LambdaXyz', () => {
});

o('should 304 if a xml is not modified', async () => {
const key = 'e0ynihP7kBO12BFGnQw+LmbRY5MBF4L8JS+Gw47erGw=';
const key = 'r3vqprE8cfTtd4j83dllmDeZydOBMv5hlan0qR/fGkc=';
const request = mockRequest('/v1/tiles/WMTSCapabilities.xml', 'get', { 'if-none-match': key });

const res = await handleRequest(request);
Expand Down
19 changes: 17 additions & 2 deletions packages/lambda-tiler/src/cli/serve.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Epsg } from '@basemaps/geo';
import { Epsg, TileMatrixSet } from '@basemaps/geo';
import { Env, LogConfig, LogType } from '@basemaps/shared';
import { HttpHeader, LambdaContext } from '@basemaps/lambda';
import express from 'express';
Expand All @@ -10,6 +10,7 @@ import { TileSets } from '../tile.set.cache';
import { WmtsCapabilities } from '../wmts.capability';
import { Provider } from '../__test__/xyz.util';
import { TileSetLocal } from './tile.set.local';
import { Tilers } from '../tiler';

const app = express();
const port = Env.getNumber('PORT', 5050);
Expand Down Expand Up @@ -105,7 +106,21 @@ async function useLocal(): Promise<void> {
const requestId = ulid.ulid();
const logger = LogConfig.get().child({ id: requestId });

const xml = WmtsCapabilities.toXml(Env.get(Env.PublicUrlBase) ?? '', Provider, [...TileSets.values()]);
const tileMatrixSets = new Map<Epsg, TileMatrixSet>();
for (const ts of TileSets.values()) {
const tiler = Tilers.get(ts.projection);
if (tiler == null) {
throw new Error("Can't find tiler for projection " + ts.projection);
}
tileMatrixSets.set(ts.projection, tiler.tms);
}

const xml = WmtsCapabilities.toXml(
Env.get(Env.PublicUrlBase) ?? '',
Provider,
[...TileSets.values()],
tileMatrixSets,
);
res.header('content-type', 'application/xml');
res.send(xml);
res.end();
Expand Down
34 changes: 28 additions & 6 deletions packages/lambda-tiler/src/routes/tile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
tileWmtsFromPath,
tileXyzFromPath,
} from '@basemaps/shared';
import { Tiler } from '@basemaps/tiler';
import { TileMakerSharp } from '@basemaps/tiler-sharp';
import { CogTiff } from '@cogeotiff/core';
import { createHash } from 'crypto';
Expand All @@ -28,8 +29,8 @@ const DefaultResizeKernel = { in: 'lanczos3', out: 'lanczos3' } as const;
const NotFound = new LambdaHttpResponse(404, 'Not Found');

/** Initialize the tiffs before reading */
async function initTiffs(tileSet: TileSet, tms: TileMatrixSet, tile: Tile, ctx: LambdaContext): Promise<CogTiff[]> {
const tiffs = tileSet.getTiffsForTile(tms, tile);
async function initTiffs(tileSet: TileSet, tiler: Tiler, tile: Tile, ctx: LambdaContext): Promise<CogTiff[]> {
const tiffs = tileSet.getTiffsForTile(tiler.tms, tile);
let failed = false;
// Remove any tiffs that failed to load
const promises = tiffs.map((c) => {
Expand Down Expand Up @@ -60,12 +61,20 @@ function checkNotModified(req: LambdaContext, cacheKey: string): LambdaHttpRespo
return null;
}

function projectionNotFound(projection: Epsg, altTms?: string): LambdaHttpResponse {
let code = projection.toEpsgString();
if (altTms != null) {
code += ':' + altTms;
}
return new LambdaHttpResponse(404, `Projection not found: ${code}`);
}

export async function tile(req: LambdaContext): Promise<LambdaHttpResponse> {
const xyzData = tileXyzFromPath(req.action.rest);
if (xyzData == null) return NotFound;
ValidateTilePath.validate(req, xyzData);
const tiler = Tilers.get(xyzData.projection);
if (tiler == null) return new LambdaHttpResponse(404, `Projection not found: ${xyzData.projection.code}`);
const tiler = Tilers.get(xyzData.projection, xyzData.altTms);
if (tiler == null) return projectionNotFound(xyzData.projection);

const { x, y, z, ext } = xyzData;

Expand All @@ -74,7 +83,7 @@ export async function tile(req: LambdaContext): Promise<LambdaHttpResponse> {
req.timer.end('tileset:load');
if (tileSet == null) return new LambdaHttpResponse(404, 'Tileset Not Found');

const tiffs = await initTiffs(tileSet, tiler.tms, xyzData, req);
const tiffs = await initTiffs(tileSet, tiler, xyzData, req);
const layers = await tiler.tile(tiffs, x, y, z);

// Generate a unique hash given the full URI, the layers used and a renderId
Expand Down Expand Up @@ -128,7 +137,20 @@ export async function wmts(req: LambdaContext): Promise<LambdaHttpResponse> {
const provider = await Aws.tileMetadata.Provider.get(TileMetadataNamedTag.Production);
if (provider == null) return NotFound;

const xml = WmtsCapabilities.toXml(host, provider, tileSets, req.apiKey);
const tileMatrixSets = new Map<Epsg, TileMatrixSet>();
if (wmtsData.projection == null) {
for (const ts of tileSets) {
const tiler = Tilers.get(ts.projection);
if (tiler == null) return projectionNotFound(ts.projection);
tileMatrixSets.set(ts.projection, tiler.tms);
}
} else {
const tiler = Tilers.get(wmtsData.projection, wmtsData.altTms);
if (tiler == null) return projectionNotFound(wmtsData.projection, wmtsData.altTms);
tileMatrixSets.set(wmtsData.projection, tiler.tms);
}

const xml = WmtsCapabilities.toXml(host, provider, tileSets, tileMatrixSets, wmtsData.altTms, req.apiKey);
if (xml == null) return NotFound;

const data = Buffer.from(xml);
Expand Down
Loading

0 comments on commit b7cfa7b

Please sign in to comment.