Source: LayoutTileImages.js

import { Layout } from './Layout.js';
import { BoundingBox } from './BoundingBox.js';
import { Tile } from './Tile.js';
import { Annotation } from './Annotation.js';
/*
 * @fileoverview
 * LayoutTileImages module provides management for collections of image tiles with associated regions.
 * This layout type is specialized for handling multiple independent image tiles, each with their own
 * position and dimensions, rather than a regular grid of tiles like LayoutTiles.
 */

/*
 * @typedef {Object} TileDescriptor
 * Properties expected in tile descriptors:
 * @property {boolean} visible - Whether the tile should be rendered
 * @property {Object} region - Position and dimensions of the tile
 * @property {number} region.x - X coordinate of tile's top-left corner
 * @property {number} region.y - Y coordinate of tile's top-left corner
 * @property {number} region.w - Width of the tile
 * @property {number} region.h - Height of the tile
 * @property {string} image - URL or path to the tile's image
 * @property {number} [publish] - Publication status (1 = published)
 */

/**
 * LayoutTileImages class manages collections of image tiles with associated regions.
 * Each tile represents an independent image with its own position and dimensions in the layout space.
 * Tiles can be individually shown or hidden and are loaded from annotation files or external descriptors.
 * @extends Layout
 */
class LayoutTileImages extends Layout {
	/**
	 * Creates a new LayoutTileImages instance.
	 * @param {string|null} url - URL to the annotation file containing tile descriptors, or null if descriptors will be set later
	 * @param {string} type - The layout type (should be 'tile_images')
	 * @param {Object} [options] - Configuration options inherited from Layout
	 */
	constructor(url, type, options) {
		super(url, null, options);
		this.setDefaults(type);
		this.init(url, type, options);

		// Contain array of records with at least visible,region,image (url of the image). 
		// Can be also a pointer to annotation array set from outside with setTileDescriptors()
		this.tileDescriptors = [];
		this.box = new BoundingBox();

		if (url != null) {
			// Read data from annotation file
			this.loadDescriptors(url);
		}
	}

	/**
	 * Gets the tile size. For this layout, tiles don't have a fixed size.
	 * @returns {number[]} Returns [0, 0] as tiles have variable sizes
	 */
	getTileSize() {
		return [0, 0];
	}

	/**
	 * Loads tile descriptors from an annotation file.
	 * @private
	 * @async
	 * @param {string} url - URL of the annotation file
	 * @fires Layout#ready - When descriptors are loaded and processed
	 * @fires Layout#updateSize - When bounding box is computed
	 */
	async loadDescriptors(url) {
		// Load tile descriptors from annotation file
		let response = await fetch(url);
		if (!response.ok) {
			this.status = "Failed loading " + url + ": " + response.statusText;
			return;
		}
		this.tileDescriptors = await response.json();
		if (this.tileDescriptors.status == 'error') {
			alert("Failed to load annotations: " + this.tileDescriptors.msg);
			return;
		}
		//this.annotations = this.annotations.map(a => '@context' in a ? Annotation.fromJsonLd(a): a);
		this.tileDescriptors = this.tileDescriptors.map(a => new Annotation(a));
		for (let a of this.tileDescriptors) {
			if (a.publish != 1)
				a.visible = false;
		}
		this.computeBoundingBox();
		this.emit('updateSize');

		if (this.path == null) {
			this.setPathFromUrl(url);
		}

		this.status = 'ready';
		this.emit('ready');
	}

	/**
	 * Computes the bounding box containing all tile regions.
	 * Updates the layout's box property to encompass all tile regions.
	 * @private
	 */
	computeBoundingBox() {
		this.box = new BoundingBox();
		for (let a of this.tileDescriptors) {
			let r = a.region;
			let b = new BoundingBox({ xLow: r.x, yLow: r.y, xHigh: r.x + r.w, yHigh: r.y + r.h });
			this.box.mergeBox(b);
		}
	}

	/**
	 * Gets the layout's bounding box.
	 * @returns {BoundingBox} The bounding box containing all tile regions
	 */
	boundingBox() {
		return this.box;
	}

