/* * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ import Enum from 'enum'; import {get,isEmpty,isObject, flattenDeep, values, isUndefined} from 'lodash'; import {WebPlotRequest, TitleOptions} from '../WebPlotRequest.js'; import {TABLE_LOADED, TABLE_SELECT,TABLE_HIGHLIGHT,TABLE_UPDATE, TABLE_REMOVE, TBL_RESULTS_ACTIVE} from '../../tables/TablesCntlr.js'; import ImagePlotCntlr, {visRoot, dispatchDeletePlotView, dispatchPlotImageOrHiPS} from '../ImagePlotCntlr.js'; import {primePlot, getDrawLayerById} from '../PlotViewUtil.js'; import {REINIT_APP} from '../../core/AppDataCntlr.js'; import {doFetchTable, getTblById, getActiveTableId, getTableInGroup, isTableUsingRadians} from '../../tables/TableUtil.js'; import {cloneRequest, makeTableFunctionRequest, MAX_ROW } from '../../tables/TableRequestUtil.js'; import MultiViewCntlr, {getMultiViewRoot, getViewer} from '../MultiViewCntlr.js'; import {serializeDecimateInfo} from '../../tables/Decimate.js'; import {DrawSymbol} from '../draw/PointDataObj.js'; import {computeCentralPtRadiusAverage, toDegrees} from '../VisUtil.js'; import {makeWorldPt, pointEquals} from '../Point.js'; import {logError} from '../../util/WebUtil.js'; import {getCornersColumns} from '../../tables/TableInfoUtil.js'; import {dispatchCreateDrawLayer,dispatchDestroyDrawLayer, dispatchModifyCustomField, dispatchAttachLayerToPlot, getDlAry} from '../DrawLayerCntlr.js'; import Catalog from '../../drawingLayers/Catalog.js'; import {TableSelectOptions} from '../../drawingLayers/CatalogUI.jsx'; import {getNextColor} from '../draw/DrawingDef.js'; import {dispatchAddTableTypeWatcherDef} from '../../core/MasterSaga'; import {findTableCenterColumns, isCatalog, isTableWithRegion, hasCoverageData, findTableRegionColumn} from '../../util/VOAnalyzer.js'; import {parseObsCoreRegion} from '../../util/ObsCoreSRegionParser.js'; import {getAppOptions} from '../../core/AppDataCntlr'; import {getSearchTarget} from './CatalogWatcher'; import {MetaConst} from '../../data/MetaConst.js'; import {getPlotViewById} from '../PlotViewUtil'; import {isHiPS} from '../WebPlot'; export const CoverageType = new Enum(['X', 'BOX', 'REGION', 'ALL', 'GUESS']); export const FitType= new Enum (['WIDTH', 'WIDTH_HEIGHT']); const COVERAGE_TARGET = 'COVERAGE_TARGET'; const COVERAGE_RADIUS = 'COVERAGE_RADIUS'; const COVERAGE_TABLE = 'COVERAGE_TABLE'; export const COVERAGE_CREATED = 'COVERAGE_CREATED'; export const PLOT_ID= 'CoveragePlot'; /** * @global * @public * @typedef {Object} CoverageOptions * @summary options of coverage * * @prop {string} title * @prop {string} tip * @prop {string} coverageType - one of 'GUESS', 'BOX', 'REGION', 'ALL', or 'X' default is 'ALL' * @prop {string} overlayPosition search position point to overlay, e.g '149.08;68.739;EQ_J2000' * @prop {string|Object.<String,String>} symbol - symbol name one of 'X','SQUARE','CROSS','DIAMOND','DOT','CIRCLE','BOXCIRCLE', 'ARROW' * @prop {string|Object.<String,Number>} symbolSize - a number of the symbol size or an object keyed by table id and value the symbol size * @prop {string|Object.<String,String>} color - a color the symbol size or an object keyed by table id and color * @prop {number} fovMaxFitsSize how big this fits image can be (in degrees) * @prop {number} fovDegMinSize - minimum field of view size in degrees * @prop {number} fovDegFallOver - the field of view size to determine when to move between and HiPS and an image * @prop {boolean} multiCoverage - overlay more than one table on the coverage * @prop {string} gridOn - one of 'FALSE','TRUE','TRUE_LABELS_FALSE' */ const defOptions= { title: '2MASS K_s', tip: 'Coverage', getCoverageBaseTitle : (table) => '', // eslint-disable-line no-unused-vars coverageType : CoverageType.ALL, symbol : DrawSymbol.SQUARE, symbolSize : 5, overlayPosition: undefined, color : null, highlightedColor : 'blue', multiCoverage : true, // this is not longer supported, we now always do multi coverage gridOn : false, useBlankPlot : false, fitType : FitType.WIDTH_HEIGHT, ignoreCatalogs:false, useHiPS: true, hipsSourceURL : 'ivo://CDS/P/2MASS/color', // url imageSourceParams: { Service : 'TWOMASS', SurveyKey: 'asky', SurveyKeyBand: 'k', title : '2MASS K_s' }, fovDegFallOver: .13, fovMaxFitsSize: .2, autoConvertOnZoom: false, fovDegMinSize: 100/3600, //defaults to 100 arcsec viewerId:'DefCoverageId', paused: true }; const overlayCoverageDrawing= makeOverlayCoverageDrawing(); export function startCoverageWatcher(options) { dispatchAddTableTypeWatcherDef( { ...coverageWatcherDef, options }); } /** @type {TableWatcherDef} */ export const coverageWatcherDef = { id : 'CoverageWatcher', testTable : (table) => hasCoverageData(table), sharedData: { preparedTables: {}, tblCatIdMap: {}}, watcher : watchCoverage, allowMultiples: false, actions: [TABLE_LOADED, TABLE_SELECT,TABLE_HIGHLIGHT, TABLE_REMOVE, TBL_RESULTS_ACTIVE, ImagePlotCntlr.PLOT_IMAGE, ImagePlotCntlr.PLOT_HIPS, MultiViewCntlr.ADD_VIEWER, MultiViewCntlr.VIEWER_MOUNTED, MultiViewCntlr.VIEWER_UNMOUNTED] }; const getOptions= (inputOptions) => ({...defOptions, ...cleanUpOptions(inputOptions)}); const centerId = (tbl_id) => (tbl_id+'_center'); const regionId = (tbl_id) => (tbl_id+'_region'); /** * Action watcher callback: watch the tables and update coverage display * @callback actionWatcherCallback * @param tbl_id * @param action * @param cancelSelf * @param params * @param params.options read-only * @param params.sharedData read-only * @param params.sharedData.preparedTables * @param params.sharedData.tblCatIdMap * @param params.sharedData.preferredHipsSourceURL * @param params.paused */ function watchCoverage(tbl_id, action, cancelSelf, params) { const {sharedData} = params; const options= getOptions(params.options); const {viewerId}= options; let paused = isUndefined(params.paused) ? options.paused : params.paused; const {preparedTables, tblCatIdMap}= sharedData; if (paused) { paused= !get(getViewer(getMultiViewRoot(), viewerId),'mounted', false); } if (!action) { if (!paused) { preparedTables[tbl_id]= undefined; sharedData.preferredHipsSourceURL= findPreferredHiPS(tbl_id, sharedData.preferredHipsSourceURL, options.hipsSourceURL, preparedTables[tbl_id]); updateCoverage(tbl_id, viewerId, sharedData.preparedTables, options, tblCatIdMap, sharedData.preferredHipsSourceURL); } return; } const {payload}= action; if (payload.tbl_id && payload.tbl_id!==tbl_id) return; if (payload.viewerId && payload.viewerId!==viewerId) return; if (action.type===REINIT_APP) { sharedData.preparedTables= {}; cancelSelf(); return; } if (action.type===MultiViewCntlr.VIEWER_MOUNTED || action.type===MultiViewCntlr.ADD_VIEWER) { sharedData.preferredHipsSourceURL= findPreferredHiPS(tbl_id, sharedData.preferredHipsSourceURL, options.hipsSourceURL, preparedTables[tbl_id]); updateCoverage(tbl_id, viewerId, preparedTables, options, tblCatIdMap, sharedData.preferredHipsSourceURL); return {paused:false}; } if (action.type===TABLE_LOADED) preparedTables[tbl_id]= undefined; if (paused) return {paused}; switch (action.type) { case TABLE_LOADED: if (!getTableInGroup(tbl_id)) return {paused}; sharedData.preferredHipsSourceURL= findPreferredHiPS(tbl_id, sharedData.preferredHipsSourceURL, options.hipsSourceURL, preparedTables[tbl_id]); updateCoverage(tbl_id, viewerId, preparedTables, options, tblCatIdMap, sharedData.preferredHipsSourceURL); break; case TBL_RESULTS_ACTIVE: if (!getTableInGroup(tbl_id)) return {paused}; sharedData.preferredHipsSourceURL= findPreferredHiPS(tbl_id, sharedData.preferredHipsSourceURL, options.hipsSourceURL, preparedTables[tbl_id]); updateCoverage(tbl_id, viewerId, preparedTables, options, tblCatIdMap, sharedData.preferredHipsSourceURL); break; case TABLE_REMOVE: removeCoverage(payload.tbl_id, preparedTables); if (!isEmpty(preparedTables)) { sharedData.preferredHipsSourceURL= findPreferredHiPS(tbl_id, sharedData.preferredHipsSourceURL, options.hipsSourceURL, preparedTables[tbl_id]); updateCoverage(getActiveTableId(), viewerId, preparedTables, options, tblCatIdMap, sharedData.preferredHipsSourceURL); } cancelSelf(); break; case TABLE_SELECT: tblCatIdMap[tbl_id].forEach(( cId ) => { dispatchModifyCustomField(cId, {selectInfo: action.payload.selectInfo}); }); break; case TABLE_HIGHLIGHT: case TABLE_UPDATE: tblCatIdMap[tbl_id].forEach(( cId ) => { dispatchModifyCustomField(cId, {highlightedRow: action.payload.highlightedRow}); }); break; case MultiViewCntlr.VIEWER_UNMOUNTED: paused = true; break; case ImagePlotCntlr.PLOT_IMAGE: case ImagePlotCntlr.PLOT_HIPS: if (action.payload.plotId===PLOT_ID) overlayCoverageDrawing(preparedTables,options, tblCatIdMap); break; } return {paused}; } function removeCoverage(tbl_id, preparedTables) { if (tbl_id) Reflect.deleteProperty(preparedTables, tbl_id); if (isEmpty(Object.keys(preparedTables))) { dispatchDeletePlotView({plotId:PLOT_ID}); } } /** * @param {string} tbl_id * @param {string} viewerId * @param preparedTables * @param {CoverageOptions} options * @param {object} tblCatIdMap * @param {string} preferredHipsSourceURL */ function updateCoverage(tbl_id, viewerId, preparedTables, options, tblCatIdMap, preferredHipsSourceURL) { try { const table = getTblById(tbl_id); if (!table) return; if (preparedTables[tbl_id] === 'WORKING') return; const params = { startIdx: 0, pageSize: MAX_ROW, inclCols: getCovColumnsForQuery(options, table) }; let req = cloneRequest(table.request, params); if (table.totalRows > 10000) { const cenCol = findTableCenterColumns(table); if (!cenCol) return; const sreq = cloneRequest(table.request, {inclCols: `"${cenCol.lonCol}","${cenCol.latCol}"`}); req = makeTableFunctionRequest(sreq, 'DecimateTable', 'coverage', {decimate: serializeDecimateInfo(cenCol.lonCol, cenCol.latCol, 10000), pageSize: MAX_ROW}); } req.tbl_id = `cov-${tbl_id}`; if (preparedTables[tbl_id] /*&& preparedTables[tbl_id].tableMeta.resultSetID===table.tableMeta.resultSetID*/) { //todo support decimated data updateCoverageWithData(viewerId, table, options, tbl_id, preparedTables[tbl_id], preparedTables, isTableUsingRadians(table), tblCatIdMap, preferredHipsSourceURL ); } else { preparedTables[tbl_id] = 'WORKING'; doFetchTable(req).then( (allRowsTable) => { if (get(allRowsTable, ['tableData', 'data'], []).length > 0) { preparedTables[tbl_id] = allRowsTable; const isRegion = isTableWithRegion(allRowsTable); //const isCatalog = findTableCenterColumns(allRowsTable); tblCatIdMap[tbl_id] = (isRegion) ? [centerId(tbl_id), regionId(tbl_id)] : [tbl_id]; updateCoverageWithData(viewerId, table, options, tbl_id, allRowsTable, preparedTables, isTableUsingRadians(table), tblCatIdMap, preferredHipsSourceURL ); } } ).catch( (reason) => { preparedTables[tbl_id] = undefined; tblCatIdMap[tbl_id] = undefined; logError(`Failed to catalog plot data: ${reason}`, reason); } ); } } catch (e) { logError('Error updating coverage'); console.log(e); } } /** * * @param {string} viewerId * @param {TableData} table * @param {CoverageOptions} options * @param {string} tbl_id * @param allRowsTable * @param preparedTables * @param usesRadians * @param {object} tblCatIdMap * @param {string} preferredHipsSourceURL */ function updateCoverageWithData(viewerId, table, options, tbl_id, allRowsTable, preparedTables, usesRadians, tblCatIdMap, preferredHipsSourceURL ) { const {maxRadius, avgOfCenters}= computeSize(options, preparedTables, usesRadians); if (!avgOfCenters || maxRadius<=0) return; const plot= primePlot(visRoot(), PLOT_ID); if (plot && pointEquals(avgOfCenters,plot.attributes[COVERAGE_TARGET]) && plot.attributes[COVERAGE_RADIUS]===maxRadius ) { overlayCoverageDrawing(preparedTables, options, tblCatIdMap); return; } const {fovDegFallOver, fovMaxFitsSize, autoConvertOnZoom, imageSourceParams, fovDegMinSize, overlayPosition= avgOfCenters}= options; let plotAllSkyFirst= false; let allSkyRequest= null; const size= Math.max(maxRadius*2.2, fovDegMinSize); if (size>160) { allSkyRequest= WebPlotRequest.makeAllSkyPlotRequest(); allSkyRequest.setTitleOptions(TitleOptions.PLOT_DESC); allSkyRequest= initRequest(allSkyRequest, viewerId, PLOT_ID, overlayPosition); plotAllSkyFirst= true; } let imageRequest= WebPlotRequest.makeFromObj(imageSourceParams) || WebPlotRequest.make2MASSRequest(avgOfCenters, 'asky', 'k', size); imageRequest= initRequest(imageRequest, viewerId, PLOT_ID, overlayPosition, avgOfCenters); const hipsRequest= initRequest(WebPlotRequest.makeHiPSRequest(preferredHipsSourceURL, null), viewerId, PLOT_ID, overlayPosition, avgOfCenters); hipsRequest.setSizeInDeg(size); dispatchPlotImageOrHiPS({ plotId: PLOT_ID, viewerId, hipsRequest, imageRequest, allSkyRequest, fovDegFallOver, fovMaxFitsSize, autoConvertOnZoom, plotAllSkyFirst, pvOptions: {userCanDeletePlots:false, displayFixedTarget:false}, attributes: { [COVERAGE_TARGET]: avgOfCenters, [COVERAGE_RADIUS]: maxRadius, [COVERAGE_TABLE]: tbl_id, [COVERAGE_CREATED]: true } }); } /** * * @param r * @param viewerId * @param plotId * @param overlayPos * @param wp * @return {*} */ function initRequest(r,viewerId,plotId, overlayPos, wp) { if (!r) return undefined; r= r.makeCopy(); r.setPlotGroupId(viewerId); r.setPlotId(plotId); r.setOverlayPosition(overlayPos); if (wp) r.setWorldPt(wp); return r; } /** * * @param {CoverageOptions} options * @param preparedTables * @param usesRadians * @return {*} */ function computeSize(options, preparedTables, usesRadians) { const ary= values(preparedTables); const testAry= ary .filter( (t) => t && t!=='WORKING') .map( (t) => { let ptAry= []; const covType= getCoverageType(options,t); switch (covType) { case CoverageType.X: ptAry= getPtAryFromTable(options,t, usesRadians); break; case CoverageType.BOX: ptAry= getBoxAryFromTable(options,t, usesRadians); break; case CoverageType.REGION: ptAry = getRegionAryFromTable(options, t, usesRadians); break; } return flattenDeep(ptAry); } ); return computeCentralPtRadiusAverage(testAry); } function makeOverlayCoverageDrawing() { const drawingOptions= {}; const selectOps = {}; /** * * @param preparedTables * @param {CoverageOptions} options * @param {object} tblCatIdMap */ return (preparedTables, options, tblCatIdMap) => { const plot= primePlot(visRoot(),PLOT_ID); if (!plot) return; const tbl_id= plot.attributes[COVERAGE_TABLE]; if (!tbl_id || !preparedTables[tbl_id] || !getTblById(tbl_id)) return; const table= getTblById(tbl_id); if (isCatalog(table) && options.ignoreCatalogs) return; // let the catalog watcher just handle the drawing overlays if (tblCatIdMap[tbl_id]) { tblCatIdMap[tbl_id].forEach((cId) => { const layer = getDrawLayerById(getDlAry(), cId); if (layer) { drawingOptions[cId] = layer.drawingDef; // drawingDef and selectOption is stored as layer based selectOps[cId] = layer.selectOption; dispatchDestroyDrawLayer(cId); } }); } const overlayAry= Object.keys(preparedTables); overlayAry.forEach( (id) => { tblCatIdMap[id] && tblCatIdMap[id].forEach((cId) => { if (!drawingOptions[cId]) drawingOptions[cId] = {}; if (!drawingOptions[cId].color) drawingOptions[cId].color = lookupOption(options, 'color', cId) || getNextColor(); if (selectOps[cId]) drawingOptions[cId].selectOption = selectOps[cId]; }); const oriTable= getTblById(id); const arTable= preparedTables[id]; if (oriTable && arTable) addToCoverageDrawing(PLOT_ID, options, oriTable, arTable, drawingOptions); }); }; } /** * * @param {string} plotId * @param {CoverageOptions} options * @param {TableData} table * @param {TableData} allRowsTable * @param {string} drawOp */ function addToCoverageDrawing(plotId, options, table, allRowsTable, drawOp) { if (allRowsTable==='WORKING') return; const covType= getCoverageType(options,allRowsTable); const {tableMeta, tableData}= allRowsTable; const angleInRadian= isTableUsingRadians(tableMeta); const createDrawLayer = (cId, dataType, isFromRegion=false) => { const columns = dataType === CoverageType.REGION ? findTableRegionColumn(allRowsTable) : (covType === CoverageType.BOX ? getCornersColumns(allRowsTable) : findTableCenterColumns(allRowsTable)); if (isEmpty(columns)) return; const dl = getDlAry().find((dl) => dl.drawLayerTypeId === Catalog.TYPE_ID && dl.catalogId === cId); if (!dl) { const {showCatalogSearchTarget}= getAppOptions(); const searchTarget= showCatalogSearchTarget ? getSearchTarget(table.request, lookupOption(options, 'searchTarget', cId), lookupOption(options, 'overlayPosition', cId)) : undefined; dispatchCreateDrawLayer(Catalog.TYPE_ID, { catalogId: cId, tblId: table.tbl_id, title: `Coverage: ${table.title || table.tbl_id}` + (isFromRegion ? (dataType===CoverageType.REGION ? ' regions' : ' positions') : ''), color: drawOp[cId].color, tableData, tableMeta, tableRequest: table.request, highlightedRow: table.highlightedRow, catalog: dataType === CoverageType.X, boxData: dataType !== CoverageType.X, dataType: dataType.key, isFromRegion, columns, symbol: drawOp.symbol || lookupOption(options, 'symbol', cId), size: drawOp.size || lookupOption(options, 'symbolSize', cId), selectInfo: table.selectInfo, angleInRadian, dataTooBigForSelection: table.totalRows > 10000, tableSelection: (dataType === CoverageType.REGION) ? (drawOp[cId].selectOption || TableSelectOptions.all.key) : null, searchTarget, }); dispatchAttachLayerToPlot(cId, plotId); } }; if (covType === CoverageType.BOX) { createDrawLayer(table.tbl_id, covType); } else if (covType === CoverageType.X) { createDrawLayer(table.tbl_id, covType); } else if (covType === CoverageType.REGION) { const layerType = [CoverageType.X, CoverageType.REGION]; const layerId = [centerId(table.tbl_id), regionId(table.tbl_id)]; layerType.forEach((type, idx) => createDrawLayer(layerId[idx], layerType[idx], true)); } } /** * look up a value from the CoverageOptions * @param {CoverageOptions} options * @param {string} key * @param {string} tbl_id * @return {*} */ function lookupOption(options, key, tbl_id) { const value= options[key]; if (!value) return undefined; return isObject(value) ? value[tbl_id] : value; } function getCoverageType(options,table) { if (options.coverageType===CoverageType.GUESS || options.coverageType===CoverageType.REGION || options.coverageType===CoverageType.BOX || options.coverageType===CoverageType.ALL) { return isTableWithRegion(table) ? CoverageType.REGION : (hasCorners(options,table) ? CoverageType.BOX : CoverageType.X); } return options.coverageType; } function hasCorners(options, table) { const cornerColumns= getCornersColumns(table); if (isEmpty(cornerColumns)) return false; const dataCnt= table.tableData.data.reduce( (tot, row) => cornerColumns.every( (cDef) => row[cDef.lonIdx]!=='' && row[cDef.latIdx]!=='') ? tot+1 : tot ,0); return dataCnt/table.tableData.data.length > .1; } function toAngle(d, radianToDegree) { const v= Number(d); return (!isNaN(v) && radianToDegree) ? toDegrees(v): v; } function makePt(lonStr,latStr, csys, radianToDegree) { return makeWorldPt(toAngle(lonStr,radianToDegree), toAngle(latStr, radianToDegree), csys); } function getPtAryFromTable(options,table, usesRadians){ const cDef= findTableCenterColumns(table); if (isEmpty(cDef)) return []; const {lonIdx,latIdx,csys}= cDef; return table.tableData.data .map( (row) => (row[lonIdx]!=='' && row[latIdx]!=='') ? makePt(row[lonIdx], row[latIdx], csys, usesRadians) : undefined ) .filter( (v) => v); } function getBoxAryFromTable(options,table, usesRadians){ const cDefAry= getCornersColumns(table); return table.tableData.data .map( (row) => cDefAry .map( (cDef) => (row[cDef.lonIdx]!=='' && row[cDef.latIdx]!=='') ? makePt(row[cDef.lonIdx], row[cDef.latIdx], cDef.csys, usesRadians) : undefined)) .filter( (row) => row.every( (v) => v)); } function getRegionAryFromTable(options, table, usesRadians) { const rCol = findTableRegionColumn(table); return table.tableData.data.map((row) => { const cornerInfo = parseObsCoreRegion(row[rCol.regionIdx], rCol.unit, true); return cornerInfo.valid ? cornerInfo.corners : []; }).filter((r) => !isEmpty(r)); } function getCovColumnsForQuery(options, table) { const cAry= [...getCornersColumns(table), findTableCenterColumns(table), findTableRegionColumn(table)]; // column names should be in quotes // there should be no duplicates const base = cAry.filter((c)=>!isEmpty(c)) .map( (c)=> (c.type === 'region') ? `"${c.regionCol}"` : `"${c.lonCol}","${c.latCol}"`) .filter((v,i,a) => a.indexOf(v) === i) .join(); return base+',"ROW_IDX"'; } function cleanUpOptions(options) { const opStrList= Object.keys(defOptions); return Object.keys(options).reduce( (result, key) => { const properKey= opStrList.find( (testKey) => testKey.toLowerCase()===key.toLowerCase()); result[properKey||key]= options[key]; return result; },{}); } /** * * @param tbl_id * @param prevPreferredHipsSourceURL * @param optionHipsSourceURL * @param preparedTable * @return {*} */ function findPreferredHiPS(tbl_id,prevPreferredHipsSourceURL, optionHipsSourceURL, preparedTable) { if (!preparedTable) { // if a new table then the meta takes precedence const table = getTblById(tbl_id); if (table && table.tableMeta[MetaConst.COVERAGE_HIPS]) return table.tableMeta[MetaConst.COVERAGE_HIPS]; } const plot= primePlot(visRoot(), PLOT_ID); if (isHiPS(plot)) { return plot.hipsUrlRoot; } if (prevPreferredHipsSourceURL) return prevPreferredHipsSourceURL; if (optionHipsSourceURL) return optionHipsSourceURL; }