layer_BaseTileMaterialLayer.ts

import { Layer } from "./Layer";
import type { ILayerParams } from "./Layer";
import { Material } from "./Material";
import type { NumberArray4 } from "../math/Vec4";
import type { Node } from "../quadTree/Node";
import { Segment } from "../segment/Segment";

export interface IBaseTileMaterialLayerParams extends ILayerParams {
    minNativeZoom?: number;
    maxNativeZoom?: number;
}

/**
 * Shared tile material application for imagery/canvas tile layers.
 * It can be applied to any type of tiled data that can be displayed on the planet in either of these two ways, such as BUildings, vector tiles etc.
 */
export abstract class BaseTileMaterialLayer extends Layer {
    public minNativeZoom: number = 0;
    public maxNativeZoom: number = 50;

    constructor(name?: string | null, options: IBaseTileMaterialLayerParams = {}) {
        super(name, options);
        this.minNativeZoom = options.minNativeZoom ?? this.minNativeZoom;
        this.maxNativeZoom = options.maxNativeZoom ?? this.maxNativeZoom;
    }

    public override applyMaterial(material: Material, forceLoading: boolean = false): NumberArray4 {
        if (this.waitForParentMaterial) {
            return this._applyMaterialDefaultCore(material, forceLoading);
        }
        return this._applyMaterialFastCore(material, forceLoading);
    }

    protected _applyMaterialDefaultCore(material: Material, forceLoading: boolean = false): NumberArray4 {
        if (material.isReady) {
            this._onMaterialReady(material);
            return material.texOffset;
        } else if (material.segment.tileZoom < this.minNativeZoom) {
            material.textureNotExists();
        } else {
            const segment = material.segment;
            const layerId = this.__id;

            if (segment.passReady || this._allowsLoadWithoutPassReady()) {
                let node = segment.node;
                let targetNode: Node | null = 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._triggerDefaultMaterialLoad(mat, material, targetNode, forceLoading);
                    }
                }
            }

            this._applyReadyParentTexture(material, segment, layerId);
        }

        return material.texOffset;
    }

    protected _applyMaterialFastCore(material: Material, forceLoading: boolean = false): NumberArray4 {
        if (material.isReady) {
            this._onMaterialReady(material);
            return material.texOffset;
        } else if (material.segment.tileZoom < this.minNativeZoom) {
            material.textureNotExists();
        } else {
            const segment = material.segment;
            let pn = segment.node;
            let parentTextureExists = false;

            this._beforeFastMaterialParentWalk(material, segment, forceLoading);

            const mId = this.__id;
            let psegm = material;
            while (pn.parentNode) {
                pn = pn.parentNode;
                psegm = pn.segment.materials[mId];
                if (psegm && this._hasParentMaterial(psegm)) {
                    parentTextureExists = true;
                    break;
                }
            }

            this._applyFastPassReadyLoading(material, segment, pn, forceLoading);

            if (parentTextureExists) {
                material.appliedNode = pn;
                material.appliedNodeId = pn.nodeId;
                material.texture = psegm.texture;
                const 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;
                this._onParentMaterialApplied(material, psegm);
            } else {
                this._applyNoParentMaterial(material, segment);
            }
        }

        return material.texOffset;
    }

    protected _applyReadyParentTexture(material: Material, segment: Segment, layerId: number) {
        let pn = segment.node;
        let psegm: Material | null = null;
        while (pn) {
            const pm = pn.segment.materials[layerId];
            if (pm && pm.isReady && this._hasParentMaterial(pm)) {
                psegm = pm;
                break;
            }
            pn = pn.parentNode!;
        }

        if (psegm && pn) {
            material.appliedNode = pn;
            material.appliedNodeId = pn.nodeId;
            material.texture = psegm.texture;
            const 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;
            this._onParentMaterialApplied(material, psegm);
        } else {
            this._applyNoParentMaterial(material, segment);
        }
    }

    protected _allowsLoadWithoutPassReady(): boolean {
        return false;
    }

    protected _hasParentMaterial(psegm: Material): boolean {
        return psegm.textureExists;
    }

    protected _applyNoParentMaterial(material: Material, segment: Segment): void {
        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;
    }

    protected _onMaterialReady(_material: Material): void {}

    protected _onParentMaterialApplied(_material: Material, _psegm: Material): void {}

    protected _beforeFastMaterialParentWalk(material: Material, segment: Segment, forceLoading: boolean): void {
        if (
            (segment.passReady || this._allowsLoadWithoutPassReady()) &&
            !material.isLoading &&
            segment.tileZoom <= this.maxNativeZoom
        ) {
            this.loadMaterial(material, forceLoading);
        }
    }

    protected _applyFastPassReadyLoading(material: Material, segment: Segment, pn: Node, forceLoading: boolean): void {
        if (!segment.passReady) {
            return;
        }

        const maxNativeZoom = this.maxNativeZoom;

        if (pn.segment.tileZoom === maxNativeZoom) {
            this._fastWhenWalkerAtMaxNativeZoom(material, segment);
            return;
        }

        if (pn.segment.tileZoom < maxNativeZoom) {
            this._fastLoadParentAtMaxNativeZoom(material, segment, maxNativeZoom);
        }
    }

    /** pn from parent walk is at maxNativeZoom. */
    protected _fastWhenWalkerAtMaxNativeZoom(material: Material, segment: Segment): void {
        if (segment.tileZoom > this.maxNativeZoom) {
            material.textureNotExists();
        }
    }

    protected _fastLoadParentAtMaxNativeZoom(material: Material, segment: Segment, maxNativeZoom: number): void {
        let pn = segment.node;
        while (pn.segment.tileZoom > maxNativeZoom) {
            pn = pn.parentNode!;
        }
        let pnm = pn.segment.materials[this.__id];
        if (pnm) {
            !pnm.isLoading && !pnm.isReady && this.loadMaterial(pnm, true);
        } else {
            pnm = pn.segment.materials[material.layer.__id] = material.layer.createMaterial(pn.segment);
            this.loadMaterial(pnm, true);
        }
    }

    protected _triggerDefaultMaterialLoad(
        mat: Material,
        material: Material,
        targetNode: Node,
        forceLoading: boolean
    ): void {
        this.loadMaterial(mat, this._defaultMaterialLoadForce(mat, material, targetNode, forceLoading));
    }

    protected _defaultMaterialLoadForce(
        _mat: Material,
        material: Material,
        targetNode: Node,
        forceLoading: boolean
    ): boolean {
        return targetNode === material.segment.node ? forceLoading : true;
    }

    public override clearMaterial(material: Material) {
        if (material.isReady && material.textureExists && material.texture && !material.texture.default) {
            material.segment.handler.gl!.deleteTexture(material.texture);
            material.texture = null;
        }

        this.abortMaterialLoading(material);

        material.isReady = false;
        material.textureExists = false;
        material.isLoading = false;

        //@ts-ignore
        material.layer = null;
        //@ts-ignore
        material.segment = null;

        // if (material.image) {
        //     material.image.src = "";
        //     material.image = null;
        // }
    }
}