/* * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ import {get, has, isArray, isEmpty, isObject, isString, intersection} from 'lodash'; import Enum from 'enum'; import {getColumn, getColumnIdx, getColumnValues, getColumns, getTblById, getCellValue, getColumnByID} from '../tables/TableUtil.js'; import {getCornersColumns} from '../tables/TableInfoUtil.js'; import {MetaConst} from '../data/MetaConst.js'; import {CoordinateSys} from '../visualize/CoordSys.js'; export const UCDCoord = new Enum(['eq', 'ecliptic', 'galactic']); const obsPrefix = 'obscore:'; const ColNameIdx = 0; const UcdColIdx = 1; const UtypeColIdx = 2; const mainMeta = 'meta.main'; const obsCorePosColumns = ['s_ra', 's_dec']; const obsCoreRegionColumn = 's_region'; const OBSTAPCOLUMNS = [ ['dataproduct_type', 'meta.id', 'ObsDataset.dataProductType'], ['calib_level', 'meta.code;obs.calib', 'ObsDataset.calibLevel'], ['obs_collection', 'meta.id', 'DataID.collection'], ['obs_id', 'meta.id', 'DataID.observationID'], ['obs_publisher_did', 'meta.ref.uri;meta.curation', 'Curation.publisherDID'], ['access_url', 'meta.ref.url', 'Access.reference'], ['access_format', 'meta.code.mime', 'Access.format'], ['access_estsize', 'phys.size;meta.file', 'Access.size'], ['target_name', 'meta.id;src', 'Target.name'], ['s_ra', 'pos.eq.ra', 'Char.SpatialAxis.Coverage.Location.Coord.Position2D.Value2.C1'], ['s_dec', 'pos.eq.dec', 'Char.SpatialAxis.Coverage.Location.Coord.Position2D.Value2.C2'], ['s_fov', 'phys.angSize;instr.fov', 'Char.SpatialAxis.Coverage.Bounds.Extent.diameter'], ['s_region', 'pos.outline;obs.field', 'Char.SpatialAxis.Coverage.Support.Area'], ['s_resolution', 'pos.angResolution', 'Char.SpatialAxis.Resolution.Refval.value'], ['s_xel1', 'meta.number', 'Char.SpatialAxis.numBins1'], ['s_xel2', 'meta.number', 'Char.SpatialAxis.numBins2'], ['t_min', 'time.start;obs.exposure', 'Char.TimeAxis.Coverage.Bounds.Limits.StartTime'], ['t_max', 'time.end;obs.exposure', 'Char.TimeAxis.Coverage.Bounds.Limits.StopTime'], ['t_exptime', 'time.duration;obs.exposure', 'Char.TimeAxis.Coverage.Support.Extent'], ['t_resolution', 'time.resolution', 'Char.TimeAxis.Resolution.Refval.value'], ['t_xel', 'meta.number', 'Char.TimeAxis.numBins'], ['em_min', 'em.wl;stat.min', 'Char.SpectralAxis.Coverage.Bounds.Limits.LoLimit'], ['em_max', 'imit em.wl;stat.max', 'Char.SpectralAxis.Coverage.Bounds.Limits.HiLimit'], ['em_res_power', 'spect.resolution', 'Char.SpectralAxis.Resolution.ResolPower.refVal'], ['em_xel', 'Char.SpectralAxis.numBins', 'meta.number'], ['o_ucd', 'meta.ucd', 'Char.ObservableAxis.ucd'], ['pol_states', 'meta.code;phys.polarization','Char.PolarizationAxis.stateList'], ['pol_xel', 'meta.number', 'Char.PolarizationAxis.numBins'], ['facility_name', 'meta.id;instr.tel', 'Provenance.ObsConfig.Facility.name'], ['instrument_name', 'meta.id;instr', 'Provenance.ObsConfig.Instrument.name'] ]; const OBSTAP_OPTIONAL_CNAMES = [ 'dataproduct_subtype', 'target_class', 'obs_title', 'obs_creation_date', 'obs_creator_name', 'obs_creator_did', 'obs_release_date', 'publisher_id', 'bib_reference', 'data_rights', 's_resolution_max', 's_calib_status', 's_stat_error', 's_pixel_scale', 't_refpos', 't_calib_status', 't_stat_error', 'em_ucd', 'em_unit', 'em_calib_status', 'em_res_power_min', 'em_res_power_max', 'em_resolution', 'em_stat_error', 'o_unit', 'o_calib_status', 'o_stat_error', 'pol_states', 'proposal_id' ]; const OBSTAP_CNAMES = OBSTAPCOLUMNS.map((row) => row[ColNameIdx]).concat( OBSTAP_OPTIONAL_CNAMES); function getObsTabColEntry(title) { const e= OBSTAPCOLUMNS.find( (entry) => entry[ColNameIdx]===title); return e && {name:e[ColNameIdx], ucd: e[UcdColIdx], utype: e[UtypeColIdx]}; } function getObsCoreTableColumn(tableOrId, name) { const entry= getObsTabColEntry(name); if (!entry) return; const table= getTableModel(tableOrId); const tblRec= TableRecognizer.newInstance(table); let cols= tblRec.getTblColumnsOnUType(entry.utype); if (cols.length) { if (cols.length===1) return cols[0]; const prefUtype= cols.find( (c) => c.name===name); return prefUtype ? prefUtype : cols[0]; } cols= tblRec.getTblColumnsOnDefinedUCDValue(entry.ucd); if (cols.length) { if (cols.length===1) return cols[0]; const prefUcd= cols.find( (c) => c.name===name); return prefUcd ? prefUcd : cols[0]; } return getColumn(table,name); } function getObsCoreTableColumnName(tableOrId,name) { const col = getObsCoreTableColumn(tableOrId,name); return col ? col.name : ''; } export function getObsCoreCellValue(tableOrId, rowIdx, obsColName) { const table= getTableModel(tableOrId); if (!table) return ''; return getCellValue(table, rowIdx, getObsCoreTableColumnName(table, obsColName)) || ''; } const alternateMainPos = [['POS_EQ_RA_MAIN', 'POS_EQ_DEC_MAIN']]; export const posCol = {[UCDCoord.eq.key]: {ucd: [['pos.eq.ra', 'pos.eq.dec'],...alternateMainPos], coord: CoordinateSys.EQ_J2000, adqlCoord: 'ICRS'}, [UCDCoord.ecliptic.key]: {ucd: [['pos.ecliptic.lon', 'pos.ecliptic.lat']], coord: CoordinateSys.ECL_J2000, adqlCoord: 'ECLIPTIC'}, [UCDCoord.galactic.key]: {ucd: [['pos.galactic.lon', 'pos.galactic.lat']], coord: CoordinateSys.GALACTIC, adqlCoord: 'GALATIC'}}; function getLonLatIdx(tableModel, lonCol, latCol) { const lonIdx = getColumnIdx(tableModel, lonCol); const latIdx = getColumnIdx(tableModel, latCol); return (lonIdx >= 0 && latIdx >= 0) ? {lonIdx, latIdx} : null; } function centerColumnUTypesFromObsTap() { const obsTapColNames = OBSTAPCOLUMNS.map((col) => col[ColNameIdx]); const centerUTypes = obsCorePosColumns.map((posColName) => { const idx = obsTapColNames.indexOf(posColName); return (idx >= 0) ? OBSTAPCOLUMNS[idx][UtypeColIdx] : null; }); return centerUTypes.findIndex((oneUtype) => !oneUtype) >= 0 ? null : centerUTypes; } const UCDSyntax = new Enum(['primary', 'secondary', 'any'], {ignoreCase: true}); const ucdSyntaxMap = { 'pos.eq.ra': UCDSyntax.any, 'pos.eq.dec': UCDSyntax.any, 'meta.main': UCDSyntax.secondary, 'pos.outline': UCDSyntax.primary, 'obs.field': UCDSyntax.secondary }; /** * check if ucd value contains the searched ucd word at the right position * @param ucdValue * @param ucdWord * @param syntaxCode 'P': only first word, 'S': only secondary, 'Q' either first or secondary */ function isUCDWith(ucdValue, ucdWord, syntaxCode = UCDSyntax.any) { const atoms = ucdValue.split(';'); const idx = atoms.findIndex((atom) => { return atom.toLowerCase() === ucdWord.toLowerCase(); }); return (syntaxCode === UCDSyntax.primary && idx === 0) || (syntaxCode === UCDSyntax.secondary && idx >= 1) || (syntaxCode === UCDSyntax.any && idx >= 0); } /** * table analyzer based on table model for catalog or image metadata */ class TableRecognizer { constructor(tableModel, posCoord='eq') { this.tableModel = tableModel; this.columns = get(tableModel, ['tableData', 'columns'], []); this.obsCoreInfo = {isChecked: false, isObsCoreTable: false}; this.posCoord = posCoord; this.centerColumnsInfo = null; this.centerColumnCandidatePairs = null; this.regionColumnInfo = null; } isObsCoreTable() { if (this.obsCoreInfo.isChecked) { return this.obsCoreInfo.isObsCoreTable; } const allColNames = this.columns.map((oneCol) => oneCol.name); const nonExistCol = OBSTAPCOLUMNS .map((oneColumn) => (oneColumn[ColNameIdx])) .some((oneName) => { return !allColNames.includes(oneName); }); this.obsCoreInfo.isChecked = true; this.obsCoreInfo.isObsCoreTable = !nonExistCol; return this.obsCoreInfo.isObsCoreTable; } /** * find and fill center column info * @param colPair [lonCol, latCol] * @param csys * @returns {null|CoordColsDescription} */ setCenterColumnsInfo(colPair, csys = CoordinateSys.EQ_J2000) { this.centerColumnsInfo = null; if (isArray(colPair) && colPair.length >= 2) { const lonCol = isString(colPair[0]) ? colPair[0] : colPair[0].name; const latCol = isString(colPair[1]) ? colPair[1] : colPair[1].name; const idxs = getLonLatIdx(this.tableModel, lonCol, latCol); if (idxs) { this.centerColumnsInfo = { type: 'center', lonCol, latCol, lonIdx: idxs.lonIdx, latIdx: idxs.latIdx, csys }; } } return this.centerColumnsInfo; } setRegionColumnInfo(col) { this.regionColumnInfo = null; const idx = getColumnIdx(this.tableModel, col.name); if (idx >= 0) { this.regionColumnInfo = { type: 'region', regionCol: col.name, regionIdx: idx, unit: col.units }; } return this.regionColumnInfo; } /** * filter the columns per ucd value defined in the UCD value of relevant OBSTAP column * @param ucds ucd value defined in OBSTAP, it may contain more than one ucd values * @returns {*} */ getTblColumnsOnDefinedUCDValue(ucds) { const ucdList = ucds.split(';'); return ucdList.reduce((prev, ucd) => { prev = prev.filter((oneCol) => { return (has(oneCol, 'UCD') && isUCDWith(oneCol.UCD, ucd, get(ucdSyntaxMap, ucd))); }); return prev; }, this.columns); } /** * get columns containing the same ucd value * @param ucd * @returns {Array} */ getTblColumnsOnUCD(ucd) { return this.columns.filter((oneCol) => { return (has(oneCol, 'UCD') && isUCDWith(oneCol.UCD, ucd, get(ucdSyntaxMap, ucd))); }); } /** * get columns containing the utype * @param utype * @returns {array} */ getTblColumnsOnUType(utype) { return this.columns.filter((oneCol) => { return has(oneCol, 'utype') && oneCol.utype.includes(utype); }); } /** * get columns containing ucd word by given table columns * @param cols * @param ucdWord * @returns {array} */ getColumnsWithUCDWord(cols, ucdWord) { if (isEmpty(cols)) return []; return cols.filter((oneCol) => { return has(oneCol, 'UCD') && isUCDWith(oneCol.UCD, ucdWord, get(ucdSyntaxMap, ucdWord)); }); } /** * get center columns pairs by checking ucd values * @param coord * @returns {Array} [[pair_1_col_ra, pair_1_col_dec], ...., [pair_n_col_ra, pair_n_col_dec]] */ getCenterColumnPairsOnUCD(coord = this.posCoord || UCDCoord.eq.key) { const centerColUCDs = has(posCol, coord ) ? posCol[coord].ucd : null; const pairs = []; if (!centerColUCDs) { return pairs; } // get 'ra' column list and 'dec' column list const posPairs = centerColUCDs.reduce((prev, eqUcdPair) => { if (isArray(eqUcdPair) && eqUcdPair.length >= 2) { const colsRA = this.getTblColumnsOnUCD(eqUcdPair[0]); const colsDec = this.getTblColumnsOnUCD(eqUcdPair[1]); prev[0].push(...colsRA); prev[1].push(...colsDec); } return prev; }, [[], []]); const metaMainPair = posPairs.map((posCols, idx) => { const mainMetaCols = this.getColumnsWithUCDWord(posCols, mainMeta); if (!isEmpty(posCols) && isEmpty(mainMetaCols)) { alternateMainPos.find((oneAlt) => { const altCols = this.getColumnsWithUCDWord(posCols, oneAlt[idx], ucdSyntaxMap.any); mainMetaCols.push(...altCols); return !isEmpty(altCols); }); } return mainMetaCols; }); if (metaMainPair[0].length || metaMainPair[1].length) { if (metaMainPair[0].length === metaMainPair[1].length) { for (let i = 0; i < metaMainPair[0].length; i++) { pairs.push([metaMainPair[0][i], metaMainPair[1][i]]); //TODO: need rules to match the rest pair } } } else if (posPairs[0].length > 0 && posPairs[1].length > 0){ // find first exact match const basicPair = posPairs.map((cols, i)=>cols.find((c) => c.UCD === centerColUCDs[0][i])); if (basicPair[0] && basicPair[1]) { pairs.push(basicPair); } else if (posPairs[0].length === posPairs[1].length) { // TODO: how do we separate positions from the related fields, like variance? for (let i = 0; i < posPairs[0].length; i++) { pairs.push([posPairs[0][i], posPairs[1][i]]); //TODO: need rules to match the rest pair } } } return pairs; } getCenterColumnPairsOnUType(columnPairs) { const centerUTypes = centerColumnUTypesFromObsTap(); if (isEmpty(centerUTypes)) return columnPairs; let pairs = []; /* filter out the column with unequal utype value */ if (!isEmpty(columnPairs)) { pairs = columnPairs.filter((oneColPair) => { if ((!has(oneColPair[0], 'utype')) || (!has(oneColPair[1], 'utype')) || (oneColPair[0].utype.includes(centerUTypes[0]) && oneColPair[1].utype.includes(centerUTypes[1]))) { return oneColPair; } }); } else { // check all table columns const posPairs = centerUTypes.map((posUtype) => { return this.getTblColumnsOnUType(posUtype); }); if (posPairs[0].length === posPairs[1].length) { for (let i = 0; i < posPairs[0].length; i++) { pairs.push([posPairs[0][i], posPairs[1][i]]); //TODO: need rules to match the rest pair } } } return pairs; } getCenterColumnPairOnName(columnPairs) { if (!isEmpty(columnPairs)) { return columnPairs.find((onePair) => { return (onePair[0].name.toLowerCase() === obsCorePosColumns[0]) && (onePair[1].name.toLowerCase() === obsCorePosColumns[1]); }); } else { const cols = obsCorePosColumns.map((colName) => { return getColumn(this.tableModel, colName); }); return (cols[0] && cols[1]) ? cols : []; } } /** * * @return {String} */ getCenterColumnMetaEntry() { this.centerColumnsInfo = null; //note: CATALOG_COORD_COLS,POSITION_COORD_COLS are both deprecated and will removed in the future const {tableMeta} = this.tableModel || {}; const {CATALOG_COORD_COLS,POSITION_COORD_COLS,CENTER_COLUMN}= MetaConst; if (!tableMeta) return undefined; return tableMeta[CENTER_COLUMN] || tableMeta[CATALOG_COORD_COLS] || tableMeta[POSITION_COORD_COLS]; } /** * @returns {Boolean} */ isCenterColumnMetaDefined() { return Boolean(this.getCenterColumnMetaEntry()); } /** * get center columns pair by checking the table meta * @returns {null|CoordColsDescription} */ getCenterColumnsOnMeta() { const cenData= this.getCenterColumnMetaEntry(); if (!cenData) return undefined; const s= cenData.split(';'); if (!s || s.length !== 3) { return this.centerColumnsInfo; } return this.setCenterColumnsInfo(s, CoordinateSys.parse(s[2])); } /** * search center columns pair by checking UCD value * @returns {null|CoordColsDescription} */ getCenterColumnsOnUCD() { this.centerColumnsInfo = null; const colPairs = this.getCenterColumnPairsOnUCD(UCDCoord.eq.key); if (colPairs && colPairs.length === 1) { return this.setCenterColumnsInfo(colPairs[0], posCol[UCDCoord.eq.key].coord); } else { this.centerColumnCandidatePairs = colPairs; } return this.centerColumnsInfo; } /** * search center column pairs based on existing candidate pairs or all table columns * @param candidatePairs * @returns {null|CoordColsDescription} */ getCenterColumnsOnObsCoreUType(candidatePairs) { this.centerColumnsInfo = null; const colPairs = this.getCenterColumnPairsOnUType(candidatePairs); if (colPairs && colPairs.length === 1) { this.setCenterColumnsInfo(colPairs[0], posCol[UCDCoord.eq.key].coord); } this.centerColumnCandidatePairs = colPairs; return this.centerColumnsInfo; } /** * search center column pair by checking ObsCore columns on existing candidate pairs or all table columns * @param candidatePairs * @returns {null|CoordColsDescription} */ getCenterColumnsOnObsCoreName(candidatePairs) { this.centerColumnsInfo = null; const leftMostCol = (isEmpty(candidatePairs)) ? null : candidatePairs[0]; const colPair = this.getCenterColumnPairOnName(candidatePairs); if (isArray(colPair) && colPair.length === 2) { return this.setCenterColumnsInfo(colPair, posCol[UCDCoord.eq.key].coord); } else { return leftMostCol? this.setCenterColumnsInfo(leftMostCol, posCol[UCDCoord.eq.key].coord) : this.centerColumnsInfo; } } /** * search center columns pair by guessing the column name * @returns {null|CoordColsDescription} */ guessCenterColumnsByName() { this.centerColumnsInfo = undefined; const columns= getColumns(this.tableModel); const findColumn = (colName, regExp) => { return columns.find((c) => (c.name === colName || (regExp && regExp.test(c.name))) ); }; const guess = (lon, lat, useReg=false) => { let lonCol; let latCol; if (useReg) { const reLon= new RegExp(`^[A-z]*[-_]?(${lon})[1-9]*$`); const reLat= new RegExp(`^[A-z]*[-_]?(${lat})[1-9]*$`); lonCol = findColumn(lon,reLon); latCol = findColumn(lat,reLat); if (lonCol && latCol) { if (lonCol.name.replace(lon,'') !== latCol.name.replace(lat,'')) return undefined; } } else { lonCol = findColumn(lon); latCol = findColumn(lat); } return (lonCol && latCol) ? this.setCenterColumnsInfo([lonCol, latCol]) : undefined; }; return (guess('ra','dec') || guess('lon', 'lat') || guess('ra','dec',true) || guess('lon', 'lat',true)); } /** * find center columns as defined in some vo standard * @returns {null|CoordColsDescription} */ getVODefinedCenterColumns() { return this.getCenterColumnsOnUCD() || this.getCenterColumnsOnObsCoreUType(this.centerColumnCandidatePairs) || this.getCenterColumnsOnObsCoreName(this.centerColumnCandidatePairs); } /** * return center position or catalog coordinate columns and the associate*d coordinate system * by checking table meta, UCD values, Utype, ObsCore column name and guessing. * @returns {null|CoordColsDescription} */ getCenterColumns() { if (this.isCenterColumnMetaDefined()) return this.getCenterColumnsOnMeta(); return this.getVODefinedCenterColumns() || (isEmpty(this.centerColumnCandidatePairs) && this.guessCenterColumnsByName()); } getRegionColumnOnUCD(cols) { this.regionColumnInfo = null; const columns = !isEmpty(cols) ? cols : this.columns; const ucds = get(getObsTabColEntry(obsCoreRegionColumn), 'ucd', '').split(';'); const regionCols = ucds.reduce((prev, oneUcd) => { if (prev.length > 0) { prev = this.getColumnsWithUCDWord(prev, oneUcd); } return prev; }, columns); if (regionCols.length === 1) { this.setRegionColumnInfo(regionCols[0]); } else if (regionCols.length > 1) { if (!this.getRegionColumnOnObsCoreName(regionCols)) { this.setRegionColumnInfo(regionCols[0]); } } return this.regionColumnInfo; } getRegionColumnOnObsCoreUType(cols) { const columns = !isEmpty(cols) ? cols : this.columns; const obsUtype = get(getObsTabColEntry(obsCoreRegionColumn), 'utype', ''); this.regionColumnInfo = null; const regionCols = (obsUtype) && !isEmpty(columns) && columns.filter((col) => { return (has(col, 'utype') && col.utype.includes(obsUtype)); }); if (regionCols.length === 1) { this.setRegionColumnInfo(regionCols[0]); } else if (regionCols.length > 1) { if (!this.getRegionColumnOnObsCoreName(regionCols)) { this.setRegionColumnInfo(regionCols[0]); } } return this.regionColumnInfo; } getRegionColumnOnObsCoreName(cols) { this.regionColumnInfo = null; const columns = !isEmpty(cols) ? cols : this.columns; const regionCol = !isEmpty(columns) && columns.find((oneCol) => oneCol.name.toLowerCase() === obsCoreRegionColumn); if (regionCol) { this.setRegionColumnInfo(regionCol); } return this.regionColumnInfo; } /** * return region column by checking column name or UCD values * @returns {null|ColDescription} */ getVODefinedRegionColumn() { return this.getRegionColumnOnUCD() || this.getRegionColumnOnObsCoreUType()|| this.getRegionColumnOnObsCoreName(); } getRegionColumn() { return this.getVODefinedRegionColumn(); } static newInstance(tableModel) { return new TableRecognizer(tableModel); } } /** * find the center column base on the table model of catalog or image metadata * Investigate table meta data a return a CoordColsDescription for two columns that represent and object in the row * @param table * @return {CoordColsDescription|null} */ export function findTableCenterColumns(table) { const tblRecog = get(table, ['tableData', 'columns']) && TableRecognizer.newInstance(table); return tblRecog && tblRecog.getCenterColumns(); } /** * find ObsCore defined 's_region' column * @param table * @return {RegionColDescription|null} return ObsCore defined s_region column */ export function findTableRegionColumn(table) { const tblRecog = get(table, ['tableData', 'columns']) && TableRecognizer.newInstance(table); return tblRecog && tblRecog.getRegionColumn(); } /** * find the ObsCore defined 'access_url' column * @param table * @return {TableColumn|null} return ObsCore defined access_url column */ export function findTableAccessURLColumn(table) { const urlCol = getObsCoreTableColumn(table, 'access_url'); return isEmpty(urlCol) ? null : urlCol; } /** * Given a TableModel or a table id return a table model * @param {TableModel|String} tableOrId - a table model or a table id * @return {TableModel} */ function getTableModel(tableOrId) { if (isString(tableOrId)) return getTblById(tableOrId); // was passed a table Id if (isObject(tableOrId)) return tableOrId; return undefined; } /** * table analyzer based on the table model for columns which contains column_name & ucd columns */ class ColumnRecognizer { constructor(columnsModel, posCoord = 'eq') { this.columnsModel = columnsModel; this.ucds = getColumnValues(columnsModel, 'ucd'); this.column_names = getColumnValues(columnsModel, 'column_name'); this.centerColumnsInfo = null; this.posCoord = posCoord; } setCenterColumnsInfo(colPair, csys = CoordinateSys.EQ_J2000) { this.centerColumnsInfo = { lonCol: colPair[0], latCol: colPair[1], csys }; return this.centerColumnsInfo; } getColumnsWithUCDWord(cols, ucdWord) { if (isEmpty(cols)) return []; return cols.filter((oneCol) => { return has(oneCol, 'ucd') && isUCDWith(oneCol.ucd, ucdWord, get(ucdSyntaxMap, ucdWord)); }); } getCenterColumnPairsOnUCD(coord) { const centerColUCDs = has(posCol, coord ) ? posCol[coord].ucd : null; const pairs = []; if (!centerColUCDs) { return pairs; } // get 'ra' column list and 'dec' column list // output in form of [ <ra column array>, <dec column array> ] and each column is like {ucd: column_name: } const posPairs = centerColUCDs.reduce((prev, eqUcdPair) => { if (isArray(eqUcdPair) && eqUcdPair.length >= 2) { const colsRA = this.ucds.reduce((p, ucd, i) => { if (ucd.includes(eqUcdPair[0])) { p.push({ucd, column_name: this.column_names[i]}); } return p; }, []); const colsDec = this.ucds.reduce((p, ucd, i) => { if (ucd.includes(eqUcdPair[1])) { p.push({ucd, column_name: this.column_names[i]}); } return p; }, []); prev[0].push(...colsRA); prev[1].push(...colsDec); } return prev; }, [[], []]); const metaMainPair = posPairs.map((posCols, idx) => { const mainMetaCols = this.getColumnsWithUCDWord(posCols, mainMeta); if (!isEmpty(posCols) && isEmpty(mainMetaCols)) { alternateMainPos.find((oneAlt) => { const altCols = this.getColumnsWithUCDWord(posCols, oneAlt[idx], ucdSyntaxMap.any); mainMetaCols.push(...altCols); return !isEmpty(altCols); }); } return mainMetaCols; }); if (metaMainPair[0].length || metaMainPair[1].length) { // get the column with ucd containing meta.main if (metaMainPair[0].length === metaMainPair[1].length) { for (let i = 0; i < metaMainPair[0].length; i++) { pairs.push([metaMainPair[0][i], metaMainPair[1][i]]); //TODO: need rules to match the rest pair } } } else if (posPairs[0].length > 0 && posPairs[1].length > 0){ // find first exact match const basicPair = posPairs.map((cols, i)=>cols.find((c) => c.ucd === centerColUCDs[0][i])); if (basicPair[0] && basicPair[1]) { pairs.push(basicPair); } else if (posPairs[0].length === posPairs[1].length) { // TODO: how do we separate positions from the related fields, like variance? for (let i = 0; i < posPairs[0].length; i++) { pairs.push([posPairs[0][i], posPairs[1][i]]); //TODO: need rules to match the rest pair } } } return pairs; } getCenterColumnsOnUCD() { let colPairs; const coordSet = this.posCoord ? [UCDCoord[this.posCoord].key] : [UCDCoord.eq.key, UCDCoord.galactic.key, UCDCoord.ecliptic.key]; coordSet.find((oneCoord) => { colPairs = this.getCenterColumnPairsOnUCD(oneCoord); if (colPairs && colPairs.length >= 1) { this.setCenterColumnsInfo(colPairs[0], posCol[oneCoord].coord); // get the first pair return true; } else { return false; } }); return this.centerColumnsInfo; } guessCenterColumnsByName() { this.centerColumnsInfo = null; const findColumn = (colName, regExp) => { let col; this.column_names.find((name, i) => { if (name === colName || (regExp && regExp.test(name))) { col = {column_name: name, ucd: this.ucds[i]}; return true; } else { return false; } }); return col; }; const guess = (lon, lat, useReg=false) => { let lonCol; let latCol; if (useReg) { const reLon= new RegExp(`^[A-z]?[-_]?(${lon})[1-9]*$`); const reLat= new RegExp(`^[A-z]?[-_]?(${lat})[1-9]*$`); lonCol = findColumn(lon,reLon); latCol = findColumn(lat,reLat); } else { lonCol = findColumn(lon); latCol = findColumn(lat); } return (lonCol && latCol) ? this.setCenterColumnsInfo([lonCol, latCol]) : this.centerColumnsInfo; }; return (guess('ra','dec') || guess('lon', 'lat') || guess('ra','dec',true) || guess('lon', 'lat',true)); } getCenterColumns() { return this.getCenterColumnsOnUCD()|| this.guessCenterColumnsByName(); } static newInstance(tableModel) { return new ColumnRecognizer(tableModel); } } /** * find the center columns based on the columns table model * @param columnsModel * @returns {*|{lonCol: {ucd, column_name}, latCol: {ucd, column_name}, csys}|*} */ export function findCenterColumnsByColumnsModel(columnsModel) { const colRecog = columnsModel && get(columnsModel, ['tableData', 'columns']) && ColumnRecognizer.newInstance(columnsModel); return colRecog && colRecog.getCenterColumns(); } /** * Test to see it this is a catalog. A catalog must have one of the following: * - CatalogOverlayType meta data entry defined and not equal to 'FALSE' and we must be able to find the columns * either by meta data or by guessing * - We find the columns by some vo standard * * Note- if the CatalogOverlayType meta toUpperCase === 'FALSE' then we will treat it as not a catalog no matter how the * vo columns might be defined. * * @param {TableModel|String} tableOrId - a table model or a table id * @return {boolean} True if the table is a catalog * @see MetaConst.CATALOG_OVERLAY_TYPE */ export function isCatalog(tableOrId) { const table= getTableModel(tableOrId); if (!table) return false; if (isTableWithRegion(table)) return false; const {tableMeta, tableData}= table; if (!get(tableData, 'columns') || !tableMeta) return false; if (isString(tableMeta[MetaConst.CATALOG_OVERLAY_TYPE])) { if (tableMeta[MetaConst.CATALOG_OVERLAY_TYPE].toUpperCase()==='FALSE') return false; return Boolean(TableRecognizer.newInstance(table).getCenterColumns()); } else { return Boolean(TableRecognizer.newInstance(table).getVODefinedCenterColumns()); } } export function isTableWithRegion(tableOrId) { const table= getTableModel(tableOrId); if (!table) return false; return Boolean(TableRecognizer.newInstance(table).getVODefinedRegionColumn()); } /** * @summary check if there is center column or corner columns defined, if so this table has coverage information * @param {TableModel|String} tableOrId - a table model or a table id * @returns {boolean} True if there is coverage data in this table */ export function hasCoverageData(tableOrId) { const table= getTableModel(tableOrId); if (!table) return false; if (!table.totalRows) return false; return !isEmpty(findTableRegionColumn(table)) || !isEmpty(findTableCenterColumns(table)) || !isEmpty(getCornersColumns(table)); } /** * Guess if this table contains image meta data. It contains image meta data if IMAGE_SOURCE_ID is defined * or a DATA_SOURCE column name is defined * @param {TableModel|String} tableOrId - a table model or a table id * @return {boolean} true if there is image meta data * @see MetaConst.DATA_SOURCE * @see MetaConst.IMAGE_SOURCE_ID */ export function isMetaDataTable(tableOrId) { const table= getTableModel(tableOrId); const dataSourceUpper = MetaConst.DATA_SOURCE.toUpperCase(); if (isEmpty(table)) return false; const {tableMeta, totalRows} = table; if (!tableMeta || !totalRows) return false; const hasDsCol = Boolean(Object.keys(tableMeta).find((key) => key.toUpperCase() === dataSourceUpper)); return Boolean(tableMeta[MetaConst.IMAGE_SOURCE_ID] || tableMeta[MetaConst.DATASET_CONVERTER] || hasDsCol || hasObsCoreLikeDataProducts(table) || isTableWithRegion(tableOrId)); } /** * Return true this this table can be access as an ObsCore data * @param tableOrId * @return {boolean} */ export function hasObsCoreLikeDataProducts(tableOrId) { const table= getTableModel(tableOrId); const hasUrl= getObsCoreTableColumn(table,'access_url'); const hasFormat= getObsCoreTableColumn(table,'access_format'); const hasProdType= getObsCoreProdTypeCol(table); return Boolean(hasUrl && hasFormat && hasProdType); } /** * return access_format cell data * @param {TableModel|String} tableOrId - a table model or a table id * @param rowIdx * @return {string} */ export const getObsCoreAccessFormat= (tableOrId, rowIdx) => getObsCoreCellValue(tableOrId,rowIdx, 'access_format'); /** * return access_url cell data * @param {TableModel|String} tableOrId - a table model or a table id * @param rowIdx * @return {string} */ export const getObsCoreAccessURL= (tableOrId, rowIdx) => getObsCoreCellValue(tableOrId,rowIdx, 'access_url'); /** * return dataproduct_type cell data * @param {TableModel|String} tableOrId - a table model or a table id * @param rowIdx * @return {string} */ export const getObsCoreProdType= (tableOrId, rowIdx) => getObsCoreCellValue(tableOrId,rowIdx, 'dataproduct_type'); /** * Return the dataproduct_type column * @param {TableModel|String} tableOrId - a table model or a table id * @return {TableColumn} */ export const getObsCoreProdTypeCol= (tableOrId) => getObsCoreTableColumn(tableOrId, 'dataproduct_type'); /** * check to see if dataproduct_type cell a votable * @param {TableModel|String} tableOrId - a table model or a table id * @param rowIdx * @return {boolean} */ export const isFormatVoTable= (tableOrId, rowIdx) => getObsCoreAccessFormat(tableOrId, rowIdx).toLowerCase().includes('votable'); /** * check to see if dataproduct_type is a datalink * @param {TableModel|String} tableOrId - a table model or a table id * @param rowIdx * @return {boolean} */ export function isFormatDataLink(tableOrId, rowIdx) { const accessFormat= getObsCoreAccessFormat(tableOrId,rowIdx).toLowerCase(); return accessFormat.includes('votable') && accessFormat.includes('content=datalink'); } /** * * @param {TableModel|string} originTableOrId * @param {TableModel} dataLinkTable * @param {string} filterStr a string to filter out content_type of the results by * @param {number} maxSize if defined, don't return anything greater then max size * @return {Array.<{url, contentType, size, semantics}>} return an array of objects with url and contentType keys */ export function getDataLinkAccessUrls(originTableOrId, dataLinkTable, filterStr='', maxSize=0 ) { const originTable= getTableModel(originTableOrId); const data= get(dataLinkTable, 'tableData.data', []); if (!data.length) return []; const urlOptions= dataLinkTable.tableData.data.map( (row,idx) => { const url= getCellValue(dataLinkTable,idx,'access_url' ); const contentType= getCellValue(dataLinkTable,idx,'content_type' ); const size= Number(getCellValue(dataLinkTable,idx,'content_length' )); const semantics= getCellValue(dataLinkTable,idx,'semantics' ); return {url,contentType, size, semantics }; }).filter( ({url}) => url && url.startsWith('http')); return filterStr||maxSize ? urlOptions.filter( ({contentType,size}) => contentType.toLowerCase() .includes(filterStr) && (maxSize===0 || size<=maxSize )) : urlOptions; } const DEFAULT_TNAME_OPTIONS = [ 'name', // generic 'pscname', // IRAS 'target', // our own table output 'designation', // 2MASS 'starid' // PCRS ]; // moved from java, if we decide to use it this is what we had before export const findTargetName = (columns) => columns.find( (c) => DEFAULT_TNAME_OPTIONS.includes(c)); /** * @global * @public * @typedef {Object} CoordColsDescription * * @summary And object that describes a pairs of columns in a table that makes up a coordinate * * @prop {string} type - content type of the columns, 'center' * @prop {string} lonCol - name of the longitudinal column * @prop {string} latCol - name of the latitudinal column * @prop {number} lonIdx - column index for the longitudinal column * @prop {number} latIdx - column index for the latitudinal column * @prop {CoordinateSys} csys - the coordinate system to use */ /** * @global * @public * @typedef {Object} ColsDescription * * @summary An object that describe a single column in a table * * @prop {string} colName - name of the column * @prop {number} colIdx - column index for the column * @prop {string} unit - unit for the column */ /** * @global * @public * @typedef {Object} RegionColDescription * * @summary An object that describes the column which is ObsCore defined 's_region' * * @prop {string} type - content type of the column, 'region' * @prop {string} regionCol - name of the column * @prop {number} regionIdx - column index for the column * @prop {string} unit - unit of the measurement of the region */ /** * @see {@link http://www.ivoa.net/documents/VOTable/20130920/REC-VOTable-1.3-20130920.html#ToC54} * A.1 link substitution * @param tableModel table model with data and columns info * @param href the href value of the LINK * @param rowIdx row index to be resolved * @param defval the field's value, or cell data. Append field's value to href, if no substitution is needed. * @returns {string} the resolved href after subsitution */ export function resolveHRefVal(tableModel, href='', rowIdx, defval='') { const vars = href.match(/\${[\w -.]+}/g); if (vars) { let rhref = href; vars.forEach((v) => { const [,cname] = v.match(/\${([\w -.]+)}/) || []; const col = getColumnByID(tableModel, cname) || getColumn(tableModel, cname); const rval = col ? getCellValue(tableModel, rowIdx, col.name) : v; // if the variable cannot be resolved, show it as is. rhref = rhref.replace(v, rval); }); return rhref; } else { return href + defval; } } /** * Guess if this table has enough ObsCore attributes to be considered an ObsCore table. * - any column contains utype with 'obscore:' prefix * - matches 3 or more of ObsCore column names * @param tableModel * @returns {boolean} */ export function isObsCoreLike(tableModel) { const cols = getColumns(tableModel); if (cols.findIndex((c) => get(c, 'utype', '').startsWith(obsPrefix)) >= 0) { return true; }; const v = intersection(cols.map( (c) => c.name), OBSTAP_CNAMES); return v.length > 2; }