	/**
	 * Sets the base path for tile URLs based on the annotation file location.
	 * @private
	 * @param {string} url - URL of the annotation file
	 */
	setPathFromUrl(url) {
		// Assume annotations in dir of annotation.json + /annot/
		const myArray = url.split("/");
		const N = myArray.length;
		this.path = "";
		for (let i = 0; i < N - 1; ++i) {
			this.path += myArray[i] + "/";
		}
		this.getTileURL = (id, tile) => {
			const url = this.path + '/' + this.tileDescriptors[tile.index].image;
			return url;
		}
		//this.path += "/annot/";
	}

	/**
	 * Sets tile descriptors programmatically instead of loading from a file.
	 * @param {Annotation[]} tileDescriptors - Array of tile descriptors
	 * @fires Layout#ready
	 */
	setTileDescriptors(tileDescriptors) {
		this.tileDescriptors = tileDescriptors;

		this.status = 'ready';
		this.emit('ready');
	}

	/**
	 * Gets the URL for a specific tile.
	 * @param {number} id - Channel/raster ID
	 * @param {TileObj} tile - Tile object
	 * @returns {string} URL to fetch tile image
	 */
	getTileURL(id, tile) {
		const url = this.path + '/' + this.tileDescriptors[id].image;
		return url;
	}

	/**
	 * Sets visibility for a specific tile.
	 * @param {number} index - Index of the tile
	 * @param {boolean} visible - Visibility state to set
	 */
	setTileVisible(index, visible) {
		this.tileDescriptors[index].visible = visible;
	}

	/**
	 * Sets visibility for all tiles.
	 * @param {boolean} visible - Visibility state to set for all tiles
	 */
	setAllTilesVisible(visible) {
		const N = this.tileCount();

		for (let i = 0; i < N; ++i) {
			this.tileDescriptors[i].visible = visible;
		}
	}

	/**
	 * Maps tile coordinates to a linear index.
	 * In this layout, x directly maps to the index as tiles are stored in a flat list.
	 * @param {number} level - Zoom level (unused in this layout)
	 * @param {number} x - X coordinate (used as index)
	 * @param {number} y - Y coordinate (unused in this layout)
	 * @returns {number} Linear index of the tile
	 */
	index(level, x, y) {
		// Map x to index (flat list)
		return x;
	}

	/**
	 * Gets coordinates for a tile in both image space and texture space.
	 * @param Obj} tile - The tile to get coordinates for
	 * @returns {Object} Coordinate data
	 * @returns {Float32Array} .coords - Image space coordinates [x,y,z, x,y,z, x,y,z, x,y,z]
	 * @returns {Float32Array} .tcoords - Texture coordinates [u,v, u,v, u,v, u,v]
	 */
	tileCoords(tile) {
		const r = this.tileDescriptors[tile.index].region;
		const x0 = r.x;
		const y0 = r.y
		const x1 = x0 + r.w;
		const y1 = y0 + r.h;

		return {
			coords: new Float32Array([x0, y0, 0, x0, y1, 0, x1, y1, 0, x1, y0, 0]),

			//careful: here y is inverted due to textures not being flipped on load (Firefox fault!).
			tcoords: new Float32Array([0, 1, 0, 0, 1, 0, 1, 1])
		};
	}

