import { ShapesFactory } from "./olqv_shapes.js";
import { FITS_HEADER } from "./fitsheader.js";
import { getProjection } from "./olqv_projections.js";
import { createAndAppendFromHTML, inRange, DecDeg2HMS, DecDeg2DMS, degToRad, HMS2DecDeg, DMS2DecDeg } from "./utils.js";
import { EventFactory } from './customevents.js'
import {MarkerButton} from './olqv_olbuttons.js';
class Marker{
constructor(RA, RAunit, DEC, DECunit, label){
this.RA = {"value" : RA, "unit" : RAunit};
this.DEC = {"value" : DEC, "unit" : DECunit};
this.label = {"value" : label};
}
}
/**
* Class representing an input list of markers
*/
class MarkerList {
/**
* @constructor
* @param {string} dataId id of element containing the list of markers
* @param {string} triggerId id of element triggering the list validation
* @param {string} clearId id of the element triggering a data clearing
*/
constructor(dataId, triggerId, clearId) {
// object listening events from this list
this._listeners = [];
this._markers = new Set([]);
this._dataElement = document.getElementById(dataId);
this._dataElement.value = "";
this._clearElement = document.getElementById(clearId);
this._triggerElement = document.getElementById(triggerId);
this._triggerElement.addEventListener("click", ()=>{this._updateMarkers()});
this._clearElement.addEventListener("click", () => { this._clearMarkers() });
}
/**
* Adds a markers in the list corresponding to an entry clicked in the NED table
* Action triggered by a SourceTable instance
*
* @param {Event} event event sent by a SourceTable instance
*/
sourceTableCall(event) {
if (!this._markers.has(event.detail.object)) {
console.log("### source table call");
console.log(event.detail.ra + " " + event.detail.dec);
console.log(DecDeg2HMS(event.detail.ra) + " " + DecDeg2DMS(event.detail.dec));
const source = DecDeg2HMS(event.detail.ra) + ";" + DecDeg2DMS(event.detail.dec) + ";" + event.detail.object;
if (this._dataElement.value.trim() !== "")
this._dataElement.value = this._dataElement.value + "\n" + source.trim();
else
this._dataElement.value = source.trim();
let marker = new Marker(event.detail.object.ra, "",
event.detail.object.dec, "",
event.detail.object.label);
this._markers.add(marker);
//this._markers.add(event.detail.object);
}
}
/**
* Adds an object listening to event from this object
* @param {Object} listener
*/
addListener(listener) {
this._listeners.push(listener);
}
/**
* Removes an object listening to event from this object
* @param {Object} listener
*/
removeListener(listener) {
for (let i = 0; i < this._listeners.length; i++) {
if (this._listeners[i] === listener) {
this._listeners.splice(i, 1);
return true;
}
}
}
/**
* Returns the event sent to listening objects
* It contains RA, DEC coordinates and a label of an object
*
* @returns {CustomEvent}
*/
_getEvent() {
let data = this._dataElement.value;
const lines = data.trim();
let markers = [];
if(lines !== ""){
for (let line of lines.split("\n")) {
const marker = line.split(";");
markers.push({ "ra": marker[0], "dec": marker[1], "label": marker[2] });
}
}
let event = EventFactory.getEvent(EventFactory.EVENT_TYPES.custom, {
detail: {
markers: markers,
action: "plot"
}
});
return event;
}
/**
* Notifies all listeners that the list has been updated
*/
_updateMarkers() {
const event = this._getEvent();
for (let l of this._listeners) {
try{
l.markerListUpdate(event);
}catch(error){
return;
}
}
}
/**
* Notifies all listeners that the list has been cleared
*/
_clearMarkers() {
this._dataElement.value = "";
this._markers.clear();
for (let l of this._listeners) {
l.markerListClear();
}
}
}
/**
* Display markers on a map, used in 3D
*/
class MarkerManager {
/**
* @constructor
* @param {Map} map target open layer Map object
* @param {boolean} isMultiMarkers displays only last marker if false, all markers if true
*/
constructor(map, isMultiMarkers) {
this._map = map;
this._multiMarkers = isMultiMarkers;
this._source = new ol.source.Vector();
this._layer = new ol.layer.Group({});
this._layer.getLayers().push(new ol.layer.Vector({ source: this._source, style: null }));
this._markers = new Set([]);
this._initialized = false;
}
getMarkers(){
return this._markers;
}
/**
* Adds a new marker on map
* @param {number} pixRa RA in pixels (int)
* @param {number} pixDec DEC in pixels (int)
* @param {string} label label of the marker
*/
addMarker(pixRa, pixDec, label) {
const marker_id = label.trim();
// remove previous markers
if (!this._multiMarkers) {
this._source.clear();
}
//this does not work if done in constructor, why ?
if (!this._initialized) {
this._map.addLayer(this._layer);
this._initialized = true;
}
if (!this._markers.has(marker_id)) {
let f = new ol.Feature({ geometry: new ol.geom.Point([pixRa, pixDec]) });
f.setStyle(this._getStyle(label));
this._source.addFeature(f);
this._markers.add(marker_id);
this._source.refresh();
}
}
/**
* Removes all markers
*/
clearMarkers() {
this._source.clear();
this._markers.clear();
this._source.refresh();
}
/**
* Returns a Style object defining a marker
* @param {string} label
* @returns {Style} an open layers Style object
*/
_getStyle(label) {
return new ol.style.Style({
image: new ol.style.Circle({
radius: 4,
stroke: new ol.style.Stroke({
color: '#3399CC',
width: 2
}),
fill: new ol.style.Fill({
color: "magenta"
})
}),
text: new ol.style.Text({
font: '16px Calibri,sans-serif',
fill: new ol.style.Fill({ color: '#000' }),
stroke: new ol.style.Stroke({
color: '#fff',
width: 2
}),
offsetX: 10,
offsetY: -10,
text: label
})
});
}
}
/**
* A class to create and to manage markers in the 2D part of YAFITS. A marker is basically a record defined by a position, the pixel value at that position and a label on a 2D map. With YAFITS on a 2D map, one can use two possible interfaces to work on the collection of markers.
* <ul>
* <li>The graphic interface displays the markers above the map. It uses the {@link https://openlayers.org/en/v5.3.0/apidoc/| OpenLayers API}. </li>
* <li>The spreadsheet interface offers an "<i>à la Excel</i>" interaction with the collection. It uses the {@link https://bossanova.uk/jspreadsheet/v3/| Jexcel API}. </li>
* </ul>
* Both interfaces allow to edit the collection of markers.
* As announced above, a marker is a aggregate of informations which is implemented in a dictionary whose keys are defined as follows.
* <ul>
* <li><tt>id</tt> (integer) - an id for that marker. Two # markers have # ids. id == -1 invalid or empty marker.</li>
* <li><tt>label</tt> (string) - a label for that marker. No constraint.</li>
* <li><tt>RA</tt> (string) - a valid representation of a right ascension.</li>
* <li><tt>DEC</tt> (string) - a valid representation of a declination.</li>
* <li><tt><i>type</i> (<i>unit</i>)</tt> (number) - the pixel value. The field is named after FITS <tt>BTYPE</tt> and <tt>BUNIT</tt>.</li>
* </ul>
*
* <dl>
* <dt> On the spreadsheet </dt> <dd> a marker is stored in one row of a {@link https://bossanova.uk/jspreadsheet/v3/| Jexcel API} table with one value associated to one key per column. </dd>
* <dt> On the graphics interface </dt> <dd> a marker is stored in an OpenLayers {@link https://openlayers.org/en/v5.3.0/apidoc/module-ol_Feature-Feature.html|feature} </dd>
* </dl>
*
* It's the software responsibility to maintain the respective contents of the jexcel sheet and the collection of features dedicated to makers in sync.
* @extends ShapesFactory
*/
class MarkersFactory extends ShapesFactory {
static enter(what) {
//console.group(this.name + "." + what);
}
static exit() {
//console.groupEnd();
}
/**
* @constructor
* Creates an instance.
*
* @see {@link ./Viewer.html|Viewer}
* @param {Viewer} viewer instance of Viewer class that will host the markers.
*/
constructor(viewer) {
super(viewer, 'marker');
MarkersFactory.enter(this.constructor.name);
let self = this;
let context = this.infosBlock.getContext();
this.btype = context['array-phys-chars']['type'];
this.bunit = context['array-phys-chars']['unit'];
this.pixelValueColumnName = `${this.btype} (${this.bunit})`;
this.mode = this;
this.modalMarkersFormId = `ModalMarkersForm-${this.viewerId}`;
this.overlayId = `markersOverlay-${this.viewerId}`;
this.textOfMarkersId = `text_of_markers-${this.viewerId}`;
this.clearMarkersId = `clear_markers-${this.viewerId}`;
this.copyVerbatimId = `copy_verbatim-${this.viewerId}`;
this.copyAsJSONId = `copy_as_json-${this.viewerId}`;
this.refreshMarkersDisplayId = `refresh_markers_display-${this.viewerId}`;
this.closeMarkersId = `close_markers-${this.viewerId}`;
//this.raLabelFormatter = this.viewer.getCoordinatesFormatter().getRaLabelFormatter();
//this.decLabelFormatter = this.viewer.getCoordinatesFormatter().getDecLabelFormatter();
//this.raDDtoPixelConverter = this.viewer.getCoordinatesFormatter().getRaDDtoPixelConverter();
//this.decDDtoPixelConverter = this.viewer.getCoordinatesFormatter().getDecDDtoPixelConverter();
this.projection = getProjection(FITS_HEADER.projectionType);
this.indexToMarker = {};
this.selectedRowIndexes = [];
this.layer.setZIndex(13);
this.buttonObject = new MarkerButton(this.viewer);
this.seetablebutton = document.createElement("button");
this.seetablebutton.setAttribute("class", "btn btn-primary btn-sm");
this.seetablebutton.setAttribute("data-tooltip", "tooltip");
this.seetablebutton.setAttribute("title", "Markers in a spreadsheet");
this.seetablebutton.append(" See markers table");
this.seetablebutton.onclick = () => { $(`#${this.overlayId}`).css("height", "100%"); };
this.interaction = new ol.interaction.Draw({ source: this.source, type: 'Point' });
let f = function () {
if (self.isOpened) {
self.close();
self.buttonObject.unsetActive();
} else {
self.open(self.interaction, 'crosshair');
self.buttonObject.setActive();
}
}
this.buttonObject.setClickAction(f);
this.numOfMarkers = 0;
this.markerIndex = 0;
this.data = [];
this.html = `
<div class="overlay" style="overflow:hidden" id="${this.overlayId}">
<div class="row">
<div class="col">
<h1> Markers spreadsheet </h1>
<hr/>
</div>
</div>
<div class="row">
<div class="col">
<div class="btn-toolbar" role="group" aria-label="Markers actions">
<button type="button" class="btn btn-success ml-1" id="${this.refreshMarkersDisplayId}">Refresh display</button>
<button type="button" class="btn btn-success ml-1" id="${this.clearMarkersId}">Clear</button>
<button type="button" class="btn btn-primary btn-sm fas fa-files-o ml-1" id="${this.copyVerbatimId}" data-tooltip="tooltip" title="Copy verbatim"></button>
<button type="button" class="btn btn-primary btn-sm ml-1" id ="${this.copyAsJSONId}">Copy as JSON</button>
<button type="button" class="btn btn-primary ml-1" id="${this.closeMarkersId}">Back to image</button>
</div>
</div>
</div>
<div class="row">
<div class="col">
<hr/>
<div id="${this.textOfMarkersId}"></div>
<hr/>
</div>
</div>
<textarea style="width:0px;height:0px;opacity:0"></textarea>
</div>
`;
this.body = $("body")[0];
this.overlay = createAndAppendFromHTML(this.html, this.body);
this.hiddenInfosBlock = this.overlay.querySelector("textarea");
/**
* @member {dictionary[]} - Defines the title, type, width in pixels, name and read/writability of the jexcel sheet's columns.
*/
this.columns = [
{ title: 'id', type: 'numeric', width: '50px', name: 'id', readOnly: true },
{ title: 'label', type: 'text', width: '100px', name: 'label', readOnly: false },
{ title: 'RA', type: 'text', width: '150px', name: 'RA', readOnly: false },
{ title: 'DEC', type: 'text', width: '150px', name: 'DEC', readOnly: false },
{ title: 'iRA', type: 'text', width: '150px', name: 'iRA', readOnly: true },
{ title: 'iDEC', type: 'text', width: '150px', name: 'iDEC', readOnly: true },
{ title: this.pixelValueColumnName, type: 'numeric', width: '150px', name: this.pixelValueColumnName, readOnly: true }
];
this.fieldNames = this.columns.map((o) => { return o.title });
this.jexcel = this.createJexcel(`${this.textOfMarkersId}`);
this.n2i = {};
this.n2i = { 'id': 0, 'label': 1, 'RA': 2, 'DEC': 3 };
this.n2i[this.pixelValueColumnName] = 4;
/*
** What is happening when a point has been drawn ( with a simple click ) ?
*/
this.interaction.on("drawend", event => {
MarkersFactory.enter("drawend callback");
this.getMarkerFluxAndRecord(event.feature);
/*const [iRA, iDEC] = event.feature.getGeometry().getCoordinates();
const [iRAMax, iDECMax] = this.viewer.maxIndexes;
if ((inRange(iRA, 0, iRAMax)) && inRange(iDEC, 0, iDECMax)) {
var p = this.getPixelValuePromise(iRA, iDEC, this.viewer.getRelFITSFilePath());
p.then(
result => {
this.recordMarker(event.feature, result);
this.numOfMarkers += 1
},
error => { alert(error) }
)
} else {
alert(`Coordinates (${iRA}, ${iDEC} out of field`);
this.remove(event.feature);
}*/
MarkersFactory.exit();
});
$(`#${this.textOfMarkersId}`).bind('input propertychange', () => {
$("#place_markers").attr('disabled', true);
});
$(`#${this.refreshMarkersDisplayId}`).click(() => {
MarkersFactory.enter("refreshMarkers")
this.deleteMarkerFeatures(this.getAllMarkerFeatures());
this.jexcel2OL();
MarkersFactory.exit();
});
$(`#${this.clearMarkersId}`).click(() => {
$(`#${this.textOfMarkersId}`).val('');
this.deleteMarkerFeatures(this.getAllMarkerFeatures());
this.indexToMarker = {};
this.markerIndex = 0;
this.data = [];
this.jexcel.setData(this.data);
});
$(`#${this.copyVerbatimId}`).click(() => {
MarkersFactory.enter("copy verbatim");
let context = { "file": `${this.viewer.getRelFITSFilePath()}` }
if (this.viewer.is3D()) {
context['slice range'] = `${this.viewer.getSliceRange()}`;
}
let collection = this.getMarkersList();
this.hiddenInfosBlock.value = `Markers\r\n\r\n`;
this.hiddenInfosBlock.value += this.infosBlock.preamble2TXT();
collection.forEach(element => {
for (var k in element) {
const newLocal = `${k}:${this.format(element[k]["value"])} ${element[k]["unit"]}\r\n`;
this.hiddenInfosBlock.value += newLocal;
}
this.hiddenInfosBlock.value += '\r\n';
});
this.hiddenInfosBlock.select();
try {
var success = document.execCommand('copy');
} catch (err) {
alert("Copy failed. " + err);
}
MarkersFactory.exit();
});
$(`#${this.copyAsJSONId}`).click(() => {
MarkersFactory.enter("copy as JSON");
let collection = this.getMarkersList();
//let data = this.getDataAsJSON();
this.hiddenInfosBlock.value = JSON.stringify($.extend(this.infosBlock.preamble2DICT(), { 'markers': collection }), 0, 4);
this.hiddenInfosBlock.select();
try {
var success = document.execCommand('copy');
} catch (err) {
alert("Copy failed. " + err);
}
MarkersFactory.exit();
});
$(`#${this.closeMarkersId}`).click(() => { $(`#${this.overlayId}`).css("height", "0%") });
/*
** This the behaviour when a marker has been selected.
*/
this.selector.select['Point'] = (feature) => {
MarkersFactory.enter("A marker is selected");
this.selected(feature);
MarkersFactory.exit();
};
/*
** This the behaviour when a marker has been unselected.
*/
this.selector.unselect['Point'] = (feature) => {
MarkersFactory.enter("A marker is unselected");
this.unselected(feature);
MarkersFactory.exit();
};
/*
** This is the behaviour when a selected marker is going to be removed.
*/
this.selector.remove['Point'] = (f) => {
ShapesFactory.enter("A selected marker is about to be removed");
this.removeFromJExcel(f);
this.remove(f);
ShapesFactory.exit();
};
MarkersFactory.exit();
}
getMarkerFluxAndRecord(feature){
const [iRA, iDEC] = feature.getGeometry().getCoordinates();
const [iRAMax, iDECMax] = this.viewer.maxIndexes;
if ((inRange(iRA, 0, iRAMax)) && inRange(iDEC, 0, iDECMax)) {
var p = this.getPixelValuePromise(iRA, iDEC, this.viewer.getRelFITSFilePath());
p.then(
result => {
this.recordMarker(feature, result);
this.numOfMarkers += 1;
this.map.render();
},
error => {
if(error.includes("IndexError"))
// IndexError message from python server
alert(error.split("-")[1].split("'")[1]);
else
// any other error
alert(error);
}
)
} else {
alert(`Coordinates (${iRA}, ${iDEC} out of field`);
//this.remove(feature);
}
}
addMarker(iRa, iDec, label){
let f = new ol.Feature({ geometry: new ol.geom.Point([iRa, iDec]) });
f.set("label", label);
f.setStyle(this.style_f(f));
return f;
}
/**
* Returns an array of objects representing markers with all their properties
* @returns array
*/
getMarkersList(){
let collection = [];
let data = this.getDataAsJSON();
for(let marker of data){
let d = {};
for(let prop of Object.keys(marker)){
let unit = "";
if(prop === 'iRA' || prop === 'iDEC')
unit = "pixels";
d[prop] = {"value" : marker[prop], "unit": unit };
}
collection.push(d);
}
return collection;
}
/**
* Behaviour when an instance is activated.
*
* @see {@link https://openlayers.org/en/v5.3.0/apidoc/module-ol_interaction_Interaction-Interaction.html|Interaction }
* @see {@link https://developer.mozilla.org/fr/docs/Web/CSS/cursor | cursor}
* @param {Interaction} interaction
* @param {cursor} cursor
*/
open(interaction, cursor) {
MarkersFactory.enter(this.open.name);
super.open(interaction, cursor);
this.infosBlock.getInfosLine().append(this.seetablebutton);
MarkersFactory.exit();
}
/**
* Utility. Transforms an array into a dictionary. The values are taken in a, the keys in this.fieldnames
*
* Requires that condition is that a and fieldnames have the same length. Otherwise an empty dictionary
* is returned.
*
* @param {any[]} a
* @returns {dictionary}
*/
a2O(a) {
MarkersFactory.enter(this.a2O.name);
let a2O_result = {};
if (this.fieldNames.length == a.length) {
for (let i = 0; i < a.length; i++) {
a2O_result[this.fieldNames[i]] = a[i];
}
}
MarkersFactory.exit();
return a2O_result;
}
/**
* Utility. Transforms a jexcel spreadsheet as returned by {@link https://bossanova.uk/jspreadsheet/v3/docs/quick-reference| getData}, i.e. an array of arrays, into an array of dictionaries.
* @returns {dictionary[]}
*/
getDataAsJSON() {
MarkersFactory.enter(this.getDataAsJSON.name);
let json_result = [];
let data = this.jexcel.getData();
for (let i = 0; i < data.length; i++) {
let result = this.a2O(data[i]);
json_result.push(result);
}
MarkersFactory.exit();
return json_result;
}
/**
* Callback. Defines the {@link https://bossanova.uk/jspreadsheet/v3/docs/quick-reference| onselection} behaviour
* of the spreadsheet after some selection has been performed. Here the 'id' field(s) of the selected row(s) are recorded.
*
* @param {jexcel-sheet} sheet
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
*/
noteSelectedRows(sheet, x1, y1, x2, y2) {
MarkersFactory.enter(this.noteSelectedRows.name);
this.selectedRowIndexes = this.jexcel.getSelectedRows(true).map(i => this.jexcel.getRowData(i)[this.n2i['id']]);
this.selectedRowIndexes = this.selectedRowIndexes.filter(i => i != -1);
MarkersFactory.exit();
}
/**
* Callback. Defines the {@link https://bossanova.uk/jspreadsheet/v3/docs/quick-reference| oninsertrow} behaviour
* once a new (empty) row has been inserted. Here the 'id' of that row is forced to -1.
* @param {jexcel-sheet} sheet not used in this implementation
* @param {number} rowNumber the index number of the row to be inserted.
* @param {number} numOfRows not used in this implementation
* @param {number} rowRecords not used in this implementation
* @param {boolean} insertBefore how the insertion is performed.
*/
setId(sheet, rowNumber, numOfRows, rowRecords, insertBefore) {
MarkersFactory.enter(this.setId.name);
var i = insertBefore ? rowNumber : rowNumber + 1;
this.data = this.getDataAsJSON();
this.data[i]["id"] = -1;
this.data[i][this.pixelValueColumnName] = "undefined";
this.jexcel.setData(this.data);
MarkersFactory.exit();
}
/**
* Callback. Defines the {@link https://bossanova.uk/jspreadsheet/v3/docs/quick-reference| onbeforechange} behaviour (i.e. just before a change is performed).
* @param {jexcel-sheet} sheet not used in this implementation
* @param {jexcel-row} cell not used in this implementation
* @param {number} x not used in this implementation
* @param {number} y not used in this implementation
* @param {any} value not used in this implementation
*/
b4change(sheet, cell, x, y, value) {
MarkersFactory.enter(this.b4change.name);
MarkersFactory.exit();
}
/**
* Utility. Checks the content of a row considered as a dictionary. Here it verifies
* that RA and DEC fields are valid strings to express right ascension and declination.
* @param {dictionary} row
* @returns {boolean}
*/
validateRow(row) {
MarkersFactory.enter(this.validateRow.name);
var result = this.checkRADEC(row.RA, row.DEC);
MarkersFactory.exit();
return result;
}
/**
* Promise. Connect the get pixel value asynchronous calls
* with the consumer of the result to these calls.
*
* @param {number} iRA - the integer first coordinate of the pixel of interest
* @param {number} iDEC - the integer second coordinate of the pixel of interest
* @param {string} relFITSFilePath - the FITS file path
*/
getPixelValuePromise(iRA, iDEC, relFITSFilePath) {
MarkersFactory.enter(this.getPixelValuePromise.name);
MarkersFactory.exit();
var viewer = this.viewer;
return new Promise(function (resolve, reject) {
var numDims = viewer.numDims();
switch (numDims) {
case 2:
var url = "getPixelValueAtiRAiDEC";
params = "?relFITSFilePath=" + relFITSFilePath + "&iRA=" + Math.round(iRA) + "&iDEC=" + Math.round(iDEC);
break;
case 3:
var iFreq = viewer.getSliceRange();
if (typeof iFreq != "number") {
reject(`Invalid value of iFreq (${iFreq}). An integer is expected `);
}
var url = "getPixelValueAtiFreqiRAiDEC";
var params = "?relFITSFilePath=" + relFITSFilePath + "&iRA=" + Math.round(iRA) + "&iDEC=" + Math.round(iDEC) + "&iFREQ=" + Math.round(iFreq);
break;
default:
reject(`Can't proceed with ${numDims}D data`);
}
$.get(url + params, (data, status, xhr) => {
MarkersFactory.enter("Back from GET pixel value");
if (status != "success") {
reject("A problem occurred with the request/server");
} else {
if (!data["status"]) {
reject(data["message"]);
} else {
resolve(data["result"]);
}
}
MarkersFactory.exit();
});
});
}
/**
*
* Callback. Defines the {@link https://bossanova.uk/jspreadsheet/v3/docs/quick-reference| onchange} behaviour (i.e. just when a change occurred).
* @param {jexcel-sheet} sheet - the sheet where change occurred
* @param {jexcel-cell} cell - the cell where change occurred
* @param {number} x - the column number
* @param {number} y - the row number
* @param {any} - the new value
*/
changed(sheet, cell, x, y, value) {
MarkersFactory.enter(this.changed.name);
this.data = this.getDataAsJSON();
const [iRAMax, iDECMax] = this.viewer.maxIndexes;
let row = this.data[y];
let self = this;
var p = new Promise((resolve, reject)=>{
const { RA, DEC } = this.validateRow(row);
const iRaiDec = this.projection.raDecToiRaiDec(RA.decdegree, DEC.decdegree);
// if RA/DEC values have been modified
row.iRA = iRaiDec.iRa;
row.iDEC = iRaiDec.iDec;
if (RA && DEC) {
if (inRange(iRaiDec.iRa, 0, iRAMax) && inRange(iRaiDec.iDec, 0, iDECMax)) {
var q = self.getPixelValuePromise(iRaiDec.iRa, iRaiDec.iDec, self.viewer.getRelFITSFilePath());
q.then(
result => {
resolve(result)
},
message => {
reject(message)
}
);
} else {
reject(`Coordinates (${row.RA}, ${row.DEC} out of field`);
}
} else {
reject("Mysterious !")
}
}).then(
result => {
row[this.pixelValueColumnName] = result["value"];
//let context = this.infosBlock.getContext();
console.dir(row);
if (row.id == -1) {
row.id = this.markerIndex;
this.markerIndex += 1;
}
},
error => {
console.log(error);
if (error.length > 0) alert(error);
row.id = -1;
row[this.pixelValueColumnName] = undefined;
}
).finally(() => {
this.data[y] = row;
this.jexcel.setData(this.data);
this.jexcel2OL();
});
MarkersFactory.exit();
}
/**
* Callback. Defines the {@link https://bossanova.uk/jspreadsheet/v3/docs/quick-reference| onload} behaviour (i.e. setData).
*
*/
loaded() {
MarkersFactory.enter(this.loaded.name);
$(".jexcel > tbody > tr > td.readonly").css("color", "purple");
MarkersFactory.exit();
}
/**
*
* Callback. Defines the {@link https://bossanova.uk/jspreadsheet/v3/docs/quick-reference| ondeleterow} behaviour.
*/
deleteSelectedMarkers() {
MarkersFactory.enter(this.deleteSelectedMarkers.name);
this.data = this.getDataAsJSON();
var aof = this.selectedRowIndexes.map((index) => { return this.indexToMarker[index] });
this.deleteMarkerFeatures(aof);
for (var i = 0; i < this.selectedRowIndexes.length; i++) {
delete this.indexToMarker[this.selectedRowIndexes[i]];
}
this.selectedRowIndexes = [];
MarkersFactory.exit();
}
/**
* Utility. Produces a graphic representation of the collection of markers from the content of the jexcel sheet.
* (Jexcel to OpenLayers)
*/
jexcel2OL() {
MarkersFactory.enter(this.jexcel2OL.name);
// Delete all the markers present on the OL view
this.deleteMarkerFeatures(this.getAllMarkerFeatures());
this.numOfMarkers = 0;
this.indexToMarker = {};
// For each valid row in the jexcel sheet create a marker on the OL view
var validRows = this.getDataAsJSON().filter(x => x.id != -1);
let self = this;
validRows.forEach(row => {
var RAinDD = self.checkRA(row.RA).decdegree;
var DECinDD = self.checkDEC(row.DEC).decdegree;
if (RAinDD && DECinDD) {
var label = row.label;
var f = new ol.Feature({ geometry: new ol.geom.Point([row.iRA, row.iDEC]) });
f.set("label", label);
f.setStyle(this.style_f(f));
self.numOfMarkers += 1;
self.source.addFeature(f);
self.indexToMarker[row.id] = f;
}
});
this.source.refresh({force:true});
this.source.changed();
this.layer.changed();
this.map.render();
MarkersFactory.exit();
}
/**
* Creates an empty jexcel sheet and defines its behaviour.
*
* <ul>
* <li> The sheet will be a child of <tt>parentId</tt>.</li>
* <li> Its columns are defined by [MarkersFactory's columns member]{@link MarkersFactory#columns}
* @param {string} parentId - the id of parent element.
* </ul>
*/
createJexcel(parentId) {
MarkersFactory.enter(this.createJexcel.name);
var result = jexcel(document.getElementById(parentId), {
columns: this.columns,
allowInsertRow: true,
onload: this.loaded.bind(this),
onselection: this.noteSelectedRows.bind(this),
ondeleterow: this.deleteSelectedMarkers.bind(this),
oninsertrow: this.setId.bind(this),
onchange: this.changed.bind(this),
onbeforechange: this.b4change.bind(this)
});
MarkersFactory.exit();
return result;
}
/**
* Utility. Returns the toolbox button that will activate this instance.
* @returns {button}
*/
getButtonObject() {
return this.buttonObject;
}
/**
*
* @returns a {@link https://developer.mozilla.org/fr/docs/Web/HTML/Element/Button|button}
*/
markersCount() {
MarkersFactory.enter(this.markersCount.name);
var result = this.source.getFeatures().filter(f => f.getGeometry().getType() == 'Point').length;
MarkersFactory.exit();
return result;
}
/**
* Utility. Record a new marker after a click on the graphic interface.
* Both graphic and jexcel interfaces are updated.
*
* @param {event} event - the click event that triggers the marker's creation
* @param {dictionary} flux - a dictionary {"value":.., "unit":..., "type:..."}
*/
recordMarker(feature, flux) {
MarkersFactory.enter(this.recordMarker.name);
let coordinates = feature.getGeometry().getCoordinates();
let label = `m_${this.numOfMarkers}`;
if(feature.get("label") !== undefined)
label = label + "-" + feature.get("label");
feature.set('label', label);
// get ra/dec coorinates with projection
const radec = this.projection.iRaiDecToHMSDMS(coordinates[0], coordinates[1]);
feature.set('RA', radec['ra']);
feature.set('DEC', radec['dec']);
feature.set(this.btype, flux["value"]);
feature.setId(this.markerIndex);
feature.setStyle(this.style_f(feature));
let properties = {};
let measurements = {};
measurements["iRA"] = { "value": Math.round(coordinates[0]), "unit": "pixel" };
measurements["iDEC"] = { "value": Math.round(coordinates[1]), "unit": "pixel" };
measurements["RA"] = { "value": radec['ra'], "unit": "HMS" };
measurements["DEC"] = { "value": radec['dec'], "unit": "DMS" };
measurements[this.btype] = flux;
properties["measurements"] = measurements;
feature.set("properties", properties);
this.indexToMarker[this.markerIndex] = feature;
let d = {
'id': this.markerIndex,
'RA': radec['ra'],
'DEC': radec['dec'],
'iRA' : Math.round(coordinates[0]),
'iDEC' : Math.round(coordinates[1]),
'label': label
};
d[this.pixelValueColumnName] = flux["value"];
this.data.push(d);
this.jexcel.setData(this.data);
this.markerIndex += 1;
MarkersFactory.exit();
}
/**
* Utility. Removes a marker, i.e. one row, from the jexcel sheet given its OpenLayers counterpart (feature).
* @param {feature} markerFeature
*/
removeFromJExcel(markerFeature) {
MarkersFactory.enter(this.removeFromJExcel.name);
let id = markerFeature.getId();
this.data = this.getDataAsJSON();
const equid = (element) => element.id == id;
let index = this.data.findIndex(equid);
if (index != -1) {
this.data.splice(index, 1);
this.jexcel.setData(this.data);
}
MarkersFactory.exit();
}
// Still to be worked on.
addMarkers(arrayOfMarkers) {
MarkersFactory.enter(this.addMarkers.name);
for (let i = 0; i < arrayOfMarkers.length; i++) {
const raInRad = degToRad(arrayOfMarkers[i]["RAInDD"]);
const decInRad = degToRad(arrayOfMarkers[i]["DECInDD"]);
let result = this.projection.raDecToiRaiDec(arrayOfMarkers[i]["RAInDD"], arrayOfMarkers[i]["DECInDD"]);
//var [iRA, iDEC] = this.absToPixelConverter(arrayOfMarkers[i]["RAInDD"], arrayOfMarkers[i]["DECInDD"]);
//this.addMarker([result["x"], result["y"]], arrayOfMarkers[i]["label"]);
console.log("iRA iDec", result["x"], result["y"]);
let f = new ol.Feature({ geometry: new ol.geom.Point([result["iRa"], result["iDec"]]) });
f.set("label", arrayOfMarkers[i]["label"]);
f.setStyle(this.style_f(f));
this.source.addFeature(f);
//this.addMarker(result.iRa, result.iDec, arrayOfMarkers[i]["label"]);
this.getMarkerFluxAndRecord(f);
//this.recordMarker(f, {"value":10});
//this.numOfMarkers += 1;
}
//this.source.refresh();
//this.layer.changed();
//this.map.render();
MarkersFactory.exit()
}
/**
* Utility. Return an array of all the OpenLayers features representing markers.
* @returns {feature[]}
*/
getAllMarkerFeatures() {
MarkersFactory.enter(this.getAllMarkerFeatures.name)
var allMarkers = [];
this.source.getFeatures().forEach(function (feature) {
if (feature.getGeometry().getType() == 'Point') {
allMarkers.push(feature);
}
});
MarkersFactory.exit();
return allMarkers;
}
/**
* Utility. Delete OpenLayers features representing markers.
* @param {feature[]} arrayOfFeatures the features to delete
*/
deleteMarkerFeatures(arrayOfFeatures) {
MarkersFactory.enter(this.deleteMarkerFeatures.name);
for (var i = 0; i < arrayOfFeatures.length; i++) {
this.remove(arrayOfFeatures[i]);
}
this.source.refresh();
MarkersFactory.exit();
}
// Still to be worked on.
matchAndEvaluate(value, pat) {
MarkersFactory.enter(this.matchAndEvaluate.name);
var result = value.match(pat);
if (result) result = result.map(function (elem, index) {
if (index > 0) return Number(elem);
else return elem;
});
MarkersFactory.exit();
return result;
}
// Not used apparently
/*RAtoString(RA) {
return RA.hour + "h" + RA.minute + "m" + RA.second + "s";
}
// Not used apparently
RAtoDecimalDegree(RA) {
return RA.hour * 15 + RA.minute / 4 + RA.second / 240;
}*/
/**
* Utility. Check that a string expresses correctly a Right Ascension.
* @param {string} RAText
* @returns a dictionary {hour:.., minute:..., second:..., decdegree:...} if yes and null if no (decdegree === decimal degree)
*/
checkRA(RAText) {
MarkersFactory.enter(this.checkRA.name);
var RA = { hour: 0, minute: 0, second: 0, degree: 0, decdegree: 0 };
var value = RAText;
var pat = /(\d{1,2})[hH:\s]\s*(\d{1,2})[mM:\s]\s*(\d{1,2}(\.\d+)?)[sS]?/;
var result = this.matchAndEvaluate(value, pat);
if (result &&
(result[1] < 24) &&
(result[2] < 60) &&
(result[3] < 60)) {
RA.hour = result[1];
RA.minute = result[2];
RA.second = result[3];
RA.decdegree = RA.hour * 15 + RA.minute / 4 + RA.second / 240;
RA.rad = degToRad(RA.decdegree);
result = RA;
} else {
pat = /(\d{1,3})\.(\d+)/;
result = this.matchAndEvaluate(value, pat);
if (result) {
RA.decdegree = parseFloat(value);
RA.rad = degToRad(RA.decdegree);
RA.hour = Math.floor(RA.decdegree / 15);
RA.minute = Math.floor((RA.decdegree / 15 - RA.hour) * 60);
RA.second = (RA.decdegree / 15 - RA.hour - RA.minute / 60) * 3600;
RA.second = Math.floor(RA.second * 1000) / 1000.;
XPathResult = RA;
result = RA
} else {
alert("Wrong input for RA. Valid symbols for hours : [hH: ], minutes : [mM: ] and seconds : [sS]");
result = null;
}
}
if (result == null) {
alert("Wrong input for RA. Valid symbols for hours : [hH: ], minutes : [mM': ] and seconds : [sS\"]");
}
MarkersFactory.exit();
return result;
}
// Not used apparently
/*DECtoString(DEC) {
return (DEC.negative ? '-' : '') + DEC.degree + "\xB0" + DEC.arcminute + "'" + DEC.arcsecond + "\"";
}
// Not used apparently
DECtoDecimalDegree(DEC) {
var result = DEC.degree + DEC.arcminute / 60. + DEC.arcsecond / 3600.
if (DEC.negative) result = -result;
return result;
}*/
/**
* Utility. Check that a string expresses correctly a Declination.
* @param { string } DECText
* @returns {dictionary} a dictionary { degree:.., minute:..., second:..., decdegree:... } if yes and null if no (decdegree === decimale degree)
*/
checkDEC(DECText) {
MarkersFactory.enter(this.checkDEC.name);
var DEC = { negative: false, degree: 0, arcminute: 0, arcsecond: 0, decdegree: 0. };
var value = DECText.trim();
var pat = /-?(\d{1,2})[dD\xB0:\s]\s*(\d{1,2})['mM:\s]\s*(\d{1,2}(\.\d+)?)[s"]?/;
var result = this.matchAndEvaluate(value, pat);
if (result &&
((result[1] == 90 && result[2] == 0 && result[3] == 0) ||
(result[1] < 90 && result[2] < 60 && result[3] < 60))) {
if (result[0][0] == '-' || result[0].charCodeAt(0) == 8722 || result[0].charCodeAt(0) == 45) {
DEC.negative = true;
}
DEC.degree = result[1];
DEC.minute = result[2];
DEC.second = result[3];
DEC.decdegree = (DEC.degree + DEC.minute / 60. + DEC.second / 3600.) * (DEC.negative ? -1 : 1);
DEC.rad = degToRad(DEC.decdegree);
result = DEC;
} else {
if (value.charCodeAt(0) == 8722 || value.charCodeAt(0) == 45) { value = "-" + value.slice(1); }
var pat1 = /-?(\d{1,3}(\.\d+)?)/;
var result1 = this.matchAndEvaluate(value, pat1);
if (result1) {
DEC.decdegree = result1[1];
DEC.rad = degToRad(DEC.decdegree);
if (value[0] == "-") {
DEC.decdegree = -DEC.decdegree;
}
DEC.negative = DEC.decdegree < 0.0;
//
DEC.degree = Math.abs(Math.floor(DEC.decdegree));
DEC.minute = Math.floor((DEC.decdegree - DEC.degree) * 60);
DEC.second = (DEC.decdegree - DEC.degree - DEC.minute / 60) * 3600;
if (DEC.second < 0.) DEC.second = 0.;
if (DEC.second > 60) DEC.second = 60.;
result = DEC;
}
}
if (result == null) {
alert("Wrong input for DEC. Valid symbols for degrees : [\xB0dD: ], minutes : [mM': ] and seconds : [sS\"]");
}
MarkersFactory.exit();
return result;
}
/**
* Utility. Check that a pair of string expresses correctly a pair (Right Ascension, Declination)
* @param {string} RAText
* @param {string} DECText
* @returns {dictionary} a dictionary { RA: result of checkRA(RAText), DEC: result of checkDEC(DECText)}
*/
checkRADEC(RAText, DECText) {
MarkersFactory.enter(this.checkRADEC.name);
var result = { 'RA': this.checkRA(RAText), 'DEC': this.checkDEC(DECText) };
MarkersFactory.exit();
return result;
}
enablePlaceMarkers() {
$(`#${this.refreshMarkersDisplayId}`).attr('disabled', false);
}
// Not used apparently.
parseTextOfMarkers() {
MarkersFactory.enter(this.parseTextOfMarkers.name);
var markers = [];
var status = true;
var content = $(`#${this.textOfMarkersId}`).val();
var lines = content.split(/\r|\r\n|\n/);
for (var i = 0; i < lines.length; i++) {
let line = lines[i].trim();
if (line.length > 0) {
var fields = line.split(",");
if (fields.length < 2) {
alert("Line '" + line + "' looks incomplete");
status = false;
break;
}
var label = "" + i;
if (fields.length >= 3) {
label = fields[2];
}
var RA = null;
var DEC = null;
if ((RA = this.checkRA(fields[0])) && (DEC = this.checkDEC(fields[1]))) {
markers.push({ "RAInDD": RA.decdegree, "DECInDD": DEC.decdegree, "id": (fields.length == 3) ? fields[2] : "", "label": label });
} else {
status = false;
break;
}
}
}
if (status) {
this.enablePlaceMarkers();
} else {
markers = [];
}
MarkersFactory.exit();
return markers;
}
// Not used apparently.
appendMarkerTA(RA, DEC, label, iRA, iDEC, id = null, withFlux = false, FITSHeader = null) {
var ta = $(`#${this.textOfMarkersId}`);
ta.val(ta.val() + RA + ", " + DEC + (id ? id : "") + ", " + label + ", " + iRA + ", " + iDEC + "\n");
}
// Not used apparently
cleanMarkerTA() {
$(`#${this.textOfMarkersId}`).val("");
}
// Still to be worked on.
checkAndDrawSampMarkers() {
MarkersFactory.enter(this.checkAndDrawSampMarkers.name);
var new_markers = this.parseTextOfMarkers();
this.deleteMarkerFeatures(this.getAllMarkerFeatures());
this.addMarkers(new_markers);
MarkersFactory.exit();
}
/**
* Utility. Format the textual expression of a float number.
* @param {number} floatValue
* @returns {string} a string as produced by toExponential(4)
*/
format(floatValue) {
let result = floatValue;
if (typeof result === "number" && !Number.isInteger(result)) {
result = result.toExponential(4);
}
return result;
};
}
export {
Marker,
MarkerList,
MarkerManager,
MarkersFactory
}