import {
shiftFrequencyByZ,
unshiftFrequencyByZ,
shiftFrequencyByV,
unshiftFrequencyByV,
v2f,
f2v,
sumArr,
unitRescale,
standardDeviation,
jyperk
} from "../utils.js";
import { LinePlotter } from '../olqv_spectro.js';
import { dataPaths, URL_ROOT, URL_3D_TO_1D, yafitsTarget, testMode } from '../init.js';
import { ServerApi } from '../serverApi.js'
import { FITS_HEADER } from '../fitsheader.js';
import { Constants } from "../constants.js";
import { sAMPPublisher, setOnHubAvailability } from "../samp_utils.js";
import { DOMAccessor } from "../domelements.js";
import {Slice} from "../olqv_slice.js";
import {
XDataCompute, FrequencyXData, VelocityXData, VeloLSRXData, WaveXData, WavnXData, AWavXData
} from "./xdata.js";
import {
YDataCompute, FrequencyYData, VelocityYData, VeloLSRYData, WaveYData, WavnYData, AWavYData
} from "./ydata.js";
import{
ChartLegend, SitelleLegend, CasaLegend, GildasLegend, MuseLegend, MiriadLegend, MeerkatLegend, NenufarLegend
} from './chartlegend.js';
/**
* Triggers Highcharts selection event on the given chart
* @param {Highcharts.chart} chart event target
* @param {number} xMin minimum selected value on xAxis
* @param {number} xMax maximum selected value on xAxis
*/
function fireChartSelectionEvent(chart, xMin, xMax){
Highcharts.fireEvent(chart, 'selection', {
xAxis: [{
min: xMin,
max: xMax
}],
});
}
/**
* Returns a formatted string containing a displayable unit name
* @param {string} unit the source unit
* @returns {string} a formatted unit
*/
function displayableBUnit(unit) {
if(unit === undefined)
return "";
else{
switch (unit) {
case "Jy/beam":
return "Jy";
case "erg/s/cm^2/A/arcsec^2":
return "erg/s/cm^2/A";
default:
return "";
}
}
}
/**
* Returns an object containing the configuration of a point when it will be displayed
* in Highcharts graph (plot type, colors, line width ...)
*
* Radius (5) and color (red) are hard coded for now
*
* @param {number} x coordinate on x axis (float)
* @param {number} y coordinate on y axis (float)
* @param {boolean} visible visibility
* @returns {object} a point object to plot in Hightcharts
*/
function getPoint(x, y, visible) {
return {
type: 'scatter',
name: '',
showInLegend: false,
visible: visible,
zIndex: 1,
enableMouseTracking: false,
marker: {
radius: 8
},
data: [{
x: x,
y: y,
color: '#BF0B23'
}]
}
}
/**
* Returns an object containing the configuration of the X axis when it will be displayed
* in Highcharts graph (plot type, colors, line width ...)
* @returns {Object} configuration of x axis for Highcharts
*/
function getXAxisConfiguration() {
let axis = {
type: 'scatter',
marker: {
color: '#1f77b4',
size: 0,
radius: 0
},
line: {
color: '#1f77b4',
width: 1
},
connectgaps: 'true',
hoverinfo: 'x+y',
xaxis: 'x'
};
return axis;
// commented for now, does not seem useful
//return JSON.parse(JSON.stringify(axis))
}
/**
* Returns an event signaling that a slice has been modified
* data packed inside the event are used in testing context
* @param {string} type type of the modified slice ( single or summed)
* @param {object} data spectrum data
* @returns Event
*/
function get3DSpectrumUpdateEvent(type, data, meta){
let event = new Event(type);
if (type === "single"){
event.freqMin = meta.spectrumValues.freqMin;
event.freqMax = meta.spectrumValues.freqMax;
event.jyperk = jyperk(FITS_HEADER.restfreq, FITS_HEADER.bmin, FITS_HEADER.bmaj);
event.iRA = meta.iRA;
event.iDEC = meta.iDEC;
if(meta.ytitle !== undefined){
let parts = meta.ytitle.split('(');
if(parts.length > 1){
event.unit = parts[1].replace(')', '');
}else{
event.unit = parts[0];
}
}
event.rmsValue = standardDeviation(data.y);
event.minValue = Math.min(...data.y);
event.maxValue = Math.max(...data.y);
}else if(type === "summed"){
if(meta.lowerIntegratedValues.flux !== undefined){
event.fluxValue = meta.lowerIntegratedValues.flux.value;
event.fluxUnit = meta.lowerIntegratedValues.flux.unit;
}
if(meta.lowerIntegratedValues.vmin !== undefined){
event.vminValue = meta.lowerIntegratedValues.vmin.value;
event.vminUnit = meta.lowerIntegratedValues.vmin.unit;
}
if(meta.lowerIntegratedValues.vmax !== undefined){
event.vmaxValue = meta.lowerIntegratedValues.vmax.value;
event.vmaxUnit = meta.lowerIntegratedValues.vmax.unit;
}
event.imin = meta.lowerIntegratedValues.imin;
event.imax = meta.lowerIntegratedValues.imax;
if(meta.ytitle !== undefined){
let parts = meta.ytitle.split('(');
if(parts.length > 1){
event.unit = parts[1].replace(')', '');
}else{
event.unit = parts[0];
}
}
event.rmsValue = standardDeviation(data.y);
event.minValue = Math.min(...data.y);
event.maxValue = Math.max(...data.y);
}else if(type === "bounds"){
event.iFreqMin = meta.iFreqMin;
event.iFreqMax = meta.iFreqMax;
/*event.freqMin = meta.freqMin;
event.freqMax = meta.freqMax;*/
event.velMin = meta.velMin;
event.velMax = meta.velMax;
}
return event;
}
/**
* Returns an event signaling that a slice has been modified
* data packed inside the event are used in testing context
* @param {object} data spectrum data
* @returns Event
*/
function get1DSpectrumUpdateEvent(data, meta){
let event = new Event("1d");
if(meta.ytitle !== undefined){
let parts = meta.ytitle.split('(');
if(parts.length > 1){
event.unit = parts[1].replace(')', '');
}else{
event.unit = parts[0];
}
}
event.rmsValue = standardDeviation(data.y);
event.minValue = meta.minVal;
event.maxValue = meta.maxVal;
event.freqMin = meta.freqMin;
event.freqMax = meta.freqMax;
event.iRA = 0;
event.iDEC = 0;
return event;
}
class SpectrumViewer {
static objectType = "SPECTRUM";
/**
* @constructor
* @param {Object} paths dataPaths object
* @param {string} containerId id of graph container
*/
constructor(paths, containerId, width, heightWidthRatio, spectroUI) {
this.spectrumChart = null;
this._containerId = containerId;
this._viewLinker = null;
this._spectroUI = spectroUI;
this.linePlotter = null;
this.spectrumLoadedlisteners = [];
this._spectrumUnit = displayableBUnit(FITS_HEADER.bunit);
this._width = width;
this._heightWidthRatio = heightWidthRatio;
this._xtitle = "undefined";
this._ytitle = "undefined";
this._xMinZoom = null;
this._xMaxZoom = null;
this._relFITSFilePath = paths.relFITSFilePath;
this._sampButton = undefined;
this._datatype = "";
this._legendObject = new ChartLegend();
this._xDataComputer = new XDataCompute()
this._yDataComputer = new YDataCompute()
this._initLegendObject();
this. _initDataComputers();
}
_initLegendObject(){
let strategy = null;
if (FITS_HEADER.isSITELLE()) {
strategy = new SitelleLegend();
} else if (FITS_HEADER.isCASA()) {
strategy = new CasaLegend();
} else if (FITS_HEADER.isGILDAS()) {
strategy = new GildasLegend();
} else if (FITS_HEADER.isMUSE()) {
strategy = new MuseLegend();
} else if (FITS_HEADER.isMIRIAD()) {
strategy = new MiriadLegend();
} else if (FITS_HEADER.isMEERKAT()) {
strategy = new MeerkatLegend();
} else if (FITS_HEADER.isNENUFAR()) {
strategy = new NenufarLegend();
} else{
throw("Strategy is unknown");
}
this._legendObject.setStrategy(strategy);
}
_initDataComputers(){
switch (FITS_HEADER.ctype3) {
case 'FREQ':
this._xDataComputer.setStrategy(new FrequencyXData());
this._yDataComputer.setStrategy(new FrequencyYData());
break;
case 'VRAD':
this._xDataComputer.setStrategy(new VelocityXData());
this._yDataComputer.setStrategy(new VelocityYData());
break;
case 'VELO-LSR':
this._xDataComputer.setStrategy(new VeloLSRXData());
this._yDataComputer.setStrategy(new VeloLSRYData());
break;
case 'WAVE':
this._xDataComputer.setStrategy(new WaveXData());
this._yDataComputer.setStrategy(new WaveYData());
break;
case 'WAVN':
this._xDataComputer.setStrategy(new WavnXData());
this._yDataComputer.setStrategy(new WavnYData());
break;
case 'AWAV':
this._xDataComputer.setStrategy(new AWavXData());
this._yDataComputer.setStrategy(new AWavYData());
break;
default:
throw("ctype3 case not recognized : " + FITS_HEADER.ctype3);
}
}
/**
* Returns Y value in spectrum data for a given X.
* Values are sorted in descending ordre in spectrumData,
* Y value is returned as soon as X > spectrumData[i]
* @param {number} x X value (float)
* @param {array} spectrumData an array of [x,y] tuples
* @returns
*/
getYValueAtX(x, spectrumData){
if(x <= spectrumData[0][0] && x >= spectrumData[spectrumData.length -1][0]){
for(let i=0; i < spectrumData.length; i++){
if(x >= spectrumData[i][0])
return spectrumData[i][1];
}
} else
throw RangeError(x + " value not found in spectrum data");
}
setSpectrumSize(width, ratio){
this.spectrumChart.setSize(width, ratio);
this._width = width;
this._heightWidthRatio = ratio;
}
addSpectrumLoadedListener(listener){
this.spectrumLoadedlisteners.push(listener);
}
removeSpectrumLoadedListener(listener){
for(let i=0; i < this.spectrumLoadedlisteners.length; i++){
if(this.spectrumLoadedlisteners[i] === listener){
this.spectrumLoadedlisteners.splice(i, 1);
}
}
}
_executeSpectrumLoadedListener(event) {
for (let l of this.spectrumLoadedlisteners) {
l.spectrumLoaded(event);
}
}
/**
* @param {ViewLinker} viewLinker ViewLinker object managing interactions between elements
*/
setViewLinker(viewLinker) {
this._viewLinker = viewLinker;
}
/**
* Returns the xaxis of the chart and its datatype
* @returns {Object} an object containing the xaxis and a datatype
*/
getSpectrumChartXAxis() {
return {
axis: this.spectrumChart.xAxis[0],
datatype: this._datatype
};
}
/**
* Called when NED table object triggers an event, refreshes lines display
*
* @param {Event} event event that triggered the call
*/
sourceTableCall(event) {
if (this.linePlotter != undefined) {
this.linePlotter.refresh();
}
}
refreshChartLegend() {
throw new Error("This method must be implemented");
}
getExportMenuItems(mode, sampConnected){
let result = ['downloadPNG'];
if(mode === "OBSPM"){
result.push('downloadFits');
}
result.push("sendTo1D");
if(sampConnected){
result.push("sendWithSamp");
}
return result;
}
/**
* Toggles samp button visibility
* @param {boolean} state status of button visibility
*/
setSampButtonVisible(state) {
let menu = { exporting: { buttons: { contextButton: { menuItems: this.getExportMenuItems(yafitsTarget, state) } } } };
//this can occur before chart is initialized
if(this.spectrumChart !== null){
this.spectrumChart.update(menu);
}
}
_showCoordinates(coords){
throw new Error("This method must be implemented");
}
/**
* Returns integral value of selected area in the spectrum
* One case for a graph in radial velocity, one for all other cases
* @param {*} yData
* @param {*} imin
* @param {*} imax
* @returns
*/
_getSelectedSpectrumValue(yData, imin, imax) {
let result = 0;
if (FITS_HEADER.ctype3 === 'VRAD') {
let copy = (x) => x;
let arraycopy = yData.map(copy);
result = sumArr(arraycopy.reverse(), imin, imax, FITS_HEADER.cdelt3prim);
} else {
result = sumArr(yData, imin, imax, FITS_HEADER.cdelt3prim);
}
return result / unitRescale(this._spectrumUnit);
}
/**
* Returns the channel corresponding to the given input value (frequency or velocity)
* @param {number} value (float)
* @returns {float}
*/
_getCalculatedIndex(value) {
let result = 0;
if (FITS_HEADER.ctype3 === 'VRAD') {
let step1 = (Constants.UNIT_FACTOR[Constants.DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]] / Constants.UNIT_FACTOR[FITS_HEADER.cunit3]) / FITS_HEADER.cdelt3;
let crval3 = FITS_HEADER.crval3 / (Constants.UNIT_FACTOR[Constants.DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]] / Constants.UNIT_FACTOR[FITS_HEADER.cunit3]);
result = (value - crval3) * step1 + FITS_HEADER.crpix3 - 1;
} else if (FITS_HEADER.ctype3 === 'FREQ') {
// if ctype is FREQ we have to read Constants.DEFAULT_OUTPUT_UNIT['VRAD']*
let vcenter = 0; //SPEED_OF_LIGHT * (FITS_HEADER.crval3 - FITS_HEADER.restfreq) / FITS_HEADER.restfreq;
let step1 = v2f(value * Constants.UNIT_FACTOR[Constants.DEFAULT_OUTPUT_UNIT['VRAD']], FITS_HEADER.restfreq, vcenter);
let step2 = (step1 - FITS_HEADER.crval3) / FITS_HEADER.cdelt3;
result = step2 + FITS_HEADER.crpix3 - 1 /*+ FITS_HEADER.naxis3 / 2*/;
}
if (FITS_HEADER.cdelt3 >= 0) {
result = FITS_HEADER.naxis3 - result - 1;
}
if(Math.round(result) < 0){
console.error("Calculated index can not be a negative value : " + result);
} else
return Math.round(result);
}
/**
* Returns x axis coordinates. They are calculated from center position and step between each value (CDELT3)
* The formula changes according to the type of data on x axis (CTYPE3)
*
* @param {number} rlen number of points on x axis (int)
* @returns {array} an array of x values
*/
_getXData(rlen) {
throw new Error("This method must be implemented");
}
/**
* Returns an array of ydata from the data passed in parameter.
* The parameter array must be reverted if CDELT3 > 0 in case of a frequency
* and if CDELT3 < 0 in case of a radial velocity
* It is rescaled in case of Sitelle data.
* It is returned unchanged in any other case
*
* @param {array} data
* @returns {array}
*/
_getYData(data) {
throw new Error("This method must be implemented");
}
}
/**
* A class displaying a spectrum, using the Highcharts library
*
* @property {Object} spectrumChart a highchart chart object
*/
class SingleSpectrumViewer extends SpectrumViewer {
/**
* @constructor
* @param {Object} paths dataPaths object
* @param {string} containerId id of graph container
*/
constructor(paths, containerId, width, heightWidthRatio, spectroUI) {
super(paths, containerId, width, heightWidthRatio, spectroUI);
this.iRA = undefined;
this._iDEC = undefined;
this._ifrequencyMarker = 0;
this.toptitle = "undefined";
this._datatype = "frequency";
this._computeSliceIndex = this._computeSliceIndex.bind(this);
this.refreshChartLegend();
}
/**
Returns index of slice to be displayed
plotData : spectrum
x : x position clicked on graph
*/
/**
* Returns index of slice to be displayed when spectrum is clicked
* @param {Object} plotData object containing arrays of x and y values of the graph
* @param {number} x x position clicked on graph (float)
* @returns
*/
_computeSliceIndex(plotData, x) {
var rlen = plotData.x.length;
switch (FITS_HEADER.ctype3) {
case 'FREQ':
if (FITS_HEADER.cdelt3 > 0) {
var forigin = plotData.x[rlen - 1];
var deltaf = plotData.x[0] - plotData.x[1];
} else {
var forigin = plotData.x[0];
var deltaf = plotData.x[1] - plotData.x[0];
}
break;
case 'VRAD':
case 'VELO-LSR':
case 'WAVE':
case 'WAVN':
case 'AWAV':
if (FITS_HEADER.cdelt3 > 0) {
var forigin = plotData.x[0];
var deltaf = plotData.x[1] - plotData.x[0];
} else {
var forigin = plotData.x[rlen - 1];
var deltaf = plotData.x[0] - plotData.x[1];
}
break;
default:
console.log("This should not happen");
}
// phys2Index
return Math.round((x - forigin) / deltaf);
}
/**
* Called when NED table object triggers an event, refreshes lines display
*
* @param {Event} event event that triggered the call
*/
sourceTableCall(event) {
this.refresh();
}
/**
* Sets the title of the graph, x and y axis
* the format of the title depends on the type o displayed data.
* currently considered are : sitelle, casa, muse, gildas, miriad
*
* An alert is displayed in any other case
*/
refreshChartLegend() {
this._legendObject.defineSpectrumLegend(this);
if(this.spectrumChart !== null){
this.spectrumChart.xAxis[0].axisTitle.attr({
text: this._xtitle
});
}
}
_showCoordinates(coords){
DOMAccessor.getSingleChartCoordinates().innerText = "@Pixel x=" + Math.round(coords[0]) + " y=" + Math.round(coords[1]);
}
/**
* Sets the index of selected frequency value on graph
* @param {number} i index of selected frequency on graph (int)
*/
setFrequencyMarker(i) {
console.log("setFrequencyMarker: entering.");
switch (FITS_HEADER.ctype3) {
case 'FREQ':
if (FITS_HEADER.cdelt3 > 0) {
this._ifrequencyMarker = FITS_HEADER.naxis3 - 1 - i;
} else {
this._ifrequencyMarker = i;
}
break;
case 'VRAD':
if (FITS_HEADER.cdelt3 > 0) {
this._ifrequencyMarker = i;
} else {
this._ifrequencyMarker = FITS_HEADER.naxis3 - 1 - i;
}
break;
// equivalent to VRAD
case 'VELO-LSR':
if (FITS_HEADER.cdelt3 > 0) {
this._ifrequencyMarker = i;
} else {
this._ifrequencyMarker = FITS_HEADER.naxis3 - 1 - i;
}
break;
case 'WAVE':
case 'WAVN':
case 'AWAV':
this._ifrequencyMarker = i;
break;
default:
console.log("This should not happen");
break;
}
console.log("setFrequencyMarker: exiting.");
}
/**
* Creates and returns a Highcharts chart
* @param {Object} plotData Data plotted in graph
* @param {string} xtitle x axis title
* @param {string} ytitle y axis title
* @returns {chart}
*/
_getChart(plotData, xtitle, ytitle) {
let self = this;
let kpj = FITS_HEADER.kelvinPerJansky();
let spectrumData = [];
for (let i = 0; i < plotData.x.length; i++) {
spectrumData.push([plotData.x[i], plotData.y[i]]);
}
let container = document.getElementById(this._containerId);
return Highcharts.chart(container, {
title: {
text: ''
},
chart: {
type: 'line',
width: self._width,
height: self._heightWidthRatio,
animation: false,
zoomType: 'xz',
panning: true,
panKey: 'shift',
responsive: {
rules: [{
condition: {
maxWidth: self._width,
maxHeight: self._heightWidthRatio
},
chartOptions: {
xAxis: {
labels: {
formatter: function () {
return this.value.charAt(0);
}
}
},
yAxis: {
labels: {
align: 'left',
x: 0,
y: -5
},
title: {
text: null
}
}
}
}]
},
events: {
render: function (event) {
// hides zoom button when it is displayed
// we only use zoom button defined in yafits UI
if (this.resetZoomButton !== undefined) {
this.resetZoomButton.hide();
}
},
load: function (event) {
// graph is loaded
DOMAccessor.showLoaderAction(false);
},
click: function (event) {
let sliceIndex = self._computeSliceIndex(plotData, event.xAxis[0].value);
// Display slice at index sliceIndex
if (self._viewLinker !== null) {
self._viewLinker.getAndPlotSingleSlice(sliceIndex);
self._viewLinker.setFluxDensityInPopup(event.yAxis[0].value, SpectrumViewer.objectType);
}
const yvalue = self.getYValueAtX(event.xAxis[0].value, spectrumData);
this.series[1].update(getPoint(event.xAxis[0].value, yvalue, true));
}
}
},
boost: {
useGPUTranslations: true,
usePreAllocated: true
},
xAxis: {
gridLineWidth: 1,
lineColor: '#FFFFFF',
title: {
text: xtitle
},
crosshair: true,
reversed: true,
maxPadding : 0,
endOnTick : false,
minPadding : 0,
startOnTick : false,
events: {
// called when boudaries of spectrum are modified
setExtremes: function (event) {
if ((event.min === undefined || event.max === undefined) && self._viewLinker !== null) {
self._viewLinker.summedPixelsSpectrumViewer.spectrumChart.xAxis[0].setExtremes(
self._viewLinker.summedPixelsSpectrumViewer.spectrumChart.xAxis[0].dataMin,
self._viewLinker.summedPixelsSpectrumViewer.spectrumChart.xAxis[0].dataMax);
} else {
let restfreq = FITS_HEADER.restfreq
if(self._spectroUI.getVelocity("m/s") === 0 || self._spectroUI.getVelocity("m/s") === undefined ){
if(self._spectroUI.getRedshift() !== undefined){
restfreq = restfreq * (1 + self._spectroUI.getRedshift());
}
}
let minval, maxval;
//velocity is undefined if a z value has been entered
if( self._spectroUI.getVelocity("m/s") === undefined ){
minval = Math.round(f2v(event.min * 1e9, restfreq, 0) / 1e3);
maxval = Math.round(f2v(event.max * 1e9, restfreq, 0) / 1e3);
}else{
minval = Math.round(f2v(event.min * 1e9, restfreq, self._spectroUI.getVelocity("m/s")) / 1e3);
maxval = Math.round(f2v(event.max * 1e9, restfreq, self._spectroUI.getVelocity("m/s")) / 1e3);
}
//exchange min/max if min > max
if (minval > maxval) {
let tmp = minval;
minval = maxval;
maxval = tmp;
}
if (self._viewLinker !== null) {
self._viewLinker.summedPixelsSpectrumViewer.spectrumChart.xAxis[0].setExtremes(minval, maxval);
}
}
},
}
},
yAxis: {
lineColor: '#FFFFFF',
gridLineWidth: 1,
lineWidth: 1,
opposite: true,
title: {
text: ytitle
},
labels: {
// returns ticks to be displayed on Y axis
formatter: function () {
let label = '';
// value already in K
if (FITS_HEADER.isSpectrumInK()) {
label = 'K';
}
// result can be NaN if _bmin/_bmaj not available
// then nothing to display, else value is converted in K
else if (!isNaN(kpj)) {
label = " <br/> " + Number(this.value * kpj).toExponential(2) + " K";
}
return Number(this.value).toExponential(2) + label;
}
}
},
plotOptions: {
series: {
cursor: 'pointer',
step: 'center',
color: Constants.PLOT_DEFAULT_COLOR,
animation: {
duration: 0
},
lineWidth: Constants.PLOT_DEFAULT_LINE_WIDTH,
events: {
click: function (event) {
//console.clear();
let sliceIndex = self._computeSliceIndex(plotData, event.point.x);
DOMAccessor.setSliceChannel("Chan#" + sliceIndex);
const yvalue = self.getYValueAtX(event.point.x, spectrumData);
this.chart.series[1].update(getPoint(event.point.x, yvalue, true));
if (this._viewLinker !== null) {
// Display slice at index sliceIndex
self._viewLinker.setFluxDensityInPopup(event.point.y, SpectrumViewer.objectType);
self._viewLinker.getAndPlotSingleSlice(sliceIndex);
}
}
}
},
marker: {
radius: 0
}
},
exporting: {
menuItemDefinitions: {
// Custom definition
downloadFits: {
onclick: function () {
window.open(URL_ROOT + dataPaths.spectrum, '_blank');
},
text: 'Download FITS file'
},
sendWithSamp: {
onclick: function (event) {
sAMPPublisher.sendSpectrumToAll(URL_ROOT + dataPaths.spectrum, "Artemix");
event.stopPropagation();
},
text: 'Send with SAMP'
},
sendTo1D: {
onclick: function (event) {
window.open(URL_3D_TO_1D + dataPaths.spectrum, '_blank');
},
text: 'Open in 1D viewer'
}
},
buttons: {
contextButton: {
menuItems: self.getExportMenuItems(yafitsTarget, false)
}
}
},
tooltip: {
// displayed when the mouse if above the graph
formatter: function () {
// get channel number
let sliceIndex = self._computeSliceIndex(plotData, this.x);
let label = '( ' + this.x.toFixed(4) + ', ' + this.y.toFixed(4) + ') ';
if (!isNaN(kpj) && !FITS_HEADER.isSpectrumInK()) {
label = label + ", " + Number(this.y * kpj).toExponential(2) + " K";
}
return " Chan#" + sliceIndex + " " + label;
}
},
series: [{
// unlimited number of points when zooming
cropThreshold: Infinity,
showInLegend: false,
data: spectrumData,
zIndex: 0,
marker: {
radius: 0
},
},
//series of frequency markers, must not be empty to create it, this point is hidden
getPoint(0, 0, false)],
});
}
/**
* Returns x axis coordinates. They are calculated from center position and step between each value (CDELT3)
* The formula changes according to the type of data on x axis (CTYPE3)
*
* @param {number} rlen number of points on x axis (int)
* @returns {array} an array of x values
*/
_getXData(rlen) {
return this._xDataComputer.computeSpectrum(rlen, this._spectroUI.getVelocity("m/s"), this._spectroUI.getRedshift());
}
/**
* Returns an array of ydata from the data passed in parameter.
* The parameter array must be reverted if CDELT3 > 0 in case of a frequency
* and if CDELT3 < 0 in case of a radial velocity
* It is rescaled in case of Sitelle data.
* It is returned unchanged in any other case
*
* @param {array} data
* @returns {array}
*/
_getYData(data) {
let result = this._yDataComputer.computeSpectrum(data);
if (FITS_HEADER.isSITELLE()) {
if (FITS_HEADER.cdelt1 && FITS_HEADER.cdelt2) {
let pixel2arcsec = Math.abs(3600 * 3600 * FITS_HEADER.cdelt1 * FITS_HEADER.cdelt2);
let temparr = result.map(function (x) {
return x * unitRescale(FITS_HEADER.bunit) / pixel2arcsec
});
result = temparr;
}
}
return result;
}
/**
* Calls createFits function of server to create a fits file corresponding to iRa/iDec
* Path of created file is stored in dataPaths.spectrum
*
* createFits parameters are relFITSFilePath, iRA, iDEC
*
* @param {number} iRA index of selected RA value (int)
* @param {number} iDEC index of selected DEC value (int)
*/
_createFitsFile(iRA, iDEC) {
if (FITS_HEADER.ctype3 === "FREQ" || FITS_HEADER.ctype3 === "VRAD") {
let apiQuery = new ServerApi();
apiQuery.createFitsFile(iRA, iDEC, this._relFITSFilePath, (resp)=>{
let response = JSON.parse(resp);
dataPaths.spectrum = response.result;
});
}
}
refresh(){
this.plot(this._iRA, this._iDEC, undefined);
this.refreshChartLegend();
}
/**
* Calls the getSpectrum function of the server to get the spectrum data and plot them
*
* getSpectrum parameters are : relFITSFilePath, iRA, iDEC, iFREQ0, iFREQ1
*
* Here we want data for all frequencies so iFREQ0 = 0 and iFREQ1 = NAXIS3 - 1
* if iRA or iDEC are undefined, we use a centered value NAXIS1 / 2 and NAXIS2 / 2 respectively
*
* @param {number} iRA
* @param {number} iDEC
* @param {callbackFucntion} cb a function called when data have been returned from server
*/
plot(iRA, iDEC, cb) {
this._iRA = iRA;
this._iDEC = iDEC;
let self = this;
if (typeof iRA === 'undefined') {
iRA = Math.floor(FITS_HEADER.naxis1 / 2);
}
if (typeof iDEC === 'undefined') {
iDEC = Math.floor(FITS_HEADER.naxis2 / 2);
}
let queryApi = new ServerApi();
queryApi.getSingleSpectrum(iRA, iDEC, this._relFITSFilePath,(resp)=>{
let plotData = getXAxisConfiguration();
let jsonTextContent = JSON.stringify(resp);
let data;
// NaN values are replaced by null if they exist
if(jsonTextContent.includes("NaN")){
let newResp = JSON.stringify(resp).replace(/\bNaN\b/g, "null");
let test = JSON.parse(newResp);
data = JSON.parse(test.data);
}else{
data = resp.data;
}
plotData.x = self._getXData(data.result.length); // abscissa ( frequency, wavelength, velocity, ...);
plotData.xaxis = "x";
plotData.y = self._getYData(data.result);
self.spectrumChart = self._getChart(plotData, self._xtitle, self._ytitle);
// pixel coordinates above chart
self._showCoordinates([iRA, iDEC]);
self._createFitsFile(iRA, iDEC);
if(testMode){
let meta = {iRA : iRA,
iDEC : iDEC,
ytitle : self._ytitle,
spectrumValues : {
freqMin : Math.min(...plotData.x),
freqMax : Math.max(...plotData.x),
freqcenter : plotData.x[Math.floor(FITS_HEADER.naxis3 / 2)],
}
};
self._executeSpectrumLoadedListener(get3DSpectrumUpdateEvent("single",plotData, meta));
}
// callback function called at the end of loading process
// typical use is restore previous graph limits
if (cb !== undefined) {
cb();
}
if (self._viewLinker !== null) {
// set value of flux density in popup, should me moved out of this function
self._viewLinker.setFluxDensityInPopup(plotData.y[self._ifrequencyMarker], Slice.objectType);
if (self._viewLinker.summedPixelsSpectrumViewer.linePlotter !== null && self._viewLinker.spectroUI.getSelectedDatabase() !== "off") {
self._viewLinker.summedPixelsSpectrumViewer.linePlotter.loadAndPlotLines(self._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMin,
self._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMax,
[self._viewLinker.summedPixelsSpectrumViewer.getSpectrumChartXAxis(), self.getSpectrumChartXAxis()]);
}
}
});
};
}
/**
* A class displaying a spectrum, using the Highcharts library
*
* @property {Object} spectrumChart a highchart chart object
*/
class SingleSpectrumViewer1D extends SingleSpectrumViewer {
constructor(paths, containerId, width, heightWidthRatio, spectroUI) {
super(paths, containerId, width, heightWidthRatio, spectroUI);
this.detailChart;
this._detailData = [];
// min x value in detail chart
this._xDetailMin = null;
// max x value in detail chart
this._xDetailMax = null;
this._shiftMode = null;
//xMin for manual user selection, redshift is applied
this._xSelectionMin = null;
//xMax for manual user selection, redshift is applied
this._xSelectionMax = null;
this.plottedData;
// draw lines on master chart
this.linePlotter = null;
// draw lines on detailed chart
this.detailLinePlotter = null;
this._masterYData = [];
// reference to last lines drawer called
// this is used by the navigation buttons in
// the web UI
this.activePlotter = null;
//this.initialized = false;
this.isRefreshable = true;
this._isInit = true;
}
/**
* Resets zoom of detail chart to its initial value by triggering a selection event on the chart
* with this._xDetailMin and this._xDetailMax values
* Master chart is fixed and can not be zoomed
*/
resetZoom(){
Highcharts.fireEvent(this.detailChart, 'selection', {
xAxis: [{
min: this._xDetailMin,
max: this._xDetailMax
}],
});
}
/**
* Called when NED table object triggers an event, refreshes lines display
*
* @param {Event} event event that triggered the call
*/
sourceTableCall(event) {
this._shiftMode = "z";
this.refresh("z");
}
/**
* Returns data in the min/max interval from this._detailData
* @param {number} min minimum value in interval (float)
* @param {number} max maximum value in interval (float)
* @returns array
*/
_getIntervalData(dataArray, min, max){
let result = [];
dataArray.forEach(point => {
if (point[0] > min && point[0] < max) {
result.push([point[0], point[1]]);
}
});
return result;
}
setDetailedSpectrumSize(width, ratio){
this.detailChart.setSize(width, ratio);
this._width = width;
this._heightWidthRatio = ratio;
}
/**
* Creates the detailed chart zooming content from master chart
* @param {string} xtitle X axis title
* @param {string} ytitle Y axis title
*/
_getDetailChart(srcData){
let self = this;
let kpj = FITS_HEADER.kelvinPerJansky();
this.detailChart = Highcharts.chart('detail-container', {
chart: {
type : "line",
animation : false,
width: self._width,
height: self._heightWidthRatio,
zoomType: 'x',
panning: true,
panKey: 'shift',
/*responsive: {
rules: [{
condition: {
maxWidth: self._width,
maxHeight: self._heightWidthRatio
},
chartOptions: {
xAxis: {
labels: {
formatter: function () {
return this.value.charAt(0);
}
}
},
yAxis: {
labels: {
align: 'left',
x: 0,
y: -5
},
title: {
text: null
}
}
}
}]
},*/
events : {
selection: function (event) {
if(self.isRefreshable){
var extremesObject = event.xAxis[0],
min = extremesObject.min,
max = extremesObject.max;
this.xAxis[0].setExtremes(min, max);
self.refreshMasterBands(min, max);
// spectral lines
self.detailChartLines();
self.activePlotter = self.detailLinePlotter;
}else{
alert("Display can not be refreshed. Velocity or redshift value has changed. Please press enter in this field to validate.");
}
return false;
},
pan : function(event){
self.refreshMasterBands(event.target.xAxis[0].min, event.target.xAxis[0].max);
}
},
},
credits: {
enabled: false
},
title: {
text: '',
},
tooltip: {
// displayed when the mouse if above the graph
formatter: function () {
// get channel number
let sliceIndex = self._computeSliceIndex(srcData, this.x);
let label = '( ' + this.x.toFixed(4) + ', ' + this.y.toFixed(4) + ') ';
return " Chan#" + sliceIndex + " " + label;
}
},
xAxis: {
gridLineWidth: 1,
type : "line",
lineColor: '#FFFFFF',
title: {
text: self._xtitle
},
crosshair: true,
reversed: false,
maxPadding : 0,
endOnTick : false,
minPadding : 0,
startOnTick : false
},
yAxis: {
lineColor: '#FFFFFF',
gridLineWidth: 1,
lineWidth: 1,
opposite: true,
title: {
text: self._ytitle
},
labels: {
// returns ticks to be displayed on Y axis
formatter: function () {
let label = '';
// value already in K
if (FITS_HEADER.isSpectrumInK()) {
label = 'K';
}
// result can be NaN if _bmin/_bmaj not available
// then nothing to display, else value is converted in K
else if (!isNaN(kpj)) {
label = " <br/> " + Number(this.value * kpj).toExponential(2) + " K";
}
return Number(this.value).toExponential(2) + label;
}
}
},
legend: {
enabled: false
},
exporting: {
menuItemDefinitions: {
},
buttons: {
contextButton: {
menuItems: ['downloadPNG']
}
}
},
plotOptions: {
series: {
cursor: 'pointer',
step: 'center',
color: Constants.PLOT_DEFAULT_COLOR,
lineWidth: Constants.PLOT_DEFAULT_LINE_WIDTH,
animation: {
duration: 0
}
},
marker: {
radius: 0
}
},
series: [{
marker: {
radius: 0
}
}],
});
}
/**
* Shows the selected interval on the master graph.
* Area is coloured between [xAxis[0], xMin] and [xMax, xAxis[xAxis.length -1]]
* @param {number} xMin minimum value seelcted by user on X axis
* @param {number} xMax maximum value seelcted by user on X axis
*/
refreshMasterBands(xMin, xMax){
if(this.spectrumChart !== null){
if(this._shiftMode === 'z'){
this._xSelectionMin = unshiftFrequencyByZ(xMin, this._spectroUI.getRedshift());
this._xSelectionMax = unshiftFrequencyByZ(xMax, this._spectroUI.getRedshift());
}else if(this._shiftMode === 'v'){
this._xSelectionMin = unshiftFrequencyByV(xMin, this._spectroUI.getVelocity());
this._xSelectionMax = unshiftFrequencyByV(xMax, this._spectroUI.getVelocity());
}
// move the plot bands to reflect the new detail span
this.spectrumChart.xAxis[0].removePlotBand('mask');
this.spectrumChart.xAxis[0].addPlotBand({
id: 'mask',
from: xMin,
to: xMax,
color: 'rgba(51, 153, 255, 0.2)'
});
}
}
/**
* Returns the xaxis of the chart and its datatype
* @returns {Object} an object containing the xaxis and a datatype
*/
getDetailedSpectrumChartXAxis() {
return {
axis: this.detailChart.xAxis[0],
datatype: this._datatype
};
}
_getAxes(){
return [{
axis: this.spectrumChart.xAxis[0],
datatype: this._datatype
},{
axis: this.detailChart.xAxis[0],
datatype: this._datatype
}
];
}
masterChartLines(){
// if not disabled
if (this._spectroUI.getSelectedDatabase() !== "off") {
if (this.linePlotter === null) {
this.linePlotter = new LinePlotter(this._spectroUI);
}
const graphs = this._getAxes();
try {
this.linePlotter.loadAndPlotLines(this._xDetailMin,
this._xDetailMax,
graphs);
} catch (e) {
console.log(e);
alert("Lines can not be displayed, please verify that redshift and/or velocity is defined.");
}
}
//}
}
detailChartLines(){
// spectral lines
//if (this._spectroUI.isEnabled()) {
if (this.detailLinePlotter === null) {
this.detailLinePlotter = new LinePlotter(this._spectroUI);
}
const graphs = this._getAxes();
try {
this.detailLinePlotter.loadAndPlotLines(this.detailChart.xAxis[0].getExtremes().min,
this.detailChart.xAxis[0].getExtremes().max,
graphs);
} catch (e) {
console.log(e);
alert("Lines can not be displayed, please verify that redshift and/or velocity is defined.");
}
//}
}
/**
* Creates and returns a Highcharts chart
* @param {Object} spectrumData Data plotted in graph
* @param {string} xtitle x axis title
* @param {string} ytitle y axis title
* @returns {chart}
*/
_getChart(srcData, spectrumData) {
let self = this;
let kpj = FITS_HEADER.kelvinPerJansky();
let result = Highcharts.chart("master-container", {
title: {
text: ''
},
chart: {
type: 'line',
width: self._width,
height: self._heightWidthRatio,
animation: false,
zoomType: 'x',
panning: true,
panKey: 'shift',
/*responsive: {
rules: [{
condition: {
maxWidth: self._width,
maxHeight: self._heightWidthRatio
},
chartOptions: {
xAxis: {
labels: {
formatter: function () {
return this.value.charAt(0);
}
}
},
yAxis: {
labels: {
align: 'left',
x: 0,
y: -5
},
title: {
text: null
}
}
}
}]
},*/
events: {
load: function (event) {
// graph is loaded
//result.series[0].setVisible(false);
let meta = {
yTitle: self._ytitle,
freqMin : this.xAxis[0].min,
freqMax : this.xAxis[0].max,
minVal : this.series[0].dataMin,
maxVal : this.series[0].dataMax,
}
let t = get1DSpectrumUpdateEvent(srcData, meta);
self._executeSpectrumLoadedListener(get1DSpectrumUpdateEvent(srcData, meta));
//DOMAccessor.showLoaderAction(true);
DOMAccessor.markLoadingDone();
},
selection: function (event) {
if(self.isRefreshable) {
var extremesObject = event.xAxis[0],
min = extremesObject.min,
max = extremesObject.max,
detailData = self._getIntervalData(spectrumData, min, max);
// show selected interval
self.refreshMasterBands(min ,max);
// set data on detailed chart and refresh x axis limits
self.detailChart.series[0].setData(detailData, true, false, false);
self.detailChart.xAxis[0].setExtremes(min, max);
// save global selection
self._detailData = detailData;
self._xDetailMin = min;
self._xDetailMax = max;
self._xSelectionMin = min;
self._xSelectionMax = max;
// spectral lines
self.masterChartLines();
self.activePlotter = self.linePlotter;
}else{
alert("Display can not be refreshed. Velocity or redshift value has changed. Please press enter in this field to validate.");
}
return false;
}
}
},
boost: {
useGPUTranslations: true,
usePreAllocated: true
},
xAxis: {
gridLineWidth: 1,
lineColor: '#FFFFFF',
title: {
text: self._xtitle
},
crosshair: true,
reversed: false,
maxPadding : 0,
endOnTick : false,
minPadding : 0,
startOnTick : false
},
yAxis: {
lineColor: '#FFFFFF',
gridLineWidth: 1,
lineWidth: 1,
opposite: true,
title: {
text: self._ytitle
},
labels: {
// returns ticks to be displayed on Y axis
formatter: function () {
let label = '';
// value already in K
if (FITS_HEADER.isSpectrumInK()) {
label = 'K';
}
// result can be NaN if _bmin/_bmaj not available
// then nothing to display, else value is converted in K
else if (!isNaN(kpj)) {
label = " <br/> " + Number(this.value * kpj).toExponential(2) + " K";
}
return Number(this.value).toExponential(2) + label;
}
}
},
plotOptions: {
series: {
cursor: 'pointer',
step: 'center',
color: Constants.PLOT_DEFAULT_COLOR,
animation: {
duration: 0
},
lineWidth: Constants.PLOT_DEFAULT_LINE_WIDTH,
},
marker: {
radius: 0
}
},
tooltip: {
// displayed when the mouse if above the graph
formatter: function () {
// get channel number
let sliceIndex = self._computeSliceIndex(srcData, this.x);
let label = '( ' + this.x.toFixed(4) + ', ' + this.y.toFixed(4) + ') ';
if (!isNaN(kpj) && !FITS_HEADER.isSpectrumInK()) {
label = label + ", " + Number(this.y * kpj).toExponential(2) + " K";
}
return " Chan#" + sliceIndex + " " + label;
}
},
exporting: {
menuItemDefinitions: {
// Custom definition
downloadFits: {
onclick: function () {
window.open(URL_ROOT + dataPaths.spectrum, '_blank');
},
text: 'Download FITS file'
},
sendWithSamp: {
onclick: function (event) {
sAMPPublisher.sendSpectrumToAll(URL_ROOT + dataPaths.spectrum, "Artemix");
event.stopPropagation();
},
text: 'Send with SAMP'
},
},
buttons: {
contextButton: {
menuItems: self.getExportMenuItems(yafitsTarget, false)
}
}
},
series: [{
// unlimited number of points when zooming
cropThreshold: Infinity,
showInLegend: false,
data: self.plottedData,
zIndex: 0,
marker: {
radius: 0
}
},
//series of frequency markers, must not be empty to create it, this point is hidden
getPoint(0, 0, false)],
}, function (chart) {
// this is called when spectrum is exported as an image
// the detailed chart must not be recreated then
//if(!self.initialized){
self._getDetailChart(srcData);
//}
//self.initialized = true;
});
return result;
}
/**
* Refresh both charts display
*/
refresh(mode = null){
this._shiftMode = mode;
// set data on detailed chart and refresh x axis limits
this.plot(this._iRA, this._iDEC, ()=>{
let xMin = this._xSelectionMin;
let xMax = this._xSelectionMax;
this._xDetailMin = xMin;
this._xDetailMax = xMax;
if(this._shiftMode === "z"){
xMin = shiftFrequencyByZ(this._xSelectionMin, this._spectroUI.getRedshift());
xMax = shiftFrequencyByZ(this._xSelectionMax, this._spectroUI.getRedshift());
}else if(this._shiftMode === "v"){
xMin = shiftFrequencyByV(this._xSelectionMin, this._spectroUI.getVelocity());
xMax = shiftFrequencyByV(this._xSelectionMax, this._spectroUI.getVelocity());
}
this.refreshChartLegend();
this.detailChart.series[0].setData(this.plottedData, true, false, false);
this.detailChart.xAxis[0].setExtremes(xMin, xMax);
//this.detailChart.xAxis[0].setExtremes(this.detailChart.xAxis[0].getExtremes().min, this.detailChart.xAxis[0].getExtremes().max);
if (this.linePlotter !== null) {
this.linePlotter.setTargets(this._getAxes());
this.masterChartLines();
this.detailChartLines();
}
this.detailChart.xAxis[0].axisTitle.attr({
text: this._xtitle
});
});
}
/**
* Calls the getSpectrum function of the server to get the spectrum data and plot them
*
* getSpectrum parameters are : relFITSFilePath, iRA, iDEC, iFREQ0, iFREQ1
*
* Here we want data for all frequencies so iFREQ0 = 0 and iFREQ1 = NAXIS3 - 1
* if iRA or iDEC are undefined, we use a centered value NAXIS1 / 2 and NAXIS2 / 2 respectively
*
* @param {number} iRA
* @param {number} iDEC
* @param {callbackFucntion} cb a function called when data have been returned from server
*/
plot(iRA, iDEC, cb) {
let self = this;
this._iRA = iRA;
this._iDEC = iDEC;
if (typeof iRA === 'undefined') {
iRA = Math.floor(FITS_HEADER.naxis1 / 2);
}
if (typeof iDEC === 'undefined') {
iDEC = Math.floor(FITS_HEADER.naxis2 / 2);
}
this.refreshChartLegend();
DOMAccessor.showLoaderAction(true);
let qp = new ServerApi();
qp.getSingleSpectrum(iRA, iDEC, this._relFITSFilePath,(resp)=>{
let content = resp;
let test = JSON.stringify(resp);
let data;
// fix for cubes containing NaN
if(test.includes("NaN")){
let newResp = test.replace(/\bNaN\b/g, "null");
content = JSON.parse(newResp);
//for some reason the data element is still
//a string after data have been parsed above
data = JSON.parse(content.data);
}else{
data = resp.data;
}
let plotData = getXAxisConfiguration();
// in a 1D spectra, getXData will always return data sorted in reverse order
plotData.x = self._getXData(data["result"].length).reverse();
plotData.xaxis = "x";
plotData.y = self._getYData(data["result"]).reverse();
self._masterYData = plotData.y;
let spectrumData = [];
for (let i = 0; i < plotData.x.length; i++) {
spectrumData.push([plotData.x[i], plotData.y[i]]);
}
this.plottedData = spectrumData;
self.spectrumChart = self._getChart(plotData, spectrumData);
if(self._xSelectionMin !== null){
let xMin = this._xSelectionMin;
let xMax= this._xSelectionMax;
if(this._shiftMode === "z"){
xMin = shiftFrequencyByZ(this._xSelectionMin, this._spectroUI.getRedshift());
xMax = shiftFrequencyByZ(this._xSelectionMax, this._spectroUI.getRedshift());
}else if(this._shiftMode === "v"){
xMin = shiftFrequencyByV(this._xSelectionMin, this._spectroUI.getVelocity());
xMax = shiftFrequencyByV(this._xSelectionMax, this._spectroUI.getVelocity());
}
self.refreshMasterBands(xMin, xMax);
}
self._createFitsFile(iRA, iDEC);
if(testMode && self._isInit){
self._isInit = false;
}
// callback function called at the end of loading process
// typical use is restore previous graph limits
if (cb !== undefined) {
cb();
}
DOMAccessor.showLoaderAction(false);
});
};
}
/**
* A class displaying an averaged spectrum, using the Highcharts library
* Initial interval selection is defined in _getInitialSelectionRange
*
* @property {Object} summedPixelsSpectrumChart a highchart chart object
* @property {LinePlotter} linePlotter an object drawing spectral lines on the chart
*/
class SummedPixelsSpectrumViewer extends SpectrumViewer {
/**
*
* @param {*} paths
* @param {*} containerId
* @param {*} viewLinker
*/
constructor(paths, containerId, width, heightWidthRatio, spectroUI) {
super(paths, containerId, width, heightWidthRatio, spectroUI);
this._iRA0 = null;
this._iRA1 = null;
this._iDEC0 = null;
this._iDEC1 = null;
this._toptitle_unit = "";
this._flux_unit = "";
this._averageSpectrum = null;
this._vmin = null;
this._vmax = null;
this._isInit = true;
// Interval of values selected by default of the graph
// Those are the indexes of the values to select not values themselves
// If they are not null they will bypass the values coming from the selection event
this.defaultIndexMax = null;
const freqs = this._getInitialSelectionRange();
this.defaultIndexMin = freqs[0];
this.defaultIndexMax = freqs[1];
this._summedData = {
x: [],
};
this._datatype = "velocity";
// surface under the chart in area selected by user, Jy/km/s
//this._selectedSurface = 0;
this.refreshChartLegend();
}
get vmin() {
return this._vmin;
}
get vmax() {
return this._vmax;
}
set vmin(vmin) {
this._vmin = vmin;
}
set vmax(vmax) {
this._vmax = vmax;
}
get summedData() {
return this._summedData;
}
/**
* Initial frequency range selected on the spectrum
* @returns array
*/
_getInitialSelectionRange() {
const naxis3Index = FITS_HEADER.naxis3 -1;
const iFREQ0 = Math.round(naxis3Index / 2 - naxis3Index / 8);
const iFREQ1 = Math.round(naxis3Index / 2 + naxis3Index / 8);
/*const iFREQ0 = 0;
const iFREQ1 = naxis3Index;*/
return [iFREQ0, iFREQ1];
}
_showCoordinates(coords){
DOMAccessor.getSummedChartCoordinates().innerText = "@Box xmin=" + Math.round(coords[0]) +
" xmax=" + Math.round(coords[1]) +
" ymin=" + Math.round(coords[2]) +
" ymax=" + Math.round(coords[3]);
}
/**
* Sets title of spectrum, x and y axis
*
* Title of the spectrum depends on its type (Sitelle, Casa, Gildas, Muse)
*/
refreshChartLegend() {
// prefix and suffix for GILDAS and ALMA title
this._legendObject.defineSummedSpectrumLegend(this);
if(this.spectrumChart !== null){
this.spectrumChart.xAxis[0].axisTitle.attr({
text: this._xtitle
});
}
}
/**
* Returns the title displayed above the graph
* @param {number} value integral of selected interval on the graph (float)
* @param {string} unit unit of integral value
* @param {number} vmin minimum selected velocity value (float)
* @param {number} vmax maximum selected velocity value (float)
* @param {number} imin minimum selected channel index (int)
* @param {number} imax maximum selected channel index (int)
* @returns {string}
*/
_getTopTitle(value, unit, vmin, vmax, imin, imax) {
// do not display unit (Jy.km/s) if bmin/bmax are not defined
// because it has no meaning in that case
let result_unit = "";
if (FITS_HEADER.bmin !== undefined) {
result_unit = unit;
}
return '<span id="selected-surface">' + value.toExponential(2) + "</span> " +
result_unit + ", vmin=" +
vmin.toFixed(2) + " " + "km/s" + " , vmax=" +
vmax.toFixed(2) + " " + "km/s, imin=" + imin + ", imax=" + imax;
}
/**
* Sets the title diplayed above the graph
* @param {string} title
*/
setChartTitle(title) {
DOMAccessor.getAverageSpectrumTitle().innerHTML = title;
}
refresh(){
this.plot(this._iRA0, this._iRA1, this._iDEC0, this._iDEC1, undefined);
this.refreshChartLegend();
}
/**
* Updates the displayed averaged slice image, with respect to selected interval in graph
* Note : this function should be removed from this class and place in ViewLinker
*
* @param {number} min minimum selected value (float)
* @param {number} max maximum selected value (float)
*/
_updateSummedSlices(min, max) {
if (this._viewLinker !== null) {
const imin = Math.round((min - this._summedData.x[0]) / (this._summedData.x[1] - this._summedData.x[0]));
const imax = Math.round((max - this._summedData.x[0]) / (this._summedData.x[1] - this._summedData.x[0]));
if (FITS_HEADER.cunit3 in Constants.UNIT_FACTOR) {
switch (FITS_HEADER.ctype3) {
case 'FREQ':
if (FITS_HEADER.cdelt3 > 0) {
this._viewLinker.getAndPlotSummedSlices(this._summedData.x.length - 1 - imax, this._summedData.x.length - 1 - imin);
} else {
this._viewLinker.getAndPlotSummedSlices(imin, imax);
}
break;
case 'VRAD':
if (FITS_HEADER.cdelt3 > 0) {
this._viewLinker.getAndPlotSummedSlices(imin, imax);
} else {
this._viewLinker.getAndPlotSummedSlices(this._summedData.x.length - 1 - imax, this._summedData.x.length - 1 - imin);
}
break;
//equivalent to VRAD
case 'VELO-LSR':
if (FITS_HEADER.cdelt3 > 0) {
this._viewLinker.getAndPlotSummedSlices(imin, imax);
} else {
this._viewLinker.getAndPlotSummedSlices(this._summedData.x.length - 1 - imax, this._summedData.x.length - 1 - imin);
}
break;
case 'WAVE':
break
case 'WAVN':
break
case 'AWAV':
if (FITS_HEADER.cdelt3 > 0) {
this._viewLinker.getAndPlotSummedSlices(imin, imax);
} else {
this._viewLinker.getAndPlotSummedSlices(this._summedData.x.length - 1 - imax, this._summedData.x.length - 1 - imin);
}
break;
default:
alert('Unknown value for ctype3');
console.log("This should not happen");
}
}
}
}
/**
* Returns a Highchart chart
* @returns {chart}
*/
_getChart() {
let self = this;
let target = document.getElementById(this._containerId);
return Highcharts.chart(target, {
title: {
text: ''
},
chart: {
type: 'line',
animation: false,
width: self._width,
height: self._heightWidthRatio,
zoomType: 'x',
panning: true,
panKey: 'shift',
//marginLeft: 0, // Set the left margin to 0
// marginRight: 0, // Set the right margin to 0
responsive: {
rules: [{
condition: {
maxWidth: self._width,
maxHeight: self._heightWidthRatio
},
chartOptions: {
xAxis: {
labels: {
formatter: function () {
return this.value.charAt(0);
}
}
},
yAxis: {
labels: {
align: 'left',
x: 0,
y: -5
},
title: {
text: null
}
}
}
}]
},
events: {
click: function (event) {
console.log("A click occurred on the spectrum : enter");
},
redraw: function(event){
// make sure chart title are always up to date
self.refreshChartLegend();
},
selection: function (event) {
if(self._viewLinker.isRefreshable){
let velMin, velMax = null;
// if true, refresh displayed summmed slice after the selection
let refreshSlice = true;
// if default values exists for the interval use them then they are reseted
if (self.defaultIndexMin !== null && self.defaultIndexMax !== null) {
velMin = self.spectrumChart.series[0].xData[self.defaultIndexMin];
velMax = self.spectrumChart.series[0].xData[self.defaultIndexMax];
// slice is not refreshed
refreshSlice = false;
self.defaultIndexMin = null;
self.defaultIndexMax = null;
}
// values selected by the user
else {
velMin = event.xAxis[0].min;
velMax = event.xAxis[0].max;
}
// overplot selected area in blue
this.xAxis[0].update({
plotBands: [{
from: velMin,
to: velMax,
color: 'rgba(68, 170, 213, .2)'
}]
});
self._vmin = velMin;
self._vmax = velMax;
//toggle-lines-search
if (self._viewLinker !== null) {
self._spectroUI.hideEnergyGroupLines();
if (self.linePlotter === null) {
self.linePlotter = new LinePlotter(self._spectroUI);
}
}
// refreshes summed slice display according to selection
if(refreshSlice === true){
self._updateSummedSlices(velMin, velMax);
}
let ivalues = [self._getCalculatedIndex(velMin), self._getCalculatedIndex(velMax)].sort();
const imin = ivalues[0];
const imax = ivalues[1];
let selectedSurface = 0;
if (self._viewLinker !== null) {
selectedSurface = self._getSelectedSpectrumValue(self._averageSpectrum, imin, imax);
self._spectroUI.setSelectedSurface(selectedSurface);
}
self.setChartTitle(self._getTopTitle(selectedSurface, self._toptitle_unit,
self._vmin, self._vmax, imin, imax));
let factor = 1;
if(self._spectroUI.getVelocity("m/s") === 0 || self._spectroUI.getVelocity("m/s") === undefined ){
factor = 1 + self._spectroUI.getRedshift();
}
const obsFreqMin = v2f(velMax * 10 ** 3, FITS_HEADER.restfreq * factor, self._spectroUI.getVelocity("m/s")) / 10 ** 9;
const obsFreqMax = v2f(velMin * 10 ** 3, FITS_HEADER.restfreq * factor, self._spectroUI.getVelocity("m/s")) / 10 ** 9;
if (self._viewLinker !== null && self._viewLinker.spectroUI.getSelectedDatabase() !== "off") {
const graphs = [self._viewLinker.spectrumViewer.getSpectrumChartXAxis(),
self.getSpectrumChartXAxis()
];
self._viewLinker.updateSummedSlicesFreqIndexes(imin, imax);
if((self._isInit && FITS_HEADER.velolsr != undefined) || (!self._isInit)){
try {
self.linePlotter.loadAndPlotLines(obsFreqMin,
obsFreqMax,
graphs);
} catch (e) {
console.log(e);
alert("Lines can not be displayed, please verify that redshift and/or velocity is defined.");
}
}
}
if(testMode && self._isInit){
let meta = {
iFreqMin : imin,
iFreqMax : imax,
freqMin: obsFreqMin,
freqMax: obsFreqMax,
velMin : velMin,
velMax : velMax
};
//this._meta = meta;
self._executeSpectrumLoadedListener(
get3DSpectrumUpdateEvent("bounds",self._summedData, meta));
DOMAccessor.markLoadingDone();
}
self._isInit = false;
}else{
alert("Display can not be refreshed. Velocity or redshift value has changed. Please press enter in this field to validate.");
}
return false;
}
}
},
boost: {
useGPUTranslations: true
},
xAxis: {
title: {
text: self._xtitle
},
crosshair: true,
reversed: false,
gridLineWidth: 1,
lineColor: '#FFFFFF',
maxPadding : 0,
endOnTick : false,
minPadding : 0,
startOnTick : false
},
yAxis: {
gridLineWidth: 1,
lineColor: '#FFFFFF',
lineWidth: 1,
opposite: true,
title: {
text: self._ytitle
},
labels:{
formatter: function(){
return Number(this.value).toExponential(2);
}
}
},
plotOptions: {
series: {
step: 'center',
zoneAxis: 'x',
animation: {
duration: 0
},
lineWidth: Constants.PLOT_DEFAULT_LINE_WIDTH,
events: {
click: function (event) {
console.log("A click occurred on the LINE : enter");
}
}
},
marker: {
radius: 0
}
},
exporting: {
menuItemDefinitions: {
// Custom definition
downloadFits: {
onclick: function () {
window.open(URL_ROOT + dataPaths.averageSpectrum, '_blank');
},
text: 'Download FITS file'
},
sendWithSamp: {
onclick: function (event) {
sAMPPublisher.sendSpectrumToAll(URL_ROOT + dataPaths.averageSpectrum, "Artemix");
event.stopPropagation();
},
text: 'Send with SAMP'
},
sendTo1D: {
onclick: function (event) {
window.open(URL_3D_TO_1D + dataPaths.averageSpectrum, '_blank');
},
text: 'Open in 1D viewer'
}
},
buttons: {
contextButton: {
menuItems: self.getExportMenuItems(yafitsTarget, false)
}
}
},
tooltip: {
formatter: function () {
const index = self._getCalculatedIndex(this.x);
return 'Chan# ' + index + ' ( ' + this.x.toFixed(4) + ', ' + this.y.toFixed(4) + ')';
}
},
series: [{
color: Constants.PLOT_DEFAULT_COLOR
}]
});
}
/**
* Returns x axis coordinates. They are calculated from center position and step between each value (CDELT3)
* The formula changes according to the type of data on x axis (CTYPE3)
*
* @param {number} rlen number of points on x axis (int)
* @returns {array} an array of x values
*/
_getXData(rlen) {
return this._xDataComputer.computeSummedSpectrum(rlen, this._spectroUI.getVelocity("m/s"));
}
/**
* Returns an array of ydata from the data passed in parameter.
* The parameter array must be reverted if CDELT3 > 0 in case of a frequency
* and if CDELT3 < 0 in case of a radial velocity
* It is rescaled in case of Sitelle data.
* It is returned unchanged in any other case
*
* @param {array} averageSpectrum
* @returns {array}
*/
_getYData(averageSpectrum) {
return this._yDataComputer.computeSummedSpectrum(averageSpectrum, this._spectrumUnit);
}
/**
* Calls the getAverageSpectrum function of the server to get the spectrum data and plot them
*
* getAverageSpectrum parameters are : relFITSFilePath, iRA0, iDEC0, iRA1, iDEC1
*
*
* @param {number} iRA0 minimum selected index value on RA axis (int)
* @param {number} iDEC0 minimum selected index value on DEC axis (int)
* @param {number} iRA1 maximum selected index value on RA axis (int)
* @param {number} iDEC1 maximum selected index value on DEC axis (int)
*/
plot(iRA0, iRA1, iDEC0, iDEC1, callback) {
this._iRA0 = iRA0;
this._iRA1 = iRA1;
this._iDEC0 = iDEC0;
this._iDEC1 = iDEC1;
if(this.spectrumChart !== null){
this._xMinZoom = this.spectrumChart.xAxis[0].getExtremes().min;
this._xMaxZoom = this.spectrumChart.xAxis[0].getExtremes().max;
}
this.spectrumChart = this._getChart();
let self = this;
let queryApi = new ServerApi();
queryApi.getSummedSpectrum(iRA0, iRA1, iDEC0, iDEC1, self._relFITSFilePath, (resp)=>{
let x = JSON.parse(resp);
if (x.result.averageSpectrum == null) {
alert("No data for average spectrum");
throw ("No data for average spectrum");
}
// Let's inform the SAMP hub
if ("absFITSFilePath" in x["result"]) {
dataPaths.averageSpectrum = x["result"]["absFITSFilePath"];
} else {
console.log("We should have found a key 'absFITSFilePath'");
}
let averageSpectrum = x["result"]["averageSpectrum"];
// Draw x-axis in Velocities (plot on bottom right)
self._summedData.x = self._getXData(averageSpectrum.length);
// only at initialization
if (self._vmin === null && self._vmax === null) {
self._vmin = self._summedData.x[0];
self._vmax = self._summedData.x[self._summedData.x.length - 1];
}
// box coordinates above chart
self._showCoordinates([iRA0, iRA1, iDEC0, iDEC1]);
// change name of function
averageSpectrum = self._getYData(averageSpectrum);
self._summedData.y = averageSpectrum;
let chartData = [];
for (let i = 0; i < self._summedData.x.length; i++) {
chartData.push([self._summedData.x[i], self._summedData.y[i]]);
}
self._averageSpectrum = averageSpectrum;
self.spectrumChart.series[0].update({
name: '',
// unlimited number of points when zooming
cropThreshold: Infinity,
showInLegend: false,
marker: {
radius: 0
},
data: chartData,
});
/**
Add a series where Y=0 in the given chart
*/
let addYAxisSeries = function (chart) {
chart.addSeries({
lineWidth: 1,
enableMouseTracking: false,
showInLegend: false,
color: "#000000",
marker: {
enabled: false
},
data: [
[chart.xAxis[0].dataMin, 0],
[chart.xAxis[0].dataMax, 0]
]
});
}
addYAxisSeries(self.spectrumChart);
if (self._viewLinker !== null) {
addYAxisSeries(self._viewLinker.spectrumViewer.spectrumChart);
if (self._viewLinker.summedPixelsSpectrumViewer.linePlotter !== null && self._viewLinker.spectroUI.getSelectedDatabase() !== "off") {
self.linePlotter.loadAndPlotLines(self.linePlotter.obsFreqMin,
self.linePlotter.obsFreqMax,
[self.getSpectrumChartXAxis(), self._viewLinker.spectrumViewer.getSpectrumChartXAxis()]);
}
}
if(self._xMinZoom !== null && self._xMaxZoom !== null){
self.spectrumChart.xAxis[0].setExtremes(self._xMinZoom, self._xMaxZoom);
}
if(testMode){
let meta = {
iRA : Math.min(iRA0, iRA1),
iDEC : Math.max(iDEC0, iDEC1),
ytitle : self._ytitle,
xbox : Math.abs(iRA1-iRA0),
ybox : Math.abs(iDEC1-iDEC0),
lowerIntegratedValues : {
flux : {value: self._getSelectedSpectrumValue(self._averageSpectrum, 0, self._summedData.x.length -1), unit : self._toptitle_unit},
vmin : {value : self._vmin, unit : "km/s"},
vmax : {value : self._vmax, unit : "km/s"},
imin : 0,
imax : self._summedData.x.length -1,
}
};
//this._meta = meta;
self._executeSpectrumLoadedListener(get3DSpectrumUpdateEvent("summed",self._summedData, meta));
}
if (callback !== undefined)
callback();
})
}
/**
* Replot the spectrum according to the received parameters
* The plot is done through a call to this.plot()
*
* The displayed spectral lines will be updated if they exist
*
* @param {number} xMin start x position (float)
* @param {number} xMax end x position (float)
* @param {number} yMin start y position (float)
* @param {number} yMax end y position (float)
*/
replot(xMin, xMax, yMin, yMax) {
// replot the spectrum and keep the selection interval if it exists
this.plot(xMin, xMax, yMin, yMax, () => {
if (this.spectrumChart !== null && this._vmin !== null && this._vmax !== null) {
fireChartSelectionEvent(this.spectrumChart, this._vmin, this._vmax);
}
});
}
/**
* Called when NED table object triggers an event, refreshes graph title
*
* @param {Event} event event that triggered the call
*/
sourceTableCall(event) {
this.refreshChartLegend();
}
}
/**
* Initializes and return the spectrum
* @param {*} spectroUI
* @param {*} sourceTable
* @param {*} withSAMP true if SAMP is enabled
* @returns
*/
function getSingleSpectrum1D(spectroUI, sourceTable, withSAMP) {
let spectrumViewer = new SingleSpectrumViewer1D(dataPaths, 'spectrum',
Constants.PLOT_WIDTH_1D_LARGE,
Constants.PLOT_HEIGHT_RATIO_1D_LARGE,
spectroUI);
//spectrumViewer.setSpectroUI(spectroUI);
sourceTable.addListener(spectrumViewer);
spectrumViewer.plot(Math.floor(FITS_HEADER.naxis1 / 2), Math.floor(FITS_HEADER.naxis2 / 2), ()=>{
// select all the spectrum by default is velolsr is defined
// this plot all the lines
//if(FITS_HEADER.velolsr !== undefined){
let icenter = Math.round(spectrumViewer.spectrumChart.xAxis[0].series[0].processedXData.length/2);
let imin = icenter - Math.round(FITS_HEADER.naxis3 / 8);
let imax = icenter + Math.round(FITS_HEADER.naxis3 / 8);
fireChartSelectionEvent(spectrumViewer.spectrumChart,
spectrumViewer.spectrumChart.xAxis[0].series[0].processedXData[imin],
spectrumViewer.spectrumChart.xAxis[0].series[0].processedXData[imax] );
//}
});
if (withSAMP) {
// all implement setSampButtonVisible
setOnHubAvailability([spectrumViewer]);
}
return spectrumViewer;
}
export{
fireChartSelectionEvent, SingleSpectrumViewer,
SingleSpectrumViewer1D, SummedPixelsSpectrumViewer,
getSingleSpectrum1D
}