/* * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ import 'isomorphic-fetch'; import React from 'react'; import ReactDOM from 'react-dom'; import {set, get, defer} from 'lodash'; import 'styles/global.css'; import {APP_LOAD, dispatchAppOptions, dispatchUpdateAppData} from './core/AppDataCntlr.js'; import {FireflyViewer} from './templates/fireflyviewer/FireflyViewer.js'; import {FireflySlate} from './templates/fireflyslate/FireflySlate.jsx'; import {LcViewer} from './templates/lightcurve/LcViewer.jsx'; import {HydraViewer} from './templates/hydra/HydraViewer.jsx'; import {initApi} from './api/ApiBuild.js'; import {dispatchUpdateLayoutInfo} from './core/LayoutCntlr.js'; import {dispatchChangeReadoutPrefs} from './visualize/MouseReadoutCntlr.js'; import {showInfoPopup} from './ui/PopupUtil'; import {reduxFlux} from './core/ReduxFlux.js'; import {wsConnect} from './core/messaging/WebSocketClient.js'; import {ActionEventHandler} from './core/messaging/MessageHandlers.js'; import {init} from './rpc/CoreServices.js'; import {getPropsWith, mergeObjectOnly} from './util/WebUtil.js'; import {initLostConnectionWarning} from './ui/LostConnection.jsx'; import {dispatchChangeTableAutoScroll, dispatchWcsMatch, visRoot} from './visualize/ImagePlotCntlr'; export const flux = reduxFlux; var initDone = false; /** * A list of available templates * @enum {string} */ export const Templates = { /** * This templates has multiple views: 'images', 'tables', and 'xyPlots'. * They can be combined with ' | ', i.e. 'images | tables' */ FireflyViewer, FireflySlate, LightCurveViewer : LcViewer, HydraViewer }; /** * @global * @public * @typedef {Object} AppProps * @summary A property object used for customizing the application * * @prop {String} [template] - UI template to display. API mode if not given * @prop {string} [views] - some template may have multiple views. If not given, the default view of the template will be used. * @prop {string} [div=app] - ID of a div to place the viewer in. * @prop {string} [appTitle] - title of this application. * @prop {boolean} [showUserInfo=false] - show user information. This is used when authentication is available * @prop {boolean} [showViewsSwitch] - show/hide the swith views buttons * @prop {Array.<function>} [rightButtons] - function(s) returning a button to be displayed on the top-right of the result page. * * * @prop {Object} menu custom menu bar * @prop {string} menu.label button's label * @prop {string} menu.action action to fire on button clicked * @prop {string} menu.type use 'COMMAND' for actions that's not drop-down related. */ /** * @global * @public * @typedef {Object} FireflyOptions * * @summary An object that is defined in the html that has configuration options for Firefly * * * @prop {Object} MenuItemKeys - an object the references MenuItemKeys.js that can turn on or off buttons on the image tool bar * @prop {Array.<string> } imageTabs - specifies the order of the time in the image dialog e.g. - [ 'fileUpload', 'url', '2mass', 'wise', 'sdss', 'msx', 'dss', 'iras' ] * @prop {string|function} irsaCatalogFilter - a function or a predefined key that specifies how the catalogs are filter in the UI * @prop {string} catalogSpacialOp - two values undefined or 'polygonWhenPlotExist'. when catalogSpacialOp === 'polygonWhenPlotExist' then * the catalog panel will show the polygon option as default when possible * @prop {Array.<string> } imageMasterSources - default - ['ALL'], source to build image master data from * @prop {Array.<string> } imageMasterSourcesOrder - for the image dialog sort order of the projects, anything not listed is put on bottom * */ /** @type {AppProps} */ const defAppProps = { div: 'app', template: undefined, // don't set a default value for this. it's also used as a switch for API vs UI mode appTitle: '', showUserInfo: false, showViewsSwitch: false, rightButtons: undefined, menu: [ {label:'Images', action:'ImageSelectDropDownCmd'}, {label:'Catalogs', action:'IrsaCatalogDropDown'}, {label:'TAP Searches', action: 'TAPSearch'}, {label:'Charts', action:'ChartSelectDropDownCmd'}, {label:'Upload', action: 'FileUploadDropDownCmd'}, //{label:'Workspace', action: 'WorkspaceDropDownCmd'} ], }; /** @type {FireflyOptions} */ const defFireflyOptions = { MenuItemKeys: {}, imageTabs: undefined, irsaCatalogFilter: undefined, catalogSpacialOp: undefined, imageMasterSources: ['ALL'], showCatalogSearchTarget: true, imageMasterSourcesOrder: undefined, workspace : { showOptions: false}, wcsMatchType: false, imageScrollsToHighlightedTableRow: true, charts: { defaultDeletable: undefined, // by default if there are more than one chart in container, all charts are deletable maxRowsForScatter: 5000, // maximum table rows for scatter chart support, heatmap is created for larger tables minScatterGLRows: 1000, // minimum number of points to use WebGL 'scattergl' instead of SVG 'scatter' singleTraceUI: false, // by default we support multi-trace in UI upperLimitUI: false, // by default user can not set upper limit column in scatter options ui: {HistogramOptions: {fixedAlgorithm: undefined}} // by default we allow both "uniform binning" and "bayesian blocks" }, hips : { useForImageSearch: true, hipsSources: 'all', defHipsSources: {source: 'irsa', label: 'Featured'}, mergedListPriority: 'irsa' }, coverage : { // TODO: need to define all options with defaults here. used in FFEntryPoint.js }, tap : { services: [ { label: 'IRSA https://irsa.ipac.caltech.edu/TAP', value: 'https://irsa.ipac.caltech.edu/TAP' }, { label: 'NED https://ned.ipac.caltech.edu/tap', value: 'https://ned.ipac.caltech.edu/tap/' }, { label: 'CADC https://www.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/tap', value: 'https://www.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/tap' }, { label: 'GAIA https://gea.esac.esa.int/tap-server/tap', value: 'https://gea.esac.esa.int/tap-server/tap' }, { label: 'GAVO http://dc.g-vo.org/tap', value: 'http://dc.g-vo.org/tap'}, { label: 'MAST https://vao.stsci.edu/CAOMTAP/TapService.aspx', value: 'https://vao.stsci.edu/CAOMTAP/TapService.aspx' } ], defaultMaxrec: 50000 } }; /** * * @param props * @param options */ function fireflyInit(props, options={}) { if (initDone) return; props = mergeObjectOnly(defAppProps, props); props.renderTreeId= undefined; // in not API usages, renderTreeId is not used options = mergeObjectOnly(defFireflyOptions, options); const {template} = props; const viewer = get(Templates, template); const touch= false; // ToDo: determine if we are on a touch device if (touch) { React.initializeTouchEvents(true); } // setup application options dispatchAppOptions(options); if (options.disableDefaultDropDown) { dispatchUpdateLayoutInfo({disableDefaultDropDown:true}); } if (options.readoutDefaultPref) { dispatchChangeReadoutPrefs(options.readoutDefaultPref); } if (options.wcsMatchType) { dispatchWcsMatch({matchType:options.wcsMatchType, lockMatch:true}); } if (options.imageScrollsToHighlightedTableRow!==visRoot().autoScrollToHighlightedTableRow) { dispatchChangeTableAutoScroll(options.imageScrollsToHighlightedTableRow); } // initialize UI or API depending on entry mode. if (viewer) { if (window.document.readyState==='complete' || window.document.readyState==='interactive') { renderRoot(viewer, props); } else { console.log('Waiting for document to finish loading'); window.addEventListener('load', () => renderRoot(viewer, props) ); // maybe could use: document.addEventListener('DOMContentLoaded' } } else { initApi(); } initDone = true; } /* * * @param {string} divId * @param {Object} props * @param {Object} options * @return {Object} return object has two properties unrender and render */ export function startAsAppFromApi(divId, props={}, options={}) { const defProps= {...defAppProps}; props = mergeObjectOnly(defProps, props); props= {...props, ...{div:divId}}; const {template} = props; const viewer = get(Templates, template); options = mergeObjectOnly(defFireflyOptions, options); dispatchAppOptions(options); if (props.disableDefaultDropDown) { dispatchUpdateLayoutInfo({disableDefaultDropDown:true}); } if (props.readoutDefaultPref) { dispatchChangeReadoutPrefs(options.readoutDefaultPref); } if (viewer) { const element= document.getElementById(props.div); const controlObj= { unrender: () => ReactDOM.unmountComponentAtNode(element), render: () => renderRoot(viewer, props) }; controlObj.render(); return controlObj; } } /** * returns version information in a key/value object. * @returns {VersionInfo} */ export function getVersion() { return getPropsWith('version.'); } export const firefly = { bootstrap, addListener: flux.addListener, process: flux.process, }; /** * boostrap Firefly api or application. * @param {AppProps} props - application properties * @param {FireflyOptions} options - startup options * @returns {Promise.<boolean>} */ function bootstrap(props, options) { // if initialized, don't run it again. if (window.firefly && window.firefly.initialized) return Promise.resolve(); set(window, 'firefly.initialized', true); return new Promise((resolve) => { flux.bootstrap(); flux.process( {type : APP_LOAD} ); // setup initial store/state ensureUsrKey(); // establish websocket connection first before doing anything else. wsConnect((client) => { fireflyInit(props, options); client.addListener(ActionEventHandler); window.firefly.wsClient = client; init(); //TODO.. need to add spaName when we decide to support it. initLostConnectionWarning(); resolve && resolve(); }); }).then(() => { // when all is done.. mark app as 'ready' defer(() => dispatchUpdateAppData({isReady: true})); }); } function renderRoot(viewer, props) { const e= document.getElementById(props.div); if (e) { ReactDOM.render(React.createElement(viewer, props), e); } else { showInfoPopup('HTML page is not setup correctly, Firefly cannot start.'); console.log(`DOM Element "${props.div}" is not found in the document, Firefly cannot start.`); } } function ensureUsrKey() { if (hasOldUsrKey()) { document.cookie = 'usrkey=;path=/;max-age=-1'; document.cookie = `usrkey=;path=${location.pathname};max-age=-1`; } const usrKey = getCookie('usrkey'); if (!usrKey) { document.cookie = `usrkey=${uuid()};max-age=${3600 * 24 * 7 * 2}`; } } function uuid() { var seed = Date.now(); if (window.performance && typeof window.performance.now === 'function') { seed += performance.now(); } var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = (seed + Math.random() * 16) % 16 | 0; seed = Math.floor(seed/16); return (c === 'x' ? r : r & (0x3|0x8)).toString(16); }); return uuid; } function hasOldUsrKey() { return document.cookie.split(';').map((s) => s.trim()) .some( (c) => { const [name='', val=''] = c.split('='); return name === 'usrkey' && val.includes('/'); }); } function getCookie(name) { return ('; ' + document.cookie) .split('; ' + name + '=') .pop() .split(';') .shift(); }