layer_CanvasTiles.ts

import * as quadTree from "../quadTree/quadTree";
import type { EventCallback, EventsHandler } from "../Events";
import { BaseTileMaterialLayer } from "./BaseTileMaterialLayer";
import type { IBaseTileMaterialLayerParams } from "./BaseTileMaterialLayer";
import type { LayerEventsList } from "./Layer";
import { Material } from "../layer/Material";
import { Planet } from "../scene/Planet";

type ApplyImageFunc = (material: HTMLCanvasElement | ImageBitmap | HTMLImageElement) => void;
type DrawTileCallback = (material: Material, applyImage: ApplyImageFunc) => void;

export interface ICanvasTilesParams extends IBaseTileMaterialLayerParams {
    drawTile: DrawTileCallback;
    animated?: boolean;
    minNativeZoom?: number;
    maxNativeZoom?: number;
}

type CanvasTilesEventsList = ["load", "loadend"];

type CanvasTilesEventsType = EventsHandler<CanvasTilesEventsList> & EventsHandler<LayerEventsList>;

const CANVASTILES_EVENTS: CanvasTilesEventsList = [
    /**
     * Triggered when the current tile image has loaded before rendering.
     * @event load
     */
    "load",

    /**
     * Triggered when all tiles have loaded or loading has stopped.
     * @event loadend
     */
    "loadend"
];

/**
 * Layer that renders each tile as a separate canvas object.
 * @class
 * @extends {Layer}
 * @param {string} [name="noname"] - Layer name.
 * @param {ICanvasTilesParams} options - Layer options.
 * @param {number} [options.opacity=1.0] - Layer opacity.
 * @param {number} [options.minZoom=0] - Minimal visibility zoom level.
 * @param {number} [options.maxZoom=50] - Maximal visibility zoom level.
 * @param {string} [options.attribution] - Layer attribution shown in the attribution area.
 * @param {boolean} [options.isBaseLayer=false] - Base layer flag.
 * @param {boolean} [options.visibility=true] - Layer visibility.
 * @param {boolean} [options.animated=false] - Re-draw ready tiles every frame.
 * @param {number} [options.minNativeZoom=0] - Minimal zoom level where native tile drawing is allowed.
 * @param {number} [options.maxNativeZoom=100] - Maximal zoom level where native tile drawing is allowed.
 * @param {DrawTileCallback} options.drawTile - Draw tile callback.
 * @fires load
 * @fires loadend
 */
class CanvasTiles extends BaseTileMaterialLayer {
    static MAX_REQUESTS: number = 20;
    static __requestsCounter: number = 0;

    public override events: CanvasTilesEventsType;

    public animated: boolean;

    /**
     * Current creating tiles counter.
     * @protected
     * @type {number}
     */
    protected _counter: number;

    /**
     * Queue of pending tiles waiting to be created.
     * @protected
     * @type {Material[]}
     */
    protected _pendingsQueue: Material[]; // new og.QueueArray();

    /**
     * Draw tile callback.
     * @type {DrawTileCallback}
     * @public
     */
    public drawTile: DrawTileCallback;

    protected _onLoadend_: EventCallback | null;

    constructor(name: string | null, options: ICanvasTilesParams) {
        super(name, options);

        //@ts-ignore
        this.events = this.events.registerNames(CANVASTILES_EVENTS);

        this.animated = options.animated || false;

        this.minNativeZoom = options.minNativeZoom || 0;
        this.maxNativeZoom = options.maxNativeZoom || 100;

        this._counter = 0;

        this._pendingsQueue = []; // new og.QueueArray();

        this.drawTile = options.drawTile;

        this._onLoadend_ = null;
    }

    public override addTo(planet: Planet) {
        this._onLoadend_ = this._onLoadend.bind(this);
        this.events.on("loadend", this._onLoadend_!, this);
        return super.addTo(planet);
    }

