Source: public/javascript/modules/spectrum/olqv_spectrum.js

  1. import {
  2. shiftFrequencyByZ,
  3. unshiftFrequencyByZ,
  4. shiftFrequencyByV,
  5. unshiftFrequencyByV,
  6. v2f,
  7. f2v,
  8. sumArr,
  9. unitRescale,
  10. standardDeviation,
  11. jyperk
  12. } from "../utils.js";
  13. import { LinePlotter } from '../olqv_spectro.js';
  14. import { dataPaths, URL_ROOT, URL_3D_TO_1D, yafitsTarget, testMode } from '../init.js';
  15. import { ServerApi } from '../serverApi.js'
  16. import { FITS_HEADER } from '../fitsheader.js';
  17. import { Constants } from "../constants.js";
  18. import { sAMPPublisher, setOnHubAvailability } from "../samp_utils.js";
  19. import { DOMAccessor } from "../domelements.js";
  20. import {Slice} from "../olqv_slice.js";
  21. import {
  22. XDataCompute, FrequencyXData, VelocityXData, VeloLSRXData, WaveXData, WavnXData, AWavXData
  23. } from "./xdata.js";
  24. import {
  25. YDataCompute, FrequencyYData, VelocityYData, VeloLSRYData, WaveYData, WavnYData, AWavYData
  26. } from "./ydata.js";
  27. import{
  28. ChartLegend, SitelleLegend, CasaLegend, GildasLegend, MuseLegend, MiriadLegend, MeerkatLegend, NenufarLegend
  29. } from './chartlegend.js';
  30. /**
  31. * Triggers Highcharts selection event on the given chart
  32. * @param {Highcharts.chart} chart event target
  33. * @param {number} xMin minimum selected value on xAxis
  34. * @param {number} xMax maximum selected value on xAxis
  35. */
  36. function fireChartSelectionEvent(chart, xMin, xMax){
  37. Highcharts.fireEvent(chart, 'selection', {
  38. xAxis: [{
  39. min: xMin,
  40. max: xMax
  41. }],
  42. });
  43. }
  44. /**
  45. * Returns a formatted string containing a displayable unit name
  46. * @param {string} unit the source unit
  47. * @returns {string} a formatted unit
  48. */
  49. function displayableBUnit(unit) {
  50. if(unit === undefined)
  51. return "";
  52. else{
  53. switch (unit) {
  54. case "Jy/beam":
  55. return "Jy";
  56. case "erg/s/cm^2/A/arcsec^2":
  57. return "erg/s/cm^2/A";
  58. default:
  59. return "";
  60. }
  61. }
  62. }
  63. /**
  64. * Returns an object containing the configuration of a point when it will be displayed
  65. * in Highcharts graph (plot type, colors, line width ...)
  66. *
  67. * Radius (5) and color (red) are hard coded for now
  68. *
  69. * @param {number} x coordinate on x axis (float)
  70. * @param {number} y coordinate on y axis (float)
  71. * @param {boolean} visible visibility
  72. * @returns {object} a point object to plot in Hightcharts
  73. */
  74. function getPoint(x, y, visible) {
  75. return {
  76. type: 'scatter',
  77. name: '',
  78. showInLegend: false,
  79. visible: visible,
  80. zIndex: 1,
  81. enableMouseTracking: false,
  82. marker: {
  83. radius: 8
  84. },
  85. data: [{
  86. x: x,
  87. y: y,
  88. color: '#BF0B23'
  89. }]
  90. }
  91. }
  92. /**
  93. * Returns an object containing the configuration of the X axis when it will be displayed
  94. * in Highcharts graph (plot type, colors, line width ...)
  95. * @returns {Object} configuration of x axis for Highcharts
  96. */
  97. function getXAxisConfiguration() {
  98. let axis = {
  99. type: 'scatter',
  100. marker: {
  101. color: '#1f77b4',
  102. size: 0,
  103. radius: 0
  104. },
  105. line: {
  106. color: '#1f77b4',
  107. width: 1
  108. },
  109. connectgaps: 'true',
  110. hoverinfo: 'x+y',
  111. xaxis: 'x'
  112. };
  113. return axis;
  114. // commented for now, does not seem useful
  115. //return JSON.parse(JSON.stringify(axis))
  116. }
  117. /**
  118. * Returns an event signaling that a slice has been modified
  119. * data packed inside the event are used in testing context
  120. * @param {string} type type of the modified slice ( single or summed)
  121. * @param {object} data spectrum data
  122. * @returns Event
  123. */
  124. function get3DSpectrumUpdateEvent(type, data, meta){
  125. let event = new Event(type);
  126. if (type === "single"){
  127. event.freqMin = meta.spectrumValues.freqMin;
  128. event.freqMax = meta.spectrumValues.freqMax;
  129. event.jyperk = jyperk(FITS_HEADER.restfreq, FITS_HEADER.bmin, FITS_HEADER.bmaj);
  130. event.iRA = meta.iRA;
  131. event.iDEC = meta.iDEC;
  132. if(meta.ytitle !== undefined){
  133. let parts = meta.ytitle.split('(');
  134. if(parts.length > 1){
  135. event.unit = parts[1].replace(')', '');
  136. }else{
  137. event.unit = parts[0];
  138. }
  139. }
  140. event.rmsValue = standardDeviation(data.y);
  141. event.minValue = Math.min(...data.y);
  142. event.maxValue = Math.max(...data.y);
  143. }else if(type === "summed"){
  144. if(meta.lowerIntegratedValues.flux !== undefined){
  145. event.fluxValue = meta.lowerIntegratedValues.flux.value;
  146. event.fluxUnit = meta.lowerIntegratedValues.flux.unit;
  147. }
  148. if(meta.lowerIntegratedValues.vmin !== undefined){
  149. event.vminValue = meta.lowerIntegratedValues.vmin.value;
  150. event.vminUnit = meta.lowerIntegratedValues.vmin.unit;
  151. }
  152. if(meta.lowerIntegratedValues.vmax !== undefined){
  153. event.vmaxValue = meta.lowerIntegratedValues.vmax.value;
  154. event.vmaxUnit = meta.lowerIntegratedValues.vmax.unit;
  155. }
  156. event.imin = meta.lowerIntegratedValues.imin;
  157. event.imax = meta.lowerIntegratedValues.imax;
  158. if(meta.ytitle !== undefined){
  159. let parts = meta.ytitle.split('(');
  160. if(parts.length > 1){
  161. event.unit = parts[1].replace(')', '');
  162. }else{
  163. event.unit = parts[0];
  164. }
  165. }
  166. event.rmsValue = standardDeviation(data.y);
  167. event.minValue = Math.min(...data.y);
  168. event.maxValue = Math.max(...data.y);
  169. }else if(type === "bounds"){
  170. event.iFreqMin = meta.iFreqMin;
  171. event.iFreqMax = meta.iFreqMax;
  172. /*event.freqMin = meta.freqMin;
  173. event.freqMax = meta.freqMax;*/
  174. event.velMin = meta.velMin;
  175. event.velMax = meta.velMax;
  176. }
  177. return event;
  178. }
  179. /**
  180. * Returns an event signaling that a slice has been modified
  181. * data packed inside the event are used in testing context
  182. * @param {object} data spectrum data
  183. * @returns Event
  184. */
  185. function get1DSpectrumUpdateEvent(data, meta){
  186. let event = new Event("1d");
  187. if(meta.ytitle !== undefined){
  188. let parts = meta.ytitle.split('(');
  189. if(parts.length > 1){
  190. event.unit = parts[1].replace(')', '');
  191. }else{
  192. event.unit = parts[0];
  193. }
  194. }
  195. event.rmsValue = standardDeviation(data.y);
  196. event.minValue = meta.minVal;
  197. event.maxValue = meta.maxVal;
  198. event.freqMin = meta.freqMin;
  199. event.freqMax = meta.freqMax;
  200. event.iRA = 0;
  201. event.iDEC = 0;
  202. return event;
  203. }
  204. class SpectrumViewer {
  205. static objectType = "SPECTRUM";
  206. /**
  207. * @constructor
  208. * @param {Object} paths dataPaths object
  209. * @param {string} containerId id of graph container
  210. */
  211. constructor(paths, containerId, width, heightWidthRatio, spectroUI) {
  212. this.spectrumChart = null;
  213. this._containerId = containerId;
  214. this._viewLinker = null;
  215. this._spectroUI = spectroUI;
  216. this.linePlotter = null;
  217. this.spectrumLoadedlisteners = [];
  218. this._spectrumUnit = displayableBUnit(FITS_HEADER.bunit);
  219. this._width = width;
  220. this._heightWidthRatio = heightWidthRatio;
  221. this._xtitle = "undefined";
  222. this._ytitle = "undefined";
  223. this._xMinZoom = null;
  224. this._xMaxZoom = null;
  225. this._relFITSFilePath = paths.relFITSFilePath;
  226. this._sampButton = undefined;
  227. this._datatype = "";
  228. this._legendObject = new ChartLegend();
  229. this._xDataComputer = new XDataCompute()
  230. this._yDataComputer = new YDataCompute()
  231. this._initLegendObject();
  232. this. _initDataComputers();
  233. }
  234. _initLegendObject(){
  235. let strategy = null;
  236. if (FITS_HEADER.isSITELLE()) {
  237. strategy = new SitelleLegend();
  238. } else if (FITS_HEADER.isCASA()) {
  239. strategy = new CasaLegend();
  240. } else if (FITS_HEADER.isGILDAS()) {
  241. strategy = new GildasLegend();
  242. } else if (FITS_HEADER.isMUSE()) {
  243. strategy = new MuseLegend();
  244. } else if (FITS_HEADER.isMIRIAD()) {
  245. strategy = new MiriadLegend();
  246. } else if (FITS_HEADER.isMEERKAT()) {
  247. strategy = new MeerkatLegend();
  248. } else if (FITS_HEADER.isNENUFAR()) {
  249. strategy = new NenufarLegend();
  250. } else{
  251. throw("Strategy is unknown");
  252. }
  253. this._legendObject.setStrategy(strategy);
  254. }
  255. _initDataComputers(){
  256. switch (FITS_HEADER.ctype3) {
  257. case 'FREQ':
  258. this._xDataComputer.setStrategy(new FrequencyXData());
  259. this._yDataComputer.setStrategy(new FrequencyYData());
  260. break;
  261. case 'VRAD':
  262. this._xDataComputer.setStrategy(new VelocityXData());
  263. this._yDataComputer.setStrategy(new VelocityYData());
  264. break;
  265. case 'VELO-LSR':
  266. this._xDataComputer.setStrategy(new VeloLSRXData());
  267. this._yDataComputer.setStrategy(new VeloLSRYData());
  268. break;
  269. case 'WAVE':
  270. this._xDataComputer.setStrategy(new WaveXData());
  271. this._yDataComputer.setStrategy(new WaveYData());
  272. break;
  273. case 'WAVN':
  274. this._xDataComputer.setStrategy(new WavnXData());
  275. this._yDataComputer.setStrategy(new WavnYData());
  276. break;
  277. case 'AWAV':
  278. this._xDataComputer.setStrategy(new AWavXData());
  279. this._yDataComputer.setStrategy(new AWavYData());
  280. break;
  281. default:
  282. throw("ctype3 case not recognized : " + FITS_HEADER.ctype3);
  283. }
  284. }
  285. /**
  286. * Returns Y value in spectrum data for a given X.
  287. * Values are sorted in descending ordre in spectrumData,
  288. * Y value is returned as soon as X > spectrumData[i]
  289. * @param {number} x X value (float)
  290. * @param {array} spectrumData an array of [x,y] tuples
  291. * @returns
  292. */
  293. getYValueAtX(x, spectrumData){
  294. if(x <= spectrumData[0][0] && x >= spectrumData[spectrumData.length -1][0]){
  295. for(let i=0; i < spectrumData.length; i++){
  296. if(x >= spectrumData[i][0])
  297. return spectrumData[i][1];
  298. }
  299. } else
  300. throw RangeError(x + " value not found in spectrum data");
  301. }
  302. setSpectrumSize(width, ratio){
  303. this.spectrumChart.setSize(width, ratio);
  304. this._width = width;
  305. this._heightWidthRatio = ratio;
  306. }
  307. addSpectrumLoadedListener(listener){
  308. this.spectrumLoadedlisteners.push(listener);
  309. }
  310. removeSpectrumLoadedListener(listener){
  311. for(let i=0; i < this.spectrumLoadedlisteners.length; i++){
  312. if(this.spectrumLoadedlisteners[i] === listener){
  313. this.spectrumLoadedlisteners.splice(i, 1);
  314. }
  315. }
  316. }
  317. _executeSpectrumLoadedListener(event) {
  318. for (let l of this.spectrumLoadedlisteners) {
  319. l.spectrumLoaded(event);
  320. }
  321. }
  322. /**
  323. * @param {ViewLinker} viewLinker ViewLinker object managing interactions between elements
  324. */
  325. setViewLinker(viewLinker) {
  326. this._viewLinker = viewLinker;
  327. }
  328. /**
  329. * Returns the xaxis of the chart and its datatype
  330. * @returns {Object} an object containing the xaxis and a datatype
  331. */
  332. getSpectrumChartXAxis() {
  333. return {
  334. axis: this.spectrumChart.xAxis[0],
  335. datatype: this._datatype
  336. };
  337. }
  338. /**
  339. * Called when NED table object triggers an event, refreshes lines display
  340. *
  341. * @param {Event} event event that triggered the call
  342. */
  343. sourceTableCall(event) {
  344. if (this.linePlotter != undefined) {
  345. this.linePlotter.refresh();
  346. }
  347. }
  348. refreshChartLegend() {
  349. throw new Error("This method must be implemented");
  350. }
  351. getExportMenuItems(mode, sampConnected){
  352. let result = ['downloadPNG'];
  353. if(mode === "OBSPM"){
  354. result.push('downloadFits');
  355. }
  356. result.push("sendTo1D");
  357. if(sampConnected){
  358. result.push("sendWithSamp");
  359. }
  360. return result;
  361. }
  362. /**
  363. * Toggles samp button visibility
  364. * @param {boolean} state status of button visibility
  365. */
  366. setSampButtonVisible(state) {
  367. let menu = { exporting: { buttons: { contextButton: { menuItems: this.getExportMenuItems(yafitsTarget, state) } } } };
  368. //this can occur before chart is initialized
  369. if(this.spectrumChart !== null){
  370. this.spectrumChart.update(menu);
  371. }
  372. }
  373. _showCoordinates(coords){
  374. throw new Error("This method must be implemented");
  375. }
  376. /**
  377. * Returns integral value of selected area in the spectrum
  378. * One case for a graph in radial velocity, one for all other cases
  379. * @param {*} yData
  380. * @param {*} imin
  381. * @param {*} imax
  382. * @returns
  383. */
  384. _getSelectedSpectrumValue(yData, imin, imax) {
  385. let result = 0;
  386. if (FITS_HEADER.ctype3 === 'VRAD') {
  387. let copy = (x) => x;
  388. let arraycopy = yData.map(copy);
  389. result = sumArr(arraycopy.reverse(), imin, imax, FITS_HEADER.cdelt3prim);
  390. } else {
  391. result = sumArr(yData, imin, imax, FITS_HEADER.cdelt3prim);
  392. }
  393. return result / unitRescale(this._spectrumUnit);
  394. }
  395. /**
  396. * Returns the channel corresponding to the given input value (frequency or velocity)
  397. * @param {number} value (float)
  398. * @returns {float}
  399. */
  400. _getCalculatedIndex(value) {
  401. let result = 0;
  402. if (FITS_HEADER.ctype3 === 'VRAD') {
  403. let step1 = (Constants.UNIT_FACTOR[Constants.DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]] / Constants.UNIT_FACTOR[FITS_HEADER.cunit3]) / FITS_HEADER.cdelt3;
  404. let crval3 = FITS_HEADER.crval3 / (Constants.UNIT_FACTOR[Constants.DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]] / Constants.UNIT_FACTOR[FITS_HEADER.cunit3]);
  405. result = (value - crval3) * step1 + FITS_HEADER.crpix3 - 1;
  406. } else if (FITS_HEADER.ctype3 === 'FREQ') {
  407. // if ctype is FREQ we have to read Constants.DEFAULT_OUTPUT_UNIT['VRAD']*
  408. let vcenter = 0; //SPEED_OF_LIGHT * (FITS_HEADER.crval3 - FITS_HEADER.restfreq) / FITS_HEADER.restfreq;
  409. let step1 = v2f(value * Constants.UNIT_FACTOR[Constants.DEFAULT_OUTPUT_UNIT['VRAD']], FITS_HEADER.restfreq, vcenter);
  410. let step2 = (step1 - FITS_HEADER.crval3) / FITS_HEADER.cdelt3;
  411. result = step2 + FITS_HEADER.crpix3 - 1 /*+ FITS_HEADER.naxis3 / 2*/;
  412. }
  413. if (FITS_HEADER.cdelt3 >= 0) {
  414. result = FITS_HEADER.naxis3 - result - 1;
  415. }
  416. if(Math.round(result) < 0){
  417. console.error("Calculated index can not be a negative value : " + result);
  418. } else
  419. return Math.round(result);
  420. }
  421. /**
  422. * Returns x axis coordinates. They are calculated from center position and step between each value (CDELT3)
  423. * The formula changes according to the type of data on x axis (CTYPE3)
  424. *
  425. * @param {number} rlen number of points on x axis (int)
  426. * @returns {array} an array of x values
  427. */
  428. _getXData(rlen) {
  429. throw new Error("This method must be implemented");
  430. }
  431. /**
  432. * Returns an array of ydata from the data passed in parameter.
  433. * The parameter array must be reverted if CDELT3 > 0 in case of a frequency
  434. * and if CDELT3 < 0 in case of a radial velocity
  435. * It is rescaled in case of Sitelle data.
  436. * It is returned unchanged in any other case
  437. *
  438. * @param {array} data
  439. * @returns {array}
  440. */
  441. _getYData(data) {
  442. throw new Error("This method must be implemented");
  443. }
  444. }
  445. /**
  446. * A class displaying a spectrum, using the Highcharts library
  447. *
  448. * @property {Object} spectrumChart a highchart chart object
  449. */
  450. class SingleSpectrumViewer extends SpectrumViewer {
  451. /**
  452. * @constructor
  453. * @param {Object} paths dataPaths object
  454. * @param {string} containerId id of graph container
  455. */
  456. constructor(paths, containerId, width, heightWidthRatio, spectroUI) {
  457. super(paths, containerId, width, heightWidthRatio, spectroUI);
  458. this.iRA = undefined;
  459. this._iDEC = undefined;
  460. this._ifrequencyMarker = 0;
  461. this.toptitle = "undefined";
  462. this._datatype = "frequency";
  463. this._computeSliceIndex = this._computeSliceIndex.bind(this);
  464. this.refreshChartLegend();
  465. }
  466. /**
  467. Returns index of slice to be displayed
  468. plotData : spectrum
  469. x : x position clicked on graph
  470. */
  471. /**
  472. * Returns index of slice to be displayed when spectrum is clicked
  473. * @param {Object} plotData object containing arrays of x and y values of the graph
  474. * @param {number} x x position clicked on graph (float)
  475. * @returns
  476. */
  477. _computeSliceIndex(plotData, x) {
  478. var rlen = plotData.x.length;
  479. switch (FITS_HEADER.ctype3) {
  480. case 'FREQ':
  481. if (FITS_HEADER.cdelt3 > 0) {
  482. var forigin = plotData.x[rlen - 1];
  483. var deltaf = plotData.x[0] - plotData.x[1];
  484. } else {
  485. var forigin = plotData.x[0];
  486. var deltaf = plotData.x[1] - plotData.x[0];
  487. }
  488. break;
  489. case 'VRAD':
  490. case 'VELO-LSR':
  491. case 'WAVE':
  492. case 'WAVN':
  493. case 'AWAV':
  494. if (FITS_HEADER.cdelt3 > 0) {
  495. var forigin = plotData.x[0];
  496. var deltaf = plotData.x[1] - plotData.x[0];
  497. } else {
  498. var forigin = plotData.x[rlen - 1];
  499. var deltaf = plotData.x[0] - plotData.x[1];
  500. }
  501. break;
  502. default:
  503. console.log("This should not happen");
  504. }
  505. // phys2Index
  506. return Math.round((x - forigin) / deltaf);
  507. }
  508. /**
  509. * Called when NED table object triggers an event, refreshes lines display
  510. *
  511. * @param {Event} event event that triggered the call
  512. */
  513. sourceTableCall(event) {
  514. this.refresh();
  515. }
  516. /**
  517. * Sets the title of the graph, x and y axis
  518. * the format of the title depends on the type o displayed data.
  519. * currently considered are : sitelle, casa, muse, gildas, miriad
  520. *
  521. * An alert is displayed in any other case
  522. */
  523. refreshChartLegend() {
  524. this._legendObject.defineSpectrumLegend(this);
  525. if(this.spectrumChart !== null){
  526. this.spectrumChart.xAxis[0].axisTitle.attr({
  527. text: this._xtitle
  528. });
  529. }
  530. }
  531. _showCoordinates(coords){
  532. DOMAccessor.getSingleChartCoordinates().innerText = "@Pixel x=" + Math.round(coords[0]) + " y=" + Math.round(coords[1]);
  533. }
  534. /**
  535. * Sets the index of selected frequency value on graph
  536. * @param {number} i index of selected frequency on graph (int)
  537. */
  538. setFrequencyMarker(i) {
  539. console.log("setFrequencyMarker: entering.");
  540. switch (FITS_HEADER.ctype3) {
  541. case 'FREQ':
  542. if (FITS_HEADER.cdelt3 > 0) {
  543. this._ifrequencyMarker = FITS_HEADER.naxis3 - 1 - i;
  544. } else {
  545. this._ifrequencyMarker = i;
  546. }
  547. break;
  548. case 'VRAD':
  549. if (FITS_HEADER.cdelt3 > 0) {
  550. this._ifrequencyMarker = i;
  551. } else {
  552. this._ifrequencyMarker = FITS_HEADER.naxis3 - 1 - i;
  553. }
  554. break;
  555. // equivalent to VRAD
  556. case 'VELO-LSR':
  557. if (FITS_HEADER.cdelt3 > 0) {
  558. this._ifrequencyMarker = i;
  559. } else {
  560. this._ifrequencyMarker = FITS_HEADER.naxis3 - 1 - i;
  561. }
  562. break;
  563. case 'WAVE':
  564. case 'WAVN':
  565. case 'AWAV':
  566. this._ifrequencyMarker = i;
  567. break;
  568. default:
  569. console.log("This should not happen");
  570. break;
  571. }
  572. console.log("setFrequencyMarker: exiting.");
  573. }
  574. /**
  575. * Creates and returns a Highcharts chart
  576. * @param {Object} plotData Data plotted in graph
  577. * @param {string} xtitle x axis title
  578. * @param {string} ytitle y axis title
  579. * @returns {chart}
  580. */
  581. _getChart(plotData, xtitle, ytitle) {
  582. let self = this;
  583. let kpj = FITS_HEADER.kelvinPerJansky();
  584. let spectrumData = [];
  585. for (let i = 0; i < plotData.x.length; i++) {
  586. spectrumData.push([plotData.x[i], plotData.y[i]]);
  587. }
  588. let container = document.getElementById(this._containerId);
  589. return Highcharts.chart(container, {
  590. title: {
  591. text: ''
  592. },
  593. chart: {
  594. type: 'line',
  595. width: self._width,
  596. height: self._heightWidthRatio,
  597. animation: false,
  598. zoomType: 'xz',
  599. panning: true,
  600. panKey: 'shift',
  601. responsive: {
  602. rules: [{
  603. condition: {
  604. maxWidth: self._width,
  605. maxHeight: self._heightWidthRatio
  606. },
  607. chartOptions: {
  608. xAxis: {
  609. labels: {
  610. formatter: function () {
  611. return this.value.charAt(0);
  612. }
  613. }
  614. },
  615. yAxis: {
  616. labels: {
  617. align: 'left',
  618. x: 0,
  619. y: -5
  620. },
  621. title: {
  622. text: null
  623. }
  624. }
  625. }
  626. }]
  627. },
  628. events: {
  629. render: function (event) {
  630. // hides zoom button when it is displayed
  631. // we only use zoom button defined in yafits UI
  632. if (this.resetZoomButton !== undefined) {
  633. this.resetZoomButton.hide();
  634. }
  635. },
  636. load: function (event) {
  637. // graph is loaded
  638. DOMAccessor.showLoaderAction(false);
  639. },
  640. click: function (event) {
  641. let sliceIndex = self._computeSliceIndex(plotData, event.xAxis[0].value);
  642. // Display slice at index sliceIndex
  643. if (self._viewLinker !== null) {
  644. self._viewLinker.getAndPlotSingleSlice(sliceIndex);
  645. self._viewLinker.setFluxDensityInPopup(event.yAxis[0].value, SpectrumViewer.objectType);
  646. }
  647. const yvalue = self.getYValueAtX(event.xAxis[0].value, spectrumData);
  648. this.series[1].update(getPoint(event.xAxis[0].value, yvalue, true));
  649. }
  650. }
  651. },
  652. boost: {
  653. useGPUTranslations: true,
  654. usePreAllocated: true
  655. },
  656. xAxis: {
  657. gridLineWidth: 1,
  658. lineColor: '#FFFFFF',
  659. title: {
  660. text: xtitle
  661. },
  662. crosshair: true,
  663. reversed: true,
  664. maxPadding : 0,
  665. endOnTick : false,
  666. minPadding : 0,
  667. startOnTick : false,
  668. events: {
  669. // called when boudaries of spectrum are modified
  670. setExtremes: function (event) {
  671. if ((event.min === undefined || event.max === undefined) && self._viewLinker !== null) {
  672. self._viewLinker.summedPixelsSpectrumViewer.spectrumChart.xAxis[0].setExtremes(
  673. self._viewLinker.summedPixelsSpectrumViewer.spectrumChart.xAxis[0].dataMin,
  674. self._viewLinker.summedPixelsSpectrumViewer.spectrumChart.xAxis[0].dataMax);
  675. } else {
  676. let restfreq = FITS_HEADER.restfreq
  677. if(self._spectroUI.getVelocity("m/s") === 0 || self._spectroUI.getVelocity("m/s") === undefined ){
  678. if(self._spectroUI.getRedshift() !== undefined){
  679. restfreq = restfreq * (1 + self._spectroUI.getRedshift());
  680. }
  681. }
  682. let minval, maxval;
  683. //velocity is undefined if a z value has been entered
  684. if( self._spectroUI.getVelocity("m/s") === undefined ){
  685. minval = Math.round(f2v(event.min * 1e9, restfreq, 0) / 1e3);
  686. maxval = Math.round(f2v(event.max * 1e9, restfreq, 0) / 1e3);
  687. }else{
  688. minval = Math.round(f2v(event.min * 1e9, restfreq, self._spectroUI.getVelocity("m/s")) / 1e3);
  689. maxval = Math.round(f2v(event.max * 1e9, restfreq, self._spectroUI.getVelocity("m/s")) / 1e3);
  690. }
  691. //exchange min/max if min > max
  692. if (minval > maxval) {
  693. let tmp = minval;
  694. minval = maxval;
  695. maxval = tmp;
  696. }
  697. if (self._viewLinker !== null) {
  698. self._viewLinker.summedPixelsSpectrumViewer.spectrumChart.xAxis[0].setExtremes(minval, maxval);
  699. }
  700. }
  701. },
  702. }
  703. },
  704. yAxis: {
  705. lineColor: '#FFFFFF',
  706. gridLineWidth: 1,
  707. lineWidth: 1,
  708. opposite: true,
  709. title: {
  710. text: ytitle
  711. },
  712. labels: {
  713. // returns ticks to be displayed on Y axis
  714. formatter: function () {
  715. let label = '';
  716. // value already in K
  717. if (FITS_HEADER.isSpectrumInK()) {
  718. label = 'K';
  719. }
  720. // result can be NaN if _bmin/_bmaj not available
  721. // then nothing to display, else value is converted in K
  722. else if (!isNaN(kpj)) {
  723. label = " <br/> " + Number(this.value * kpj).toExponential(2) + " K";
  724. }
  725. return Number(this.value).toExponential(2) + label;
  726. }
  727. }
  728. },
  729. plotOptions: {
  730. series: {
  731. cursor: 'pointer',
  732. step: 'center',
  733. color: Constants.PLOT_DEFAULT_COLOR,
  734. animation: {
  735. duration: 0
  736. },
  737. lineWidth: Constants.PLOT_DEFAULT_LINE_WIDTH,
  738. events: {
  739. click: function (event) {
  740. //console.clear();
  741. let sliceIndex = self._computeSliceIndex(plotData, event.point.x);
  742. DOMAccessor.setSliceChannel("Chan#" + sliceIndex);
  743. const yvalue = self.getYValueAtX(event.point.x, spectrumData);
  744. this.chart.series[1].update(getPoint(event.point.x, yvalue, true));
  745. if (this._viewLinker !== null) {
  746. // Display slice at index sliceIndex
  747. self._viewLinker.setFluxDensityInPopup(event.point.y, SpectrumViewer.objectType);
  748. self._viewLinker.getAndPlotSingleSlice(sliceIndex);
  749. }
  750. }
  751. }
  752. },
  753. marker: {
  754. radius: 0
  755. }
  756. },
  757. exporting: {
  758. menuItemDefinitions: {
  759. // Custom definition
  760. downloadFits: {
  761. onclick: function () {
  762. window.open(URL_ROOT + dataPaths.spectrum, '_blank');
  763. },
  764. text: 'Download FITS file'
  765. },
  766. sendWithSamp: {
  767. onclick: function (event) {
  768. sAMPPublisher.sendSpectrumToAll(URL_ROOT + dataPaths.spectrum, "Artemix");
  769. event.stopPropagation();
  770. },
  771. text: 'Send with SAMP'
  772. },
  773. sendTo1D: {
  774. onclick: function (event) {
  775. window.open(URL_3D_TO_1D + dataPaths.spectrum, '_blank');
  776. },
  777. text: 'Open in 1D viewer'
  778. }
  779. },
  780. buttons: {
  781. contextButton: {
  782. menuItems: self.getExportMenuItems(yafitsTarget, false)
  783. }
  784. }
  785. },
  786. tooltip: {
  787. // displayed when the mouse if above the graph
  788. formatter: function () {
  789. // get channel number
  790. let sliceIndex = self._computeSliceIndex(plotData, this.x);
  791. let label = '( ' + this.x.toFixed(4) + ', ' + this.y.toFixed(4) + ') ';
  792. if (!isNaN(kpj) && !FITS_HEADER.isSpectrumInK()) {
  793. label = label + ", " + Number(this.y * kpj).toExponential(2) + " K";
  794. }
  795. return " Chan#" + sliceIndex + " " + label;
  796. }
  797. },
  798. series: [{
  799. // unlimited number of points when zooming
  800. cropThreshold: Infinity,
  801. showInLegend: false,
  802. data: spectrumData,
  803. zIndex: 0,
  804. marker: {
  805. radius: 0
  806. },
  807. },
  808. //series of frequency markers, must not be empty to create it, this point is hidden
  809. getPoint(0, 0, false)],
  810. });
  811. }
  812. /**
  813. * Returns x axis coordinates. They are calculated from center position and step between each value (CDELT3)
  814. * The formula changes according to the type of data on x axis (CTYPE3)
  815. *
  816. * @param {number} rlen number of points on x axis (int)
  817. * @returns {array} an array of x values
  818. */
  819. _getXData(rlen) {
  820. return this._xDataComputer.computeSpectrum(rlen, this._spectroUI.getVelocity("m/s"), this._spectroUI.getRedshift());
  821. }
  822. /**
  823. * Returns an array of ydata from the data passed in parameter.
  824. * The parameter array must be reverted if CDELT3 > 0 in case of a frequency
  825. * and if CDELT3 < 0 in case of a radial velocity
  826. * It is rescaled in case of Sitelle data.
  827. * It is returned unchanged in any other case
  828. *
  829. * @param {array} data
  830. * @returns {array}
  831. */
  832. _getYData(data) {
  833. let result = this._yDataComputer.computeSpectrum(data);
  834. if (FITS_HEADER.isSITELLE()) {
  835. if (FITS_HEADER.cdelt1 && FITS_HEADER.cdelt2) {
  836. let pixel2arcsec = Math.abs(3600 * 3600 * FITS_HEADER.cdelt1 * FITS_HEADER.cdelt2);
  837. let temparr = result.map(function (x) {
  838. return x * unitRescale(FITS_HEADER.bunit) / pixel2arcsec
  839. });
  840. result = temparr;
  841. }
  842. }
  843. return result;
  844. }
  845. /**
  846. * Calls createFits function of server to create a fits file corresponding to iRa/iDec
  847. * Path of created file is stored in dataPaths.spectrum
  848. *
  849. * createFits parameters are relFITSFilePath, iRA, iDEC
  850. *
  851. * @param {number} iRA index of selected RA value (int)
  852. * @param {number} iDEC index of selected DEC value (int)
  853. */
  854. _createFitsFile(iRA, iDEC) {
  855. if (FITS_HEADER.ctype3 === "FREQ" || FITS_HEADER.ctype3 === "VRAD") {
  856. let apiQuery = new ServerApi();
  857. apiQuery.createFitsFile(iRA, iDEC, this._relFITSFilePath, (resp)=>{
  858. let response = JSON.parse(resp);
  859. dataPaths.spectrum = response.result;
  860. });
  861. }
  862. }
  863. refresh(){
  864. this.plot(this._iRA, this._iDEC, undefined);
  865. this.refreshChartLegend();
  866. }
  867. /**
  868. * Calls the getSpectrum function of the server to get the spectrum data and plot them
  869. *
  870. * getSpectrum parameters are : relFITSFilePath, iRA, iDEC, iFREQ0, iFREQ1
  871. *
  872. * Here we want data for all frequencies so iFREQ0 = 0 and iFREQ1 = NAXIS3 - 1
  873. * if iRA or iDEC are undefined, we use a centered value NAXIS1 / 2 and NAXIS2 / 2 respectively
  874. *
  875. * @param {number} iRA
  876. * @param {number} iDEC
  877. * @param {callbackFucntion} cb a function called when data have been returned from server
  878. */
  879. plot(iRA, iDEC, cb) {
  880. this._iRA = iRA;
  881. this._iDEC = iDEC;
  882. let self = this;
  883. if (typeof iRA === 'undefined') {
  884. iRA = Math.floor(FITS_HEADER.naxis1 / 2);
  885. }
  886. if (typeof iDEC === 'undefined') {
  887. iDEC = Math.floor(FITS_HEADER.naxis2 / 2);
  888. }
  889. let queryApi = new ServerApi();
  890. queryApi.getSingleSpectrum(iRA, iDEC, this._relFITSFilePath,(resp)=>{
  891. let plotData = getXAxisConfiguration();
  892. let jsonTextContent = JSON.stringify(resp);
  893. let data;
  894. // NaN values are replaced by null if they exist
  895. if(jsonTextContent.includes("NaN")){
  896. let newResp = JSON.stringify(resp).replace(/\bNaN\b/g, "null");
  897. let test = JSON.parse(newResp);
  898. data = JSON.parse(test.data);
  899. }else{
  900. data = resp.data;
  901. }
  902. plotData.x = self._getXData(data.result.length); // abscissa ( frequency, wavelength, velocity, ...);
  903. plotData.xaxis = "x";
  904. plotData.y = self._getYData(data.result);
  905. self.spectrumChart = self._getChart(plotData, self._xtitle, self._ytitle);
  906. // pixel coordinates above chart
  907. self._showCoordinates([iRA, iDEC]);
  908. self._createFitsFile(iRA, iDEC);
  909. if(testMode){
  910. let meta = {iRA : iRA,
  911. iDEC : iDEC,
  912. ytitle : self._ytitle,
  913. spectrumValues : {
  914. freqMin : Math.min(...plotData.x),
  915. freqMax : Math.max(...plotData.x),
  916. freqcenter : plotData.x[Math.floor(FITS_HEADER.naxis3 / 2)],
  917. }
  918. };
  919. self._executeSpectrumLoadedListener(get3DSpectrumUpdateEvent("single",plotData, meta));
  920. }
  921. // callback function called at the end of loading process
  922. // typical use is restore previous graph limits
  923. if (cb !== undefined) {
  924. cb();
  925. }
  926. if (self._viewLinker !== null) {
  927. // set value of flux density in popup, should me moved out of this function
  928. self._viewLinker.setFluxDensityInPopup(plotData.y[self._ifrequencyMarker], Slice.objectType);
  929. if (self._viewLinker.summedPixelsSpectrumViewer.linePlotter !== null && self._viewLinker.spectroUI.getSelectedDatabase() !== "off") {
  930. self._viewLinker.summedPixelsSpectrumViewer.linePlotter.loadAndPlotLines(self._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMin,
  931. self._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMax,
  932. [self._viewLinker.summedPixelsSpectrumViewer.getSpectrumChartXAxis(), self.getSpectrumChartXAxis()]);
  933. }
  934. }
  935. });
  936. };
  937. }
  938. /**
  939. * A class displaying a spectrum, using the Highcharts library
  940. *
  941. * @property {Object} spectrumChart a highchart chart object
  942. */
  943. class SingleSpectrumViewer1D extends SingleSpectrumViewer {
  944. constructor(paths, containerId, width, heightWidthRatio, spectroUI) {
  945. super(paths, containerId, width, heightWidthRatio, spectroUI);
  946. this.detailChart;
  947. this._detailData = [];
  948. // min x value in detail chart
  949. this._xDetailMin = null;
  950. // max x value in detail chart
  951. this._xDetailMax = null;
  952. this._shiftMode = null;
  953. //xMin for manual user selection, redshift is applied
  954. this._xSelectionMin = null;
  955. //xMax for manual user selection, redshift is applied
  956. this._xSelectionMax = null;
  957. this.plottedData;
  958. // draw lines on master chart
  959. this.linePlotter = null;
  960. // draw lines on detailed chart
  961. this.detailLinePlotter = null;
  962. this._masterYData = [];
  963. // reference to last lines drawer called
  964. // this is used by the navigation buttons in
  965. // the web UI
  966. this.activePlotter = null;
  967. //this.initialized = false;
  968. this.isRefreshable = true;
  969. this._isInit = true;
  970. }
  971. /**
  972. * Resets zoom of detail chart to its initial value by triggering a selection event on the chart
  973. * with this._xDetailMin and this._xDetailMax values
  974. * Master chart is fixed and can not be zoomed
  975. */
  976. resetZoom(){
  977. Highcharts.fireEvent(this.detailChart, 'selection', {
  978. xAxis: [{
  979. min: this._xDetailMin,
  980. max: this._xDetailMax
  981. }],
  982. });
  983. }
  984. /**
  985. * Called when NED table object triggers an event, refreshes lines display
  986. *
  987. * @param {Event} event event that triggered the call
  988. */
  989. sourceTableCall(event) {
  990. this._shiftMode = "z";
  991. this.refresh("z");
  992. }
  993. /**
  994. * Returns data in the min/max interval from this._detailData
  995. * @param {number} min minimum value in interval (float)
  996. * @param {number} max maximum value in interval (float)
  997. * @returns array
  998. */
  999. _getIntervalData(dataArray, min, max){
  1000. let result = [];
  1001. dataArray.forEach(point => {
  1002. if (point[0] > min && point[0] < max) {
  1003. result.push([point[0], point[1]]);
  1004. }
  1005. });
  1006. return result;
  1007. }
  1008. setDetailedSpectrumSize(width, ratio){
  1009. this.detailChart.setSize(width, ratio);
  1010. this._width = width;
  1011. this._heightWidthRatio = ratio;
  1012. }
  1013. /**
  1014. * Creates the detailed chart zooming content from master chart
  1015. * @param {string} xtitle X axis title
  1016. * @param {string} ytitle Y axis title
  1017. */
  1018. _getDetailChart(srcData){
  1019. let self = this;
  1020. let kpj = FITS_HEADER.kelvinPerJansky();
  1021. this.detailChart = Highcharts.chart('detail-container', {
  1022. chart: {
  1023. type : "line",
  1024. animation : false,
  1025. width: self._width,
  1026. height: self._heightWidthRatio,
  1027. zoomType: 'x',
  1028. panning: true,
  1029. panKey: 'shift',
  1030. /*responsive: {
  1031. rules: [{
  1032. condition: {
  1033. maxWidth: self._width,
  1034. maxHeight: self._heightWidthRatio
  1035. },
  1036. chartOptions: {
  1037. xAxis: {
  1038. labels: {
  1039. formatter: function () {
  1040. return this.value.charAt(0);
  1041. }
  1042. }
  1043. },
  1044. yAxis: {
  1045. labels: {
  1046. align: 'left',
  1047. x: 0,
  1048. y: -5
  1049. },
  1050. title: {
  1051. text: null
  1052. }
  1053. }
  1054. }
  1055. }]
  1056. },*/
  1057. events : {
  1058. selection: function (event) {
  1059. if(self.isRefreshable){
  1060. var extremesObject = event.xAxis[0],
  1061. min = extremesObject.min,
  1062. max = extremesObject.max;
  1063. this.xAxis[0].setExtremes(min, max);
  1064. self.refreshMasterBands(min, max);
  1065. // spectral lines
  1066. self.detailChartLines();
  1067. self.activePlotter = self.detailLinePlotter;
  1068. }else{
  1069. alert("Display can not be refreshed. Velocity or redshift value has changed. Please press enter in this field to validate.");
  1070. }
  1071. return false;
  1072. },
  1073. pan : function(event){
  1074. self.refreshMasterBands(event.target.xAxis[0].min, event.target.xAxis[0].max);
  1075. }
  1076. },
  1077. },
  1078. credits: {
  1079. enabled: false
  1080. },
  1081. title: {
  1082. text: '',
  1083. },
  1084. tooltip: {
  1085. // displayed when the mouse if above the graph
  1086. formatter: function () {
  1087. // get channel number
  1088. let sliceIndex = self._computeSliceIndex(srcData, this.x);
  1089. let label = '( ' + this.x.toFixed(4) + ', ' + this.y.toFixed(4) + ') ';
  1090. return " Chan#" + sliceIndex + " " + label;
  1091. }
  1092. },
  1093. xAxis: {
  1094. gridLineWidth: 1,
  1095. type : "line",
  1096. lineColor: '#FFFFFF',
  1097. title: {
  1098. text: self._xtitle
  1099. },
  1100. crosshair: true,
  1101. reversed: false,
  1102. maxPadding : 0,
  1103. endOnTick : false,
  1104. minPadding : 0,
  1105. startOnTick : false
  1106. },
  1107. yAxis: {
  1108. lineColor: '#FFFFFF',
  1109. gridLineWidth: 1,
  1110. lineWidth: 1,
  1111. opposite: true,
  1112. title: {
  1113. text: self._ytitle
  1114. },
  1115. labels: {
  1116. // returns ticks to be displayed on Y axis
  1117. formatter: function () {
  1118. let label = '';
  1119. // value already in K
  1120. if (FITS_HEADER.isSpectrumInK()) {
  1121. label = 'K';
  1122. }
  1123. // result can be NaN if _bmin/_bmaj not available
  1124. // then nothing to display, else value is converted in K
  1125. else if (!isNaN(kpj)) {
  1126. label = " <br/> " + Number(this.value * kpj).toExponential(2) + " K";
  1127. }
  1128. return Number(this.value).toExponential(2) + label;
  1129. }
  1130. }
  1131. },
  1132. legend: {
  1133. enabled: false
  1134. },
  1135. exporting: {
  1136. menuItemDefinitions: {
  1137. },
  1138. buttons: {
  1139. contextButton: {
  1140. menuItems: ['downloadPNG']
  1141. }
  1142. }
  1143. },
  1144. plotOptions: {
  1145. series: {
  1146. cursor: 'pointer',
  1147. step: 'center',
  1148. color: Constants.PLOT_DEFAULT_COLOR,
  1149. lineWidth: Constants.PLOT_DEFAULT_LINE_WIDTH,
  1150. animation: {
  1151. duration: 0
  1152. }
  1153. },
  1154. marker: {
  1155. radius: 0
  1156. }
  1157. },
  1158. series: [{
  1159. marker: {
  1160. radius: 0
  1161. }
  1162. }],
  1163. });
  1164. }
  1165. /**
  1166. * Shows the selected interval on the master graph.
  1167. * Area is coloured between [xAxis[0], xMin] and [xMax, xAxis[xAxis.length -1]]
  1168. * @param {number} xMin minimum value seelcted by user on X axis
  1169. * @param {number} xMax maximum value seelcted by user on X axis
  1170. */
  1171. refreshMasterBands(xMin, xMax){
  1172. if(this.spectrumChart !== null){
  1173. if(this._shiftMode === 'z'){
  1174. this._xSelectionMin = unshiftFrequencyByZ(xMin, this._spectroUI.getRedshift());
  1175. this._xSelectionMax = unshiftFrequencyByZ(xMax, this._spectroUI.getRedshift());
  1176. }else if(this._shiftMode === 'v'){
  1177. this._xSelectionMin = unshiftFrequencyByV(xMin, this._spectroUI.getVelocity());
  1178. this._xSelectionMax = unshiftFrequencyByV(xMax, this._spectroUI.getVelocity());
  1179. }
  1180. // move the plot bands to reflect the new detail span
  1181. this.spectrumChart.xAxis[0].removePlotBand('mask');
  1182. this.spectrumChart.xAxis[0].addPlotBand({
  1183. id: 'mask',
  1184. from: xMin,
  1185. to: xMax,
  1186. color: 'rgba(51, 153, 255, 0.2)'
  1187. });
  1188. }
  1189. }
  1190. /**
  1191. * Returns the xaxis of the chart and its datatype
  1192. * @returns {Object} an object containing the xaxis and a datatype
  1193. */
  1194. getDetailedSpectrumChartXAxis() {
  1195. return {
  1196. axis: this.detailChart.xAxis[0],
  1197. datatype: this._datatype
  1198. };
  1199. }
  1200. _getAxes(){
  1201. return [{
  1202. axis: this.spectrumChart.xAxis[0],
  1203. datatype: this._datatype
  1204. },{
  1205. axis: this.detailChart.xAxis[0],
  1206. datatype: this._datatype
  1207. }
  1208. ];
  1209. }
  1210. masterChartLines(){
  1211. // if not disabled
  1212. if (this._spectroUI.getSelectedDatabase() !== "off") {
  1213. if (this.linePlotter === null) {
  1214. this.linePlotter = new LinePlotter(this._spectroUI);
  1215. }
  1216. const graphs = this._getAxes();
  1217. try {
  1218. this.linePlotter.loadAndPlotLines(this._xDetailMin,
  1219. this._xDetailMax,
  1220. graphs);
  1221. } catch (e) {
  1222. console.log(e);
  1223. alert("Lines can not be displayed, please verify that redshift and/or velocity is defined.");
  1224. }
  1225. }
  1226. //}
  1227. }
  1228. detailChartLines(){
  1229. // spectral lines
  1230. //if (this._spectroUI.isEnabled()) {
  1231. if (this.detailLinePlotter === null) {
  1232. this.detailLinePlotter = new LinePlotter(this._spectroUI);
  1233. }
  1234. const graphs = this._getAxes();
  1235. try {
  1236. this.detailLinePlotter.loadAndPlotLines(this.detailChart.xAxis[0].getExtremes().min,
  1237. this.detailChart.xAxis[0].getExtremes().max,
  1238. graphs);
  1239. } catch (e) {
  1240. console.log(e);
  1241. alert("Lines can not be displayed, please verify that redshift and/or velocity is defined.");
  1242. }
  1243. //}
  1244. }
  1245. /**
  1246. * Creates and returns a Highcharts chart
  1247. * @param {Object} spectrumData Data plotted in graph
  1248. * @param {string} xtitle x axis title
  1249. * @param {string} ytitle y axis title
  1250. * @returns {chart}
  1251. */
  1252. _getChart(srcData, spectrumData) {
  1253. let self = this;
  1254. let kpj = FITS_HEADER.kelvinPerJansky();
  1255. let result = Highcharts.chart("master-container", {
  1256. title: {
  1257. text: ''
  1258. },
  1259. chart: {
  1260. type: 'line',
  1261. width: self._width,
  1262. height: self._heightWidthRatio,
  1263. animation: false,
  1264. zoomType: 'x',
  1265. panning: true,
  1266. panKey: 'shift',
  1267. /*responsive: {
  1268. rules: [{
  1269. condition: {
  1270. maxWidth: self._width,
  1271. maxHeight: self._heightWidthRatio
  1272. },
  1273. chartOptions: {
  1274. xAxis: {
  1275. labels: {
  1276. formatter: function () {
  1277. return this.value.charAt(0);
  1278. }
  1279. }
  1280. },
  1281. yAxis: {
  1282. labels: {
  1283. align: 'left',
  1284. x: 0,
  1285. y: -5
  1286. },
  1287. title: {
  1288. text: null
  1289. }
  1290. }
  1291. }
  1292. }]
  1293. },*/
  1294. events: {
  1295. load: function (event) {
  1296. // graph is loaded
  1297. //result.series[0].setVisible(false);
  1298. let meta = {
  1299. yTitle: self._ytitle,
  1300. freqMin : this.xAxis[0].min,
  1301. freqMax : this.xAxis[0].max,
  1302. minVal : this.series[0].dataMin,
  1303. maxVal : this.series[0].dataMax,
  1304. }
  1305. let t = get1DSpectrumUpdateEvent(srcData, meta);
  1306. self._executeSpectrumLoadedListener(get1DSpectrumUpdateEvent(srcData, meta));
  1307. //DOMAccessor.showLoaderAction(true);
  1308. DOMAccessor.markLoadingDone();
  1309. },
  1310. selection: function (event) {
  1311. if(self.isRefreshable) {
  1312. var extremesObject = event.xAxis[0],
  1313. min = extremesObject.min,
  1314. max = extremesObject.max,
  1315. detailData = self._getIntervalData(spectrumData, min, max);
  1316. // show selected interval
  1317. self.refreshMasterBands(min ,max);
  1318. // set data on detailed chart and refresh x axis limits
  1319. self.detailChart.series[0].setData(detailData, true, false, false);
  1320. self.detailChart.xAxis[0].setExtremes(min, max);
  1321. // save global selection
  1322. self._detailData = detailData;
  1323. self._xDetailMin = min;
  1324. self._xDetailMax = max;
  1325. self._xSelectionMin = min;
  1326. self._xSelectionMax = max;
  1327. // spectral lines
  1328. self.masterChartLines();
  1329. self.activePlotter = self.linePlotter;
  1330. }else{
  1331. alert("Display can not be refreshed. Velocity or redshift value has changed. Please press enter in this field to validate.");
  1332. }
  1333. return false;
  1334. }
  1335. }
  1336. },
  1337. boost: {
  1338. useGPUTranslations: true,
  1339. usePreAllocated: true
  1340. },
  1341. xAxis: {
  1342. gridLineWidth: 1,
  1343. lineColor: '#FFFFFF',
  1344. title: {
  1345. text: self._xtitle
  1346. },
  1347. crosshair: true,
  1348. reversed: false,
  1349. maxPadding : 0,
  1350. endOnTick : false,
  1351. minPadding : 0,
  1352. startOnTick : false
  1353. },
  1354. yAxis: {
  1355. lineColor: '#FFFFFF',
  1356. gridLineWidth: 1,
  1357. lineWidth: 1,
  1358. opposite: true,
  1359. title: {
  1360. text: self._ytitle
  1361. },
  1362. labels: {
  1363. // returns ticks to be displayed on Y axis
  1364. formatter: function () {
  1365. let label = '';
  1366. // value already in K
  1367. if (FITS_HEADER.isSpectrumInK()) {
  1368. label = 'K';
  1369. }
  1370. // result can be NaN if _bmin/_bmaj not available
  1371. // then nothing to display, else value is converted in K
  1372. else if (!isNaN(kpj)) {
  1373. label = " <br/> " + Number(this.value * kpj).toExponential(2) + " K";
  1374. }
  1375. return Number(this.value).toExponential(2) + label;
  1376. }
  1377. }
  1378. },
  1379. plotOptions: {
  1380. series: {
  1381. cursor: 'pointer',
  1382. step: 'center',
  1383. color: Constants.PLOT_DEFAULT_COLOR,
  1384. animation: {
  1385. duration: 0
  1386. },
  1387. lineWidth: Constants.PLOT_DEFAULT_LINE_WIDTH,
  1388. },
  1389. marker: {
  1390. radius: 0
  1391. }
  1392. },
  1393. tooltip: {
  1394. // displayed when the mouse if above the graph
  1395. formatter: function () {
  1396. // get channel number
  1397. let sliceIndex = self._computeSliceIndex(srcData, this.x);
  1398. let label = '( ' + this.x.toFixed(4) + ', ' + this.y.toFixed(4) + ') ';
  1399. if (!isNaN(kpj) && !FITS_HEADER.isSpectrumInK()) {
  1400. label = label + ", " + Number(this.y * kpj).toExponential(2) + " K";
  1401. }
  1402. return " Chan#" + sliceIndex + " " + label;
  1403. }
  1404. },
  1405. exporting: {
  1406. menuItemDefinitions: {
  1407. // Custom definition
  1408. downloadFits: {
  1409. onclick: function () {
  1410. window.open(URL_ROOT + dataPaths.spectrum, '_blank');
  1411. },
  1412. text: 'Download FITS file'
  1413. },
  1414. sendWithSamp: {
  1415. onclick: function (event) {
  1416. sAMPPublisher.sendSpectrumToAll(URL_ROOT + dataPaths.spectrum, "Artemix");
  1417. event.stopPropagation();
  1418. },
  1419. text: 'Send with SAMP'
  1420. },
  1421. },
  1422. buttons: {
  1423. contextButton: {
  1424. menuItems: self.getExportMenuItems(yafitsTarget, false)
  1425. }
  1426. }
  1427. },
  1428. series: [{
  1429. // unlimited number of points when zooming
  1430. cropThreshold: Infinity,
  1431. showInLegend: false,
  1432. data: self.plottedData,
  1433. zIndex: 0,
  1434. marker: {
  1435. radius: 0
  1436. }
  1437. },
  1438. //series of frequency markers, must not be empty to create it, this point is hidden
  1439. getPoint(0, 0, false)],
  1440. }, function (chart) {
  1441. // this is called when spectrum is exported as an image
  1442. // the detailed chart must not be recreated then
  1443. //if(!self.initialized){
  1444. self._getDetailChart(srcData);
  1445. //}
  1446. //self.initialized = true;
  1447. });
  1448. return result;
  1449. }
  1450. /**
  1451. * Refresh both charts display
  1452. */
  1453. refresh(mode = null){
  1454. this._shiftMode = mode;
  1455. // set data on detailed chart and refresh x axis limits
  1456. this.plot(this._iRA, this._iDEC, ()=>{
  1457. let xMin = this._xSelectionMin;
  1458. let xMax = this._xSelectionMax;
  1459. this._xDetailMin = xMin;
  1460. this._xDetailMax = xMax;
  1461. if(this._shiftMode === "z"){
  1462. xMin = shiftFrequencyByZ(this._xSelectionMin, this._spectroUI.getRedshift());
  1463. xMax = shiftFrequencyByZ(this._xSelectionMax, this._spectroUI.getRedshift());
  1464. }else if(this._shiftMode === "v"){
  1465. xMin = shiftFrequencyByV(this._xSelectionMin, this._spectroUI.getVelocity());
  1466. xMax = shiftFrequencyByV(this._xSelectionMax, this._spectroUI.getVelocity());
  1467. }
  1468. this.refreshChartLegend();
  1469. this.detailChart.series[0].setData(this.plottedData, true, false, false);
  1470. this.detailChart.xAxis[0].setExtremes(xMin, xMax);
  1471. //this.detailChart.xAxis[0].setExtremes(this.detailChart.xAxis[0].getExtremes().min, this.detailChart.xAxis[0].getExtremes().max);
  1472. if (this.linePlotter !== null) {
  1473. this.linePlotter.setTargets(this._getAxes());
  1474. this.masterChartLines();
  1475. this.detailChartLines();
  1476. }
  1477. this.detailChart.xAxis[0].axisTitle.attr({
  1478. text: this._xtitle
  1479. });
  1480. });
  1481. }
  1482. /**
  1483. * Calls the getSpectrum function of the server to get the spectrum data and plot them
  1484. *
  1485. * getSpectrum parameters are : relFITSFilePath, iRA, iDEC, iFREQ0, iFREQ1
  1486. *
  1487. * Here we want data for all frequencies so iFREQ0 = 0 and iFREQ1 = NAXIS3 - 1
  1488. * if iRA or iDEC are undefined, we use a centered value NAXIS1 / 2 and NAXIS2 / 2 respectively
  1489. *
  1490. * @param {number} iRA
  1491. * @param {number} iDEC
  1492. * @param {callbackFucntion} cb a function called when data have been returned from server
  1493. */
  1494. plot(iRA, iDEC, cb) {
  1495. let self = this;
  1496. this._iRA = iRA;
  1497. this._iDEC = iDEC;
  1498. if (typeof iRA === 'undefined') {
  1499. iRA = Math.floor(FITS_HEADER.naxis1 / 2);
  1500. }
  1501. if (typeof iDEC === 'undefined') {
  1502. iDEC = Math.floor(FITS_HEADER.naxis2 / 2);
  1503. }
  1504. this.refreshChartLegend();
  1505. DOMAccessor.showLoaderAction(true);
  1506. let qp = new ServerApi();
  1507. qp.getSingleSpectrum(iRA, iDEC, this._relFITSFilePath,(resp)=>{
  1508. let content = resp;
  1509. let test = JSON.stringify(resp);
  1510. let data;
  1511. // fix for cubes containing NaN
  1512. if(test.includes("NaN")){
  1513. let newResp = test.replace(/\bNaN\b/g, "null");
  1514. content = JSON.parse(newResp);
  1515. //for some reason the data element is still
  1516. //a string after data have been parsed above
  1517. data = JSON.parse(content.data);
  1518. }else{
  1519. data = resp.data;
  1520. }
  1521. let plotData = getXAxisConfiguration();
  1522. // in a 1D spectra, getXData will always return data sorted in reverse order
  1523. plotData.x = self._getXData(data["result"].length).reverse();
  1524. plotData.xaxis = "x";
  1525. plotData.y = self._getYData(data["result"]).reverse();
  1526. self._masterYData = plotData.y;
  1527. let spectrumData = [];
  1528. for (let i = 0; i < plotData.x.length; i++) {
  1529. spectrumData.push([plotData.x[i], plotData.y[i]]);
  1530. }
  1531. this.plottedData = spectrumData;
  1532. self.spectrumChart = self._getChart(plotData, spectrumData);
  1533. if(self._xSelectionMin !== null){
  1534. let xMin = this._xSelectionMin;
  1535. let xMax= this._xSelectionMax;
  1536. if(this._shiftMode === "z"){
  1537. xMin = shiftFrequencyByZ(this._xSelectionMin, this._spectroUI.getRedshift());
  1538. xMax = shiftFrequencyByZ(this._xSelectionMax, this._spectroUI.getRedshift());
  1539. }else if(this._shiftMode === "v"){
  1540. xMin = shiftFrequencyByV(this._xSelectionMin, this._spectroUI.getVelocity());
  1541. xMax = shiftFrequencyByV(this._xSelectionMax, this._spectroUI.getVelocity());
  1542. }
  1543. self.refreshMasterBands(xMin, xMax);
  1544. }
  1545. self._createFitsFile(iRA, iDEC);
  1546. if(testMode && self._isInit){
  1547. self._isInit = false;
  1548. }
  1549. // callback function called at the end of loading process
  1550. // typical use is restore previous graph limits
  1551. if (cb !== undefined) {
  1552. cb();
  1553. }
  1554. DOMAccessor.showLoaderAction(false);
  1555. });
  1556. };
  1557. }
  1558. /**
  1559. * A class displaying an averaged spectrum, using the Highcharts library
  1560. * Initial interval selection is defined in _getInitialSelectionRange
  1561. *
  1562. * @property {Object} summedPixelsSpectrumChart a highchart chart object
  1563. * @property {LinePlotter} linePlotter an object drawing spectral lines on the chart
  1564. */
  1565. class SummedPixelsSpectrumViewer extends SpectrumViewer {
  1566. /**
  1567. *
  1568. * @param {*} paths
  1569. * @param {*} containerId
  1570. * @param {*} viewLinker
  1571. */
  1572. constructor(paths, containerId, width, heightWidthRatio, spectroUI) {
  1573. super(paths, containerId, width, heightWidthRatio, spectroUI);
  1574. this._iRA0 = null;
  1575. this._iRA1 = null;
  1576. this._iDEC0 = null;
  1577. this._iDEC1 = null;
  1578. this._toptitle_unit = "";
  1579. this._flux_unit = "";
  1580. this._averageSpectrum = null;
  1581. this._vmin = null;
  1582. this._vmax = null;
  1583. this._isInit = true;
  1584. // Interval of values selected by default of the graph
  1585. // Those are the indexes of the values to select not values themselves
  1586. // If they are not null they will bypass the values coming from the selection event
  1587. this.defaultIndexMax = null;
  1588. const freqs = this._getInitialSelectionRange();
  1589. this.defaultIndexMin = freqs[0];
  1590. this.defaultIndexMax = freqs[1];
  1591. this._summedData = {
  1592. x: [],
  1593. };
  1594. this._datatype = "velocity";
  1595. // surface under the chart in area selected by user, Jy/km/s
  1596. //this._selectedSurface = 0;
  1597. this.refreshChartLegend();
  1598. }
  1599. get vmin() {
  1600. return this._vmin;
  1601. }
  1602. get vmax() {
  1603. return this._vmax;
  1604. }
  1605. set vmin(vmin) {
  1606. this._vmin = vmin;
  1607. }
  1608. set vmax(vmax) {
  1609. this._vmax = vmax;
  1610. }
  1611. get summedData() {
  1612. return this._summedData;
  1613. }
  1614. /**
  1615. * Initial frequency range selected on the spectrum
  1616. * @returns array
  1617. */
  1618. _getInitialSelectionRange() {
  1619. const naxis3Index = FITS_HEADER.naxis3 -1;
  1620. const iFREQ0 = Math.round(naxis3Index / 2 - naxis3Index / 8);
  1621. const iFREQ1 = Math.round(naxis3Index / 2 + naxis3Index / 8);
  1622. /*const iFREQ0 = 0;
  1623. const iFREQ1 = naxis3Index;*/
  1624. return [iFREQ0, iFREQ1];
  1625. }
  1626. _showCoordinates(coords){
  1627. DOMAccessor.getSummedChartCoordinates().innerText = "@Box xmin=" + Math.round(coords[0]) +
  1628. " xmax=" + Math.round(coords[1]) +
  1629. " ymin=" + Math.round(coords[2]) +
  1630. " ymax=" + Math.round(coords[3]);
  1631. }
  1632. /**
  1633. * Sets title of spectrum, x and y axis
  1634. *
  1635. * Title of the spectrum depends on its type (Sitelle, Casa, Gildas, Muse)
  1636. */
  1637. refreshChartLegend() {
  1638. // prefix and suffix for GILDAS and ALMA title
  1639. this._legendObject.defineSummedSpectrumLegend(this);
  1640. if(this.spectrumChart !== null){
  1641. this.spectrumChart.xAxis[0].axisTitle.attr({
  1642. text: this._xtitle
  1643. });
  1644. }
  1645. }
  1646. /**
  1647. * Returns the title displayed above the graph
  1648. * @param {number} value integral of selected interval on the graph (float)
  1649. * @param {string} unit unit of integral value
  1650. * @param {number} vmin minimum selected velocity value (float)
  1651. * @param {number} vmax maximum selected velocity value (float)
  1652. * @param {number} imin minimum selected channel index (int)
  1653. * @param {number} imax maximum selected channel index (int)
  1654. * @returns {string}
  1655. */
  1656. _getTopTitle(value, unit, vmin, vmax, imin, imax) {
  1657. // do not display unit (Jy.km/s) if bmin/bmax are not defined
  1658. // because it has no meaning in that case
  1659. let result_unit = "";
  1660. if (FITS_HEADER.bmin !== undefined) {
  1661. result_unit = unit;
  1662. }
  1663. return '<span id="selected-surface">' + value.toExponential(2) + "</span> " +
  1664. result_unit + ", vmin=" +
  1665. vmin.toFixed(2) + " " + "km/s" + " , vmax=" +
  1666. vmax.toFixed(2) + " " + "km/s, imin=" + imin + ", imax=" + imax;
  1667. }
  1668. /**
  1669. * Sets the title diplayed above the graph
  1670. * @param {string} title
  1671. */
  1672. setChartTitle(title) {
  1673. DOMAccessor.getAverageSpectrumTitle().innerHTML = title;
  1674. }
  1675. refresh(){
  1676. this.plot(this._iRA0, this._iRA1, this._iDEC0, this._iDEC1, undefined);
  1677. this.refreshChartLegend();
  1678. }
  1679. /**
  1680. * Updates the displayed averaged slice image, with respect to selected interval in graph
  1681. * Note : this function should be removed from this class and place in ViewLinker
  1682. *
  1683. * @param {number} min minimum selected value (float)
  1684. * @param {number} max maximum selected value (float)
  1685. */
  1686. _updateSummedSlices(min, max) {
  1687. if (this._viewLinker !== null) {
  1688. const imin = Math.round((min - this._summedData.x[0]) / (this._summedData.x[1] - this._summedData.x[0]));
  1689. const imax = Math.round((max - this._summedData.x[0]) / (this._summedData.x[1] - this._summedData.x[0]));
  1690. if (FITS_HEADER.cunit3 in Constants.UNIT_FACTOR) {
  1691. switch (FITS_HEADER.ctype3) {
  1692. case 'FREQ':
  1693. if (FITS_HEADER.cdelt3 > 0) {
  1694. this._viewLinker.getAndPlotSummedSlices(this._summedData.x.length - 1 - imax, this._summedData.x.length - 1 - imin);
  1695. } else {
  1696. this._viewLinker.getAndPlotSummedSlices(imin, imax);
  1697. }
  1698. break;
  1699. case 'VRAD':
  1700. if (FITS_HEADER.cdelt3 > 0) {
  1701. this._viewLinker.getAndPlotSummedSlices(imin, imax);
  1702. } else {
  1703. this._viewLinker.getAndPlotSummedSlices(this._summedData.x.length - 1 - imax, this._summedData.x.length - 1 - imin);
  1704. }
  1705. break;
  1706. //equivalent to VRAD
  1707. case 'VELO-LSR':
  1708. if (FITS_HEADER.cdelt3 > 0) {
  1709. this._viewLinker.getAndPlotSummedSlices(imin, imax);
  1710. } else {
  1711. this._viewLinker.getAndPlotSummedSlices(this._summedData.x.length - 1 - imax, this._summedData.x.length - 1 - imin);
  1712. }
  1713. break;
  1714. case 'WAVE':
  1715. break
  1716. case 'WAVN':
  1717. break
  1718. case 'AWAV':
  1719. if (FITS_HEADER.cdelt3 > 0) {
  1720. this._viewLinker.getAndPlotSummedSlices(imin, imax);
  1721. } else {
  1722. this._viewLinker.getAndPlotSummedSlices(this._summedData.x.length - 1 - imax, this._summedData.x.length - 1 - imin);
  1723. }
  1724. break;
  1725. default:
  1726. alert('Unknown value for ctype3');
  1727. console.log("This should not happen");
  1728. }
  1729. }
  1730. }
  1731. }
  1732. /**
  1733. * Returns a Highchart chart
  1734. * @returns {chart}
  1735. */
  1736. _getChart() {
  1737. let self = this;
  1738. let target = document.getElementById(this._containerId);
  1739. return Highcharts.chart(target, {
  1740. title: {
  1741. text: ''
  1742. },
  1743. chart: {
  1744. type: 'line',
  1745. animation: false,
  1746. width: self._width,
  1747. height: self._heightWidthRatio,
  1748. zoomType: 'x',
  1749. panning: true,
  1750. panKey: 'shift',
  1751. //marginLeft: 0, // Set the left margin to 0
  1752. // marginRight: 0, // Set the right margin to 0
  1753. responsive: {
  1754. rules: [{
  1755. condition: {
  1756. maxWidth: self._width,
  1757. maxHeight: self._heightWidthRatio
  1758. },
  1759. chartOptions: {
  1760. xAxis: {
  1761. labels: {
  1762. formatter: function () {
  1763. return this.value.charAt(0);
  1764. }
  1765. }
  1766. },
  1767. yAxis: {
  1768. labels: {
  1769. align: 'left',
  1770. x: 0,
  1771. y: -5
  1772. },
  1773. title: {
  1774. text: null
  1775. }
  1776. }
  1777. }
  1778. }]
  1779. },
  1780. events: {
  1781. click: function (event) {
  1782. console.log("A click occurred on the spectrum : enter");
  1783. },
  1784. redraw: function(event){
  1785. // make sure chart title are always up to date
  1786. self.refreshChartLegend();
  1787. },
  1788. selection: function (event) {
  1789. if(self._viewLinker.isRefreshable){
  1790. let velMin, velMax = null;
  1791. // if true, refresh displayed summmed slice after the selection
  1792. let refreshSlice = true;
  1793. // if default values exists for the interval use them then they are reseted
  1794. if (self.defaultIndexMin !== null && self.defaultIndexMax !== null) {
  1795. velMin = self.spectrumChart.series[0].xData[self.defaultIndexMin];
  1796. velMax = self.spectrumChart.series[0].xData[self.defaultIndexMax];
  1797. // slice is not refreshed
  1798. refreshSlice = false;
  1799. self.defaultIndexMin = null;
  1800. self.defaultIndexMax = null;
  1801. }
  1802. // values selected by the user
  1803. else {
  1804. velMin = event.xAxis[0].min;
  1805. velMax = event.xAxis[0].max;
  1806. }
  1807. // overplot selected area in blue
  1808. this.xAxis[0].update({
  1809. plotBands: [{
  1810. from: velMin,
  1811. to: velMax,
  1812. color: 'rgba(68, 170, 213, .2)'
  1813. }]
  1814. });
  1815. self._vmin = velMin;
  1816. self._vmax = velMax;
  1817. //toggle-lines-search
  1818. if (self._viewLinker !== null) {
  1819. self._spectroUI.hideEnergyGroupLines();
  1820. if (self.linePlotter === null) {
  1821. self.linePlotter = new LinePlotter(self._spectroUI);
  1822. }
  1823. }
  1824. // refreshes summed slice display according to selection
  1825. if(refreshSlice === true){
  1826. self._updateSummedSlices(velMin, velMax);
  1827. }
  1828. let ivalues = [self._getCalculatedIndex(velMin), self._getCalculatedIndex(velMax)].sort();
  1829. const imin = ivalues[0];
  1830. const imax = ivalues[1];
  1831. let selectedSurface = 0;
  1832. if (self._viewLinker !== null) {
  1833. selectedSurface = self._getSelectedSpectrumValue(self._averageSpectrum, imin, imax);
  1834. self._spectroUI.setSelectedSurface(selectedSurface);
  1835. }
  1836. self.setChartTitle(self._getTopTitle(selectedSurface, self._toptitle_unit,
  1837. self._vmin, self._vmax, imin, imax));
  1838. let factor = 1;
  1839. if(self._spectroUI.getVelocity("m/s") === 0 || self._spectroUI.getVelocity("m/s") === undefined ){
  1840. factor = 1 + self._spectroUI.getRedshift();
  1841. }
  1842. const obsFreqMin = v2f(velMax * 10 ** 3, FITS_HEADER.restfreq * factor, self._spectroUI.getVelocity("m/s")) / 10 ** 9;
  1843. const obsFreqMax = v2f(velMin * 10 ** 3, FITS_HEADER.restfreq * factor, self._spectroUI.getVelocity("m/s")) / 10 ** 9;
  1844. if (self._viewLinker !== null && self._viewLinker.spectroUI.getSelectedDatabase() !== "off") {
  1845. const graphs = [self._viewLinker.spectrumViewer.getSpectrumChartXAxis(),
  1846. self.getSpectrumChartXAxis()
  1847. ];
  1848. self._viewLinker.updateSummedSlicesFreqIndexes(imin, imax);
  1849. if((self._isInit && FITS_HEADER.velolsr != undefined) || (!self._isInit)){
  1850. try {
  1851. self.linePlotter.loadAndPlotLines(obsFreqMin,
  1852. obsFreqMax,
  1853. graphs);
  1854. } catch (e) {
  1855. console.log(e);
  1856. alert("Lines can not be displayed, please verify that redshift and/or velocity is defined.");
  1857. }
  1858. }
  1859. }
  1860. if(testMode && self._isInit){
  1861. let meta = {
  1862. iFreqMin : imin,
  1863. iFreqMax : imax,
  1864. freqMin: obsFreqMin,
  1865. freqMax: obsFreqMax,
  1866. velMin : velMin,
  1867. velMax : velMax
  1868. };
  1869. //this._meta = meta;
  1870. self._executeSpectrumLoadedListener(
  1871. get3DSpectrumUpdateEvent("bounds",self._summedData, meta));
  1872. DOMAccessor.markLoadingDone();
  1873. }
  1874. self._isInit = false;
  1875. }else{
  1876. alert("Display can not be refreshed. Velocity or redshift value has changed. Please press enter in this field to validate.");
  1877. }
  1878. return false;
  1879. }
  1880. }
  1881. },
  1882. boost: {
  1883. useGPUTranslations: true
  1884. },
  1885. xAxis: {
  1886. title: {
  1887. text: self._xtitle
  1888. },
  1889. crosshair: true,
  1890. reversed: false,
  1891. gridLineWidth: 1,
  1892. lineColor: '#FFFFFF',
  1893. maxPadding : 0,
  1894. endOnTick : false,
  1895. minPadding : 0,
  1896. startOnTick : false
  1897. },
  1898. yAxis: {
  1899. gridLineWidth: 1,
  1900. lineColor: '#FFFFFF',
  1901. lineWidth: 1,
  1902. opposite: true,
  1903. title: {
  1904. text: self._ytitle
  1905. },
  1906. labels:{
  1907. formatter: function(){
  1908. return Number(this.value).toExponential(2);
  1909. }
  1910. }
  1911. },
  1912. plotOptions: {
  1913. series: {
  1914. step: 'center',
  1915. zoneAxis: 'x',
  1916. animation: {
  1917. duration: 0
  1918. },
  1919. lineWidth: Constants.PLOT_DEFAULT_LINE_WIDTH,
  1920. events: {
  1921. click: function (event) {
  1922. console.log("A click occurred on the LINE : enter");
  1923. }
  1924. }
  1925. },
  1926. marker: {
  1927. radius: 0
  1928. }
  1929. },
  1930. exporting: {
  1931. menuItemDefinitions: {
  1932. // Custom definition
  1933. downloadFits: {
  1934. onclick: function () {
  1935. window.open(URL_ROOT + dataPaths.averageSpectrum, '_blank');
  1936. },
  1937. text: 'Download FITS file'
  1938. },
  1939. sendWithSamp: {
  1940. onclick: function (event) {
  1941. sAMPPublisher.sendSpectrumToAll(URL_ROOT + dataPaths.averageSpectrum, "Artemix");
  1942. event.stopPropagation();
  1943. },
  1944. text: 'Send with SAMP'
  1945. },
  1946. sendTo1D: {
  1947. onclick: function (event) {
  1948. window.open(URL_3D_TO_1D + dataPaths.averageSpectrum, '_blank');
  1949. },
  1950. text: 'Open in 1D viewer'
  1951. }
  1952. },
  1953. buttons: {
  1954. contextButton: {
  1955. menuItems: self.getExportMenuItems(yafitsTarget, false)
  1956. }
  1957. }
  1958. },
  1959. tooltip: {
  1960. formatter: function () {
  1961. const index = self._getCalculatedIndex(this.x);
  1962. return 'Chan# ' + index + ' ( ' + this.x.toFixed(4) + ', ' + this.y.toFixed(4) + ')';
  1963. }
  1964. },
  1965. series: [{
  1966. color: Constants.PLOT_DEFAULT_COLOR
  1967. }]
  1968. });
  1969. }
  1970. /**
  1971. * Returns x axis coordinates. They are calculated from center position and step between each value (CDELT3)
  1972. * The formula changes according to the type of data on x axis (CTYPE3)
  1973. *
  1974. * @param {number} rlen number of points on x axis (int)
  1975. * @returns {array} an array of x values
  1976. */
  1977. _getXData(rlen) {
  1978. return this._xDataComputer.computeSummedSpectrum(rlen, this._spectroUI.getVelocity("m/s"));
  1979. }
  1980. /**
  1981. * Returns an array of ydata from the data passed in parameter.
  1982. * The parameter array must be reverted if CDELT3 > 0 in case of a frequency
  1983. * and if CDELT3 < 0 in case of a radial velocity
  1984. * It is rescaled in case of Sitelle data.
  1985. * It is returned unchanged in any other case
  1986. *
  1987. * @param {array} averageSpectrum
  1988. * @returns {array}
  1989. */
  1990. _getYData(averageSpectrum) {
  1991. return this._yDataComputer.computeSummedSpectrum(averageSpectrum, this._spectrumUnit);
  1992. }
  1993. /**
  1994. * Calls the getAverageSpectrum function of the server to get the spectrum data and plot them
  1995. *
  1996. * getAverageSpectrum parameters are : relFITSFilePath, iRA0, iDEC0, iRA1, iDEC1
  1997. *
  1998. *
  1999. * @param {number} iRA0 minimum selected index value on RA axis (int)
  2000. * @param {number} iDEC0 minimum selected index value on DEC axis (int)
  2001. * @param {number} iRA1 maximum selected index value on RA axis (int)
  2002. * @param {number} iDEC1 maximum selected index value on DEC axis (int)
  2003. */
  2004. plot(iRA0, iRA1, iDEC0, iDEC1, callback) {
  2005. this._iRA0 = iRA0;
  2006. this._iRA1 = iRA1;
  2007. this._iDEC0 = iDEC0;
  2008. this._iDEC1 = iDEC1;
  2009. if(this.spectrumChart !== null){
  2010. this._xMinZoom = this.spectrumChart.xAxis[0].getExtremes().min;
  2011. this._xMaxZoom = this.spectrumChart.xAxis[0].getExtremes().max;
  2012. }
  2013. this.spectrumChart = this._getChart();
  2014. let self = this;
  2015. let queryApi = new ServerApi();
  2016. queryApi.getSummedSpectrum(iRA0, iRA1, iDEC0, iDEC1, self._relFITSFilePath, (resp)=>{
  2017. let x = JSON.parse(resp);
  2018. if (x.result.averageSpectrum == null) {
  2019. alert("No data for average spectrum");
  2020. throw ("No data for average spectrum");
  2021. }
  2022. // Let's inform the SAMP hub
  2023. if ("absFITSFilePath" in x["result"]) {
  2024. dataPaths.averageSpectrum = x["result"]["absFITSFilePath"];
  2025. } else {
  2026. console.log("We should have found a key 'absFITSFilePath'");
  2027. }
  2028. let averageSpectrum = x["result"]["averageSpectrum"];
  2029. // Draw x-axis in Velocities (plot on bottom right)
  2030. self._summedData.x = self._getXData(averageSpectrum.length);
  2031. // only at initialization
  2032. if (self._vmin === null && self._vmax === null) {
  2033. self._vmin = self._summedData.x[0];
  2034. self._vmax = self._summedData.x[self._summedData.x.length - 1];
  2035. }
  2036. // box coordinates above chart
  2037. self._showCoordinates([iRA0, iRA1, iDEC0, iDEC1]);
  2038. // change name of function
  2039. averageSpectrum = self._getYData(averageSpectrum);
  2040. self._summedData.y = averageSpectrum;
  2041. let chartData = [];
  2042. for (let i = 0; i < self._summedData.x.length; i++) {
  2043. chartData.push([self._summedData.x[i], self._summedData.y[i]]);
  2044. }
  2045. self._averageSpectrum = averageSpectrum;
  2046. self.spectrumChart.series[0].update({
  2047. name: '',
  2048. // unlimited number of points when zooming
  2049. cropThreshold: Infinity,
  2050. showInLegend: false,
  2051. marker: {
  2052. radius: 0
  2053. },
  2054. data: chartData,
  2055. });
  2056. /**
  2057. Add a series where Y=0 in the given chart
  2058. */
  2059. let addYAxisSeries = function (chart) {
  2060. chart.addSeries({
  2061. lineWidth: 1,
  2062. enableMouseTracking: false,
  2063. showInLegend: false,
  2064. color: "#000000",
  2065. marker: {
  2066. enabled: false
  2067. },
  2068. data: [
  2069. [chart.xAxis[0].dataMin, 0],
  2070. [chart.xAxis[0].dataMax, 0]
  2071. ]
  2072. });
  2073. }
  2074. addYAxisSeries(self.spectrumChart);
  2075. if (self._viewLinker !== null) {
  2076. addYAxisSeries(self._viewLinker.spectrumViewer.spectrumChart);
  2077. if (self._viewLinker.summedPixelsSpectrumViewer.linePlotter !== null && self._viewLinker.spectroUI.getSelectedDatabase() !== "off") {
  2078. self.linePlotter.loadAndPlotLines(self.linePlotter.obsFreqMin,
  2079. self.linePlotter.obsFreqMax,
  2080. [self.getSpectrumChartXAxis(), self._viewLinker.spectrumViewer.getSpectrumChartXAxis()]);
  2081. }
  2082. }
  2083. if(self._xMinZoom !== null && self._xMaxZoom !== null){
  2084. self.spectrumChart.xAxis[0].setExtremes(self._xMinZoom, self._xMaxZoom);
  2085. }
  2086. if(testMode){
  2087. let meta = {
  2088. iRA : Math.min(iRA0, iRA1),
  2089. iDEC : Math.max(iDEC0, iDEC1),
  2090. ytitle : self._ytitle,
  2091. xbox : Math.abs(iRA1-iRA0),
  2092. ybox : Math.abs(iDEC1-iDEC0),
  2093. lowerIntegratedValues : {
  2094. flux : {value: self._getSelectedSpectrumValue(self._averageSpectrum, 0, self._summedData.x.length -1), unit : self._toptitle_unit},
  2095. vmin : {value : self._vmin, unit : "km/s"},
  2096. vmax : {value : self._vmax, unit : "km/s"},
  2097. imin : 0,
  2098. imax : self._summedData.x.length -1,
  2099. }
  2100. };
  2101. //this._meta = meta;
  2102. self._executeSpectrumLoadedListener(get3DSpectrumUpdateEvent("summed",self._summedData, meta));
  2103. }
  2104. if (callback !== undefined)
  2105. callback();
  2106. })
  2107. }
  2108. /**
  2109. * Replot the spectrum according to the received parameters
  2110. * The plot is done through a call to this.plot()
  2111. *
  2112. * The displayed spectral lines will be updated if they exist
  2113. *
  2114. * @param {number} xMin start x position (float)
  2115. * @param {number} xMax end x position (float)
  2116. * @param {number} yMin start y position (float)
  2117. * @param {number} yMax end y position (float)
  2118. */
  2119. replot(xMin, xMax, yMin, yMax) {
  2120. // replot the spectrum and keep the selection interval if it exists
  2121. this.plot(xMin, xMax, yMin, yMax, () => {
  2122. if (this.spectrumChart !== null && this._vmin !== null && this._vmax !== null) {
  2123. fireChartSelectionEvent(this.spectrumChart, this._vmin, this._vmax);
  2124. }
  2125. });
  2126. }
  2127. /**
  2128. * Called when NED table object triggers an event, refreshes graph title
  2129. *
  2130. * @param {Event} event event that triggered the call
  2131. */
  2132. sourceTableCall(event) {
  2133. this.refreshChartLegend();
  2134. }
  2135. }
  2136. /**
  2137. * Initializes and return the spectrum
  2138. * @param {*} spectroUI
  2139. * @param {*} sourceTable
  2140. * @param {*} withSAMP true if SAMP is enabled
  2141. * @returns
  2142. */
  2143. function getSingleSpectrum1D(spectroUI, sourceTable, withSAMP) {
  2144. let spectrumViewer = new SingleSpectrumViewer1D(dataPaths, 'spectrum',
  2145. Constants.PLOT_WIDTH_1D_LARGE,
  2146. Constants.PLOT_HEIGHT_RATIO_1D_LARGE,
  2147. spectroUI);
  2148. //spectrumViewer.setSpectroUI(spectroUI);
  2149. sourceTable.addListener(spectrumViewer);
  2150. spectrumViewer.plot(Math.floor(FITS_HEADER.naxis1 / 2), Math.floor(FITS_HEADER.naxis2 / 2), ()=>{
  2151. // select all the spectrum by default is velolsr is defined
  2152. // this plot all the lines
  2153. //if(FITS_HEADER.velolsr !== undefined){
  2154. let icenter = Math.round(spectrumViewer.spectrumChart.xAxis[0].series[0].processedXData.length/2);
  2155. let imin = icenter - Math.round(FITS_HEADER.naxis3 / 8);
  2156. let imax = icenter + Math.round(FITS_HEADER.naxis3 / 8);
  2157. fireChartSelectionEvent(spectrumViewer.spectrumChart,
  2158. spectrumViewer.spectrumChart.xAxis[0].series[0].processedXData[imin],
  2159. spectrumViewer.spectrumChart.xAxis[0].series[0].processedXData[imax] );
  2160. //}
  2161. });
  2162. if (withSAMP) {
  2163. // all implement setSampButtonVisible
  2164. setOnHubAvailability([spectrumViewer]);
  2165. }
  2166. return spectrumViewer;
  2167. }
  2168. export{
  2169. fireChartSelectionEvent, SingleSpectrumViewer,
  2170. SingleSpectrumViewer1D, SummedPixelsSpectrumViewer,
  2171. getSingleSpectrum1D
  2172. }