layers_MapTile.js

import * as THREE from 'three';
const defaultUVBounds = new THREE.Box2(new THREE.Vector2(0, 0), new THREE.Vector2(1, 1));
const defaultTexture = generateDefaultTexture();
function generateDefaultTexture() {
    const canvas = document.createElement('canvas');
    canvas.width = 1;
    canvas.height = 1;

    // Get the context of the canvas
    const context = canvas.getContext('2d');

    context.fillStyle = 'rgb(8,23,54)';
    context.fillRect(0, 0, 1, 1);

    // Create a Three.js texture from the canvas
    const texture = new THREE.Texture(canvas);

    // Set texture parameters if needed
    texture.wrapS = THREE.ClampToEdgeWrapping;
    texture.wrapT = THREE.ClampToEdgeWrapping;
    texture.minFilter = THREE.NearestFilter;
    texture.magFilter = THREE.NearestFilter;
    texture.needsUpdate = true;
    return texture;
}

/**
 * Map Tile handles level of detail for maps separately from the terrain level of detail.
 */
class MapTile {
    /**
     * 
     * @param {Object} properties 
     * @param {Object} properties.reference unused, only EPSG:4326 is supported
     * @param {THREE.Box2} properties.bounds this tile's bounds
     * @param {MapTile} properties.parent this tile's direct parent
     * @param {fetchTileFunction} properties.fetchTileTextureFunction a function that fetches a tile texture through a CancellableTextureLoader and takes this MapTile's bounds and a callback as argument.
     * @param {maxLOD} properties.maxLOD a maximum recursion depth
     * @param {lod} properties.lod this tile's lod (optional)
     */
    constructor(properties) {
        this.reference = properties.reference; // currently only EPSG:4326 is supported
        this.bounds = properties.bounds;
        this.boundsWidth = (this.bounds.max.x - this.bounds.min.x);
        this.boundsHeight = (this.bounds.max.y - this.bounds.min.y);
        this.parent = properties.parent;
        this.fetchTileTextureFunction = properties.fetchTileTextureFunction;
        this.maxLOD = properties.maxLOD ? properties.maxLOD : 20;
        this.lod = properties.lod ? properties.lod : 0;
        this.users = new Set();
        this.children = [];
        this.callbacks = [];
    }

    /**
     * Request a texture and UVBounds from this tile.
     * the request fits this tile if the requestBounds width and height are equal to the tile bounds or smalle but bigger than half of the tile bounds
     * 
     * If the request fits the tile:
     * - When a texture is already loaded, it is immediately returned with matching uv bounds. The callback will never be called.
     * - when a texture is not yet loaded, a texture will be requested from the parent and the texture for this level will be requested. 
     * The callback will then be called if the requestor hasn't called the detach method in the meantime.
     * 
     * If the request does not fit the tile because the tile is too large.
     * - If the tile already has children, the request will be forwarded.
     * - If the tile does not have children, children are generated and the request is forwarded down.
     * 
     * If the request does not fit the tile because the tile bounds doesn't contain the request bounds,  an error will be thrown.
     * 
     * @param {Object} requestor 
     * @param {THREE.Box2} requestBounds 
     * @param {Function} callback a callback function called when the texture is loaded
     */
    getTextureAndUVBounds(requestor, requestBounds, callback) {
        const self = this;
        if (!self.bounds.containsBox(requestBounds)) {
            throw "Exception: MapTile bounds does not contain request bounds";
        }

        if (self.maxLOD == self.lod ||
            requestBounds.max.x - requestBounds.min.x > self.boundsWidth * 0.5 ||
            requestBounds.max.y - requestBounds.min.y > self.boundsHeight * 0.5) { //bounds fit this tile

            self.users.add(requestor);
            if (self.texture) {
                if (self.texture.isReady) {
                    return {
                        texture: self.texture,
                        uvBounds: new THREE.Box2(
                            new THREE.Vector2(
                                (requestBounds.min.x - self.bounds.min.x) / self.boundsWidth,
                                (requestBounds.min.y - self.bounds.min.y) / self.boundsHeight),
                            new THREE.Vector2(
                                (requestBounds.max.x - self.bounds.min.x) / self.boundsWidth,
                                (requestBounds.max.y - self.bounds.min.y) / self.boundsHeight)
                        ),
                        reference: self.reference
                    }
                } else {

                    self.callbacks.push((texture) => {


                        callback({
                            texture: texture,
                            uvBounds: new THREE.Box2(
                                new THREE.Vector2(
                                    (requestBounds.min.x - self.bounds.min.x) / self.boundsWidth,
                                    (requestBounds.min.y - self.bounds.min.y) / self.boundsHeight),
                                new THREE.Vector2(
                                    (requestBounds.max.x - self.bounds.min.x) / self.boundsWidth,
                                    (requestBounds.max.y - self.bounds.min.y) / self.boundsHeight)
                            ),
                            reference: self.reference
                        })
                    });
                    if (self.parent) {
                        return self.parent.getBestTextureAndUVBounds(requestor, requestBounds);
                    } else {
                        return {
                            texture: defaultTexture,
                            uvBounds: defaultUVBounds,
                            reference: self.reference
                        }
                    }
                }
            } else {
                self.texture = self.fetchTile(self.bounds);
                self.callbacks.push((texture) => {
                    callback({
                        texture: texture,
                        uvBounds: new THREE.Box2(
                            new THREE.Vector2(
                                (requestBounds.min.x - self.bounds.min.x) / self.boundsWidth,
                                (requestBounds.min.y - self.bounds.min.y) / self.boundsHeight),
                            new THREE.Vector2(
                                (requestBounds.max.x - self.bounds.min.x) / self.boundsWidth,
                                (requestBounds.max.y - self.bounds.min.y) / self.boundsHeight)
                        ),
                        reference: self.reference
                    })
                });
                if (self.parent) {
                    return self.parent.getBestTextureAndUVBounds(requestor, requestBounds);
                } else {
                    return {
                        texture: defaultTexture,
                        uvBounds: defaultUVBounds,
                        reference: self.reference
                    }
                }
            }
        } else {
            if (self.children.length == 0) {
                self.split();
            }
            for (let i = 0; i < self.children.length; i++) {
                if (self.children[i].bounds.containsBox(requestBounds)) {
                    return self.children[i].getTextureAndUVBounds(requestor, requestBounds, callback);
                }
            }
        }
    }

