From b7cfa7b8bf1351d9e57e46c180a1d3cf01c29927 Mon Sep 17 00:00:00 2001 From: Geoff Jacobsen Date: Thu, 3 Dec 2020 14:32:09 +1300 Subject: [PATCH] feat: Allow alternative TileMatrixSet definitions (#1321) https://github.com/linz/basemaps/pull/1321 --- .../cli/cogify/__test__/action.batch.test.ts | 2 +- packages/geo/src/index.ts | 1 + packages/geo/src/tile.matrix.set.ts | 45 +++ .../lambda-tiler/src/__test__/tiler.test.ts | 21 ++ .../src/__test__/wmts.capability.test.ts | 47 +++- .../lambda-tiler/src/__test__/xyz.test.ts | 56 +++- packages/lambda-tiler/src/cli/serve.ts | 19 +- packages/lambda-tiler/src/routes/tile.ts | 34 ++- packages/lambda-tiler/src/tile.set.ts | 5 +- packages/lambda-tiler/src/tiler.ts | 25 +- packages/lambda-tiler/src/wmts.capability.ts | 79 +++--- packages/lambda/src/__test__/api.path.test.ts | 11 + packages/lambda/src/validate.path.ts | 2 +- packages/landing/package.json | 2 +- packages/shared/README.md | 11 + packages/shared/src/__test__/api.path.test.ts | 26 ++ .../src/alternative.tms/alternative.tms.ts | 24 ++ .../src/alternative.tms/nztm2000.agol.ts | 266 ++++++++++++++++++ packages/shared/src/api.path.ts | 40 ++- .../projection.tile.matrix.set.test.ts | 15 + .../src/proj/projection.tile.matrix.set.ts | 29 +- packages/tiler/src/__test__/tiler.test.ts | 6 + packages/tiler/src/tiler.ts | 6 + 23 files changed, 683 insertions(+), 89 deletions(-) create mode 100644 packages/lambda-tiler/src/__test__/tiler.test.ts create mode 100644 packages/shared/src/alternative.tms/alternative.tms.ts create mode 100644 packages/shared/src/alternative.tms/nztm2000.agol.ts diff --git a/packages/cli/src/cli/cogify/__test__/action.batch.test.ts b/packages/cli/src/cli/cogify/__test__/action.batch.test.ts index 2be987633..76e22daa1 100644 --- a/packages/cli/src/cli/cogify/__test__/action.batch.test.ts +++ b/packages/cli/src/cli/cogify/__test__/action.batch.test.ts @@ -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'; diff --git a/packages/geo/src/index.ts b/packages/geo/src/index.ts index c72e2bd57..030fc7ab2 100644 --- a/packages/geo/src/index.ts +++ b/packages/geo/src/index.ts @@ -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'; diff --git a/packages/geo/src/tile.matrix.set.ts b/packages/geo/src/tile.matrix.set.ts index 6f9dc1746..e86ab778b 100644 --- a/packages/geo/src/tile.matrix.set.ts +++ b/packages/geo/src/tile.matrix.set.ts @@ -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; /** @@ -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]; @@ -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 { + const scaleMap: Map = 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; } diff --git a/packages/lambda-tiler/src/__test__/tiler.test.ts b/packages/lambda-tiler/src/__test__/tiler.test.ts new file mode 100644 index 000000000..c295e9b8d --- /dev/null +++ b/packages/lambda-tiler/src/__test__/tiler.test.ts @@ -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); + }); + }); +}); diff --git a/packages/lambda-tiler/src/__test__/wmts.capability.test.ts b/packages/lambda-tiler/src/__test__/wmts.capability.test.ts index 73aed9d37..900754b41 100644 --- a/packages/lambda-tiler/src/__test__/wmts.capability.test.ts +++ b/packages/lambda-tiler/src/__test__/wmts.capability.test.ts @@ -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 []; @@ -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.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'); @@ -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('\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'); @@ -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'); @@ -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); @@ -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); diff --git a/packages/lambda-tiler/src/__test__/xyz.test.ts b/packages/lambda-tiler/src/__test__/xyz.test.ts index 05a8fe099..e7b242bda 100644 --- a/packages/lambda-tiler/src/__test__/xyz.test.ts +++ b/packages/lambda-tiler/src/__test__/xyz.test.ts @@ -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, @@ -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); @@ -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); @@ -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); diff --git a/packages/lambda-tiler/src/cli/serve.ts b/packages/lambda-tiler/src/cli/serve.ts index 22a806017..423cf5a23 100644 --- a/packages/lambda-tiler/src/cli/serve.ts +++ b/packages/lambda-tiler/src/cli/serve.ts @@ -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'; @@ -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); @@ -105,7 +106,21 @@ async function useLocal(): Promise { 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(); + 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(); diff --git a/packages/lambda-tiler/src/routes/tile.ts b/packages/lambda-tiler/src/routes/tile.ts index 6442161da..f124249f4 100644 --- a/packages/lambda-tiler/src/routes/tile.ts +++ b/packages/lambda-tiler/src/routes/tile.ts @@ -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'; @@ -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 { - const tiffs = tileSet.getTiffsForTile(tms, tile); +async function initTiffs(tileSet: TileSet, tiler: Tiler, tile: Tile, ctx: LambdaContext): Promise { + const tiffs = tileSet.getTiffsForTile(tiler.tms, tile); let failed = false; // Remove any tiffs that failed to load const promises = tiffs.map((c) => { @@ -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 { 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; @@ -74,7 +83,7 @@ export async function tile(req: LambdaContext): Promise { 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 @@ -128,7 +137,20 @@ export async function wmts(req: LambdaContext): Promise { 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(); + 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); diff --git a/packages/lambda-tiler/src/tile.set.ts b/packages/lambda-tiler/src/tile.set.ts index 60bc1df84..33dd729f8 100644 --- a/packages/lambda-tiler/src/tile.set.ts +++ b/packages/lambda-tiler/src/tile.set.ts @@ -89,9 +89,10 @@ export class TileSet { public getTiffsForTile(tms: TileMatrixSet, tile: Tile): CogTiff[] { const output: CogTiff[] = []; const tileBounds = tms.tileToSourceBounds(tile); + const zFilter = tms.getParentZoom(tile.z); for (const rule of this.tileSet.rules) { - if (tile.z > (rule.maxZoom ?? 32)) continue; - if (tile.z < (rule.minZoom ?? 0)) continue; + if (zFilter > (rule.maxZoom ?? 32)) continue; + if (zFilter < (rule.minZoom ?? 0)) continue; const imagery = this.imagery.get(rule.imgId); if (imagery == null) continue; diff --git a/packages/lambda-tiler/src/tiler.ts b/packages/lambda-tiler/src/tiler.ts index 72406d26d..1d3180bdf 100644 --- a/packages/lambda-tiler/src/tiler.ts +++ b/packages/lambda-tiler/src/tiler.ts @@ -1,22 +1,27 @@ -import { Epsg, EpsgCode } from '@basemaps/geo'; -import { Nztm2000Tms } from '@basemaps/geo/build/tms/nztm2000'; +import { Epsg, TileMatrixSet } from '@basemaps/geo'; import { GoogleTms } from '@basemaps/geo/build/tms/google'; -import { Tiler } from '@basemaps/tiler'; +import { Nztm2000Tms } from '@basemaps/geo/build/tms/nztm2000'; +import { Nztm2000AgolTms } from '@basemaps/shared/build/alternative.tms/nztm2000.agol'; +import { Tiler } from '@basemaps/tiler/build/tiler'; + +export const DefaultTilers = [new Tiler(GoogleTms), new Tiler(Nztm2000Tms), new Tiler(Nztm2000AgolTms)]; -export const DefaultTilers = [new Tiler(GoogleTms), new Tiler(Nztm2000Tms)]; -/** * +/** * This class is to cache the creation of the tilers, while also providing access * so that they can be mocked during tests. + * + * Alternative tilers can be supported in addition to the standard Web-Mercator (3857) and NZTM2000 + * (2193) tilers. See the `@basemaps/lambda-tiler` `README.md` for instructions. */ export const Tilers = { - map: new Map(), - /** Lookup a tiler by EPSG Code */ - get(epsg: Epsg): Tiler | undefined { - return Tilers.map.get(epsg.code); + map: new Map(), + /** Lookup a tiler by EPSG Code and optional alternative TileMatrixSet */ + get(epsg: Epsg, alt?: string): Tiler | undefined { + return Tilers.map.get(TileMatrixSet.getId(epsg, alt)); }, add(tiler: Tiler): void { - Tilers.map.set(tiler.tms.projection.code, tiler); + Tilers.map.set(tiler.tms.id, tiler); }, /** Reset the tiler cache */ diff --git a/packages/lambda-tiler/src/wmts.capability.ts b/packages/lambda-tiler/src/wmts.capability.ts index d47a1d857..43898a734 100644 --- a/packages/lambda-tiler/src/wmts.capability.ts +++ b/packages/lambda-tiler/src/wmts.capability.ts @@ -1,35 +1,9 @@ import { Bounds, Epsg, TileMatrixSet, WmtsLayer, WmtsProvider } from '@basemaps/geo'; -import { GoogleTms } from '@basemaps/geo/build/tms/google'; -import { Nztm2000Tms } from '@basemaps/geo/build/tms/nztm2000'; import { Projection, TileMetadataProviderRecord, V, VNodeElement } from '@basemaps/shared'; import { ImageFormatOrder } from '@basemaps/tiler'; import { BBox, Wgs84 } from '@linzjs/geojson'; import { TileSet } from './tile.set'; -function getTileMatrixSet(projection: Epsg): TileMatrixSet { - switch (projection) { - case Epsg.Google: - return GoogleTms; - case Epsg.Nztm2000: - return Nztm2000Tms; - default: - throw new Error(`Invalid projection: ${projection.code}`); - } -} - -/** - * Get the unique list of projections needed to serve these tilesets - * @param tileSets - */ -function getTileMatrixSets(tileSets: WmtsLayer[]): TileMatrixSet[] { - const output = new Map(); - for (const ts of tileSets) { - const tms = getTileMatrixSet(ts.projection); - output.set(tms.projection.code, tms); - } - return Array.from(output.values()); -} - const CapabilitiesAttrs = { xmlns: 'http://www.opengis.net/wmts/1.0', 'xmlns:ows': 'http://www.opengis.net/ows/1.1', @@ -50,13 +24,25 @@ export class WmtsCapabilities { provider: WmtsProvider; layers: Map = new Map(); - tms: Map = new Map(); + tileMatrixSets: Map; + altTms = ''; apiKey?: string; - constructor(httpBase: string, provider: WmtsProvider, layers: WmtsLayer[], apiKey?: string) { + constructor( + httpBase: string, + provider: WmtsProvider, + layers: WmtsLayer[], + tileMatrixSets: Map, + altTms = '', + apiKey?: string, + ) { this.httpBase = httpBase; this.provider = provider; + this.tileMatrixSets = tileMatrixSets; + if (altTms != '') { + this.altTms = ':' + altTms; + } for (const layer of layers) { // TODO is grouping by name the best option @@ -135,7 +121,7 @@ export class WmtsCapabilities { 'v1', 'tiles', tileSet.taggedName, - '{TileMatrixSet}', + '{TileMatrixSet}' + this.altTms, '{TileMatrix}', '{TileCol}', `{TileRow}.${suffix}${apiSuffix}`, @@ -156,7 +142,7 @@ export class WmtsCapabilities { V('ows:Title', firstLayer.title), V('ows:Abstract', firstLayer.description), V('ows:Identifier', firstLayer.taggedName), - ...layers.map((layer) => this.buildBoundingBox(getTileMatrixSet(layer.projection), layer.extent)), + ...layers.map((layer) => this.buildBoundingBox(this.tileMatrixSets.get(layer.projection)!, layer.extent)), this.buildWgs84BoundingBox(layers), this.buildStyle(), ...ImageFormatOrder.map((fmt) => V('Format', 'image/' + fmt)), @@ -174,7 +160,6 @@ export class WmtsCapabilities { V('ows:Title', tms.def.title), tms.def.abstract ? V('ows:Abstract', tms.def.abstract) : null, V('ows:Identifier', tms.projection.toEpsgString()), - this.buildBoundingBox(tms, tms.extent), V('ows:SupportedCRS', tms.projection.toUrn()), tms.def.wellKnownScaleSet ? V('WellKnownScaleSet', tms.def.wellKnownScaleSet) : null, ...tms.def.tileMatrix.map((c) => { @@ -194,15 +179,12 @@ export class WmtsCapabilities { const layers: VNodeElement[] = []; const matrixDefs: VNodeElement[] = []; - const allTms = new Map(); for (const tileSets of this.layers.values()) { - const tms = getTileMatrixSets(tileSets); + const tms = this.getTileMatrixSets(tileSets); layers.push(this.buildLayer(tileSets, tms)); for (const matrix of tms) { - if (allTms.has(matrix.projection.code)) continue; matrixDefs.push(this.buildTileMatrixSet(matrix)); - allTms.set(matrix.projection.code, matrix); } } @@ -216,7 +198,30 @@ export class WmtsCapabilities { return '\n' + this.toVNode().toString(); } - static toXml(httpBase: string, provider: TileMetadataProviderRecord, tileSet: TileSet[], apiKey?: string): string { - return new WmtsCapabilities(httpBase, provider, tileSet, apiKey).toString(); + static toXml( + httpBase: string, + provider: TileMetadataProviderRecord, + tileSet: TileSet[], + tileMatrixSets: Map, + altTms?: string | undefined, + apiKey?: string, + ): string { + return new WmtsCapabilities(httpBase, provider, tileSet, tileMatrixSets, altTms, apiKey).toString(); + } + + /** + * Get the unique list of projections needed to serve these tilesets + * @param tileSets + */ + private getTileMatrixSets(tileSets: WmtsLayer[]): TileMatrixSet[] { + const output = new Map(); + for (const ts of tileSets) { + const tms = this.tileMatrixSets.get(ts.projection); + if (tms == null) { + throw new Error(`Invalid projection: ${ts.projection.code}`); + } + output.set(tms.projection.code, tms); + } + return Array.from(output.values()); } } diff --git a/packages/lambda/src/__test__/api.path.test.ts b/packages/lambda/src/__test__/api.path.test.ts index d6e8d68b9..3dfaef167 100644 --- a/packages/lambda/src/__test__/api.path.test.ts +++ b/packages/lambda/src/__test__/api.path.test.ts @@ -36,6 +36,13 @@ o.spec('api.path', () => { type: TileType.Attribution, name: 'aerial', projection: Epsg.Google, + altTms: undefined, + }); + o(tileAttributionFromPath(['aerial', 'EPSG:2193:agol', 'attribution.json'])).deepEquals({ + type: TileType.Attribution, + name: 'aerial', + projection: Epsg.Nztm2000, + altTms: 'agol', }); }); }); @@ -76,6 +83,7 @@ o.spec('api.path', () => { y: 3, z: 1, ext: ImageFormat.PNG, + altTms: undefined, }); }); @@ -90,6 +98,7 @@ o.spec('api.path', () => { y: 6, z: 4, ext: ImageFormat.WEBP, + altTms: undefined, }); }); @@ -102,6 +111,7 @@ o.spec('api.path', () => { type: TileType.WMTS, name: 'aerial', projection: Epsg.Google, + altTms: undefined, }); }); @@ -114,6 +124,7 @@ o.spec('api.path', () => { type: TileType.WMTS, name: 'aerial', projection: null, + altTms: undefined, }); }); }); diff --git a/packages/lambda/src/validate.path.ts b/packages/lambda/src/validate.path.ts index 81b500249..42b7cfffc 100644 --- a/packages/lambda/src/validate.path.ts +++ b/packages/lambda/src/validate.path.ts @@ -20,7 +20,7 @@ export const ValidateTilePath = { req.set('extension', ext); req.set('tileSet', xyzData.name); - const tileMatrix = ProjectionTileMatrixSet.tryGet(xyzData.projection.code); + const tileMatrix = ProjectionTileMatrixSet.tryGet(xyzData.projection.code, xyzData.altTms); if (tileMatrix == null) throw new LambdaHttpResponse(404, `Projection not found: ${xyzData.projection.code}`); if (z > tileMatrix.tms.maxZoom || z < 0) throw new LambdaHttpResponse(404, `Zoom not found: ${z}`); diff --git a/packages/landing/package.json b/packages/landing/package.json index b89c5c579..f27be4ee9 100644 --- a/packages/landing/package.json +++ b/packages/landing/package.json @@ -17,7 +17,7 @@ "types": "./build/index.d.ts", "scripts": { "test": "ospec --globs 'build/**/*.test.js' --preload ../../scripts/test.before.js", - "start": "TILE_HOST=${TILE_HOST:-https://dev.basemaps.linz.govt.nz} nodemon scripts/bundle.js -e 'ts html css' -i 'dist/*'", + "start": "TILE_HOST=${TILE_HOST:-https://dev.basemaps.linz.govt.nz} nodemon ../../scripts/bundle.js -e 'ts html css' -i 'dist/*' -- package.json", "bundle": "../../scripts/bundle.js package.json", "deploy:deploy": "node scripts/deploy.js" }, diff --git a/packages/shared/README.md b/packages/shared/README.md index a6a88ce4b..6fb2c7d5b 100644 --- a/packages/shared/README.md +++ b/packages/shared/README.md @@ -1,3 +1,14 @@ # @basemaps/shared This contains all code that is shared between multiple services, such as AWS connection logic and constant values + +### Alternative Tilers and TileMatrixSets + +Alternative tilers can be added to basemaps. These can then be accessed from the server by appending the tiler name to the EPSG projection; for example `https://dev.basemaps.linz.govt.nz/v1/tiles/aerial/EPSG:2193:agol/WMTSCapabilities.xml?api={ApiKey}` where `agol` is the alternative tiler. + +To add an alternative tiler a couple of files need to be added/altered (using `agol` as the example): + +1. add `src/alternative.tms/nztm2000.agol.ts` as the definition of the Tile matrix set. +2. edit `src/proj/projection.tile.matrix.set.ts` and add `Nztm2000AgolTms` to `AlternativeTmsList` + +This will make the alternative definition available in `@basemaps/lambda-tiler` diff --git a/packages/shared/src/__test__/api.path.test.ts b/packages/shared/src/__test__/api.path.test.ts index 2b07de631..3361aae41 100644 --- a/packages/shared/src/__test__/api.path.test.ts +++ b/packages/shared/src/__test__/api.path.test.ts @@ -13,9 +13,20 @@ o.spec('api.path', () => { y: 5432, z: 10, ext: ImageFormat.WEBP, + altTms: undefined, }); o(tileXyzFromPath([])).equals(null); o(tileXyzFromPath(['aerial', 'EPSG:3857', '10', '3456'])).equals(null); + o(tileXyzFromPath(['aerial', 'EPSG:2193:agol', '10', '3456', '5432.webp'])).deepEquals({ + type: TileType.Image, + name: 'aerial', + projection: Epsg.Nztm2000, + x: 3456, + y: 5432, + z: 10, + ext: ImageFormat.WEBP, + altTms: 'agol', + }); }); o('tileWmtsFromPath', () => { @@ -23,11 +34,19 @@ o.spec('api.path', () => { type: TileType.WMTS, name: 'aerial', projection: Epsg.Google, + altTms: undefined, + }); + o(tileWmtsFromPath(['aerial', 'EPSG:2193:agol', 'WMTSCapabilities.xml'])).deepEquals({ + type: TileType.WMTS, + name: 'aerial', + projection: Epsg.Nztm2000, + altTms: 'agol', }); o(tileWmtsFromPath([])).deepEquals({ type: TileType.WMTS, name: '', projection: null, + altTms: undefined, }); }); @@ -36,6 +55,13 @@ o.spec('api.path', () => { type: TileType.Attribution, name: 'aerial', projection: Epsg.Google, + altTms: undefined, + }); + o(tileAttributionFromPath(['aerial', 'EPSG:2193:agol', 'attribution.json'])).deepEquals({ + type: TileType.Attribution, + name: 'aerial', + projection: Epsg.Nztm2000, + altTms: 'agol', }); o(tileAttributionFromPath([])).equals(null); o(tileAttributionFromPath(['aerial', 'attribution.json'])).equals(null); diff --git a/packages/shared/src/alternative.tms/alternative.tms.ts b/packages/shared/src/alternative.tms/alternative.tms.ts new file mode 100644 index 000000000..40d9f7798 --- /dev/null +++ b/packages/shared/src/alternative.tms/alternative.tms.ts @@ -0,0 +1,24 @@ +import { TileMatrixSet, TileMatrixSetType } from '@basemaps/geo'; + +export class AlternativeTileMatrixSet extends TileMatrixSet { + altName: string; + parent: TileMatrixSet; + scaleMap: Map; + + constructor(def: TileMatrixSetType, parent: TileMatrixSet, altName: string) { + super(def); + this.parent = parent; + this.altName = altName; + this.scaleMap = TileMatrixSet.scaleMapping(this.parent, this); + } + + getParentZoom(z: number): number { + const convertZ = this.scaleMap.get(z); + if (convertZ == null) throw new Error(`No converted zoom from parent Tile Matrix for zoom: ${z}`); + return convertZ; + } + + get id(): string { + return TileMatrixSet.getId(this.projection, this.altName); + } +} diff --git a/packages/shared/src/alternative.tms/nztm2000.agol.ts b/packages/shared/src/alternative.tms/nztm2000.agol.ts new file mode 100644 index 000000000..10f3616cc --- /dev/null +++ b/packages/shared/src/alternative.tms/nztm2000.agol.ts @@ -0,0 +1,266 @@ +import { TileMatrixSetType } from '@basemaps/geo'; +import { Nztm2000Tms } from '@basemaps/geo/build/tms/nztm2000'; +import { AlternativeTileMatrixSet } from './alternative.tms'; + +const AgolTmst: TileMatrixSetType = { + type: 'TileMatrixSetType', + title: 'LINZ NZTM2000 Map Tile Grid', + abstract: + 'See https://www.linz.govt.nz/data/linz-data-service/guides-and-documentation/nztm2000-map-tile-service-schema', + identifier: 'NZTM2000', + supportedCRS: 'https://www.opengis.net/def/crs/EPSG/0/2193', + boundingBox: { + type: 'BoundingBoxType', + crs: 'https://www.opengis.net/def/crs/EPSG/0/2193', + lowerCorner: [3298950.066042575, -1910321.7309136656], + upperCorner: [7011590.824657426, 4922291.934313666], + }, + tileMatrix: [ + { + type: 'TileMatrixType', + identifier: '0', + scaleDenominator: 5.590822640285016e8, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 1, + matrixHeight: 1, + }, + { + type: 'TileMatrixType', + identifier: '1', + scaleDenominator: 2.7954113201425034e8, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 1, + matrixHeight: 1, + }, + { + type: 'TileMatrixType', + identifier: '2', + scaleDenominator: 1.3977056600712562e8, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 1, + matrixHeight: 2, + }, + { + type: 'TileMatrixType', + identifier: '3', + scaleDenominator: 6.988528300356235e7, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 2, + matrixHeight: 4, + }, + { + type: 'TileMatrixType', + identifier: '4', + scaleDenominator: 3.494264150178117e7, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 4, + matrixHeight: 7, + }, + { + type: 'TileMatrixType', + identifier: '5', + scaleDenominator: 1.7471320750890587e7, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 8, + matrixHeight: 14, + }, + { + type: 'TileMatrixType', + identifier: '6', + scaleDenominator: 8735660.375445293, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 15, + matrixHeight: 27, + }, + { + type: 'TileMatrixType', + identifier: '7', + scaleDenominator: 4367830.187722647, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 29, + matrixHeight: 54, + }, + { + type: 'TileMatrixType', + identifier: '8', + scaleDenominator: 2183915.0938617955, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 58, + matrixHeight: 107, + }, + { + type: 'TileMatrixType', + identifier: '9', + scaleDenominator: 1091957.5469304253, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 115, + matrixHeight: 214, + }, + { + type: 'TileMatrixType', + identifier: '10', + scaleDenominator: 545978.7734656851, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 229, + matrixHeight: 427, + }, + { + type: 'TileMatrixType', + identifier: '11', + scaleDenominator: 272989.38673237007, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 458, + matrixHeight: 854, + }, + { + type: 'TileMatrixType', + identifier: '12', + scaleDenominator: 136494.69336618503, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 915, + matrixHeight: 1707, + }, + { + type: 'TileMatrixType', + identifier: '13', + scaleDenominator: 68247.34668309252, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 1829, + matrixHeight: 3414, + }, + { + type: 'TileMatrixType', + identifier: '14', + scaleDenominator: 34123.67334154626, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 3657, + matrixHeight: 6828, + }, + { + type: 'TileMatrixType', + identifier: '15', + scaleDenominator: 17061.836671245605, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 7313, + matrixHeight: 13655, + }, + { + type: 'TileMatrixType', + identifier: '16', + scaleDenominator: 8530.918335622802, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 14626, + matrixHeight: 27309, + }, + { + type: 'TileMatrixType', + identifier: '17', + scaleDenominator: 4265.459167338929, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 29251, + matrixHeight: 54618, + }, + { + type: 'TileMatrixType', + identifier: '18', + scaleDenominator: 2132.729584141936, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 58501, + matrixHeight: 109235, + }, + { + type: 'TileMatrixType', + identifier: '19', + scaleDenominator: 1066.3647915984968, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 117001, + matrixHeight: 218470, + }, + { + type: 'TileMatrixType', + identifier: '20', + scaleDenominator: 533.1823957992484, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 234002, + matrixHeight: 436939, + }, + { + type: 'TileMatrixType', + identifier: '21', + scaleDenominator: 266.5911978996242, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 468004, + matrixHeight: 873878, + }, + { + type: 'TileMatrixType', + identifier: '22', + scaleDenominator: 133.2955989498121, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 936007, + matrixHeight: 1747756, + }, + { + type: 'TileMatrixType', + identifier: '23', + scaleDenominator: 66.64779947490605, + topLeftCorner: [1.99981e7, -4020900.0], + tileWidth: 256, + tileHeight: 256, + matrixWidth: 1872013, + matrixHeight: 3495511, + }, + ], +}; + +/** + * This is an alternative TileMatrixSet for NZTM2000 that is used by Esri™ and ArcGIS™ Online for + * New zealand basemaps. It is not officially supported by LINZ + */ +export const Nztm2000AgolTms = new AlternativeTileMatrixSet(AgolTmst, Nztm2000Tms, 'agol'); diff --git a/packages/shared/src/api.path.ts b/packages/shared/src/api.path.ts index a2645a067..5a2a439ed 100644 --- a/packages/shared/src/api.path.ts +++ b/packages/shared/src/api.path.ts @@ -17,10 +17,15 @@ export enum TileType { export type TileData = TileDataXyz | TileDataWmts | TileDataAttribution; -export interface TileDataXyz extends Tile { - type: TileType.Image; +interface NameProjection { name: string; projection: Epsg; + /** Has an alternative TileMatrixSet been requested */ + altTms?: string | undefined; +} + +export interface TileDataXyz extends Tile, NameProjection { + type: TileType.Image; ext: ImageFormat; } @@ -28,12 +33,11 @@ export interface TileDataWmts { type: TileType.WMTS; name: string; projection: Epsg | null; + altTms?: string | undefined; } -export interface TileDataAttribution { +export interface TileDataAttribution extends NameProjection { type: TileType.Attribution; - name: string; - projection: Epsg; } export function setNameAndProjection(req: { set: (key: string, val: any) => void }, data: TileData): void { @@ -49,10 +53,20 @@ function parseTargetEpsg(text: string): Epsg | null { return projection; } +function extractProjection(rawProj: string): { projection: Epsg | null; altTms: string | undefined } { + let altTms: string | undefined = undefined; + if (/:.*:/.test(rawProj)) { + const pos = rawProj.lastIndexOf(':'); + altTms = rawProj.slice(pos + 1); + rawProj = rawProj.slice(0, pos); + } + return { projection: parseTargetEpsg(rawProj), altTms }; +} + export function tileXyzFromPath(path: string[]): TileDataXyz | null { if (path.length < 5) return null; const name = path[0]; - const projection = parseTargetEpsg(path[1]); + const { projection, altTms } = extractProjection(path[1]); if (projection == null) return null; const z = parseInt(path[2], 10); const x = parseInt(path[3], 10); @@ -64,17 +78,17 @@ export function tileXyzFromPath(path: string[]): TileDataXyz | null { const ext = extStr ? getImageFormat(extStr) : null; if (ext == null) return null; - return { type: TileType.Image, name, projection, x, y, z, ext }; + return { type: TileType.Image, name, projection, x, y, z, ext, altTms }; } export function tileAttributionFromPath(path: string[]): TileDataAttribution | null { if (path.length < 3) return null; const name = path[0]; - const projection = Epsg.parse(path[1]); + const { projection, altTms } = extractProjection(path[1]); if (projection == null) return null; - return { type: TileType.Attribution, name, projection }; + return { type: TileType.Attribution, name, projection, altTms }; } /** @@ -90,16 +104,18 @@ export function tileWmtsFromPath(path: string[]): TileDataWmts | null { if (path.length > 3) return null; const name = path.length < 2 ? '' : path[0]; - let projection = null; if (path.length == 3) { - projection = parseTargetEpsg(path[1]); + const { projection, altTms } = extractProjection(path[1]); if (projection == null) return null; + + return { type: TileType.WMTS, name, projection, altTms }; } return { type: TileType.WMTS, name, - projection, + projection: null, + altTms: undefined, }; } diff --git a/packages/shared/src/proj/__test__/projection.tile.matrix.set.test.ts b/packages/shared/src/proj/__test__/projection.tile.matrix.set.test.ts index 3cfc6f312..67fcf0a32 100644 --- a/packages/shared/src/proj/__test__/projection.tile.matrix.set.test.ts +++ b/packages/shared/src/proj/__test__/projection.tile.matrix.set.test.ts @@ -1,9 +1,11 @@ import { Bounds, EpsgCode, QuadKey } from '@basemaps/geo'; import { GoogleTms } from '@basemaps/geo/build/tms/google'; +import { Nztm2000Tms } from '@basemaps/geo/build/tms/nztm2000'; import { Approx } from '@basemaps/test'; import { round } from '@basemaps/test/build/rounding'; import { BBox } from '@linzjs/geojson'; import o from 'ospec'; +import { Nztm2000AgolTms } from '../../alternative.tms/nztm2000.agol'; import { ProjectionTileMatrixSet } from '../projection.tile.matrix.set'; const TileSize = 256; @@ -67,6 +69,19 @@ o.spec('ProjectionTileMatrixSet', () => { }); }); + o.spec('tryGet', () => { + o('not found', () => { + o(ProjectionTileMatrixSet.tryGet(EpsgCode.Wgs84)).equals(null); + }); + o('get normal', () => { + o(ProjectionTileMatrixSet.tryGet(EpsgCode.Nztm2000)!.tms).equals(Nztm2000Tms); + }); + + o('get alternative', () => { + o(ProjectionTileMatrixSet.tryGet(EpsgCode.Nztm2000, 'agol')!.tms).equals(Nztm2000AgolTms); + }); + }); + o.spec('tileToWgs84Bbox', () => { o('should handle antimeridian', () => { const pt = nztmPtms.tileToWgs84Bbox({ x: 2, y: 1, z: 1 }); diff --git a/packages/shared/src/proj/projection.tile.matrix.set.ts b/packages/shared/src/proj/projection.tile.matrix.set.ts index f3f769a27..43d9458e3 100644 --- a/packages/shared/src/proj/projection.tile.matrix.set.ts +++ b/packages/shared/src/proj/projection.tile.matrix.set.ts @@ -2,8 +2,14 @@ import { EpsgCode, Tile, TileMatrixSet } from '@basemaps/geo'; import { GoogleTms } from '@basemaps/geo/build/tms/google'; import { Nztm2000Tms } from '@basemaps/geo/build/tms/nztm2000'; import { BBox } from '@linzjs/geojson'; +import { Nztm2000AgolTms } from '../alternative.tms/nztm2000.agol'; import { Projection } from './projection'; +/** + * The list of alternative TMS definitions see `README.md` + */ +const AlternativeTmsList = [Nztm2000AgolTms]; + export interface LatLon { lat: number; lon: number; @@ -18,6 +24,9 @@ export class ProjectionTileMatrixSet { /** Used to calculate `BlockSize = blockFactor * tms.tileSize` for generating COGs */ blockFactor: number; + /** Alternative map of tile matrix sets */ + static altMap = new Map>(); + /** * Wrapper around TileMatrixSet with utilities for converting Points and Polygons */ @@ -46,10 +55,17 @@ export class ProjectionTileMatrixSet { /** * Try to find a corresponding ProjectionTileMatrixSet for a number + * @param epsgCode + * @param alt if present use an alternative ProjectionTileMatrixSet */ - static tryGet(epsgCode?: EpsgCode): ProjectionTileMatrixSet | null { - return (epsgCode && CodeMap.get(epsgCode)) ?? null; + static tryGet(epsgCode?: EpsgCode, alt?: string): ProjectionTileMatrixSet | null { + if (epsgCode == null) return null; + if (alt != null) { + return this.altMap.get(epsgCode)?.get(alt.toLowerCase()) ?? null; + } + + return CodeMap.get(epsgCode) ?? null; } /** @@ -118,3 +134,12 @@ export class ProjectionTileMatrixSet { CodeMap.set(EpsgCode.Google, new ProjectionTileMatrixSet(GoogleTms)); CodeMap.set(EpsgCode.Nztm2000, new ProjectionTileMatrixSet(Nztm2000Tms)); + +for (const tms of AlternativeTmsList) { + let map = ProjectionTileMatrixSet.altMap.get(tms.projection.code); + if (map == null) { + map = new Map(); + ProjectionTileMatrixSet.altMap.set(tms.projection.code, map); + } + map.set(tms.altName, new ProjectionTileMatrixSet(Nztm2000AgolTms)); +} diff --git a/packages/tiler/src/__test__/tiler.test.ts b/packages/tiler/src/__test__/tiler.test.ts index d38d64469..2ce0820a9 100644 --- a/packages/tiler/src/__test__/tiler.test.ts +++ b/packages/tiler/src/__test__/tiler.test.ts @@ -36,6 +36,12 @@ o.spec('tiler.test', () => { }); }); }); + + o('getParentZoom', () => { + const tiler = new Tiler(GoogleTms); + o(tiler.tms.getParentZoom(10)).equals(10); + }); + o('createComposition should handle non square images', () => { const tiler = new Tiler(Nztm2000Tms); diff --git a/packages/tiler/src/tiler.ts b/packages/tiler/src/tiler.ts index db3211132..0ae29faee 100644 --- a/packages/tiler/src/tiler.ts +++ b/packages/tiler/src/tiler.ts @@ -20,6 +20,12 @@ export class Tiler { /** Tile size for the tiler and sub objects */ public readonly tms: TileMatrixSet; + /** + * Tiler for a TileMatrixSet + + * @param tms + * @param convertZ override the default convertZ + */ public constructor(tms: TileMatrixSet) { this.tms = tms; }