    public override remove() {
        this.events.off("loadend", this._onLoadend_);
        this._onLoadend_ = null;
        return super.remove();
    }

    public _onLoadend() {
        if (this._planet && this._planet.quadTreeStrategy._terrainCompletedActivated) {
            this._planet.events.dispatch(this._planet.events.layerloadend, this);
        }
    }

    public override get instanceName(): string {
        return "CanvasTiles";
    }

    public override get isIdle() {
        return super.isIdle && this._counter === 0;
    }

    /**
     * Abort loading tiles.
     * @public
     */
    public override abortLoading() {
        this._pendingsQueue.forEach((qi: Material) => {
            this.abortMaterialLoading(qi);
        });
        this._pendingsQueue = [];
    }

    /**
     * Sets layer visibility.
     * @public
     * @param {boolean} visibility - Layer visibility.
     */
    public override setVisibility(visibility: boolean) {
        if (visibility !== this._visibility) {
            super.setVisibility(visibility);

            if (!visibility) {
                this.abortLoading();
            }
        }
    }

    /**
     * Start to load tile material.
     * @public
     * @virtual
     * @param {Material} material -
     */
    public override loadMaterial(material: Material, _forceLoading: boolean = false) {
        let seg = material.segment;

        if (this._isBaseLayer) {
            material.texture = seg.getDefaultTexture();
        } else {
            material.texture = seg.planet.transparentTexture;
        }

        if (this._planet!.layerLock.isFree() || material.segment.tileZoom < 2) {
            material.isReady = false;
            material.isLoading = true;
            if (CanvasTiles.__requestsCounter >= CanvasTiles.MAX_REQUESTS && this._counter) {
                this._pendingsQueue.push(material);
            } else {
                this._exec(material);
            }
        }
    }

    /**
     * Loads material image and apply it to the planet segment.
     * @protected
     * @param {Material} material - Loads material image.
     */
    protected _exec(material: Material) {
        CanvasTiles.__requestsCounter++;
        this._counter++;
        const e = this.events.load!;
        if (e.handlers.length) {
            this.events.dispatch(e, material);
        }
        requestAnimationFrame(() => {
            this.drawTile(material, (canvas: HTMLCanvasElement | ImageBitmap | HTMLImageElement) => {
                this._counter--;
                CanvasTiles.__requestsCounter--;
                this._correctCounter();
                if (material.isLoading) {
                    material.applyImage(canvas);
                }
                this._dequeueRequest();
            });
        });
    }

    protected _correctCounter() {
        if (this._counter < 0) this._counter = 0;
        if (CanvasTiles.__requestsCounter < 0) CanvasTiles.__requestsCounter = 0;
    }

    /**
     * Abort exact material loading.
     * @public
     * @param {Material} material - Segment material.
     */
    public override abortMaterialLoading(material: Material) {
        if (material.isLoading) {
            this._counter--;
            CanvasTiles.__requestsCounter--;
            this._correctCounter();
            this._dequeueRequest();
        }
        material.isLoading = false;
        material.isReady = false;
    }

    protected _dequeueRequest() {
        if (this._pendingsQueue.length) {
            if (CanvasTiles.__requestsCounter < CanvasTiles.MAX_REQUESTS) {
                const pmat = this._whilePendings();
                if (pmat) {
                    this._exec(pmat);
                }
            }
        } else if (this._counter === 0 && this._planet && this._planet.quadTreeStrategy._terrainCompletedActivated) {
            this.events.dispatch(this.events.loadend);
        }
    }

    protected _whilePendings(): Material | null {
        while (this._pendingsQueue.length) {
            const pmat = this._pendingsQueue.pop();
            if (pmat && pmat.segment && pmat.segment.node) {
                if (pmat.segment.initialized && pmat.segment.node.getState() === quadTree.RENDERING) {
                    return pmat;
                }
                pmat.isLoading = false;
            }
        }
        return null;
    }

