import {
RaLabelFormatter,
DecLabelFormatter,
linearTabulator,
round,
revertArray,
v2f,
f2v,
sumArr,
unitRescale,
DecDeg2HMS,
degToRad,
AbsToPixelConverter
} from "./utils.js";
import { LinePlotter } from './olqv_spectro.js';
import { withSAMP, dataPaths, URL_ROOT, header } from './init.js';
import { FITS_HEADER } from './fitsheader.js';
import { UNIT_FACTOR, DEFAULT_OUTPUT_UNIT, PLOT_WIDTH, PLOT_HEIGHT_RATIO,
PLOT_DEFAULT_COLOR, PLOT_DEFAULT_LINE_WIDTH} from "./constants.js";
import { MarkerManager } from "./olqv_markers.js";
import { sAMPPublisher, setOnHubAvailability } from "./samp_utils.js";
import { getProjection } from "./olqv_projections.js";
import { getAverageSpectrumTitle,
getSingleSliceMousePosition, getSummedSliceMousePosition,
showLoaderAction, getConfiguration, setSliceChannel, setSliceRMS,
setSummedSliceRMS } from "./domelements.js";
/**
* Returns the channel corresponding to the given input value (frequency or velocity)
* @param {number} value (float)
* @returns {float}
*/
function getCalculatedIndex(value) {
let result = 0;
if (FITS_HEADER.ctype3 === 'VRAD') {
let step1 = (UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]] / UNIT_FACTOR[FITS_HEADER.cunit3]) / FITS_HEADER.cdelt3;
let crval3 = FITS_HEADER.crval3 / (UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]] / 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 DEFAULT_OUTPUT_UNIT['VRAD']*
let vcenter = 0; //SPEED_OF_LIGHT * (FITS_HEADER.crval3 - FITS_HEADER.restfreq) / FITS_HEADER.restfreq;
let step1 = v2f(value * UNIT_FACTOR[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;
}
return Math.round(result);
}
/**
* An object to mark informations related to a position
* in a popup bow located close to the position passed
* as a parameter.
* @typedef {Object} LastClickMarker
* @param {Map} map open layer map where the popup will be displayed
* @param {string} elementId id of DOM element containing the text
*/
function LastClickMarker(map, elementId) {
let _map = map;
let _overlay = null;
let _container = document.getElementById(elementId);
let _content = document.getElementById(elementId + '-content');
let _closer = document.getElementById(elementId + '-closer');
let _lastChanIndex = null;
let _lastCoordinate = null;
let _lastRADEC = null;
let _lastFluxDensity = null;
/**
* Popup creation and addition to an ol overlay to the map passed
* as a parameter.
*/
let _popupLastClickInfos = function() {
if (_overlay == null) {
/**
* Create an overlay to anchor the popup to the map.
*/
_overlay = new ol.Overlay({
element: _container,
autoPan: true,
autoPanAnimation: {
duration: 250
}
});
/**
* Adds a click handler to hide the popup.
* @return {boolean} Don't follow the href.
*/
_closer.onclick = function() {
_overlay.setPosition(undefined);
_closer.blur();
return false;
};
_map.addOverlay(_overlay);
}
};
/**
* Updates the content of the popup by using the informations
* stored in _lastCoordinate, _lastRADEC and _lastFluxDensity
*/
let _updateLastClickInfos = function() {
if (_lastCoordinate == null) return;
_content.innerHTML = 'Chan#' + _lastChanIndex + '<br>' + 'x = ' +
_lastCoordinate[0].toFixed(0) +
', y = ' +
_lastCoordinate[1].toFixed(0) + '<br>' +
'RA=' + _lastRADEC['RA'] + '<br>' +
'DEC=' + _lastRADEC['DEC'] + '<br>' +
'Value=' + Number(_lastFluxDensity).toExponential(4);
_overlay.setPosition(_lastCoordinate);
}
/**
* Public method to register the values to be displayed
* in the popup and update its content.
* @param {object} data an object containing displayed information
*/
this.setPositionAndFluxDensity = function(data) {
console.log("this.setPosition = function(coordinate) { : entering");
_lastChanIndex = data["chanIndex"]
_lastCoordinate = data["coordinate"];
_lastRADEC = data["RADEC"];
_lastFluxDensity = data["fluxDensity"];
_updateLastClickInfos();
console.log("this.setPosition = function(coordinate) { : exiting");
};
/**
* Public method to register the fluxDensity value passed as a parameter
* and update the popup content accordingly.
* @param {number} fluxDensity flux density (float)
* @param {number} sliceIndex index of displayed slice in cube (int)
*/
this.setFluxDensity = function(fluxDensity, sliceIndex) {
console.log("this.setFluxDensity = function(fluxDensity) { : entering");
_lastFluxDensity = fluxDensity;
_lastChanIndex = sliceIndex;
_updateLastClickInfos();
console.log("this.setFluxDensity = function(fluxDensity) { : exiting");
};
// create our popup.
_popupLastClickInfos();
};
/**
* A class creating buttons appearing on a an open layer map
* @typedef {Object} ImgControlBuilder
* @property {string} _buttonInnerHTML HTML code in the button element
* @property {string} _buttonCss button style
* @property {number} _topPosition vertical position of the button in the map (0 is on top)
* @property {string} _title title displayed as a tooltiptext
*/
class ImgControlBuilder {
constructor() {
this._buttonInnerHTML = '';
this._buttonCss = '';
this._topPosition = '';
this._title = '';
this._build = this.build.bind(this);
}
set buttonInnerHTML(innerHTML) {
this._buttonInnerHTML = innerHTML;
}
set buttonCss(buttonCss) {
this._buttonCss = buttonCss;
}
set topPosition(topPosition) {
this._topPosition = topPosition;
}
set title(title) {
this._title = title;
}
/**
*
* @returns {IMGControl} returns a button element
*/
build() {
let self = this;
let IMGControl = (function(Control) {
function IMGControl(opt_options) {
var options = opt_options || {};
var button = document.createElement('button');
button.innerHTML = self._buttonInnerHTML;
var element = document.createElement('div');
element.className = self._buttonCss + ' ol-unselectable ol-control';
element.title = self._title;
element.style.top = self._topPosition;
element.appendChild(button);
Control.call(this, {
element: element,
target: options.target
});
//this.handleImageSlice = function() {;}
//button.addEventListener('click', this.handleImageSlice.bind(this), false);
this.setHandler = function(handler) {
console.log("Changing handler");
console.log(handler);
button.addEventListener('click', handler.bind(this), false)
};
}
if (Control) IMGControl.__proto__ = Control;
IMGControl.prototype = Object.create(Control && Control.prototype);
IMGControl.prototype.constructor = IMGControl;
return IMGControl;
}(ol.control.Control));
return new IMGControl();
}
}
/**
* Base class for Slice display
* @typedef {Object} Slice
*/
class Slice {
/**
* @constructor
* @param {ViewLinker} viewLinker Object linking spectra and slices
* @param {string} sliceDivId id of the DOM element containing the slice
* @param {string} canvasId id of the canvas
* @param {Array} RADECRangeInDegrees an array containing RA/DEC boundaries of the current cube
* @param {number} width slice width in pixels (int)
* @param {number} height slice height in pixels (int)
*/
constructor(viewLinker, sliceDivId, canvasId, RADECRangeInDegrees, width, height) {
this._im_layer = null;
this._data_steps = null;
this._width = width;
this._height = height;
this._RADECRangeInDegrees = RADECRangeInDegrees;
this._RMS = null;
this._viewLinker = viewLinker;
this._map_controls = null;
this._sampButton = null;
this._initControl();
this._hidden_canvas = document.getElementById(canvasId);
this._hidden_canvas.height = height;
this._hidden_canvas.width = width;
this._projection = getProjection(FITS_HEADER.projectionType);
this._absToPix = new AbsToPixelConverter(FITS_HEADER.crpix1, FITS_HEADER.cdelt1,
FITS_HEADER.crpix2, FITS_HEADER.cdelt2,
this._projection);
this._shapesLayerGroup = new ol.layer.Group({
layers: []
});
this._shapesLayerGroup.set('title', 'shapes');
this._map = this._getMap(sliceDivId);
// initial image resolution, for later reset
this._defaultResolution = this._map.getView().getResolution();
//this._graticule = this._getGraticule();
//this._graticule.setMap(this._map);
this._markerManager = new MarkerManager(this._map, true);
this._initControl = this._initControl.bind(this);
this.updateSlice = this.updateSlice.bind(this);
this._imageLoadFunction = this._imageLoadFunction.bind(this);
this._getMap = this._getMap.bind(this);
//this._getGraticule = this._getGraticule.bind(this);
}
/**
* Sets the string RMS value with its unit
* @param {number} value (float)
* @param {string} unit
*/
setRMS(value, unit){
this._RMS = "rms=" + Number.parseFloat(value).toExponential(2)+' '+unit;
this._setRMSLabel(this._RMS);
}
/**
* Called when markers list triggers an event
*/
markerListClear() {
this._markerManager.clearMarkers();
}
/**
* Reset image position and resolution
*/
reset() {
this._map.getView().setCenter(ol.extent.getCenter(this._viewLinker.extent));
this._map.getView().setResolution(this._defaultResolution);
}
/**
* Tests if a pixel has a value, returns false if pixel has no value, i.e RGB value is 255,0,0
* returns true in any other case
* @param {number} x (int)
* @param {number} y (int)
* @returns {boolean}
*/
pixelHasValue(x, y){
let ctx = this._hidden_canvas.getContext('2d');
let pixelAtPosition = ctx.getImageData(x, this._height - y, 1, 1).data;
if(pixelAtPosition[0] == 255 && pixelAtPosition[1] == 0 && pixelAtPosition[2] == 0){
return false;
}else{
return true;
}
}
/**
* Toggles display of the samp button on the slice image
* Button will be displayed if state is true, hidden if it is false
* @param {boolean} state
*/
setSampButtonVisible(state){
if(this._sampButton !== null){
if(state){
this._sampButton.element.style.display = "block";
}else{
this._sampButton.element.style.display = "none";
}
}
}
/**
* Called when NED table object triggers an event, adds a marker on the slice pointing on an object in the sky
* @param {Event} event event received from NED table, containing ra/dec and the name of an object
*/
sourceTableCall(event) {
let res = this._absToPix.convert(degToRad(event.detail.ra), degToRad(event.detail.dec));
try {
this._markerManager.addMarker(res[0], res[1], event.detail["object"]);
} catch (error) {
console.log(error);
}
}
/**
* Called when markers list triggers an event
* @param {Event} event event containing a list of markers to plot on the slice
*/
markerListUpdate(event) {
this._markerManager.clearMarkers();
let raDec = this._projection.absToRel(degToRad(this._RADECRangeInDegrees[0][0]), degToRad(this._RADECRangeInDegrees[1][0]));
//let res = this._absToPix.convert(degToRad(this._RADECRangeInDegrees[0][0]), degToRad(event.detail.dec));
//let ra_to_pix = new RADDtoPixelConverter(this._RADECRangeInDegrees[0][0], this._RADECRangeInDegrees[1][0], 0, this._width - 1);
//let dec_to_pix = new DECDDtoPixelConverter(this._RADECRangeInDegrees[0][1], this._RADECRangeInDegrees[1][1], 0, this._height - 1);
for (const marker of event.detail.markers) {
console.log(marker);
try {
// convert RA/DEC in degrees
//const ra = HMS2DecDeg(marker["ra"]);
//const dec = DMS2DecDeg(marker["dec"]);
this._markerManager.addMarker(raDec["ra"], raDec["dec"], marker["label"]);
} catch (error) {
console.log(error);
}
}
}
/**
* What happens when the image to be displayed in 'image'
* is loaded.
* @param {Image} image an open layer image (ol.Image) that will contain src
* @param {string} src image url
*/
_imageLoadFunction(image, src) {
console.log(" _imageLoadFunction : entering");
showLoaderAction(true);
image.getImage().addEventListener('load', () => {
showLoaderAction(false);
this._hidden_canvas.getContext('2d').drawImage(image.getImage(), 0, 0);
});
image.getImage().src = src;
image.getImage().crossOrigin = "Anonymous";
console.log("_imageLoadFunction : exiting");
}
/**
* Returns an ol.Graticule open layer object, showing a grid for a coordinate system
* This is an abstract function that must be implemented in a derived class
*/
_getGraticule() {
throw new Error("This method must be implemented");
}
/**
* Returns the open layer map of the slice viewer
* This is an abstract function that must be implemented in a derived class
* @param {number} id of displayed slice in cube (int)
*/
_getMap(sliceDivId) {
throw new Error("This method must be implemented");
}
/**
* Returns the coordinates at cursor position
* This is an abstract function that must be implemented in a derived class *
* @param {Array} olc open layers coordinates
*/
coordinateFormat(olc) {
throw new Error("This method must be implemented");
}
/**
* Creates buttons on the slice viewer
* This is an abstract function that must be implemented in a derived class
*/
_initControl() {
throw new Error("This method must be implemented");
}
/**
* Updates the displayed image. Data object is generally obtained from a request to yafitss
* This is an abstract function that must be implemented in a derived class
* @param {object} data
*/
updateSlice(data) {
throw new Error("This method must be implemented");
}
/**
*
* This is an abstract function that must be implemented in a derived class
* @param {string} text a function
*/
_setRMSLabel(text) {
throw new Error("This method must be implemented");
}
}
/**
* Class displaying the content of a single slice in an image ( a channel map extracted from a channel (frequency))
* @extends Slice
*/
class SingleSlice extends Slice {
/**
* @constructor
* @param {ViewLinker} viewLinker Object linking spectra and slices
* @param {string} sliceDivId id of the DOM element containing the slice
* @param {string} canvasId id of the canvas
* @param {Array} RADECRangeInDegrees an array containing RA/DEC boundaries of the current cube
* @param {number} width slice width in pixels (int)
* @param {number} height slice height in pixels (int)
*/
constructor(viewLinker, sliceDivId, canvasId, RADECRangeInDegrees, width, height) {
super(viewLinker, sliceDivId, canvasId, RADECRangeInDegrees, width, height)
this._lastClickMarker = '';
this._map_controls = this._getMapControl();
setSliceChannel("* Chan#0");
//click in slice viewer
this._map.on("click", (event) => {
// keep old x axis limits before replot
if(this.pixelHasValue(Math.round(event.coordinate[0]), Math.round(event.coordinate[1]))){
const minVal = this._viewLinker.spectrumViewer.spectrumChart.xAxis[0].min;
const maxVal = this._viewLinker.spectrumViewer.spectrumChart.xAxis[0].max;
this._viewLinker.markLastClickInSlice(event.coordinate);
this._viewLinker.spectrumViewer.setFrequencyMarker(this._viewLinker.sliceIndex);
showLoaderAction(true);
this._viewLinker.spectrumViewer.plot(Math.floor(event.coordinate[0]),
Math.floor(event.coordinate[1]),
() => {
// keep the old min/max values on x axis when
// refreshing graph
// does not seem useful, commented for now
// replot already selected spectral lines
if (this._viewLinker.summedPixelsSpectrumViewer.linePlotter != null && this._viewLinker.spectroUI.isEnabled()) {
this._viewLinker.summedPixelsSpectrumViewer.linePlotter.loadAndPlotLines( this._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMin,
this._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMax,
[this._viewLinker.summedPixelsSpectrumViewer.getSummedPixelsSpectrumChartXAxis(), this._viewLinker.spectrumViewer.getSpectrumChartXAxis()]);
}
// set old x axis limits
this._viewLinker.spectrumViewer.spectrumChart.xAxis[0].setExtremes(minVal, maxVal);
});
}
});
this._getMapControl = this._getMapControl.bind(this);
}
/**
* Returns the open layer map of the slice viewer
* @param {number} id of displayed slice in cube (int)
*/
_getMap(sliceDivId) {
return new ol.Map({
target: sliceDivId,
view: new ol.View({
projection: this._viewLinker.olProjection,
center: ol.extent.getCenter(this._viewLinker.extent),
resolution: this._hidden_canvas.width / 512
}),
controls: this._viewLinker.controls
});
}
/**
* Returns an ol.Graticule open layer object, showing a grid for a coordinate system
* This is an abstract function that must be implemented in a derived class
* @returns {Graticule} an ol.Graticule object
*/
_getGraticule() {
return new ol.Graticule({
showLabels: true,
strokeStyle: new ol.style.Stroke({
color: 'rgba(0,0,0,0.9)',
width: 1,
lineDash: [0.5, 4]
}),
//targetSize: 75,
lonLabelFormatter: this._viewLinker.raLabelFormatter.format,
lonLabelStyle: new ol.style.Text({
font: '12px Calibri,sans-serif',
textBaseline: 'bottom',
fill: new ol.style.Fill({
color: 'rgba(0,0,0,1)'
}),
stroke: new ol.style.Stroke({
color: 'rgba(255,255,255,1)',
width: 3
})
}),
latLabelFormatter: this._viewLinker.decLabelFormatter.format
});
}
/**
* Returns the coordinates at cursor position
* @param {Array} olc open layers coordinates
* @returns {string}
*/
coordinateFormat(olc) {
let result;
let ctx = this._hidden_canvas.getContext('2d');
let pixelAtPosition = ctx.getImageData(olc[0], this._height - olc[1], 1, 1).data;
let raDec = this._projection.iRaiDecToHMSDMS(olc[0], olc[1]);
if (pixelAtPosition) {
let data_steps_index = pixelAtPosition.slice(0, 3).join('_');
if (data_steps_index !== "0_0_0") {
result = raDec['ra'] + ', ' + raDec["dec"] ;
}
} else {
result = "???";
}
return result;
}
/**
* Defines the action triggered when the mouse moves on the slice
* This creates a link between the SingleSlice and SummedSlice through the ViewLinker
*/
_getMapControl() {
return [
new ol.control.MousePosition({
className: 'custom-mouse-position',
target: getSingleSliceMousePosition(),
undefinedHTML: '',
coordinateFormat: (olc) => { return this.coordinateFormat(olc) }
}),
new ol.control.MousePosition({
className: 'custom-mouse-position',
target: getSummedSliceMousePosition(),
undefinedHTML: '',
coordinateFormat: (olc) => { return this._viewLinker.summedSlicesImage.coordinateFormat(olc) }
}),
new ol.control.FullScreen()
];
}
/**
* Creates buttons on the slice viewer
*/
_initControl() {
let builder = new ImgControlBuilder();
let controls = [];
// Reset image position
builder.buttonCss = "btn-slice-png fa fa-home";
builder.buttonInnerHTML = "R";
builder.topPosition = "480px";
builder.title = "Reset zoom and position";
let imgReset = builder.build();
imgReset.setHandler(() => {
this.reset();
});
// open image in 2D viewer
builder.buttonCss = "btn-slice-png";
builder.buttonInnerHTML = "◉";
builder.topPosition = "60px";
builder.title = "Open in 2D viewer";
let imgView = builder.build();
imgView.setHandler(() => {
let getPath = this._viewLinker._getFITSSliceImage();
getPath.then(x => {
let res = JSON.parse(x);
let url = URL_ROOT + "/visit/?relFITSFilePath=//IMG" + res.result;
window.open(url);
});
});
//download image
builder.buttonInnerHTML = "⇩";
builder.topPosition = "83px";
builder.title = "Download the 2D image in FITS on disk";
let imgDownload = builder.build();
imgDownload.setHandler(() => {
let getPath = this._viewLinker._getFITSSliceImage();
getPath.then(x => {
let res = JSON.parse(x);
let url = URL_ROOT + res.result;
window.open(url, '_blank');
});
/*var url = URL_ROOT + this._viewLinker.getFitsSliceImagePath();
window.open(url, '_blank');*/
});
controls.push(imgReset);
controls.push(imgView);
controls.push(imgDownload);
// samp button
builder.buttonCss = "samp-publish-png";
builder.buttonInnerHTML = "⇗";
builder.topPosition = "107px";
builder.title = "Send with SAMP";
this._sampButton = builder.build();
if(withSAMP){
this._sampButton.setHandler(sAMPPublisher.sendPNGSlice);
controls.push(this._sampButton);
}
this._viewLinker.controls = ol.control.defaults().extend(controls);
}
/**
* Updates the displayed image. Data object is generally obtained from a request to yafitss
* @param {object} data
*/
updateSlice(data) {
this._data_steps = data["data_steps"];
if (FITS_HEADER.isSITELLE()) {
for (var k in this._data_steps) {
if (this._data_steps.hasOwnProperty(k)) {
this._data_steps[k] /= Math.abs(3600. * 3600. * FITS_HEADER.cdelt1 * FITS_HEADER.cdelt2);
}
}
}
if (this._im_layer) {
this._map.removeLayer(this._im_layer);
}
this._im_layer = new ol.layer.Image({
source: new ol.source.ImageStatic({
url: URL_ROOT + "/" + data["path_to_png"],
projection: this._viewLinker.olProjection,
imageExtent: this._viewLinker.extent,
imageLoadFunction: this._imageLoadFunction
})
});
this._map.getLayers().insertAt(0, this._im_layer);
}
/**
*
* This is an abstract function that must be implemented in a derived class
* @param {object} fn a function
*/
_setRMSLabel(text) {
setSliceRMS(text);
}
}
/**
* Class displaying the content of a map averaged from a frequency-range (velocity-range)
* selected in the averaged spectrum
* @extends Slice
* @property {number} sliceIndex0 selected start index in averaged spectrum (int)
* @property {number} sliceIndex1 selected end index in averaged spectrum (int)
* @property {number} selectedBox box selected by the user in the image
* @property {number} regionOfInterest boundaries of the cube as found in the header
*/
class SummedSlice extends Slice {
/**
* @constructor
*
* @param {ViewLinker} viewLinker Object linking spectra and slices
* @param {string} sliceDivId id of the DOM element containing the slice
* @param {string} canvasId id of the canvas
* @param {Array} RADECRangeInDegrees an array containing RA/DEC boundaries of the current cube
* @param {number} width slice width in pixels (int)
* @param {number} height slice height in pixels (int)
*/
constructor(viewLinker, sliceDivId, canvasId, RADECRangeInDegrees, width, height) {
super(viewLinker, sliceDivId, canvasId, RADECRangeInDegrees, width, height);
// public attributes
this.sliceIndex0 = null;
this.slideIndex1 = null;
this.selectedBox = null;
this.regionOfInterest = {
iRA0: 0,
iRA1: FITS_HEADER.naxis1 - 1,
iDEC0: 0,
iDEC1: FITS_HEADER.naxis2 - 1,
iFREQ0: 0,
iFREQ1: FITS_HEADER.naxis3 - 1
};
//private attributes
this._select = this._getSelect();
this._dragBox = this._getDragBox();
this._map_controls = this._getMapControl();
this._map.addInteraction(this._select);
this._map.addInteraction(this._dragBox);
//this._graticule = this._getGraticule();
//this._graticule.setMap(this._map);
// Here we have all the stuff to create boxes on summedslices
// and trigger the update of the spectrum of sums of pixels per slice
this._box_source = new ol.source.Vector({
wrapX: false
});
this._box_layer = new ol.layer.Vector({
source: this._box_source
});
//click in summed slice viewer
this._map.on("click", (event) => {
if(this.pixelHasValue(Math.round(event.coordinate[0]), Math.round(event.coordinate[1]))){
this._viewLinker.markLastClickInSlice(event.coordinate);
this._viewLinker.markLastClickInSummedSlice(event.coordinate);
$.post("", {
"method": "getAverage",
"relFITSFilePath": this._viewLinker._relFITSFilePath,
"iRA0": event.coordinate[0],
"iRA1": event.coordinate[0],
"iDEC0": event.coordinate[1],
"iDEC1": event.coordinate[1],
"iFREQ0": this.regionOfInterest.iFREQ0,
"iFREQ1": this.regionOfInterest.iFREQ1,
"retFITS": false
}).done(
(resp) => {
console.log("getAverage callback entering");
// keep old x axis limits before replot
const minVal = this._viewLinker.spectrumViewer.spectrumChart.xAxis[0].min;
const maxVal = this._viewLinker.spectrumViewer.spectrumChart.xAxis[0].max;
let result = resp["data"];
if (result["status"])
this._viewLinker.setFluxDensityInSummedPopup(result["result"][0][0]);
this._viewLinker.spectrumViewer.setFrequencyMarker(this._viewLinker.sliceIndex);
let self = this;
this._viewLinker.spectrumViewer.plot(Math.floor(event.coordinate[0]), Math.floor(event.coordinate[1]), () => {
// set old x axis limits
self._viewLinker.spectrumViewer.spectrumChart.xAxis[0].setExtremes(minVal, maxVal);
});
console.log("getAverage callback exiting");
}
).fail(
function(err) {
var msg = "POST failed" + JSON.stringify(err, 0, 4);
console.log(msg);
alert(msg);
});
}
});
this._imageLoadFunction = this._imageLoadFunction.bind(this);
this._getSelect = this._getSelect.bind(this);
this._getDragBox = this._getDragBox.bind(this);
this._getMapControl = this._getMapControl.bind(this);
this.forgetSelectedBox = this.forgetSelectedBox.bind(this);
}
/**
* Adds a Layer the shapesLayerGroup
* @param {Layer} layer an open layer Layer object (ol.layer.Layer)
*/
addShapesLayer(layer) {
this._shapesLayerGroup.getLayers().getArray().push(layer);
}
/**
* Defines the action triggered when the mouse moves on the slice
* This creates a link between the SingleSlice and SummedSlice through the ViewLinker
*/
_getMapControl() {
return [
new ol.control.MousePosition({
className: 'custom-mouse-position',
target: getSingleSliceMousePosition(),
undefinedHTML: '',
coordinateFormat: (olc) => { return this._viewLinker.singleSliceImage.coordinateFormat(olc) }
}),
new ol.control.MousePosition({
className: 'custom-mouse-position',
target: getSummedSliceMousePosition(),
undefinedHTML: '',
coordinateFormat: (olc) => { return this.coordinateFormat(olc) }
}),
new ol.control.FullScreen()
];
}
/**
* Returns the open layer map of the slice viewer
* @param {number} id of displayed slice in cube (int)
*/
_getMap(sliceDivId) {
return new ol.Map({
target: sliceDivId,
view: new ol.View({
projection: this._viewLinker.olProjection,
center: ol.extent.getCenter(this._viewLinker.extent),
resolution: this._hidden_canvas.width / 512
}),
controls: this.controls,
layers: [this._shapesLayerGroup]
});
}
/**
* Returns an ol.Graticule open layer object, showing a grid for a coordinate system
*/
_getGraticule() {
return new ol.Graticule({
showLabels: true,
strokeStyle: new ol.style.Stroke({
color: 'rgba(0,0,0,0.9)',
width: 1,
lineDash: [0.5, 4]
}),
targetSize: 100,
lonLabelFormatter: this._viewLinker.raLabelFormatter.format,
latLabelFormatter: this._viewLinker.decLabelFormatter.format,
decLabelPosition: 0.92
});
}
/**
* Returns the coordinates at cursor position
* @param {Array} olc open layers coordinates
*/
coordinateFormat(olc) {
let result;
let ctx = this._hidden_canvas.getContext('2d');
let pixelAtPosition = ctx.getImageData(olc[0], this._height - olc[1], 1, 1).data;
let raDec = this._projection.iRaiDecToHMSDMS(olc[0], olc[1]);
if (pixelAtPosition) {
let data_steps_index = pixelAtPosition.slice(0, 3).join('_');
if (data_steps_index !== "0_0_0") {
result = raDec["ra"] + ', ' + raDec["dec"];
}
} else {
result = "???";
}
return result;
}
/**
* Creates buttons on the slice viewer
*/
_initControl() {
let controls = [];
// object building buttons
let builder = new ImgControlBuilder()
// open image 2D viewer
builder.buttonCss = "btn-slice-png";
builder.buttonInnerHTML = "◉";
builder.topPosition = "60px";
builder.title = "Open in 2D viewer";
let imgView = builder.build();
imgView.setHandler(() => {
var getPath = this._viewLinker._getFITSSumSliceImage();
getPath.then(x => {
var res = JSON.parse(x);
var url = URL_ROOT + "/visit/?relFITSFilePath=//IMG" + res.result;
window.open(url);
});
//var url = URL_ROOT + "/visit/?relFITSFilePath=//IMG" + this._viewLinker.getFitsSumSliceImagePath();
//window.open(url);
});
//download image
builder.buttonInnerHTML = "⇩";
builder.topPosition = "83px";
builder.title = "Download the 2D image in FITS on disk";
let imgDownload = builder.build();
imgDownload.setHandler(() => {
var getPath = this._viewLinker._getFITSSumSliceImage();
getPath.then(result => {
var res = JSON.parse(result);
var url = URL_ROOT + res.result;
window.open(url, '_blank');;
})
//var url = URL_ROOT + this._viewLinker.getFitsSumSliceImagePath();
//window.open(url, '_blank');
});
controls.push(imgView);
controls.push(imgDownload);
// open image in Aladin
builder.buttonCss = "samp-publish-png";
builder.buttonInnerHTML = "⇗";
builder.topPosition = "107px";
builder.title = "Send with SAMP";
this._sampButton = builder.build();
if(withSAMP){
this._sampButton.setHandler(sAMPPublisher.sendPNGSummedSlices);
controls.push(this._sampButton);
}
//add all controls to list
this.controls = ol.control.defaults().extend(controls);
}
/**
* Returns a Select object, defining what happens when user clicks on a box in the slice
* @returns {Select} ol.interaction.Select
*/
_getSelect() {
let select = new ol.interaction.Select({
condition: ol.events.pointerMove
});
select.on('select', (e) => {
if (e.selected.length) {
this.selectedBox = e.selected[0];
var extent = e.selected[0].getGeometry().getExtent();
// keep current limits for x axis when refreshing the plot
this._viewLinker.summedPixelsSpectrumViewer.replot(extent[0], extent[2], extent[1], extent[3]);
/*let minVal = this._viewLinker.summedPixelsSpectrumViewer.summedPixelsSpectrumChart.xAxis[0].min;
let maxVal = this._viewLinker.summedPixelsSpectrumViewer.summedPixelsSpectrumChart.xAxis[0].max;
this._viewLinker.summedPixelsSpectrumViewer.plot(extent[0], extent[2], extent[1], extent[3]);
// replot already selected spectral lines
if (this._viewLinker.summedPixelsSpectrumViewer.linePlotter !== null) {
this._viewLinker.summedPixelsSpectrumViewer.linePlotter.plotSpectroscopicDataOnGraph(this._viewLinker.summedPixelsSpectrumViewer.linePlotter.transitions, this._viewLinker.summedPixelsSpectrumViewer.getSummedPixelsSpectrumChartXAxis());
this._viewLinker.summedPixelsSpectrumViewer.linePlotter.plotSpectroscopicDataOnGraph(this._viewLinker.summedPixelsSpectrumViewer.linePlotter.transitions, this._viewLinker.spectrumViewer.getSpectrumChartXAxis());
}
// and apply again
this._viewLinker.summedPixelsSpectrumViewer.summedPixelsSpectrumChart.xAxis[0].setExtremes(minVal, maxVal);*/
//summedPixelsSpectrumChart.xAxis[0].setExtremes(minVal, maxVal);
this.regionOfInterest.iRA0 = Math.round(extent[0]);
this.regionOfInterest.iRA1 = Math.round(extent[2]);
this.regionOfInterest.iDEC0 = Math.round(extent[1]);
this.regionOfInterest.iDEC1 = Math.round(extent[3]);
}
});
return select;
}
/**
* Returns a DragBox object, defining what happens when user creates a box in the slice
* @returns {DragBox} ol.interaction.DragBox
*/
_getDragBox() {
let dragBox = new ol.interaction.DragBox();
dragBox.on('boxend', () => {
var extent = this._dragBox.getGeometry().getExtent();
var tl = ol.extent.getTopLeft(extent);
var tr = ol.extent.getTopRight(extent);
var br = ol.extent.getBottomRight(extent);
var bl = ol.extent.getBottomLeft(extent);
if( this.pixelHasValue(tl[0], tl[1]) &&
this.pixelHasValue(tr[0], tr[1]) &&
this.pixelHasValue(br[0], br[1]) &&
this.pixelHasValue(bl[0], bl[1])
){
var corners = []
corners.push(tl, tr, br, bl, tl);
/*
var ln = new ol.geom.LinearRing(corners);
var style = {
strokeColor: "#00FF00",
strokeOpacity: 1,
strokewidth: 3,
fillColor: "#00FF00",
fillOpacity: 0.8,
};*/
var pf = new ol.Feature({
geometry: new ol.geom.Polygon([corners])
});
this._box_source.addFeature(pf);
}else{
alert("At least one of selected points have no value.");
}
});
return dragBox;
}
/**
* Updates the displayed image. Data object is generally obtained from a request to yafitss
* This is an abstract function that must be implemented in a derived class
* @param {object} data
*/
updateSlice(data) {
this._data_steps = data["data_steps"];
let path_to_png = data["path_to_png"];
if (this._im_layer) {
this._map.removeLayer(this._im_layer);
this._map.removeLayer(this._box_layer);
}
this._im_layer = new ol.layer.Image({
source: new ol.source.ImageStatic({
url: URL_ROOT + "/" + path_to_png,
projection: this._viewLinker.olProjection,
imageExtent: this._viewLinker.extent,
imageLoadFunction: this._imageLoadFunction
})
});
this._map.getLayers().insertAt(0, this._im_layer);
this._map.getLayers().insertAt(1, this._box_layer);
}
/**
* Deletes the currently selected box, if it exists
*/
forgetSelectedBox() {
console.log('this.forgetSelectedBox = function() {: entering');
var styleForget = function() {
return [new ol.style.Style({
stroke: new ol.style.Stroke({
color: [255, 0, 0, 1]
})
})];
};
if (this.selectedBox) {
this.selectedBox.setStyle(styleForget);
this._box_source.removeFeature(this.selectedBox);
this._box_source.refresh();
this._viewLinker.summedPixelsSpectrumViewer.replot(0, this._width - 1, 0, this._height - 1)
this.regionOfInterest.iRA0 = Math.round(0);
this.regionOfInterest.iRA1 = Math.round(this._width - 1);
this.regionOfInterest.iDEC0 = Math.round(0);
this.regionOfInterest.iDEC1 = Math.round(this._height - 1);
}
console.log('this.forgetSelectedBox = function() {: exiting');
}
/**
*
* This is an abstract function that must be implemented in a derived class
* @param {object} fn a function
*/
_setRMSLabel(text) {
setSummedSliceRMS('* ' + text);
}
}
/**
* Class creating link between slices and spectra
* @typedef {Object} ViewLinker
*
* @property {SpectrumViewer} spectrumViewer
* @property {SummedPixelsSpectrumViewer} summedPixelsSpectrumViewer
* @property {array} extent
* @property {RaLabelFormatter} raLabelFormatter
* @property {SpectruDecLabelFormattermViewer} decLabelFormatter
* @property {SliceViewer} singleSliceImage
* @property {SummedSliceViewer} summedSlicesImage
* @property {Projection} coordsProjection
* @property {SpectroscopyUI} spectroUI
*
*/
class ViewLinker {
constructor(paths, width, height,
RADECRangeInDegrees, divSlice, divSummedSlices, spectroUI) {
// public attributes
this.spectrumViewer = null;
this.summedPixelsSpectrumViewer = null;
this.spectroUI = spectroUI;
this.extent = [0, 0, width - 1, height - 1];
this.raLabelFormatter = new RaLabelFormatter(this.extent[0], this.extent[2],
RADECRangeInDegrees[0][0], RADECRangeInDegrees[1][0]);
this.decLabelFormatter = new DecLabelFormatter(this.extent[1], this.extent[3],
RADECRangeInDegrees[0][1], RADECRangeInDegrees[1][1]);
this.singleSliceImage = new SingleSlice(this, divSlice, "hidden-" + divSlice, RADECRangeInDegrees, width, height);
this.summedSlicesImage = new SummedSlice(this, divSummedSlices, "hidden-" + divSummedSlices, RADECRangeInDegrees, width, height);
//this.singleSliceImage._getMapControl(this.summedSlicesImage);
// open layers projection for image display
/*this.olProjection = new ol.proj.Projection({
code: 'local_image',
units: 'pixels',
extent: this.extent,
worldExtent: [...this.extent]
});*/
// projection object for coordinates calculation
try{
this.coordsProjection = getProjection(FITS_HEADER.projectionType);
}catch(e){
alert(e);
}
this.sliceIndex = null;
// private attributes
this._relFITSFilePath = paths.relFITSFilePath;
this._lastClickMarker = new LastClickMarker(this.singleSliceImage._map, 'popup-single');
this._lastClickMarkerSummed = new LastClickMarker(this.summedSlicesImage._map, 'popup-summed');
console.log("_ra0 = " + DecDeg2HMS(RADECRangeInDegrees[0][0]) + " _ra1 =" + DecDeg2HMS(RADECRangeInDegrees[1][0]));
console.log("_dec0 = " + DecDeg2HMS(RADECRangeInDegrees[0][1]) + " _dec1 =" + DecDeg2HMS(RADECRangeInDegrees[1][1]));
//events
this.singleSliceImage._map.getView().on('change:resolution', (event) => {
this.updateView(event, this.summedSlicesImage._map.getView());
});
this.singleSliceImage._map.getView().on('change:center', (event) => {
this.updateView(event, this.summedSlicesImage._map.getView());
});
this.summedSlicesImage._map.getView().on('change:resolution', (event) => {
this.updateView(event, this.singleSliceImage._map.getView());
});
this.summedSlicesImage._map.getView().on('change:center', (event) => {
this.updateView(event, this.singleSliceImage._map.getView());
});
}
/**
* Sets spectrumViewer
* @param {SpectrumViewer} spectrumViewer
*/
setSpectrumViewer(spectrumViewer) {
this.spectrumViewer = spectrumViewer
}
/**
* Sets summedPixelsSpectrumViewer
* @param {SummedPixelsSpectrumViewer} summedPixelsSpectrumViewer
*/
setSummedPixelsSpectrumViewer(summedPixelsSpectrumViewer) {
this.summedPixelsSpectrumViewer = summedPixelsSpectrumViewer
}
/**
* Executes a POST request and updates singleSlice.
* Request parameters are :
* -for slice retrieval : selected slice index, fits file path
* -for image configuration : ittName, lutName, vmName
*
* @param {number} sliceIndex index of searched slice (int)
*/
getAndPlotSingleSlice(sliceIndex) {
console.log('getAndPlotSingleSlice: entering');
showLoaderAction(true);
let self = this;
let config = getConfiguration();
this.sliceIndex = sliceIndex;
$.post('png', {
'si': this.sliceIndex,
'relFITSFilePath': this._relFITSFilePath,
'ittName': config.ittName,
'lutName': config.lutName,
'vmName': config.vmName
}).done(
function(resp) {
console.log("$.post('/png', {'si': sliceIndex, 'relFITSFilePath': _relFITSFilePath}).done(: entering");
if (resp["status"] == false) {
alert("Something went wrong during the generation of the image. The message was '" + resp["message"] + "'");
} else {
self.singleSliceImage.updateSlice(resp["result"]);
self.singleSliceImage.setRMS(parseFloat(resp["result"]["statistics"]["stdev"]), FITS_HEADER.bunit);
if (withSAMP) {
dataPaths.relSlicePNG = resp["result"]["path_to_png"];
}
}
showLoaderAction(false);
console.log("$.post('/png', {'si': sliceIndex, 'path': _path}).done(: exiting");
}
)
console.log('_updateSliceWithPOST: exiting');
}
/**
* Executes a POST request and updates summedSlice
* Request parameters are :
* -for slice retrieval : start index (on averaged spectrum), end index (on averaged spectrum), fits file path
* -for image configuration : ittName, lutName, vmName
*
* @param {number} sliceIndex0 start index (int)
* @param {number} sliceIndex1 end index (int)
*/
getAndPlotSummedSlices(sliceIndex0, sliceIndex1) {
console.log(" getAndPlotSummedSlices : entering");
showLoaderAction(true);
let self = this;
let config = getConfiguration();
this.summedSlicesImage.sliceIndex0 = sliceIndex0;
this.summedSlicesImage.sliceIndex1 = sliceIndex1;
$.post('sumpng', {
'si0': self.summedSlicesImage.sliceIndex0,
'si1': self.summedSlicesImage.sliceIndex1,
'relFITSFilePath': self._relFITSFilePath,
'ittName': config.ittName,
'lutName': config.lutName,
'vmName': config.vmName
}).done(
function(resp) {
console.log("$.post('/sumpng', {'si0': sliceIndex0, 'si1': sliceIndex1, 'relFITSFilePath': relFITSFilePath}).done() : entering");
console.log("in _updateSummedSlicesWithPOST");
if (resp["status"] == false) {
alert("Something went wrong during the generation of the image. The message was " +
resp["message"] + "'");
} else {
self.summedSlicesImage.updateSlice(resp["result"]);
self.summedSlicesImage.setRMS(parseFloat(resp["result"]["statistics"]["stdev"]), FITS_HEADER.bunit+"*km/s");
if (withSAMP) {
dataPaths.relSummedSlicesPNG = resp["result"]["path_to_png"];
}
}
showLoaderAction(false);
console.log("$.post('/sumpng', {'si0': sliceIndex0, 'si1': sliceIndex1, 'relFITSFilePath': relFITSFilePath}).done() : exiting");
});
console.log("_updateSummedSlicesWithPOST : exiting");
}
/**
* Refreshes both slices display from current parameters
*/
refresh() {
console.log("refresh: entering")
this.getAndPlotSingleSlice(this.sliceIndex);
this.getAndPlotSummedSlices(this.summedSlicesImage.sliceIndex0, this.summedSlicesImage.sliceIndex1);
console.log("refresh: exiting")
}
/**
* Notifies to the view that an action occured on an image (image has been moved or zoomed in/out)
* @param {*} event type of action
* @param {*} viewRef modified view
*/
updateView(event, viewRef) {
let newValue = event.target.get(event.key);
viewRef.set(event.key, newValue);
}
/**
* Removes the currently selected box on the summedSlicesViewer
*/
forgetSelectedBox() {
this.summedSlicesImage.forgetSelectedBox();
}
/**
* Returns a promise object querying the server to create and get a fits file corresponding to the current slice
* It calls the createFITSSliceImage endpoint of the server, with relFITSFilePath ( currently opened fits file)
* and iFREQ (current slice index) as parameters.
*
* @returns {Promise} Promise object containing a query to the server to create a fits file corresponding to the current slice
*/
_getFITSSliceImage() {
//Create a FITS file containing the image to download.
return $.post("", {
"method": "createFITSSliceImage",
"relFITSFilePath": this._relFITSFilePath,
"iFREQ": this.sliceIndex
}).done(
function(resp) {
console.log("A FITS file has been created for the upper image.");
let x = JSON.parse(resp);
if (!x["status"]) {
console.log(`Something went wrong during the generation of the image FITS file, the message was
${x["message"]}`);
alert(x["message"]);
//self._fitsSliceImagePath = x["result"];
}
/*else {
console.log(`Something went wrong during the generation of the image FITS file, the message was
${x["message"]}`);
alert(x["message"]);
}*/
}
).fail(
function(err) {
let msg = "POST failed" + JSON.stringify(err, 0, 4);
console.log(msg);
alert(msg);
}
);
}
/**
* Returns a promise object querying the server to create and get a fits file corresponding to the current slice
* It calls the createFITSSumSliceImage endpoint of the server, with relFITSFilePath ( currently opened fits file),
* iFREQ0 (average start index) and iFREQ1 (average end index) as parameters.
*
* @returns {Promise} Promise object containing a query to the server to create a fits file corresponding to the current averaged slice
*/
_getFITSSumSliceImage() {
//Create a FITS file containing the bottom image to download.
return $.post("", {
"method": "createFITSSumSliceImage",
"relFITSFilePath": this._relFITSFilePath,
"iFREQ0": this.summedSlicesImage.sliceIndex0,
"iFREQ1": this.summedSlicesImage.sliceIndex1
}).done(
function(resp) {
console.log("A FITS file has been created for the bottom image." + resp);
let x = JSON.parse(resp);
if (!x["status"]) {
console.log(`Something went wrong during the generation of the bottom image FITS file, the message was
${x["message"]}`);
alert(x["message"]);
//self._fitsSumSliceImagePath = x["result"];
}
/* else {
console.log(`Something went wrong during the generation of the bottom image FITS file, the message was
${x["message"]}`);
alert(x["message"]);
}*/
}
).fail(
function(err) {
let msg = "POST failed" + JSON.stringify(err, 0, 4);
console.log(msg);
alert(msg);
}
);
}
/**
* Displays a popup at the position clicked in the slice. It shows the coordinates of the click in the image,
* the chanel index, ra/dec values and flux density. Flux density is always set as "To Be Determined" because a query to the server
* it necessary to get the value. "t.b.d." will be replaced once the value has been obtained.
* @param {Slice} target the slice where the click occured
* @param {array} coordinate a 2 elements array containing the x,y coordinates
*/
_markLastClickInSlice(target, coordinate) {
let raDec = this.coordsProjection.iRaiDecToHMSDMS(coordinate[0], coordinate[1]);
target.setPositionAndFluxDensity({
"coordinate": coordinate,
"chanIndex": this.sliceIndex,
"RADEC": { 'RA': raDec["ra"],
'DEC': raDec["dec"]
},
"fluxDensity": "t.b.d."
});
};
/**
* Calls _markLastClickInSlice when slice image is clicked
* @param {array} coordinate a 2 elements array containing the x,y coordinates
*/
markLastClickInSlice(coordinate) {
this._markLastClickInSlice(this._lastClickMarker, coordinate);
};
/**
* Calls _markLastClickInSlice when averaged slice image is clicked
* @param {array} coordinate a 2 elements array containing the x,y coordinates
*/
markLastClickInSummedSlice(coordinate) {
this._markLastClickInSlice(this._lastClickMarkerSummed, coordinate);
};
/**
* Sets the value of flux density in a popup in a slice
* @param {Slice} target the target slice
* @param {number} density density value (float)
*/
_setFluxDensityInPopup(target, density) {
target.setFluxDensity(density * unitRescale(FITS_HEADER.bunit), this.sliceIndex);
}
/**
* Sets the value of flux density in a popup in the single slice ( by calling _setFluxDensityInPopup)
* @param {Slice} target the target slice
* @param {number} density density value (float)
*/
setFluxDensityInPopup(density) {
this._setFluxDensityInPopup(this._lastClickMarker, density);
}
/**
* Sets the value of flux density in a popup in the averaged slice (by calling _setFluxDensityInPopup)
* @param {Slice} target the target slice
* @param {number} density density value (float)
*/
setFluxDensityInSummedPopup(density) {
this._setFluxDensityInPopup(this._lastClickMarkerSummed, density);
}
}
/**
* A class displaying a spectrum, using the Highcharts library
*
* @property {Object} spectrumChart a highchart chart object
*/
class SpectrumViewer {
/**
* @constructor
* @param {Object} paths dataPaths object
* @param {string} containerId id of graph container
* @param {ViewLinker} viewLinker ViewLinker object managing interactions between elements
*/
constructor(paths, containerId, viewLinker) {
this.spectrumChart = null;
this._ifrequencyMarker = 0;
this._containerId = containerId;
this._viewLinker = viewLinker;
this._xtitle = "undefined";
this._ytitle = "undefined";
this.toptitle = "undefined";
this._relFITSFilePath = paths.relFITSFilePath;
this._sampButton = undefined;
this._initTitles();
this._computeSliceIndex = this._computeSliceIndex.bind(this);
}
/**
* 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: "frequency"
};
}
/**
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);
}
/**
* 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
*/
_initTitles() {
if (FITS_HEADER.isSITELLE()) {
this._xtitle = FITS_HEADER.ctype3 + " (" + FITS_HEADER.cunit3 + ")";
this._ytitle = "FLUX (" + FITS_HEADER.bunit + ")";
this.toptitle = "";
} else if (FITS_HEADER.isCASA()) {
this._xtitle = "Sky Frequency (GHz) - " + FITS_HEADER.specsys;
this._ytitle = "Flux density (" + FITS_HEADER.bunit + ")";
let coeff = Math.PI / 180. / 4.86e-6;
this.toptitle = "B: " + round(FITS_HEADER.bmaj * coeff, 1) + "x" +
round(FITS_HEADER.bmin * coeff, 1) + " PA " +
round(FITS_HEADER.bpa, 0) + "°";
} else if (FITS_HEADER.isGILDAS()) {
this._xtitle = "Sky Frequency (GHz)";
this._ytitle = "Flux density (" + FITS_HEADER.bunit + ")";
this.toptitle = "";
} else if (FITS_HEADER.isMUSE()) {
this._xtitle = FITS_HEADER.ctype3 + " (" + FITS_HEADER.cunit3 + ")";
this._ytitle = "FLUX (" + FITS_HEADER.bunit + ")";
this.toptitle = "";
} else if (FITS_HEADER.isMIRIAD()) {
this._xtitle = FITS_HEADER.ctype3 + " (" + FITS_HEADER.cunit3 + ")";
this._ytitle = "FLUX (" + FITS_HEADER.bunit + ")";
this.toptitle = "";
} else {
alert("Warning, unknown instrument : " + FITS_HEADER.instrume);
}
}
/**
* 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
*/
_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))
}
/**
* 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.");
}
/**
* 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
*/
_getPoint = function(x, y, visible) {
return {
type: 'scatter',
name: '',
showInLegend: false,
visible: visible,
zIndex: 1,
enableMouseTracking: false,
marker: {
radius: 5
},
data: [{
x: x,
y: y,
color: '#BF0B23'
}]
}
}
/**
* Toggles samp button visibility
* @param {boolean} state status of button visibility
*/
setSampButtonVisible(state) {
if (this._sampButton !== undefined) {
if (state === true) {
this._sampButton.show();
} else {
this._sampButton.hide();
}
}
}
/**
* 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: PLOT_WIDTH,
height: PLOT_HEIGHT_RATIO,
animation: false,
zoomType: 'x',
panning: true,
panKey: 'shift',
events: {
load: function(event) {
// graph is loaded
showLoaderAction(false);
},
click: function(event) {
//console.clear();
console.log("A click occurred on the spectrum : enter");
let sliceIndex = self._computeSliceIndex(plotData, event.xAxis[0].value);
// Display slice at index sliceIndex
self._viewLinker.getAndPlotSingleSlice(sliceIndex);
self._viewLinker.setFluxDensityInPopup(event.yAxis[0].value);
this.series[1].update(self._getPoint(event.xAxis[0].value, 0));
}
}
},
boost: {
useGPUTranslations: true,
usePreAllocated: true
},
xAxis: {
gridLineWidth: 1,
lineColor: '#FFFFFF',
title: {
text: xtitle
},
crosshair: true,
reversed: true,
events: {
// called when boudaries of spectrum are modified
setExtremes: function(event) {
if (event.min === undefined || event.max === undefined) {
self._viewLinker.summedPixelsSpectrumViewer.summedPixelsSpectrumChart.xAxis[0].setExtremes(
self._viewLinker.summedPixelsSpectrumViewer.summedPixelsSpectrumChart.xAxis[0].dataMin,
self._viewLinker.summedPixelsSpectrumViewer.summedPixelsSpectrumChart.xAxis[0].dataMax);
} else {
let vcenter = FITS_HEADER.getVCenter();
let restfreq = FITS_HEADER.restfreq;
let minval = Math.round(f2v(event.min * 1e9, restfreq, vcenter) / 1e3);
let maxval = Math.round(f2v(event.max * 1e9, restfreq, vcenter) / 1e3);
//exchange min/max if min > max
if (minval > maxval) {
let tmp = minval;
minval = maxval;
maxval = tmp;
}
self._viewLinker.summedPixelsSpectrumViewer.summedPixelsSpectrumChart.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: PLOT_DEFAULT_COLOR,
animation: {
duration: 0
},
lineWidth: PLOT_DEFAULT_LINE_WIDTH,
events: {
click: function(event) {
//console.clear();
console.log("A click occurred on the LINE : enter");
let sliceIndex = self._computeSliceIndex(plotData, event.point.x);
setSliceChannel("* Chan#"+sliceIndex);
// Display slice at index sliceIndex
self._viewLinker.setFluxDensityInPopup(event.point.y);
self._viewLinker.getAndPlotSingleSlice(sliceIndex);
this.chart.series[1].update(self._getPoint(event.point.x, 0));
}
}
},
marker: {
radius: 0
}
},
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
self._getPoint(0, 0, false)],
}, function(chart) { // on complete \⇩
// save spectrum data
chart.renderer.button('Save', 10, 10)
.attr({
zIndex: 3,
title : "Download the 1D spectrum in FITS format (readable in GILDAS/CLASS)"
})
.on('click', function(event) {
console.log("A click occurred on the save button : enter");
window.open(URL_ROOT + dataPaths.spectrum, '_blank');
event.stopPropagation();
})
.add();
// send with samp
self._sampButton = chart.renderer.button('Samp', 60, 10)
.attr({
zIndex: 3,
title : "Send with SAMP",
id: "samp"
})
.on('click', function(event) {
sAMPPublisher.sendSpectrumToAll(URL_ROOT + dataPaths.spectrum, "Artemix");
event.stopPropagation();
})
.add();
//initially there is no samp connection
self._sampButton.hide();
});
}
/**
* 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) {
let xData = new Array(rlen);
for (var i = 0; i < rlen; i++) {
let tmp = linearTabulator(FITS_HEADER.crval3, FITS_HEADER.cdelt3, FITS_HEADER.crpix3, i + 1);
if (FITS_HEADER.cunit3 in UNIT_FACTOR) {
switch (FITS_HEADER.ctype3) {
case 'FREQ':
if (FITS_HEADER.cdelt3 > 0) {
xData[rlen - i - 1] = tmp * UNIT_FACTOR[FITS_HEADER.cunit3] / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]];
} else {
xData[i] = tmp * UNIT_FACTOR[FITS_HEADER.cunit3] / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]];
}
break;
case 'VRAD':
// ### TO COMPLETE, crpix3 = 66, frest = 230.538, NOU=channel 0
// if centerVal = 0 : lines are correct but zoom does not work (ex M83)
// if centerVal = crval3 : zoom is correct but lines do not work
let tmp1 = v2f(tmp * UNIT_FACTOR[FITS_HEADER.cunit3], FITS_HEADER.restfreq, 0 /*FITS_HEADER.crval3*/ ) / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT['FREQ']];
if (FITS_HEADER.cdelt3 > 0) {
xData[i] = tmp1;
} else {
xData[rlen - i - 1] = tmp1;
}
break;
case 'VELO-LSR':
// This case is equivalent to VRAD above
let tmpvelo = v2f(tmp * UNIT_FACTOR[FITS_HEADER.cunit3], FITS_HEADER.restfreq, 0 /*FITS_HEADER.crval3*/ ) / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT['FREQ']];
if (FITS_HEADER.cdelt3 > 0) {
xData[i] = tmpvelo;
} else {
xData[rlen - i - 1] = tmpvelo;
}
break;
case 'WAVE':
xData[i] = tmp;
//alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
case 'WAVN':
xData[i] = tmp;
//alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
case 'AWAV':
xData[i] = tmp;
break;
default:
console.log("This should not happen");
alert("ctype3 case not recognized : " + FITS_HEADER.ctype3);
break;
}
}
}
return xData;
}
/**
* 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 = null;
switch (FITS_HEADER.ctype3) {
case 'FREQ':
if (FITS_HEADER.cdelt3 > 0) {
result = revertArray(data);
} else {
result = data;
}
break;
case 'VRAD':
if (FITS_HEADER.cdelt3 > 0) {
result = data;
} else {
result = revertArray(data);
}
break;
// equivalent to VRAD
case 'VELO-LSR':
if (FITS_HEADER.cdelt3 > 0) {
result = data;
} else {
result = revertArray(data);
}
break;
case 'WAVE':
result = data;
//alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
case 'WAVN':
result = data;
//alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
case 'AWAV':
result = data;
break;
default:
console.log("This should not happen");
alert("ctype3 case not recognized : " + FITS_HEADER.ctype3);
break;
}
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)
*/
_createFile(iRA, iDEC) {
if (FITS_HEADER.ctype3 === "FREQ" || FITS_HEADER.ctype3 === "VRAD") {
$.post("", {
"method": "createFits",
"relFITSFilePath": this._relFITSFilePath,
"iRA": iRA,
"iDEC": iDEC
})
.done(
function(resp) {
console.log("A FITS file has been created for the spectrum.");
var x = JSON.parse(resp);
if (x["status"]) {
dataPaths.spectrum = x["result"];
} else {
console.log(`Something went wrong during the generation of
the spectrum FITS file, the message was ${x["message"]}`);
alert(x["message"]);
}
}
)
.fail(
function(err) {
var msg = "POST failed" + JSON.stringify(err, 0, 4);
console.log(msg);
alert(msg);
}
);
}
}
/**
* 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) {
console.log("plot: entering.");
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);
}
// get spectrum
$.post("", {
"method": "getSpectrum",
"relFITSFilePath": this._relFITSFilePath,
"iRA": iRA,
"iDEC": iDEC,
"iFREQ0": 0,
"iFREQ1": FITS_HEADER.naxis3 - 1
}).done(
/*
** This is the function which actually performs the plot as a callback on
** return from a call to the FITS file server in order to get the spectrum to draw.
*/
function(resp) {
console.log("getSpectrum callback : entering");
if (resp.data["status"] == false) {
alert(resp.data["message"]);
showLoaderAction(false);
return;
}
let plotData = self._getXAxisConfiguration();
plotData.x = self._getXData(resp.data["result"].length); // abscissa ( frequency, wavelength, velocity, ...);
plotData.xaxis = "x";
plotData.y = self._getYData(resp.data["result"]);
self.spectrumChart = self._getChart(plotData, self._xtitle, self._ytitle);
//if (withSAMP) {
//Create a FITS file containing the spectrum
self._createFile(iRA, iDEC);
//}
//self._frequencyMarker.x = [plotData.x[self._ifrequencyMarker]];
// set value of flux density in popup, should me moved out of this function
self._viewLinker.setFluxDensityInPopup(plotData.y[self._ifrequencyMarker]);
// callback function called at the end of loading process
// typical use is restore previous graph limits
if (cb !== undefined) {
cb();
}
if (self._viewLinker.summedPixelsSpectrumViewer.linePlotter !== null && self._viewLinker.spectroUI.isEnabled()) {
self._viewLinker.summedPixelsSpectrumViewer.linePlotter.loadAndPlotLines(self._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMin,
self._viewLinker.summedPixelsSpectrumViewer.linePlotter.obsFreqMax,
[self._viewLinker.summedPixelsSpectrumViewer.getSummedPixelsSpectrumChartXAxis(), self.getSpectrumChartXAxis()]);
}
console.log("getSpectrum callback : exiting");
}
);
console.log("plot: exiting.");
};
}
/**
* A class displaying an averaged spectrum, using the Highcharts library
*
* @property {Object} summedPixelsSpectrumChart a highchart chart object
* @property {LinePlotter} linePlotter an object drawing spectral lines on the chart
*/
class SummedPixelsSpectrumViewer {
/**
*
* @param {*} paths
* @param {*} containerId
* @param {*} viewLinker
*/
constructor(paths, containerId, viewLinker) {
this.summedPixelsSpectrumChart = null;
this.linePlotter = null;
this._spectroUI = viewLinker.spectroUI;
this._containerId = containerId;
this._viewLinker = viewLinker;
this._xtitle = "undefined";
this._ytitle = "undefined";
this._toptitle_unit = "";
this._flux_unit = "";
this._averageSpectrum = null;
this._sampButton = undefined;
this._relFITSFilePath = paths.relFITSFilePath;
this._summedData = {
// styling of selected area, not used for now
/*type: 'scatter',
marker: {
color: '#1f77b4',
size: 5.
},
line: {
color: '#1f77b4',
width: 1
},
connectgaps: 'true',
hoverinfo: 'x+y',
xaxis: 'x',*/
//selected data
x : [],
y : []
};
// surface under the chart in area selected by user, Jy/km/s
this._selectedSurface = 0;
this._initTitles();
}
/**
* Returns the xaxis of the chart and its datatype
* @returns {Object} an object containing the xaxis and a datatype
*/
getSummedPixelsSpectrumChartXAxis() {
return {
axis: this.summedPixelsSpectrumChart.xAxis[0],
datatype: "velocity"
};
}
/**
* Replot the spectrum according to the received parameters
* The plot is done trhough a call to this.plot()
*
* The displayed spectral lines will be updated if they exist
*
* @param {number} x start x position (float)
* @param {number} xWidth width selected on x-axis (float)
* @param {number} y start y position (float)
* @param {number} yWidth width selected on y-axis (float)
*/
replot(x, xWidth, y, yWidth) {
let minVal = this.summedPixelsSpectrumChart.xAxis[0].min;
let maxVal = this.summedPixelsSpectrumChart.xAxis[0].max;
this.plot(x, xWidth, y, yWidth);
/*if (this.linePlotter !== null) {
this.linePlotter.removeLines();
}*/
// replot already selected spectral lines
if (this.linePlotter !== null && this._viewLinker.spectroUI.isEnabled()) {
this.linePlotter.loadAndPlotLines( this.linePlotter.obsFreqMin,
this.linePlotter.obsFreqMax,
[this.getSummedPixelsSpectrumChartXAxis(), this._viewLinker.spectrumViewer.getSpectrumChartXAxis()]);
}
// and apply again
this.summedPixelsSpectrumChart.xAxis[0].setExtremes(minVal, maxVal);
}
/**
* 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();
}
}
/**
* Sets title of spectrum, x and y axis
*
* Title of the spectrum depends on its type (Sitelle, Casa, Gildas, Muse)
*/
_initTitles() {
if (FITS_HEADER.bmin !== undefined)
this._flux_unit = "(" + this._summedPixelsSpectrumUnit(FITS_HEADER.bunit) + ")";
if (FITS_HEADER.isSITELLE()) {
this._xtitle = FITS_HEADER.ctype3 + " (" + FITS_HEADER.cunit3 + ")";
this._ytitle = "FLUX (" + this._summedPixelsSpectrumUnit(header["BUNIT"]) + ")";
this._toptitle_unit = "ergs/s/cm^2";
} else if (FITS_HEADER.isCASA()) {
this._xtitle = "Velocity (km/s) - " + FITS_HEADER.specsys;
this._ytitle = "Int. flux density " + this._flux_unit;
this._toptitle_unit = "Jy.km/s";
} else if (FITS_HEADER.isGILDAS()) {
this._xtitle = "Velocity (km/s)"
this._toptitle_unit = "Jy.km/s";
// special case
if (FITS_HEADER.isSpectrumInK()) {
this._ytitle = "Int. flux density (K)";
this._toptitle_unit = "K.km/s";
}
// common case
else {
this._ytitle = "Int. flux density " + this._flux_unit;
}
} else if (FITS_HEADER.isMUSE()) {
this._xtitle = FITS_HEADER.ctype3 + " (" + FITS_HEADER.cunit3 + ")";
this._ytitle = "Int. flux)";
this._toptitle_unit = "";
} else {
alert("Unknown instrument : " + FITS_HEADER.instrume);
}
}
/**
* Returns a formatted string containing a displayable unit name
* @param {string} unit the source unit
* @returns {string} a formatted unit
*/
_summedPixelsSpectrumUnit(unit) {
switch (unit) {
case "Jy/beam":
return "Jy";
case "erg/s/cm^2/A/arcsec^2":
return "erg/s/cm^2/A";
default:
return "";
}
}
/**
* Returns integral value of selected area in the spectrum
* One case for a graph in radial velocity, one for all other cases
* @param {*} avgSpectrum
* @param {*} imin
* @param {*} imax
* @returns
*/
_getSummedSpectrumValue(avgSpectrum, imin, imax) {
let result = 0;
if (FITS_HEADER.ctype3 === 'VRAD') {
let copy = (x) => x;
let arraycopy = avgSpectrum.map(copy);
result = sumArr(arraycopy.reverse(), imin, imax, FITS_HEADER.cdelt3prim);
} else {
result = sumArr(avgSpectrum, imin, imax, FITS_HEADER.cdelt3prim);
}
return result / unitRescale(this._summedPixelsSpectrumUnit(FITS_HEADER.bunit));
}
/**
* 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) {
getAverageSpectrumTitle().innerHTML = title;
}
/**
* Toggles SAMP button visibility
* @param {boolean} state a boolean value corresponding to the new state of the button
*/
setSampButtonVisible(state) {
if (this._sampButton !== undefined) {
if (state === true) {
this._sampButton.show();
} else {
this._sampButton.hide();
}
}
}
/**
* 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) {
var imin = Math.round((min - this._summedData.x[0]) / (this._summedData.x[1] - this._summedData.x[0]));
var imax = Math.round((max - this._summedData.x[0]) / (this._summedData.x[1] - this._summedData.x[0]));
if (FITS_HEADER.cunit3 in 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() {
var self = this;
let target = document.getElementById(this._containerId);
return Highcharts.chart(target, {
title: {
text: ''
},
chart: {
type: 'line',
animation: false,
width: PLOT_WIDTH,
height: PLOT_HEIGHT_RATIO,
zoomType: 'x',
panning: true,
panKey: 'shift',
events: {
click: function(event) {
console.log("A click occurred on the spectrum : enter");
//self._viewLinker.setFluxDensityInSummedPopup(event.yAxis[0].value);
},
selection: function(event) {
//console.clear();
// overplot selected area in blue
this.xAxis[0].update({
plotBands: [{
from: event.xAxis[0].min,
to: event.xAxis[0].max,
color: 'rgba(68, 170, 213, .2)'
}]
});
//toggle-lines-search
self._spectroUI.hideEnergyGroupLines();
const velMin = Math.min(event.xAxis[0].min, event.xAxis[0].max);
const velMax = Math.max(event.xAxis[0].min, event.xAxis[0].max);
const obsFreqMin = v2f(velMax * 10 ** 3, FITS_HEADER.restfreq, FITS_HEADER.getVCenter()) / 10 ** 9;
const obsFreqMax = v2f(velMin * 10 ** 3, FITS_HEADER.restfreq, FITS_HEADER.getVCenter()) / 10 ** 9;
self.summedPixelsSpectrumChart.obsFreqMin = obsFreqMin;
self.summedPixelsSpectrumChart.obsFreqMax = obsFreqMax;
const graphs = [self._viewLinker.spectrumViewer.getSpectrumChartXAxis(),
self.getSummedPixelsSpectrumChartXAxis()
];
if (self.linePlotter === null) {
self.linePlotter = new LinePlotter(self._spectroUI);
}
if (self._spectroUI.isEnabled()) {
self.linePlotter.loadAndPlotLines(obsFreqMin, obsFreqMax, graphs);
}
// update global variable containing spectrum
self._updateSummedSlices(event.xAxis[0].min, event.xAxis[0].max);
let xData = [];
for (let i = 0; i < this.series[0].data.length; i++) {
xData.push(this.series[0].data[i].x);
}
let imin = getCalculatedIndex(event.xAxis[0].min);
let imax = getCalculatedIndex(event.xAxis[0].max);
self._selectedSurface = self._getSummedSpectrumValue(self._averageSpectrum, imin, imax);
self.setChartTitle(self._getTopTitle(self._selectedSurface, self._toptitle_unit, event.xAxis[0].min, event.xAxis[0].max, imin, imax));
self._spectroUI.setLineLuminosityValue(self._selectedSurface);
if (!self._spectroUI.isLineConfigurationOk()) {
alert("Lines can not be displayed, please verify that redshift and/or velocity is defined.");
}
return false;
}
}
},
boost: {
useGPUTranslations: true
},
xAxis: {
title: {
text: self._xtitle
},
crosshair: true,
reversed: false,
gridLineWidth: 1,
lineColor: '#FFFFFF'
},
yAxis: {
gridLineWidth: 1,
lineColor: '#FFFFFF',
lineWidth: 1,
opposite: true,
title: {
text: self._ytitle
}
},
plotOptions: {
series: {
step: 'center',
zoneAxis: 'x',
animation: {
duration: 0
},
lineWidth: PLOT_DEFAULT_LINE_WIDTH,
events: {
click: function(event) {
//console.clear();
console.log("A click occurred on the LINE : enter");
// Display slice at index sliceIndex
self._viewLinker.setFluxDensityInSummedPopup(event.point.y);
}
}
},
marker: {
radius: 0
}
},
tooltip: {
formatter: function() {
const index = getCalculatedIndex(this.x);
return 'Chan# ' + index + ' ( ' + this.x.toFixed(4) + ', ' + this.y.toFixed(4) + ')';
}
},
series: [{
color: PLOT_DEFAULT_COLOR
}]
}, function(chart) { // on complete \⇩
chart.myButton = chart.renderer.button('Save', 10, 10)
.attr({
zIndex: 3,
title : "Download the 1D spectrum in FITS format (readable in GILDAS/CLASS)"
})
.on('click', function(event) {
window.open(URL_ROOT + dataPaths.averageSpectrum, '_blank');
event.stopPropagation();
})
.add();
// send with samp
self._sampButton = chart.renderer.button('Samp', 60, 10)
.attr({
zIndex: 3,
title : "Send with SAMP"
})
.on('click', function(event) {
//console.log("A click occurred on the samp button : enter");
sAMPPublisher.sendSpectrumToAll(URL_ROOT + dataPaths.averageSpectrum, "Artemix");
event.stopPropagation();
})
.add();
//initially there is no samp connection
self._sampButton.hide();
});
}
/**
* 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) {
let xData = new Array(rlen);
for (var i = 0; i < rlen; i++) {
let tmp = linearTabulator(FITS_HEADER.crval3, FITS_HEADER.cdelt3, FITS_HEADER.crpix3, i + 1);
let tmpCenter = tmp - (FITS_HEADER.restfreq - FITS_HEADER.crval3) / FITS_HEADER.cdelt3; /*- FITS_HEADER.naxis3 / 2 * FITS_HEADER.cdelt3*/ ;
if (FITS_HEADER.cunit3 in UNIT_FACTOR) {
switch (FITS_HEADER.ctype3) {
case 'FREQ':
// centered at 0, restfreq = crval3, crval3 = 0
//let tmp1 = f2v(tmpCenter * UNIT_FACTOR[FITS_HEADER.cunit3], FITS_HEADER.crval3, 0) / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT['VRAD']];
const frequency = tmpCenter * UNIT_FACTOR[FITS_HEADER.cunit3];
const restfreq = FITS_HEADER.restfreq;
const vcenter = 0;
let tmp1 = f2v(frequency, restfreq, vcenter) / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT['VRAD']];
if (FITS_HEADER.cdelt3 > 0) {
xData[rlen - i - 1] = tmp1;
} else {
xData[i] = tmp1;
}
break;
case 'VRAD':
if (FITS_HEADER.cdelt3 > 0) {
xData[i] = tmp * UNIT_FACTOR[FITS_HEADER.cunit3] / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]];
} else {
xData[rlen - i - 1] = tmp * UNIT_FACTOR[FITS_HEADER.cunit3] / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]];
}
break;
// equivalent to VRAD
case 'VELO-LSR':
if (FITS_HEADER.cdelt3 > 0) {
xData[i] = tmp * UNIT_FACTOR[FITS_HEADER.cunit3] / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]];
} else {
xData[rlen - i - 1] = tmp * UNIT_FACTOR[FITS_HEADER.cunit3] / UNIT_FACTOR[DEFAULT_OUTPUT_UNIT[FITS_HEADER.ctype3]];
}
break;
case 'WAVE':
// alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
if (FITS_HEADER.cdelt3 > 0) {
xData[i] = tmp;
} else {
xData[rlen - i - 1] = tmp;
}
break;
case 'WAVN':
// alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
if (FITS_HEADER.cdelt3 > 0) {
xData[i] = tmp;
} else {
xData[rlen - i - 1] = tmp;
}
break;
case 'AWAV':
if (FITS_HEADER.cdelt3 > 0) {
xData[i] = tmp;
} else {
xData[rlen - i - 1] = tmp;
}
break;
default:
console.log("This should not happen");
alert("ctype3 case not recognized : " + FITS_HEADER.ctype3);
}
}
}
return xData;
}
/**
* 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, unit) {
switch (FITS_HEADER.ctype3) {
case 'FREQ':
if (FITS_HEADER.cdelt3 > 0) {
return revertArray(averageSpectrum);
} else {
return averageSpectrum;
}
// M33CO
case 'VRAD':
averageSpectrum = averageSpectrum.map(function(x) {
return x * unitRescale(unit);
});
if (FITS_HEADER.cdelt3 > 0) {
return averageSpectrum;
} else {
return revertArray(averageSpectrum);
}
// equivalent to VRAD
case 'VELO-LSR':
averageSpectrum = averageSpectrum.map(function(x) {
return x * unitRescale(unit);
});
if (FITS_HEADER.cdelt3 > 0) {
return averageSpectrum;
} else {
return revertArray(averageSpectrum);
}
case 'WAVE':
// alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
averageSpectrum = averageSpectrum.map(function(x) {
return x * unitRescale(unit);
});
if (FITS_HEADER.cdelt3 > 0) {
return averageSpectrum;
} else {
return revertArray(averageSpectrum);
}
case 'WAVN':
// alert("ctype3 case not implemented : " + FITS_HEADER.ctype3);
averageSpectrum = averageSpectrum.map(function(x) {
return x * unitRescale(unit);
});
if (FITS_HEADER.cdelt3 > 0) {
return averageSpectrum;
} else {
return revertArray(averageSpectrum);
}
case 'AWAV':
averageSpectrum = averageSpectrum.map(function(x) {
return x * unitRescale(unit);
});
if (FITS_HEADER.cdelt3 > 0) {
return averageSpectrum;
} else {
return revertArray(averageSpectrum);
}
default:
console.log("This should not happen");
alert("ctype3 case not recognized : " + FITS_HEADER.ctype3);
break;
}
}
/**
* 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) {
this.summedPixelsSpectrumChart = this._getChart();
showLoaderAction(true);
let self = this;
$.post("", {
"method": "getAverageSpectrum",
"relFITSFilePath": self._relFITSFilePath,
"iRA0": iRA0,
"iRA1": iRA1,
"iDEC0": iDEC0,
"iDEC1": iDEC1
}).done(function(resp) {
console.log("SummedPixelsSpectrumViewer : callback of getAverageSpectrum: entering ");
showLoaderAction(false);
if (resp["status"] == false) {
alert(`Something went wrong with the calculation of the average spectrum. The message was '${resp["message"]}'`);
} else {
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("Strange 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);
// change name of function
averageSpectrum = self._getYData(averageSpectrum, self._summedPixelsSpectrumUnit(FITS_HEADER.bunit));
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.summedPixelsSpectrumChart.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.summedPixelsSpectrumChart);
addYAxisSeries(self._viewLinker.spectrumViewer.spectrumChart);
if (self._viewLinker.summedPixelsSpectrumViewer.linePlotter != null && self.spectroUI.isEnabled()) {
self.linePlotter.loadAndPlotLines( self.linePlotter.obsFreqMin,
self.linePlotter.obsFreqMax,
[self.getSummedPixelsSpectrumChartXAxis(), self._viewLinker.spectrumViewer.getSpectrumChartXAxis()]);
}
let title = self._getSummedSpectrumValue(averageSpectrum, 0, averageSpectrum.length - 1);
self.setChartTitle(self._getTopTitle(title, self._toptitle_unit, self._summedData.x[0], self._summedData.x[averageSpectrum.length - 1], 0, averageSpectrum.length - 1));
}
console.log("SummedPixelsSpectrumViewer : callback of getAverageSpectrum: exiting ");
})
}
}
/**
* Returns a ViewLinker object that provides an interface to manipulate Spectrums and Slices
*
* @param {array} radecRange RA/DEC ranges corresponding to opened fits file
* @param {SpectroUI} spectroUI spectroscopy interface control object
* @param {SourceTable} sourceTable NED interface object
* @param {MarkerList} markerList list of markers
* @returns {ViewLinker}
*/
function getViewLinker(radecRange, spectroUI, sourceTable, markerList) {
let RADECRangeInDegrees = radecRange;
console.log("Data of '" + dataPaths.relFITSFilePath + "' are contained in " + JSON.stringify(radecRange));
let viewLinker = new ViewLinker(dataPaths, FITS_HEADER.width, FITS_HEADER.height, RADECRangeInDegrees, "slice",
"summed-slices", spectroUI);
// naxis3 is slice number
viewLinker.getAndPlotSingleSlice(0);
viewLinker.getAndPlotSummedSlices(0, FITS_HEADER.naxis3 - 1);
let spectrumViewer = new SpectrumViewer(dataPaths, 'spectrum', viewLinker);
viewLinker.setSpectrumViewer(spectrumViewer);
spectrumViewer.plot(FITS_HEADER.width / 2, FITS_HEADER.height / 2);
let summedPixelsSpectrumViewer = new SummedPixelsSpectrumViewer(dataPaths, 'summed-pixels-spectrum', viewLinker, spectroUI);
viewLinker.setSummedPixelsSpectrumViewer(summedPixelsSpectrumViewer);
summedPixelsSpectrumViewer.plot(0, FITS_HEADER.width - 1, 0, FITS_HEADER.height - 1);
//refresh lines display when redshift selection changes
sourceTable.addListener(summedPixelsSpectrumViewer);
sourceTable.addListener(viewLinker.summedSlicesImage);
sourceTable.addListener(viewLinker.singleSliceImage);
markerList.addListener(viewLinker.summedSlicesImage);
markerList.addListener(viewLinker.singleSliceImage);
if (withSAMP) {
// all implement setSampButtonVisible
setOnHubAvailability([ spectrumViewer,
summedPixelsSpectrumViewer,
viewLinker.singleSliceImage,
viewLinker.summedSlicesImage]);
}
return viewLinker;
}
export {
getViewLinker
}