Source: Layout.js

import { BoundingBox } from "./BoundingBox";
import { addSignals } from "./Signals";
import { Tile } from "./Tile";
import { CoordinateSystem } from "./CoordinateSystem";

// Tile level x y  index ----- tex missing() start/end (tarzoom) ----- time, priority size(byte)

/**
 * @typedef {Object} TileObj
 * @property {number} level - Zoom level in the image pyramid
 * @property {number} x - Horizontal position in tile grid
 * @property {number} y - Vertical position in tile grid
 * @property {number} index - Unique tile identifier
 * @property {number} [start] - Starting byte position in dataset (for tar formats)
 * @property {number} [end] - Ending byte position in dataset (for tar formats)
 * @property {number} missing - Number of pending channel data requests
 * @property {WebGLTexture[]} tex - Array of textures (one per channel)
 * @property {number} time - Tile creation timestamp for cache management
 * @property {number} priority - Loading priority for cache management
 * @property {number} size - Total tile size in bytes
 */

/**
 * @typedef {'image'|'deepzoom'|'deepzoom1px'|'google'|'zoomify'|'iiif'|'tarzoom'|'itarzoom'} LayoutType
 * @description Supported image format types:
 * - image: Single-resolution web images (jpg, png, etc.)
 * - deepzoom: Microsoft Deep Zoom with root tile > 1px
 * - deepzoom1px: Microsoft Deep Zoom with 1px root tile
 * - google: Google Maps tiling scheme
 * - zoomify: Zoomify format
 * - iiif: International Image Interoperability Framework
 * - tarzoom: OpenLIME tar-based tiling
 * - itarzoom: OpenLIME indexed tar-based tiling
 */

/**
 * @typedef {Object} LayoutOptions
 * @property {number} [width] - Image width (required for google layout)
 * @property {number} [height] - Image height (required for google layout)
 * @property {string} [suffix='jpg'] - Tile file extension
 * @property {string} [subdomains='abc'] - Available subdomains for URL templates
 */

/**
 * Layout manages image formats and tiling schemes in OpenLIME.
 * 
 * This class is responsible for:
 * - Managing different image formats
 * - Handling tiling schemes
 * - Coordinating tile loading
 * - Converting between coordinate systems
 * - Managing tile priorities
 * 
 * Format Support:
 * 1. Single-resolution images:
 * - Direct URL to image file
 * - Supports all standard web formats (jpg, png, etc)
 * 
 * 2. Tiled formats:
 * - DeepZoom (Microsoft): Uses .dzi config file
 * - Google Maps: Direct directory structure
 * - Zoomify: Uses ImageProperties.xml
 * - IIIF: Standard server interface
 * - TarZoom: OpenLIME's optimized format
 * 
 * @fires Layout#ready - When layout is initialized and ready for use
 * @fires Layout#updateSize - When layout dimensions change
 * 
 * @example
 * ```javascript
 * // Single image
 * const imageLayout = new Layout('image.jpg', 'image');
 * 
 * // Deep Zoom
 * const dzLayout = new Layout('tiles.dzi', 'deepzoom');
 * 
 * // Google Maps format
 * const googleLayout = new Layout('tiles/', 'google', {
 *   width: 2000,
 *   height: 1500
 * });
 * ```
 */
class Layout {
	/**
	 * Creates a new Layout instance
	 * @param {string} url - URL to image or configuration file
	 * @param {LayoutType} type - Type of image layout
	 * @param {LayoutOptions} [options] - Additional configuration
	 * @throws {Error} If layout type is unknown or module not loaded
	 */
	constructor(url, type, options) {


		if (type == 'image') {
			this.setDefaults(type);
			this.init(url, type, options);

		} else if (type in this.types)
			return this.types[type](url, type, options);

		else if (type == null)
			return;

		else
			throw "Layout type: " + type + " unknown, or module not loaded";
	}

	/**
	 * Gets tile dimensions
	 * @returns {number[]} [width, height] of tiles
	 */
	getTileSize() {
		return [this.width, this.height];
	}

	/**
	 * Sets default layout properties
	 * @param {LayoutType} type - Layout type
	 * @private
	 */
	setDefaults(type) {
		Object.assign(this, {
			type: type,
			width: 0,
			height: 0,
			suffix: 'jpg',
			urls: [],
			status: null,
			subdomains: 'abc'
		});
	}

	/**
	 * Initializes layout configuration
	 * @param {string} url - Resource URL
	 * @param {LayoutType} type - Layout type
	 * @param {LayoutOptions} options - Configuration options
	 * @private
	 */
	init(url, type, options) {
		if (options)
			Object.assign(this, options);

		if (typeof (url) == 'string')
			this.setUrls([url]);
		if (this.width && this.height)
			this.status = 'ready';
	}