    protected override _onMaterialReady(material: Material): void {
        if (this.animated) {
            requestAnimationFrame(() => {
                this.drawTile(material, function (canvas) {
                    material.applyImage(canvas);
                });
            });
        }
    }

    protected override _onParentMaterialApplied(material: Material, _psegm: Material): void {
        //
        // Animated doesn't work withMaxNativeZoom
        //
        if (this.animated) {
            requestAnimationFrame(() => {
                if (material.segment) {
                    this.drawTile(material, function (canvas) {
                        material.applyImage(canvas);
                    });
                }
            });
        }
    }

    //
    // Keep it for backup.
    //
    // public override applyMaterial(material: Material): NumberArray4 {
    //     if (this.waitForParentMaterial) {
    //         return this._apllyMaterialDefault(material);
    //     } else {
    //         return this._applyMaterialFast(material);
    //     }
    // }
    //
    // protected _apllyMaterialDefault(material: Material): NumberArray4 {
    //     if (material.isReady) {
    //         // IMPORTANT!
    //         // Animated doesn't work withMaxNativeZoom
    //         // It could be fixed with call drawTile method only for parent
    //         // material (which is rendered on the current segment material),
    //         // just for one renderer frame
    //         if ((material.layer as CanvasTiles).animated) {
    //             requestAnimationFrame(() => {
    //                 this.drawTile(material, function (canvas) {
    //                     material.applyImage(canvas);
    //                 });
    //             });
    //         }
    //
    //         return material.texOffset;
    //     } else if (material.segment.tileZoom < this.minNativeZoom) {
    //         material.textureNotExists();
    //     } else {
    //         const segment = material.segment;
    //         const layerId = this.__id;
    //
    //         if (segment.passReady) {
    //             let node = segment.node;
    //             let targetNode = null;
    //
    //             while (node) {
    //                 const seg = node.segment;
    //
    //                 if (seg.tileZoom <= this.maxNativeZoom) {
    //                     const mat = seg.materials[layerId];
    //                     if (!mat || !mat.isReady) {
    //                         targetNode = node;
    //                     }
    //                 }
    //
    //                 node = node.parentNode!;
    //             }
    //
    //             if (targetNode) {
    //                 const seg = targetNode.segment;
    //                 let mat = seg.materials[layerId];
    //                 if (!mat) {
    //                     mat = seg.materials[layerId] = this.createMaterial(seg);
    //                 }
    //
    //                 if (!mat.isReady && !mat.isLoading) {
    //                     this.loadMaterial(mat);
    //                 }
    //             }
    //         }
    //
    //         let pn = segment.node;
    //         let psegm: Material | null = null;
    //         while (pn) {
    //             const pm = pn.segment.materials[layerId];
    //             if (pm && pm.isReady && pm.textureExists) {
    //                 psegm = pm;
    //                 break;
    //             }
    //             pn = pn.parentNode!;
    //         }
    //
    //         if (psegm && pn) {
    //             //
    //             // Animated doesn't work withMaxNativeZoom
    //             //
    //             if ((material.layer as CanvasTiles).animated) {
    //                 requestAnimationFrame(() => {
    //                     if (material.segment) {
    //                         this.drawTile(material, function (canvas) {
    //                             material.applyImage(canvas);
    //                         });
    //                     }
    //                 });
    //             }
    //
    //             material.appliedNode = pn;
    //             material.appliedNodeId = pn.nodeId;
    //             material.texture = psegm.texture;
    //             let dZ2 = 1.0 / (2 << (segment.tileZoom - pn.segment.tileZoom - 1));
    //             material.texOffset[0] = segment.tileX * dZ2 - pn.segment.tileX;
    //             material.texOffset[1] = segment.tileY * dZ2 - pn.segment.tileY;
    //             material.texOffset[2] = dZ2;
    //             material.texOffset[3] = dZ2;
    //         } else {
    //             material.texture = segment.planet.transparentTexture;
    //             material.texOffset[0] = 0.0;
    //             material.texOffset[1] = 0.0;
    //             material.texOffset[2] = 1.0;
    //             material.texOffset[3] = 1.0;
    //         }
    //     }
    //
    //     return material.texOffset;
    // }
    //
    // protected _applyMaterialFast(material: Material): NumberArray4 {
    //     if (material.isReady) {
    //         // IMPORTANT!
    //         // Animated doesn't work withMaxNativeZoom
    //         // It could be fixed with call drawTile method only for parent
    //         // material (which is rendered on the current segment material),
    //         // just for one renderer frame
    //         if ((material.layer as CanvasTiles).animated) {
    //             requestAnimationFrame(() => {
    //                 this.drawTile(material, function (canvas) {
    //                     material.applyImage(canvas);
    //                 });
    //             });
    //         }
    //
    //         return material.texOffset;
    //     } else if (material.segment.tileZoom < this.minNativeZoom) {
    //         material.textureNotExists();
    //     } else {
    //         let segment = material.segment;
    //         let pn = segment.node,
    //             parentTextureExists = false;
    //         let maxNativeZoom = (material.layer as CanvasTiles).maxNativeZoom;
    //
    //         if (segment.passReady && !material.isLoading && segment.tileZoom <= maxNativeZoom) {
    //             this.loadMaterial(material);
    //         }
    //
    //         let mId = this._id;
    //         let psegm = material;
    //         while (pn.parentNode) {
    //             pn = pn.parentNode;
    //             psegm = pn.segment.materials[mId];
    //             if (psegm && psegm.textureExists) {
    //                 parentTextureExists = true;
    //                 break;
    //             }
    //         }
    //
    //         if (segment.passReady) {
    //             if (pn.segment.tileZoom === maxNativeZoom) {
    //                 if (segment.tileZoom > maxNativeZoom) {
    //                     material.textureNotExists();
    //                 }
    //             } else if (pn.segment.tileZoom < maxNativeZoom) {
    //                 let pn = segment.node;
    //                 while (pn.segment.tileZoom > maxNativeZoom) {
    //                     pn = pn.parentNode!;
    //                 }
    //
    //                 let pnm = pn.segment.materials[mId];
    //                 if (pnm) {
    //                     !pnm.isLoading && !pnm.isReady && this.loadMaterial(pnm);
    //                 } else {
    //                     pnm = pn.segment.materials[material.layer._id] = material.layer.createMaterial(pn.segment);
    //                     this.loadMaterial(pnm);
    //                 }
    //             }
    //         }
    //
    //         if (parentTextureExists) {
    //             //
    //             // Animated doesn't work withMaxNativeZoom
    //             //
    //             if ((material.layer as CanvasTiles).animated) {
    //                 requestAnimationFrame(() => {
    //                     if (material.segment) {
    //                         this.drawTile(material, function (canvas) {
    //                             material.applyImage(canvas);
    //                         });
    //                     }
    //                 });
    //             }
    //
    //             material.appliedNodeId = pn.nodeId;
    //             material.texture = psegm.texture;
    //             let dZ2 = 1.0 / (2 << (segment.tileZoom - pn.segment.tileZoom - 1));
    //             material.texOffset[0] = segment.tileX * dZ2 - pn.segment.tileX;
    //             material.texOffset[1] = segment.tileY * dZ2 - pn.segment.tileY;
    //             material.texOffset[2] = dZ2;
    //             material.texOffset[3] = dZ2;
    //         } else {
    //             material.texture = segment.planet.transparentTexture;
    //             material.texOffset[0] = 0.0;
    //             material.texOffset[1] = 0.0;
    //             material.texOffset[2] = 1.0;
    //             material.texOffset[3] = 1.0;
    //         }
    //     }
    //
    //     return material.texOffset;
    // }
}

export { CanvasTiles };