/*
* 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;
}