diff --git a/packages/cli/src/cli/overview/__test__/wmts.overview.test.ts b/packages/cli/src/cli/overview/__test__/wmts.overview.test.ts new file mode 100644 index 000000000..186fec749 --- /dev/null +++ b/packages/cli/src/cli/overview/__test__/wmts.overview.test.ts @@ -0,0 +1,26 @@ +import o from 'ospec'; +import { zoomLevelsFromWmts } from '@basemaps/config'; +import { GoogleTms, Nztm2000QuadTms } from '@basemaps/geo'; +import { createOverviewWmtsCapabilities } from '../overview.wmts.js'; + +// This test should really live in @basemaps/config, but all the WMTS generation logic does not live in @basemaps/config +o.spec('zoomLevelsFromWmts', () => { + o('should extract zoom levels', () => { + const wmts = createOverviewWmtsCapabilities(Nztm2000QuadTms, 10, 'Test Title'); + o(zoomLevelsFromWmts(wmts, Nztm2000QuadTms)).deepEquals({ minZoom: 0, maxZoom: 10 }); + }); + + o('should include all zoom levels', () => { + const wmts = createOverviewWmtsCapabilities(GoogleTms, GoogleTms.maxZoom, 'Test Title'); + o(zoomLevelsFromWmts(wmts, GoogleTms)).deepEquals({ minZoom: 0, maxZoom: GoogleTms.maxZoom }); + }); + o('should not extract zoom levels for wrong projection', () => { + const wmts = createOverviewWmtsCapabilities(GoogleTms, 10, 'Test Title'); + o(zoomLevelsFromWmts(wmts, Nztm2000QuadTms)).equals(null); + }); + + o('should not return if zooms are invalid', () => { + const wmts = createOverviewWmtsCapabilities(Nztm2000QuadTms, 0, 'Test Title'); + o(zoomLevelsFromWmts(wmts, Nztm2000QuadTms)).equals(null); + }); +}); diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 9392b1c81..df8bc6ceb 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -24,7 +24,7 @@ export { ConfigDynamoBase } from './dynamo/dynamo.config.base.js'; export { ConfigProviderMemory, ConfigBundled } from './memory/memory.config.js'; export { TileSetNameComponents, TileSetNameParser } from './tile.set.name.js'; export { parseHex, parseRgba } from './color.js'; -export { ConfigJson } from './json/json.config.js'; +export { ConfigJson, zoomLevelsFromWmts } from './json/json.config.js'; export { standardizeLayerName } from './json/name.convertor.js'; export { base58, isBase58 } from './base58.js'; export { sha256base58, ensureBase58 } from './base58.node.js'; diff --git a/packages/config/src/json/json.config.ts b/packages/config/src/json/json.config.ts index 0bcbd6db7..a5a382632 100644 --- a/packages/config/src/json/json.config.ts +++ b/packages/config/src/json/json.config.ts @@ -3,7 +3,6 @@ import { GoogleTms, ImageFormat, Nztm2000QuadTms, - QuadKey, TileMatrixSet, TileMatrixSets, VectorFormat, @@ -277,45 +276,44 @@ export class ConfigJson { const exists = await fsa.exists(targetOverviews); if (!exists) return; - const smallestTiff = cfg.files.reduce((prev, current) => { - if (prev.width < current.width) return prev; - return current; - }, cfg.files[0]); - const tileMatrix = TileMatrixSets.find(cfg.tileMatrix); if (tileMatrix == null) throw new Error('Missing tileMatrix for imagery:' + cfg.id); - // Find a maximum tile that could be used in this overview - const maxZoomLoc = tileMatrix.sourceToPixels(smallestTiff.x, smallestTiff.y, tileMatrix.maxZoom); - const maxTile = tileMatrix.pixelsToTile(maxZoomLoc.x, maxZoomLoc.y, tileMatrix.maxZoom); - let qk = QuadKey.fromTile(maxTile); - const cotar = await Cotar.fromTar(fsa.source(targetOverviews)); - // Query all the quadkeys looking to see what tiles exists - const queries: Promise[] = []; - while (qk.length > 0) { - qk = QuadKey.parent(qk); - const tile = QuadKey.toTile(qk); - queries.push( - cotar.index.find(`tiles/${tile.z}/${tile.x}/${tile.y}.webp`).then((c) => { - if (c == null) return null; - return qk; - }), - ); - } + // When the cotars are made a WMTSCapabilties is added so it easy to view in something like QGIS + // We can use the WMTSCapabitities to figure out the tileMatrix and zoom levels + const wmtsRaw = await cotar.get('WMTSCapabilities.xml'); + if (wmtsRaw == null) return; - const result = await Promise.all(queries); + const wmts = Buffer.from(wmtsRaw).toString(); - const overview: ConfigImageryOverview = { path: 'overviews.tar.co', minZoom: tileMatrix.maxZoom, maxZoom: 0 }; - for (const r of result) { - if (r == null) continue; - if (r.length < overview.minZoom) overview.minZoom = r.length; - if (r.length > overview.maxZoom) overview.maxZoom = r.length; - } + const zoomLevels = zoomLevelsFromWmts(wmts, tileMatrix); + if (zoomLevels == null) return; - if (overview.minZoom === tileMatrix.maxZoom) return; - if (overview.maxZoom === 0) return; - return overview; + return { path: 'overviews.tar.co', minZoom: zoomLevels.minZoom, maxZoom: zoomLevels.maxZoom }; } } + +/** Attempt to parse a cotar WMTSCapabilties to figure out what zoom levels are applicable */ +export function zoomLevelsFromWmts( + wmts: string, + tileMatrix: TileMatrixSet, +): { minZoom: number; maxZoom: number } | null { + const owsIds = wmts + .split('\n') + .filter((f) => f.includes('ows:Identifier')) + .map((c) => c.trim().replace('', '').replace('', '')); + + const tileMatrixOffset = owsIds.indexOf(tileMatrix.identifier); + if (tileMatrixOffset === -1) return null; + + const minZoom = Number(owsIds[tileMatrixOffset + 1]); + const maxZoom = Number(owsIds[owsIds.length - 1]); + + if (isNaN(minZoom)) return null; + if (isNaN(maxZoom)) return null; + if (maxZoom < minZoom) return null; + if (maxZoom === 0) return null; + return { minZoom, maxZoom }; +} diff --git a/packages/lambda-tiler/src/wmts.capability.ts b/packages/lambda-tiler/src/wmts.capability.ts index 89143c6f4..e8ad93ab4 100644 --- a/packages/lambda-tiler/src/wmts.capability.ts +++ b/packages/lambda-tiler/src/wmts.capability.ts @@ -270,7 +270,7 @@ export class WmtsCapabilities { V('ows:Identifier', tms.identifier), V('ows:SupportedCRS', tms.projection.toUrn()), tms.def.wellKnownScaleSet ? V('WellKnownScaleSet', tms.def.wellKnownScaleSet) : null, - ...tms.def.tileMatrix.slice(this.minZoom, this.maxZoom).map((c) => { + ...tms.def.tileMatrix.slice(this.minZoom, this.maxZoom + 1).map((c) => { return V('TileMatrix', [ V('ows:Identifier', c.identifier), V('ScaleDenominator', c.scaleDenominator), diff --git a/packages/shared/package.json b/packages/shared/package.json index 4c52f702e..b5c1e8e82 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -34,14 +34,12 @@ "entities": "^4.3.0", "pino": "^8.6.1", "proj4": "^2.8.0", - "sax": "^1.2.4", "source-map-support": "^0.5.19", "ulid": "^2.3.0" }, "devDependencies": { "@types/aws-lambda": "^8.10.75", - "@types/proj4": "^2.5.2", - "@types/sax": "^1.2.1" + "@types/proj4": "^2.5.2" }, "publishConfig": { "access": "public" diff --git a/yarn.lock b/yarn.lock index fb8967d29..690ddae55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1977,13 +1977,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/sax@^1.2.1": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.2.tgz#0b8996ffb9ca0b0491e791a09b550d77fa852f5d" - integrity sha512-nfnUx5UQ1R+/riXydS0UyYJiqtxgOeObr9Hw8xSTtpB4LNoHa1z31rsvGXN0JKz/7/kDXw0bT2Q/VQkXAbCYoA== - dependencies: - "@types/node" "*" - "@types/scheduler@*": version "0.16.2" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" @@ -7295,7 +7288,7 @@ sax@1.2.1: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= -sax@>=0.6.0, sax@^1.2.4: +sax@>=0.6.0: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==