Source: visualize/iv/HiPSTileDrawer.js

/*
 * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt
 */

import Enum from 'enum';
import {createImageUrl,initOffScreenCanvas, computeBounding, isQuadTileOnScreen} from './TileDrawHelper.jsx';
import {primePlot} from '../PlotViewUtil.js';
import {getVisibleHiPSCells, getPointMaxSide, getHiPSNorderlevel, makeHiPSAllSkyUrlFromPlot} from '../HiPSUtil.js';
import {loadImage} from '../../util/WebUtil.js';
import {CysConverter} from '../CsysConverter.js';
import {findAllSkyCachedImage, findTileCachedImage, addAllSkyCachedImage} from './HiPSTileCache.js';
import {makeHipsRenderer} from './HiPSRenderer.js';


const noOp= { drawerTile : () => undefined, abort : () => undefined };



export const DrawTiming= new Enum(['IMMEDIATE','ASYNC', 'DELAY']);

/**
 * Return a function that should be called on every render to draw the image
 * @param targetCanvas
 * @return {*}
 */
export function createHiPSDrawer(targetCanvas) {
    if (!targetCanvas) return () => undefined;
    let abortLastDraw= null;
    let lastDrawNorder= 0;
    let lastFov;


    return (plot, opacity,plotView, tileProcessInfo= {shouldProcess:false}) => {
        abortLastDraw && abortLastDraw(); // stop any incomplete drawing

        const {viewDim}= plotView;
        let transitionNorder;

        const {norder, useAllSky}= getHiPSNorderlevel(plot, true);
        const {fov,centerWp}= getPointMaxSide(plot,viewDim);
        const tilesToLoad= findCellOnScreen(plot,viewDim,norder, fov, centerWp);
        let drawTiming= DrawTiming.ASYNC;

        if (useAllSky || tilesToLoad.every( (tile)=> findTileCachedImage(createImageUrl(plot,tile))) ) { // any case with all the tiles
            drawTiming= DrawTiming.IMMEDIATE;
        }
        else if (fovEqual(fov, lastFov)) { // scroll case
            drawTiming= DrawTiming.ASYNC; // in a zoom case, slow down drawing a little, to allow to the user click multiple times
            transitionNorder= lastDrawNorder-1;// for scroll transitionNorder needs to be set one back
        }
        else { // zoom or resize case, in a zoom or resize case, slow down drawing, to allow to use to finish
            drawTiming= DrawTiming.DELAY;
            transitionNorder= lastDrawNorder;
        }

        if (drawTiming!==DrawTiming.IMMEDIATE) {
            drawTransitionalImage(fov,centerWp,targetCanvas,plot, plotView,norder, transitionNorder,
                opacity, tileProcessInfo, tilesToLoad);
        }

        const offscreenCanvas = makeOffScreenCanvas(plotView,plot,drawTiming!==DrawTiming.IMMEDIATE);
        abortLastDraw= drawDisplay(targetCanvas, offscreenCanvas, plot, plotView, norder, tilesToLoad, useAllSky,
            opacity, tileProcessInfo, drawTiming);
        lastDrawNorder= norder;
        lastFov= fov;
    };
}


/**
 * draw a transitional image when scrolling or zooming.  The transitional image will be overlaid with the final image.
 * @param fov
 * @param centerWp
 * @param targetCanvas
 * @param plot
 * @param plotView
 * @param norder
 * @param transitionNorder
 * @param opacity
 * @param tileProcessInfo
 * @param finalTileToLoad
 */
