diff --git a/packages/lambda-tiler/src/cli/render.tile.ts b/packages/lambda-tiler/src/cli/render.tile.ts index cf45995ce..6d8b0d95b 100644 --- a/packages/lambda-tiler/src/cli/render.tile.ts +++ b/packages/lambda-tiler/src/cli/render.tile.ts @@ -7,20 +7,22 @@ import { Context } from 'aws-lambda'; import { extname } from 'path'; import { TileXyzRaster } from '../routes/tile.xyz.raster.js'; +import { Validate } from '../util/validate.js'; // Render configuration -const source = fsa.toUrl(`/home/blacha/data/elevation/christchurch_2020-2021/`); -const tile = fromPath('/14/7898/8615.webp'); -const pipeline: string | null = 'color-ramp'; +const source = fsa.toUrl(`/Users/blacha/data/topo50-small/`); +const tile = fromPath('/10/986/658@2x.webp'); +const pipeline: string | null = 'rgba'; let tileMatrix: TileMatrixSet | null = null; /** Convert a tile path /:z/:x/:y.png into a tile & extension */ -function fromPath(s: string): Tile & { extension: string } { +function fromPath(s: string): Tile & { extension: string; scale?: number } { const ext = extname(s).slice(1); - const parts = s.split('.')[0].split('/').map(Number); + const parts = s.split('.')[0].split('/'); if (s.startsWith('/')) parts.shift(); if (parts.length !== 3) throw new Error(`Invalid tile path ${s}`); - return { z: parts[0], x: parts[1], y: parts[2], extension: ext }; + const { scale, y } = Validate.getScale(parts[2]); + return { z: Number(parts[0]), x: Number(parts[1]), y, scale, extension: ext }; } async function main(): Promise { @@ -49,14 +51,16 @@ async function main(): Promise { tileSet.background = { r: 255, g: 0, b: 255, alpha: 0.25 }; const res = await TileXyzRaster.tile(request, tileSet, { tile, + scale: tile.scale, tileMatrix, tileSet: tileSet.id, tileType: tile.extension, pipeline, }); const pipelineName = pipeline ? `-${pipeline}` : ''; + const scaleName = (tile.scale ?? 0) > 1 ? `@${tile.scale}x` : ''; - const fileName = `./render/${tile.z}_${tile.x}_${tile.y}${pipelineName}.${tile.extension}`; + const fileName = `./render/${tile.z}_${tile.x}_${tile.y}${pipelineName}${scaleName}.${tile.extension}`; await fsa.write(fsa.toUrl(fileName), Buffer.from(res.body, 'base64')); log.info({ path: fileName, ...request.timer.metrics }, 'Tile:Write'); } diff --git a/packages/lambda-tiler/src/routes/tile.xyz.raster.ts b/packages/lambda-tiler/src/routes/tile.xyz.raster.ts index c3792e095..adce933f2 100644 --- a/packages/lambda-tiler/src/routes/tile.xyz.raster.ts +++ b/packages/lambda-tiler/src/routes/tile.xyz.raster.ts @@ -30,7 +30,8 @@ export function isArchiveTiff(x: CloudArchive): x is Tiff { return false; } -export const TileComposer = new TileMakerSharp(256); +export const TileComposer256 = new TileMakerSharp(256); +export const TileComposer512 = new TileMakerSharp(512); export const DefaultResizeKernel = { in: 'lanczos3', out: 'lanczos3' } as const; export const DefaultBackground = { r: 0, g: 0, b: 0, alpha: 0 }; @@ -117,6 +118,17 @@ export const TileXyzRaster = { return TileXyzRaster.getAssetsForBounds(req, tileSet, xyz.tileMatrix, tileBounds, xyz.tile.z); }, + /** + * Lookup a tile composer based off the provided scale + * + * @param scale tile scale generally undefined or 2 + * @returns + */ + getComposer(scale?: number): TileMakerSharp { + if (scale === 2) return TileComposer512; + return TileComposer256; + }, + async tile(req: LambdaHttpRequest, tileSet: ConfigTileSetRaster, xyz: TileXyz): Promise { const tileOutput = Validate.pipeline(tileSet, xyz.tileType, xyz.pipeline); if (tileOutput == null) return NotFound(); @@ -128,13 +140,13 @@ export const TileXyzRaster = { const assets = await TileXyzRaster.loadAssets(req, assetPaths); - const tiler = new Tiler(xyz.tileMatrix); + const tiler = new Tiler(xyz.tileMatrix, xyz.scale); const layers = tiler.tile(assets, xyz.tile.x, xyz.tile.y, xyz.tile.z); const format = getImageFormat(xyz.tileType); if (format == null) return new LambdaHttpResponse(400, 'Invalid image format: ' + xyz.tileType); - const res = await TileComposer.compose({ + const res = await this.getComposer(xyz.scale).compose({ layers, pipeline: tileOutput.pipeline, format, diff --git a/packages/lambda-tiler/src/util/validate.ts b/packages/lambda-tiler/src/util/validate.ts index e5969b736..309ee69ae 100644 --- a/packages/lambda-tiler/src/util/validate.ts +++ b/packages/lambda-tiler/src/util/validate.ts @@ -9,6 +9,12 @@ import { TileXyzGet } from '../routes/tile.xyz.js'; export interface TileXyz { /** Tile XYZ location */ tile: { x: number; y: number; z: number }; + /** + * Output scale + * + * @default 1 + */ + scale?: number; /** Name of the tile set to use */ tileSet: string; /** TileMatrix that is requested */ @@ -66,6 +72,25 @@ export const Validate = { if (isNaN(lat) || lat < -90 || lat > 90) return null; return { lon, lat }; }, + + /** + * Parse a scale component from z + * + * @example + * ```typescript + * parseScale("3272@2x") // {y: 3272, scale: 2} + * parseScale("3272") // {y:3272} + * ``` + * + * @param s string to parse + * @returns scale and component if the scale exists + */ + getScale(s: string): { y: number; scale?: number } { + const [yPart, scalePart] = s.split('@'); + if (scalePart == null) return { y: parseInt(yPart, 10) }; + + return { y: parseInt(yPart, 10), scale: parseInt(scalePart.replace('x', ''), 10) }; + }, /** * Validate that the tile request is somewhat valid * - Valid projection @@ -81,8 +106,9 @@ export const Validate = { req.set('tileSet', req.params.tileSet); + const { y, scale } = Validate.getScale(req.params.y); + const x = parseInt(req.params.x, 10); - const y = parseInt(req.params.y, 10); const z = parseInt(req.params.z, 10); const tileMatrix = Validate.getTileMatrixSet(req.params.tileMatrix); @@ -104,8 +130,16 @@ export const Validate = { const pipeline = req.query.get('pipeline'); if (pipeline) req.set('pipeline', pipeline); + if (scale != null) { + if (isNaN(scale)) throw new LambdaHttpResponse(400, 'Invalid scale'); + if (scale !== 2) throw new LambdaHttpResponse(400, 'Only 2x scale is supported'); + + req.set('scale', scale); + } + const xyzData = { tile: { x, y, z }, + scale, tileSet: req.params.tileSet, tileMatrix, tileType: req.params.tileType, diff --git a/packages/landing/static/examples/index.maplibre.compare.3857.html b/packages/landing/static/examples/index.maplibre.compare.3857.html index c4b742463..84eb35646 100644 --- a/packages/landing/static/examples/index.maplibre.compare.3857.html +++ b/packages/landing/static/examples/index.maplibre.compare.3857.html @@ -60,16 +60,16 @@ } // vector layers - const styleUrl = - 'https://basemaps.linz.govt.nz/v1/tiles/topographic/EPSG:3857/style/topographic.json?api=' + apiKey; + const url512 = + '/v1/tiles/aerial/EPSG:3857/style/{z}/{x}/{y}@2x.webp?api=' + apiKey; // Raster layers - const url = 'https://basemaps.linz.govt.nz/v1/tiles/aerial/EPSG:3857/{z}/{x}/{y}.webp?api=' + apiKey; + const url = '/v1/tiles/aerial/EPSG:3857/{z}/{x}/{y}.webp?api=' + apiKey; const startPos = [173, -40.5]; const startZoom = 6; - var raster = new maplibregl.Map({ + var raster256 = new maplibregl.Map({ container: 'raster', // Container ID style: { version: 8, @@ -91,7 +91,28 @@ center: startPos, zoom: startZoom, }); - + var raster512 = new maplibregl.Map({ + container: 'raster', // Container ID + style: { + version: 8, + sources: { + 'raster-tiles': { + type: 'raster', + tiles: [url512], + tileSize: 512, + }, + }, + layers: [ + { + id: 'LINZ Raster Basemaps', + type: 'raster', + source: 'raster-tiles', + }, + ], + }, + center: startPos, + zoom: startZoom, + }); var vector = new maplibregl.Map({ container: 'vector', // Container ID style: styleUrl, @@ -102,7 +123,7 @@ // A selector or reference to HTML element var container = '#comparison-container'; - new mapboxgl.Compare(raster, vector, container, { + new mapboxgl.Compare(raster, raster512, container, { mousemove: true, // Optional. Set to true to enable swiping during cursor movement. orientation: 'vertical', // Optional. Sets the orientation of swiper to horizontal or vertical, defaults to vertical }); diff --git a/packages/tiler/src/tiler.ts b/packages/tiler/src/tiler.ts index 29a4ed030..4221b06de 100644 --- a/packages/tiler/src/tiler.ts +++ b/packages/tiler/src/tiler.ts @@ -26,6 +26,7 @@ function isCotar(x: CloudArchive): x is Cotar { export class Tiler { /** Tile size for the tiler and sub objects */ public readonly tms: TileMatrixSet; + scale: number; /** * Tiler for a TileMatrixSet @@ -33,8 +34,9 @@ export class Tiler { * @param tms * @param convertZ override the default convertZ */ - public constructor(tms: TileMatrixSet) { + public constructor(tms: TileMatrixSet, scale: number = 1) { this.tms = tms; + this.scale = scale; } /** @@ -49,6 +51,8 @@ export class Tiler { let layers: Composition[] = []; /** Raster pixels of the output tile */ const screenPx = this.tms.tileToPixels(x, y); + + // console.log(this.tms); const screenBoundsPx = new Bounds(screenPx.x, screenPx.y, this.tms.tileSize, this.tms.tileSize); for (const asset of assets) { @@ -101,6 +105,7 @@ export class Tiler { // Validate that the requested COG tile actually intersects with the output raster const tileIntersection = target.intersection(raster.tile); if (tileIntersection == null) return null; + console.log({ source, target, tile: raster.tile, scaleFactor, scale: this.scale }); // If the output tile bounds are less than a pixel there is not much point rendering them const tileBounds = tileIntersection.subtract(target); @@ -108,7 +113,7 @@ export class Tiler { return null; } - const drawAtRegion = target.subtract(raster.tile); + const drawAtRegion = target.subtract(raster.tile).scale(this.scale); const composition: Composition = { type: 'tiff', @@ -126,7 +131,7 @@ export class Tiler { // Often COG tiles do not align to the same size as XYZ Tiles // This will scale the COG tile to the same size as a XYZ - if (source.width !== target.width || source.height !== target.height) { + if (source.width !== drawAtRegion.width || source.height !== drawAtRegion.height) { const scaleX = target.width / source.width; const scaleY = target.height / source.height; composition.resize = { width: target.width, height: target.height, scaleX, scaleY, scale: scaleFactor }; @@ -152,7 +157,7 @@ export class Tiler { // Find the best internal overview tiff to use with the desired XYZ resolution const targetResolution = this.tms.pixelScale(z); - const img = tiff.getImageByResolution(targetResolution); + const img = tiff.getImageByResolution(this.tms.pixelScale(z + (this.scale - 1))); // Often the overviews do not align to the actual resolution we want so we will need to scale the overview to the correct resolution const pixelScale = targetResolution / img.resolution[0];