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
* @description 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
* @description 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 }