Source: metaConvert/DataProductsFactory.js

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


import {get, isEmpty, isArray} from 'lodash';
import {makeWisePlotRequest} from './WiseRequestList.js';
import {make2MassPlotRequest} from './TwoMassRequestList.js';
import {makeAtlasPlotRequest} from './AtlasRequestList.js';
import {makeLsstSdssPlotRequest, makeLsstWisePlotRequest} from './LsstSdssRequestList.js';
import {WebPlotRequest, TitleOptions} from '../visualize/WebPlotRequest.js';
import {ZoomType} from '../visualize/ZoomType.js';
import {Band} from '../visualize/Band';
import {getCellValue} from '../tables/TableUtil.js';
import {makeWorldPt, parseWorldPt} from '../visualize/Point.js';
import {MetaConst} from '../data/MetaConst.js';
import {CoordinateSys} from '../visualize/CoordSys.js';
import {hasObsCoreLikeDataProducts} from '../util/VOAnalyzer.js';
import {dispatchUpdateCustom} from '../visualize/MultiViewCntlr.js';
import {makeObsCoreConverter, getObsCoreSingleDataProduct, getObsCoreGridDataProduct} from './ObsCoreConverter.js';
import {
    createGridImagesActivate,
    createRelatedDataGridActivate,
    createSingleImageActivate
} from './ImageDataProductsUtil';

const FILE= 'FILE';


function matchById(table,id)  {
    if (!id) return false;
    const value= findTableMetaEntry(table, [MetaConst.IMAGE_SOURCE_ID, MetaConst.DATASET_CONVERTER]);
    if (!value) return false;
    return value.toUpperCase()===id.toUpperCase();
}

/**
 * @param {TableModel} table
 * @param {DataProductsConvertType} converterTemplate
 * @return {DataProductsConvertType}
 */
const simpleCreate= (table, converterTemplate) => converterTemplate;


/**
 * @global
 * @public
 * @typedef {Object} DataProductsDisplayType
 * @prop {string} displayType
 * @prop {Function} activate
 * @prop {Object} menu
 *
 */

/**
 * @global
 * @public
 * @typedef {Object} ActivateParams
 * @prop {string} imageViewerId
 * @prop {string} chartViewerId
 * @prop {string} tableGroupViewerId
 * @prop {string} converterId
 *
 */


/**
 * Returns a callback function or a Promise<DataProductsDisplayType>.
 * callback: function(table:TableModel, row:String,activeParams:{imageViewerId:String,chartViewId:String,tableViewId:String,converterId:String})
 * @param makeReq
 * @return {function | promise}
 */
function getSingleDataProductWrapper(makeReq) {
    return (table, row, activateParams) => {
        const {imageViewerId, converterId}= activateParams;
        const retVal= makeReq(table, row, true);
        const r= get(retVal,'single');
        const activate= createSingleImageActivate(r,imageViewerId,converterId,table.tbl_id,row);
        return Promise.resolve( { displayType:'images', activate, menu:undefined });
    };
}


/**
 *
 * Returns a callback function or a Promise<DataProductsDisplayType>.
 * callback: function(table:TableModel, plotRows:Array.<Object>,activeParams:{imageViewerId:String,chartViewId:String,tableViewId:String,converterId:String})
 * @param makeReq
 * @return {function | promise}
 */
function getGridDataProductWrapper(makeReq) {
    return (table, plotRows, activateParams) => {
        const {imageViewerId, converterId}= activateParams;

        const reqAry= plotRows.map( (pR) => makeReq(table,pR.row,true))
            .filter( (r) => (r && r.single))
            .map( (result) => result.single);

        const activate= createGridImagesActivate(reqAry,imageViewerId,converterId,table.tbl_id,plotRows);
        return Promise.resolve( { displayType:'images', activate, menu:undefined });
    };
}


/**
 *
 * @param makeReq
 * @return {Function}
 */
function getRelatedDataProductWrapper(makeReq) {
    return (table, row, threeColorOps, highlightPlotId, activateParams) => {
        const {imageViewerId, converterId}= activateParams;
        const retVal= makeReq(table, row, false,true,threeColorOps);
        if (retVal) {
            const activate= createRelatedDataGridActivate(retVal,imageViewerId,converterId,table.tbl_id, highlightPlotId);
            return Promise.resolve( { displayType:'images', activate, menu:undefined });
        }
        else {
            return Promise.resolve( {});
        }
    };
}