    /**
     * Split this tile in four and populate this.children with them.
     */
    split() {
        const self = this;
        const midX = (self.bounds.min.x + self.bounds.max.x) / 2;
        const midY = (self.bounds.min.y + self.bounds.max.y) / 2;

        // Create four new boxes
        const childBounds = [
            new THREE.Box2(new THREE.Vector2(self.bounds.min.x, midY), new THREE.Vector2(midX, self.bounds.max.y)),
            new THREE.Box2(new THREE.Vector2(midX, midY), new THREE.Vector2(self.bounds.max.x, self.bounds.max.y)),
            new THREE.Box2(new THREE.Vector2(self.bounds.min.x, self.bounds.min.y), new THREE.Vector2(midX, midY)),
            new THREE.Box2(new THREE.Vector2(midX, self.bounds.min.y), new THREE.Vector2(self.bounds.max.x, midY))
        ];


        childBounds.forEach(cb => {
            self.children.push(
                new MapTile({
                    reference: self.reference,
                    bounds: cb,
                    parent: self,
                    fetchTileTextureFunction: self.fetchTileTextureFunction,
                    maxLOD: self.maxLOD,
                    lod: self.lod + 1
                })
            );
        });

    }
    /**
     * If this tile has a loaded texture, returns it with uv bounds that match the requested bounds,
     * else, forwards the request to it's parent.
     * if no parent is present, returns a default texture.
     * @param {Object} requestor 
     * @param {THREE.Box2} requestBounds 
     */
    getBestTextureAndUVBounds(requestor, requestBounds) {

        const self = this;
        if (self.texture && self.texture.isReady) {
            self.users.add(requestor);
            return {
                texture: self.texture,
                uvBounds: new THREE.Box2(
                    new THREE.Vector2(
                        (requestBounds.min.x - self.bounds.min.x) / self.boundsWidth,
                        (requestBounds.min.y - self.bounds.min.y) / self.boundsHeight),
                    new THREE.Vector2(
                        (requestBounds.max.x - self.bounds.min.x) / self.boundsWidth,
                        (requestBounds.max.y - self.bounds.min.y) / self.boundsHeight)
                ),
                reference: self.reference
            }
        } else if (self.parent) {
            return self.parent.getBestTextureAndUVBounds(requestor, requestBounds);
        } else {
            return {
                texture: defaultTexture,
                uvBounds: defaultUVBounds,
                reference: self.reference
            }
        }
    }

    /**
     * detach a requestor that was using this tile's texture.
     * If children have no users, they'll be garbage collected.
     * If this tile has no more users, texture will be disposed and ongoing texture requests will be cancelled.
     * @param {Object} requestor the requestor using the tile's texture
     * @param {THREE.Texture} texture (optional) the requestor will be removed from the user set ONLY if the texture matches this tile's texture when one is provided
     * @returns true if there are no users left for this tile or any children tile
     */
    detach(requestor, texture) {
        if (texture && texture == defaultTexture) return;
        const self = this;
        let emptyChildren = true;
        for (let i = 0; i < self.children.length; i++) {
            if (!self.children[i].detach(requestor, texture)) {
                emptyChildren = false;
                
            }
        }

        if (emptyChildren) {
            self.children.length = 0;
        }
        if (texture) {
            if (self.texture && texture == self.texture) {
                self.users.delete(requestor);
            }
        } else {
            self.users.delete(requestor);
        }

        if (emptyChildren && self.users.size == 0) {
            if (self.texture) {
                if(self.texture.abort) self.texture.abort();
                self.texture.dispose();
                self.texture = undefined;
            }
            return true;
        }

        return false;
    }

    fetchTile() {
        const self = this;
        return self.fetchTileTextureFunction(self.bounds, (texture) => {
            self.callbacks.forEach(callback => callback(texture));
            self.callbacks.length = 0;
        }, error => {
            if (self.texture) {
                if(self.texture.abort) self.texture.abort();
                self.texture.dispose();
                self.texture = undefined;
            }

        })
    }


} export { MapTile }