function drawTransitionalImage(fov, centerWp, targetCanvas, plot, plotView,
                               norder, transitionNorder, opacity, tileProcessInfo, finalTileToLoad) {
    const {viewDim}= plotView;
    let tilesToLoad;
    if (norder<=3) { // norder is always 3, need to fix this if
          // draw the level 2 all sky and then draw on top what ever part of the full resolution level 3 tiles that are in cache
        const tilesToLoad2= findCellOnScreen(plot,viewDim,2, fov, centerWp);
        const tilesToLoad3= findCellOnScreen(plot,viewDim,3, fov, centerWp);
        const offscreenCanvas = makeOffScreenCanvas(plotView,plot,false);
        drawDisplay(targetCanvas, offscreenCanvas, plot, plotView, 2, tilesToLoad2, true,
            opacity, tileProcessInfo, DrawTiming.IMMEDIATE, false);
        drawDisplay(targetCanvas, offscreenCanvas, plot, plotView, 3, tilesToLoad3, false,
            opacity, tileProcessInfo, DrawTiming.IMMEDIATE);
    }
    else {
        let lookMore= true;
        const offscreenCanvas = makeOffScreenCanvas(plotView,plot,false);
        // find some lower resolution norder to draw, as long as there is at least one tile available in cache, us it.
        for( let testNorder= transitionNorder; (testNorder>=3 && lookMore); testNorder--) {
            tilesToLoad= findCellOnScreen(plot,viewDim,testNorder, fov, centerWp);
            const hasSomeTiles= tilesToLoad.some( (tile)=> findTileCachedImage(createImageUrl(plot,tile)));
            if (hasSomeTiles || testNorder===3) { // if there are tiles or we need to do the allsky
                drawDisplay(targetCanvas, offscreenCanvas, plot, plotView, testNorder, tilesToLoad, testNorder===3,
                    opacity, tileProcessInfo, DrawTiming.IMMEDIATE, false);
                lookMore= false;
                // console.log(`draw transi: transitionNorder: ${transitionNorder}, testNorder: ${testNorder}`);
            }
        }
        // draw what ever part of the nornder tiles that are in cache on top
        drawDisplay(targetCanvas, offscreenCanvas, plot, plotView, norder, finalTileToLoad, false,
            opacity, tileProcessInfo, DrawTiming.IMMEDIATE);
    }
}



const fovEqual= (fov1,fov2) => Math.trunc(fov1*10000) === Math.trunc(fov2*10000);



/**
 * @global
 * @public
 * @typedef {Object} HiPSDeviceTileData
 *
 * @prop {number} tileNumber - HiPS pixel number
 * @prop {number} nside - healpix level
 * @prop {Array.<DevicePt>} devPtCorners - the target corners of the tile in device coordinates
 * @prop {number} dx - x offset into image
 * @prop {number} dy - y offset into image
 */


/**
 *
 * @param {WebPlot} plot
 * @param viewDim
 * @param {number} norder
 * @param {number} fov
 * @param {WorldPt} centerWp
 * @return {Array.<HiPSDeviceTileData>}
 */
function findCellOnScreen(plot, viewDim, norder, fov,centerWp) {
    const cells= getVisibleHiPSCells(norder,centerWp, fov, plot.dataCoordSys);
    const cc= CysConverter.make(plot);

    const retCells= [];
    let devPtCorners;
                   // this function is performance sensitive, use for loops instead of map and filter
    for(let i= 0; (i<cells.length); i++) {
        devPtCorners= [];
        for(let j=0; (j<cells[i].wpCorners.length); j++)  {
            devPtCorners[j]= cc.getDeviceCoords(cells[i].wpCorners[j]);
            if (!devPtCorners[j]) break;
        }
        if (isQuadTileOnScreen(devPtCorners, viewDim)) {
            retCells.push({devPtCorners, tileNumber:cells[i].ipix, dx:0, dy:0, nside: norder});
        }
    }
    return retCells;
}




/**
 *
 * @param targetCanvas
 * @param offscreenCanvas
 * @param plot
 * @param plotView
 * @param norder
 * @param tilesToLoad
 * @param useAllSky
 * @param opacity
 * @param tileProcessInfo
 * @param drawTiming
 * @param screenRenderEnabled
 */