/**
 * @global
 * @public
 * @typedef {Object} DataProductsConvertType
 *
 * @prop {function} tableMatches, function to test if the table fits the template form: tableMatches(TableModel): boolean
 * @prop {function(table:TableModel, template:DataProductsConvertType):DataProductsConvertType} create - create the converter based on this template. The converter is created by a function that is
 *               passed a table and the converter template. It then may return a new converter template
 *               that could be optionally customized to the table.
 *               form: create(TableModel,DataProductsConvertType): DataProductsConvertType
 * @prop {boolean} threeColor supports three color images
 * @prop {boolean} hasRelatedBands supports groups of related images
 * @prop {boolean} canGrid support grids of images
 * @prop {number} maxPlot total number of images that can be created at a time, i.e. page size
 *
 * @prop {function(table:TableModel,row:String,activateParams:ActivateParams):function} getSingleDataProduct
 *  pass table,rowNum,activate params return a activate function
 *  required
 *
 * @prop {function(table:TableModel,plotRows:Array<{plotId:String,row:number,highlight:boolean}>,activateParams:ActivateParams):function} getGridDataProduct
 * pass(table, array of plotrows, and activateParams) return activate function.
 * Only required if canGrid is true
 *
 * @prop {function(table:TableModel,plotRows:Array<Object>,threeColorOps:Object, highlightPlotId:string,activateParams:ActivateParams):function}
 * getRelatedDataProduct, pass (table, row, threeColorOps, highlightPlotId, activateParams) , return activate function.
 * Only required if hasRelatedBands is true

 * @prop {Object} threeColorBands definition of the three color plot request
 *
 */


/**
 * @type {Array.<DataProductsConvertType>}
 */
export const converterTemplates = [
    {
        converterId : 'wise',
        tableMatches: (table) => matchById(table,'wise'),
        create : simpleCreate,
        threeColor : true,
        hasRelatedBands : true,
        canGrid : true,
        maxPlots : 12,
        getSingleDataProduct: getSingleDataProductWrapper(makeWisePlotRequest),
        getGridDataProduct: getGridDataProductWrapper(makeWisePlotRequest),
        getRelatedDataProduct: getRelatedDataProductWrapper(makeWisePlotRequest),
        threeColorBands : {
            b1 : {color : Band.RED, title: 'Band 1'},
            b2 : {color : Band.GREEN, title: 'Band 2'},
            b3 : {color : null, title: 'Band 3'},
            b4 : {color : Band.BLUE, title: 'Band 4'}
        },
    },
    {
        converterId : 'atlas',
        tableMatches: (table) => matchById(table,'atlas'),
        create : simpleCreate,
        hasRelatedBands : true,
        canGrid : true,
        maxPlots : 5,
        getSingleDataProduct: getSingleDataProductWrapper(makeAtlasPlotRequest),
        getGridDataProduct: getGridDataProductWrapper(makeAtlasPlotRequest),
        getRelatedDataProduct: getRelatedDataProductWrapper(makeAtlasPlotRequest),
    },
    {
        converterId : 'twomass',
        tableMatches: (table) => matchById(table,'twomass'),
        create : simpleCreate,
        threeColor : true,
        hasRelatedBands : true,
        canGrid : true,
        maxPlots : 12,
        getSingleDataProduct: getSingleDataProductWrapper(make2MassPlotRequest),
        getGridDataProduct: getGridDataProductWrapper(make2MassPlotRequest),
        getRelatedDataProduct: getRelatedDataProductWrapper(make2MassPlotRequest),
        threeColorBands : {
            J : {color : Band.RED, title: 'J'},
            H : {color : Band.GREEN, title: 'H'},
            K : {color : Band.BLUE, title: 'K'}
        }
    },
    {
        converterId : 'lsst_sdss',
        tableMatches: (table) => matchById(table,'lsst_sdss'),
        create : simpleCreate,
        threeColor : true,
        hasRelatedBands : true,
        canGrid : true,
        maxPlots : 12,
        getSingleDataProduct: getSingleDataProductWrapper(makeLsstSdssPlotRequest),
        getGridDataProduct: getGridDataProductWrapper(makeLsstSdssPlotRequest),
        getRelatedDataProduct: getRelatedDataProductWrapper(makeLsstSdssPlotRequest),
        threeColorBands : {
            u : {color : null, title: 'u'},
            g : {color : Band.RED, title: 'g'},
            r : {color : Band.GREEN, title: 'r'},
            i : {color : null,  title: 'i'},
            z : {color : Band.BLUE, title: 'z'}
        }
    },
    {
        converterId : 'lsst_wise',
        tableMatches: (table) => matchById(table,'lsst_wise'),
        create : simpleCreate,
        threeColor : true,
        hasRelatedBands : true,
        canGrid : true,
        maxPlots : 12,
        getSingleDataProduct: getSingleDataProductWrapper(makeLsstWisePlotRequest),
        getGridDataProduct: getGridDataProductWrapper(makeLsstWisePlotRequest),
        getRelatedDataProduct: getRelatedDataProductWrapper(makeLsstWisePlotRequest),
        threeColorBands : {
            b1 : {color : Band.RED, title: 'Band 1'},
            b2 : {color : Band.GREEN, title: 'Band 2'},
            b3 : {color : null, title: 'Band 3'},
            b4 : {color : Band.BLUE, title: 'Band 4'}
        }
    },
    {
        converterId : 'ObsCore',
        tableMatches: hasObsCoreLikeDataProducts,
        create : makeObsCoreConverter,
        threeColor : false,
        hasRelatedBands : false,
        canGrid : true,
        maxPlots : 8,
        getSingleDataProduct: getObsCoreSingleDataProduct,
        getGridDataProduct: getObsCoreGridDataProduct,
        getRelatedDataProduct: () => Promise.reject('related data products not supported')
    },
    {
        converterId : 'UNKNOWN',
        tableMatches: (table) => !isEmpty(Object.keys(findADataSourceColumn(table.tableMeta,table.tableData.columns))),
        create : simpleCreate,
        threeColor : false,
        hasRelatedBands : false,
        canGrid : true,
        maxPlots : 12,
        getSingleDataProduct: getSingleDataProductWrapper(makeRequestForUnknown),
        getGridDataProduct: () => Promise.reject('grid not supported'),
        getRelatedDataProduct: () => Promise.reject('related data products not supported')
    },
    {
        converterId : 'SimpleMoving',
        tableMatches: () => false,
        create : simpleCreate,
        threeColor : false,
        hasRelatedBands : false,
        canGrid : false,
        maxPlots : 12,
        getSingleDataProduct: getSingleDataProductWrapper(makeRequestSimpleMoving),
        getGridDataProduct: () => Promise.reject('grid not supported'),
        getRelatedDataProduct: () => Promise.reject('related data products not supported')
    }
];