	/**
	 * Sets URLs for layout resources
	 * @param {string[]} urls - Array of resource URLs
	 * @fires Layout#ready
	 * @private
	 */
	setUrls(urls) {
		/**
		* The event is fired when a layout is ready to be drawn(the single-resolution image is downloaded or the multi-resolution structure has been initialized).
		* @event Layout#ready
		*/
		this.urls = urls;
		this.getTileURL = (rasterid, tile) => { return this.urls[rasterid]; }
		this.status = 'ready';
		this.emit('ready');
	}

	/**
	 * Constructs URL for specific image plane
	 * @param {string} url - Base URL
	 * @param {string} plane - Plane identifier
	 * @returns {string} Complete URL
	 */
	imageUrl(url, plane) {
		let path = url.substring(0, url.lastIndexOf('/') + 1);
		return path + plane + '.jpg';
	}

	/**
	 * Gets URL for specific tile
	 * @param {number} id - Channel identifier
	 * @param {TileObj} tile - Tile object
	 * @returns {string} Tile URL
	 * @abstract
	 */
	getTileURL(id, tile) {
		throw Error("Layout not defined or ready.");
	}

	/**
	 * Gets layout bounds
	 * @returns {BoundingBox} Layout boundaries
	 */
	boundingBox() {
		//if(!this.width) throw "Layout not initialized still";
		return new BoundingBox({ xLow: -this.width / 2, yLow: -this.height / 2, xHigh: this.width / 2, yHigh: this.height / 2 });
	}

	/**
	 * Calculates tile coordinates
	 * @param Obj} tile - Tile to calculate coordinates for
	 * @returns {{coords: Float32Array, tcoords: Float32Array}} Image and texture coordinates
	 */
	tileCoords(tile) {
		let w = this.width;
		let h = this.height;
		//careful: here y is inverted due to textures not being flipped on load (Firefox fault!).
		var tcoords = new Float32Array([0, 1, 0, 0, 1, 0, 1, 1]);

		return {
			coords: new Float32Array([-w / 2, -h / 2, 0, -w / 2, h / 2, 0, w / 2, h / 2, 0, w / 2, -h / 2, 0]),
			tcoords: tcoords
		};
	}

	/**
	 * Creates new tile instance
	 * @param {number} index - Tile identifier
	 * @returns {TileObj} New tile object
	 * @private
	 */
	newTile(index) {
		let tile = new Tile();
		tile.index = index;
		return tile;
	}

	/**
	 * Determines required tiles for rendering
	 * @param {Object} viewport - Current viewport
	 * @param {Transform} transform - Current transform
	 * @param {Transform} layerTransform - Layer transform
	 * @param {number} border - Border size
	 * @param {number} bias - Mipmap bias
	 * @param {Map} tiles - Existing tiles
	 * @param {number} [maxtiles=8] - Maximum tiles to return
	 * @returns {TileObj[]} Array of needed tiles
	 */
	needed(viewport, transform, layerTransform, border, bias, tiles, maxtiles = 8) {
		//FIXME should check if image is withing the viewport (+ border)
		let tile = tiles.get(0) || this.newTile(0); //{ index, x, y, missing, tex: [], level };
		tile.time = performance.now();
		tile.priority = 10;

		if (tile.missing === null) // || tile.missing != 0 && !this.requested[index])
			return [tile];
		return [];
	}

	/**
	 * Gets tiles available for rendering
	 * @param {Object} viewport - Current viewport
	 * @param {Transform} transform - Current transform
	 * @param {Transform} layerTransform - Layer transform
	 * @param {number} border - Border size
	 * @param {number} bias - Mipmap bias
	 * @param {Map} tiles - Existing tiles
	 * @returns {Object.<number, Tile>} Map of available tiles
	 */
	available(viewport, transform, layerTransform, border, bias, tiles) {
		//FIXME should check if image is withing the viewport (+ border)
		let torender = {};

		if (tiles.has(0) && tiles.get(0).missing == 0)
			torender[0] = tiles.get(0); //{ index: index, level: level, x: x >> d, y: y >> d, complete: true };
		return torender;
	}

	/**
	 * Calculates viewport bounding box
	 * @param {Object} viewport - Viewport parameters
	 * @param {Transform} transform - Current transform
	 * @param {Transform} layerT - Layer transform
	 * @returns {BoundingBox} Viewport bounds in image space
	 */
	getViewportBox(viewport, transform, layerT) {
		const boxViewport = new BoundingBox({ xLow: viewport.x, yLow: viewport.y, xHigh: viewport.x + viewport.dx, yHigh: viewport.y + viewport.dy });
		return CoordinateSystem.fromViewportBoxToImageBox(boxViewport, transform, viewport, layerT, { w: this.width, h: this.height });
	}
}

Layout.prototype.types = {}

addSignals(Layout, 'ready', 'updateSize');

export { Layout }