function drawDisplay(targetCanvas, offscreenCanvas, plot, plotView, norder, tilesToLoad, useAllSky, opacity, tileProcessInfo,
                     drawTiming= DrawTiming.ASYNC, screenRenderEnabled= true) {
    const {viewDim}= plotView;
    const rootPlot= primePlot(plotView); // bounding box should us main plot not overlay plot
    const boundingBox= computeBounding(rootPlot,viewDim.width,viewDim.height);
    const offsetX= boundingBox.x>0 ? boundingBox.x : 0;
    const offsetY= boundingBox.y>0 ? boundingBox.y : 0;

    if (!targetCanvas) return noOp;
    const screenRenderParams= {plotView, plot, targetCanvas, offscreenCanvas, opacity, offsetX, offsetY};
    const drawer= makeHipsRenderer(screenRenderParams, tilesToLoad.length, !plot.asOverlay,
                                   tileProcessInfo, screenRenderEnabled);
    
    if (useAllSky) {
        const allSkyURL= makeHiPSAllSkyUrlFromPlot(plot);
        let cachedAllSkyImage= findAllSkyCachedImage(allSkyURL);
        if (cachedAllSkyImage) {
            drawer.drawAllSky(norder, cachedAllSkyImage, tilesToLoad);
        }
        else {
            loadImage(makeHiPSAllSkyUrlFromPlot(plot))
                .then( (allSkyImage) =>
                {
                    addAllSkyCachedImage(allSkyURL, allSkyImage);
                    cachedAllSkyImage= findAllSkyCachedImage(allSkyURL);
                    drawer.drawAllSky(norder, cachedAllSkyImage, tilesToLoad);
                })
                .catch( () => {
                    // this should not happen - there is no all sky image so we are looking for the full tiles.
                    // tilesToLoad.forEach( (tile) => drawer.drawTile(createImageUrl(plot,tile), tile) );
                    drawer.drawAllTilesAsync(tilesToLoad,plot);
                });
        }
    }
    else {
        switch (drawTiming) {
            case DrawTiming.IMMEDIATE:
                drawer.drawAllTilesImmediate(tilesToLoad,plot);
                break;
            case DrawTiming.ASYNC:
                drawer.drawAllTilesAsync(tilesToLoad,plot);
                break;
            case DrawTiming.DELAY:
                setTimeout( () => drawer.drawAllTilesAsync(tilesToLoad,plot), 250);
                break;
        }
    }
    return drawer.abort; // this abort function will any async promise calls stop before they draw

}

/**
 *
 * @param plotView
 * @param plot
 * @param overlayTransparent
 */
function makeOffScreenCanvas(plotView, plot, overlayTransparent) {

    const offscreenCanvas = initOffScreenCanvas(plotView.viewDim);
    const offscreenCtx = offscreenCanvas.getContext('2d');

    const {viewDim:{width,height}}=  plotView;

    offscreenCtx.fillStyle = overlayTransparent ? 'rgba(0,0,0,0)'  : 'rgba(227,227,227,1)';
    // offscreenCtx.fillRect(0, 0, width, height);


    const {fov, centerDevPt}= getPointMaxSide(plot,plotView.viewDim);
    if (fov>=180) {
        const altDevRadius= plotView.viewDim.width/2 + plotView.scrollX;
        offscreenCtx.fillStyle = 'rgba(227,227,227,1)';
        offscreenCtx.fillRect(0, 0, width, height);
        offscreenCtx.save();
        offscreenCtx.beginPath();
        offscreenCtx.lineWidth= 5;
        offscreenCtx.arc(centerDevPt.x, centerDevPt.y, altDevRadius-4, 0, 2*Math.PI, false);
        offscreenCtx.closePath();
        offscreenCtx.clip();
    }
    offscreenCtx.fillStyle = overlayTransparent ? 'rgba(0,0,0,0)'  : 'rgba(0,0,0,1)';
    offscreenCtx.fillRect(0, 0, width, height);
    return offscreenCanvas;
}