export function initImage3ColorDisplayManagement(viewerId) {
     const customEntry= converterTemplates.reduce( (newObj, template) => {
        if (!template.threeColor) return newObj;
        newObj[template.converterId]= {...template.threeColorBands, threeColorVisible:false};
        return newObj;
    }, {});
    dispatchUpdateCustom(viewerId, customEntry);
}



/**
 *
 * @param {TableModel} table
 * @return {DataProductsConvertType}
 */
export function defaultMakeDataProductsConverter(table) {
    const t= converterTemplates.find( (template) => template.tableMatches(table) );
    return t && t.create(table,t);
}
/**
 *
 * @param table
 * @param {TableModel} table
 * @return {DataProductsConvertType}
 */
let overrideMakeDataProductsConverter;

/**
 * get a convert factory for a table
 * @param {TableModel} table
 * @return {DataProductsConvertType}
 */
export const makeDataProductsConverter= (table) =>
    (overrideMakeDataProductsConverter && overrideMakeDataProductsConverter(table)) || defaultMakeDataProductsConverter(table);

export const setOverrideDataProductsConverterFactory = (f) => overrideMakeDataProductsConverter= f;


export function addTemplate(template) { converterTemplates.unshift(template); }

export function addTemplateToEnd(template) { converterTemplates.push(template); }


/**
 *  Support data the we don't know about
 * @param table
 * @param row
 * @param includeSingle
 * @param includeStandard
 * @return {{}}
 */
