Skip to content

Commit

Permalink
wip: add @2x hidpi support
Browse files Browse the repository at this point in the history
  • Loading branch information
blacha committed Dec 23, 2024
1 parent 8072eb9 commit cf5d79a
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 21 deletions.
18 changes: 11 additions & 7 deletions packages/lambda-tiler/src/cli/render.tile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
Expand Down Expand Up @@ -49,14 +51,16 @@ async function main(): Promise<void> {
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');
}
Expand Down
18 changes: 15 additions & 3 deletions packages/lambda-tiler/src/routes/tile.xyz.raster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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<LambdaHttpResponse> {
const tileOutput = Validate.pipeline(tileSet, xyz.tileType, xyz.pipeline);
if (tileOutput == null) return NotFound();
Expand All @@ -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,
Expand Down
36 changes: 35 additions & 1 deletion packages/lambda-tiler/src/util/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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,
Expand Down
33 changes: 27 additions & 6 deletions packages/landing/static/examples/index.maplibre.compare.3857.html
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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
});
Expand Down
13 changes: 9 additions & 4 deletions packages/tiler/src/tiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ 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
*
* @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;
}

/**
Expand All @@ -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) {
Expand Down Expand Up @@ -101,14 +105,15 @@ 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 });

Check failure on line 108 in packages/tiler/src/tiler.ts

View workflow job for this annotation

GitHub Actions / build-containers

Unexpected console statement

Check failure on line 108 in packages/tiler/src/tiler.ts

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Unexpected console statement

Check failure on line 108 in packages/tiler/src/tiler.ts

View workflow job for this annotation

GitHub Actions / build-deploy

Unexpected console statement

// If the output tile bounds are less than a pixel there is not much point rendering them
const tileBounds = tileIntersection.subtract(target);
if (tileBounds.height < 0.5 || tileBounds.width < 0.5) {
return null;
}

const drawAtRegion = target.subtract(raster.tile);
const drawAtRegion = target.subtract(raster.tile).scale(this.scale);

const composition: Composition = {
type: 'tiff',
Expand All @@ -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 };
Expand All @@ -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];

Expand Down

0 comments on commit cf5d79a

Please sign in to comment.