	/**
	 * Determines which tiles are needed for the current view.
	 * @param {Viewport} viewport - Current viewport
	 * @param {Transform} transform - Current transform
	 * @param {Transform} layerTransform - Layer-specific transform
	 * @param {number} border - Border size in viewport units
	 * @param {number} bias - Resolution bias (unused in this layout)
	 * @param {Map<number,Tile>} tiles - Currently available tiles
	 * @param {number} [maxtiles=8] - Maximum number of tiles to return
	 * @returns {TileObj[]} Array of needed tiles sorted by distance to viewport center
	 */
	needed(viewport, transform, layerTransform, border, bias, tiles, maxtiles = 8) {
		//look for needed nodes and prefetched nodes (on the pos destination
		const box = this.getViewportBox(viewport, transform, layerTransform);

		let needed = [];
		let now = performance.now();

		// Linear scan of all the potential tiles
		const N = this.tileCount();
		const flipY = true;
		for (let x = 0; x < N; x++) {
			let index = this.index(0, x, 0);
			let tile = tiles.get(index) || this.newTile(index);

			if (this.intersects(box, index, flipY)) {
				tile.time = now;
				tile.priority = this.tileDescriptors[index].visible ? 10 : 1;
				if (tile.missing === null)
					needed.push(tile);
			}
		}
		let c = box.center();
		//sort tiles by distance to the center TODO: check it's correct!
		needed.sort(function (a, b) { return Math.abs(a.x - c[0]) + Math.abs(a.y - c[1]) - Math.abs(b.x - c[0]) - Math.abs(b.y - c[1]); });

		return needed;
	}

	/**
	 * Gets tiles currently available for rendering.
	 * @param {Viewport} viewport - Current viewport
	 * @param {Transform} transform - Current transform
	 * @param {Transform} layerTransform - Layer-specific transform
	 * @param {number} border - Border size in viewport units
	 * @param {number} bias - Resolution bias (unused in this layout)
	 * @param {Map<number,Tile>} tiles - Available tiles
	 * @returns {Object.<number,Tile>} Map of tile index to tile object for visible, loaded tiles
	 */
	available(viewport, transform, layerTransform, border, bias, tiles) {
		//find box in image coordinates where (0, 0) is in the upper left corner.
		const box = this.getViewportBox(viewport, transform, layerTransform);

		let torender = [];

		// Linear scan of all the potential tiles
		const N = this.tileCount();
		const flipY = true;
		for (let x = 0; x < N; x++) {
			let index = this.index(0, x, 0);

			if (this.tileDescriptors[index].visible && this.intersects(box, index, flipY)) {
				if (tiles.has(index)) {
					let tile = tiles.get(index);
					if (tile.missing == 0) {
						torender[index] = tile;
					}
				}
			}
		}

		return torender;
	}

	/**
	 * Creates a new tile instance with properties from its descriptor.
	 * @param {number} index - Index of the tile descriptor
	 * @returns {TileObj} New tile instance with region and image properties
	 */
	newTile(index) {
		let tile = new Tile();
		tile.index = index;

		let descriptor = this.tileDescriptors[index];
		tile.image = descriptor.image;
		Object.assign(tile, descriptor.region);
		return tile;
	}

	/**
	 * Checks if a tile's region intersects with a given box.
	 * @private
	 * @param {BoundingBox} box - Box to check intersection with
	 * @param {number} index - Index of the tile to check
	 * @param {boolean} [flipY=true] - Whether to flip Y coordinates for texture coordinate space
	 * @returns {boolean} True if the tile intersects the box
	 */
	intersects(box, index, flipY = true) {
		const r = this.tileDescriptors[index].region;
		const xLow = r.x;
		const yLow = r.y;
		const xHigh = xLow + r.w;
		const yHigh = yLow + r.h;
		const boxYLow = flipY ? -box.yHigh : box.yLow;
		const boxYHigh = flipY ? -box.yLow : box.yHigh;

		return xLow < box.xHigh && yLow < boxYHigh && xHigh > box.xLow && yHigh > boxYLow;
	}

	/**
	 * Gets the total number of tiles in the layout.
	 * @returns {number} Number of tile descriptors
	 */
	tileCount() {
		return this.tileDescriptors.length;
	}

}
/**
 * @event Layout#ready
 * Fired when the layout is ready for rendering.
 * This occurs when:
 * - Tile descriptors are loaded from annotation file
 * - Tile descriptors are set programmatically
 */

/**
 * @event Layout#updateSize
 * Fired when the layout size changes and scene extension needs updating.
 * This occurs when:
 * - Tile descriptors are loaded and bounding box is computed
 */

// Register the tile_images layout type
Layout.prototype.types['tile_images'] = (url, type, options) => { return new LayoutTileImages(url, type, options); };

export { LayoutTileImages }