function makeRequestForUnknown(table, row, includeSingle, includeStandard) {

    const {tableMeta:meta}= table;

    const dataSource= findADataSourceColumn(meta,table.tableData.columns);
    if (!dataSource) return {};


    let positionWP= null;

    let sAry= meta[MetaConst.POSITION_COORD_COLS] && meta[MetaConst.POSITION_COORD_COLS].split(';');
    if (!sAry) sAry= meta[MetaConst.CENTER_COLUMN] && meta[MetaConst.CENTER_COLUMN].split(';');

    if (!isEmpty(sAry)) {
        const lon= Number(getCellValue(table,row,sAry[0]));
        const lat= Number(getCellValue(table,row,sAry[1]));
        const csys= CoordinateSys.parse(sAry[2]);
        positionWP= makeWorldPt(lon,lat,csys);
    }
    else if (meta[MetaConst.POSITION_COORD]) {
        positionWP= parseWorldPt(meta[MetaConst.POSITION_COORD]);
    }


    const retval= {};
    if (includeSingle) {
        retval.single= makeRequest(table,dataSource.name,positionWP, row);
    }
    
    if (includeStandard) {
        retval.standard= [makeRequest(table,dataSource.name,positionWP, row)];
        retval.highlightPlotId= retval.standard[0].getPlotId();
    }
    
    return retval;

}

function makeRequestSimpleMoving(table, row, includeSingle, includeStandard) {

    const {tableMeta:meta, tableData}= table;


    const dataSource= findADataSourceColumn(meta, tableData.columns);

    if (!dataSource) return {};


    const sAry= meta[MetaConst.POSITION_COORD_COLS].split(';');
    if (!sAry || sAry.length!== 3) return [];

    let positionWP= null;
    if (!isEmpty(sAry)) {
        const lon= Number(getCellValue(table,row,sAry[0]));
        const lat= Number(getCellValue(table,row,sAry[1]));
        const csys= CoordinateSys.parse(sAry[2]);
        positionWP= makeWorldPt(lon,lat,csys);
    }

    const retval= {};
    if (includeSingle) {
        retval.single= makeMovingRequest(table,row,dataSource.name,positionWP,'simple-moving-single-'+(row %24));
    }

    if (includeStandard) {
        retval.standard= [makeMovingRequest(table,row,dataSource.name,positionWP,'simple-moving-single')];
        retval.highlightPlotId= retval.standard[0].getPlotId();
    }

    return retval;

}


const defDataSourceGuesses= [ 'FILE', 'FITS', 'DATA', 'SOURCE', 'URL' ];
const dataSourceUpper= MetaConst.DATA_SOURCE.toUpperCase();

function findADataSourceColumn(meta,columns) {
    const dsCol= Object.keys(meta).find( (key) => key.toUpperCase()===dataSourceUpper);
    let guesses= meta[dsCol] ? [meta[dsCol],...defDataSourceGuesses] : defDataSourceGuesses;
    guesses= guesses.map( (g) => g.toUpperCase());
    return columns.find( (c) => guesses.includes(c.name.toUpperCase()));
}

function findTableMetaEntry(table,ids) {
    const testIdAry= isArray(ids) ? ids.map( (id) => id.toUpperCase()) : [ids.toUpperCase()];
    const foundKey= Object.keys(table.tableMeta)
        .find( (key) => testIdAry
            .find( (t) => t===key.toUpperCase()));
    return foundKey ? table.tableMeta[foundKey] : undefined;
}



/**
 *
 * @param table
 * @param row
 * @param dataSource
 * @param positionWP
 * @param plotId
 * @return {*}
 */
function makeMovingRequest(table, row, dataSource, positionWP, plotId) {
    const url= getCellValue(table,row,dataSource);
    const r = WebPlotRequest.makeURLPlotRequest(url, 'Fits Image');
    r.setTitleOptions(TitleOptions.FILE_NAME);
    r.setZoomType(ZoomType.TO_WIDTH_HEIGHT);
    r.setPlotId(plotId);
    r.setOverlayPosition(positionWP);
    return r;

}


/**
 *
 * @param table
 * @param dataSource
 * @param positionWP
 * @param row
 * @return {*}
 */
function makeRequest(table, dataSource, positionWP, row) {
    if (!table || !dataSource) return null;

    let r;
    const source= getCellValue(table, row, dataSource);
    if (dataSource.toLocaleUpperCase() === FILE) {
        r = WebPlotRequest.makeFilePlotRequest(source, 'Fits Image');
    }
    else {
        r = WebPlotRequest.makeURLPlotRequest(source, 'Fits Image');
    }
    r.setZoomType(ZoomType.FULL_SCREEN);
    r.setTitleOptions(TitleOptions.FILE_NAME);
    r.setPlotId(source);
    if (positionWP) r.setOverlayPosition(positionWP);

    return r;
}