@cquiroz/aladin-lite
Version:
AladinLite module
648 lines (518 loc) • 19.6 kB
JavaScript
// Copyright 2013 - UDS/CNRS
// The Aladin Lite program is distributed under the terms
// of the GNU General Public License version 3.
//
// This file is part of Aladin Lite.
//
// Aladin Lite is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Aladin Lite is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// The GNU General Public License is available in COPYING file
// along with Aladin Lite.
//
/******************************************************************************
* Aladin Lite project
*
* File Catalog
*
* Author: Thomas Boch[CDS]
*
*****************************************************************************/
import Color from './Color';
import { HealpixIndex } from './HealpixIndex';
import Utils from './Utils';
import AladinUtils from './AladinUtils';
import CooConversion from './CooConversion';
import CooFrameEnum from './CooFrameEnum';
import Source from './Source';
import Coo from './coo';
import { source } from './A'; // TODO : harmoniser parsing avec classe ProgressiveCat
var Catalog = function () {
var Catalog = function Catalog(options) {
options = options || {};
this.type = 'catalog';
this.name = options.name || "catalog";
this.color = options.color || Color.getNextColor();
this.sourceSize = options.sourceSize || 8;
this.markerSize = options.sourceSize || 12;
this.shape = options.shape || "square";
this.maxNbSources = options.limit || undefined;
this.onClick = options.onClick || undefined;
this.raField = options.raField || undefined; // ID or name of the field holding RA
this.decField = options.decField || undefined; // ID or name of the field holding dec
this.indexationNorder = 5; // à quel niveau indexe-t-on les sources
this.sources = [];
this.hpxIdx = new HealpixIndex(this.indexationNorder);
this.displayLabel = options.displayLabel || false;
this.labelColor = options.labelColor || this.color;
this.labelFont = options.labelFont || '10px sans-serif';
if (this.displayLabel) {
this.labelColumn = options.labelColumn;
if (!this.labelColumn) {
this.displayLabel = false;
}
}
if (this.shape instanceof Image || this.shape instanceof HTMLCanvasElement) {
this.sourceSize = this.shape.width;
}
this._shapeIsFunction = false; // if true, the shape is a function drawing on the canvas
if (typeof x === "function") {
this._shapeIsFunction = true;
}
this.selectionColor = '#00ff00'; // create this.cacheCanvas
// cacheCanvas permet de ne créer le path de la source qu'une fois, et de le réutiliser (cf. http://simonsarris.com/blog/427-increasing-performance-by-caching-paths-on-canvas)
this.updateShape(options);
this.cacheMarkerCanvas = document.createElement('canvas');
this.cacheMarkerCanvas.width = this.markerSize;
this.cacheMarkerCanvas.height = this.markerSize;
var cacheMarkerCtx = this.cacheMarkerCanvas.getContext('2d');
cacheMarkerCtx.fillStyle = this.color;
cacheMarkerCtx.beginPath();
var half = this.markerSize / 2.;
cacheMarkerCtx.arc(half, half, half - 2, 0, 2 * Math.PI, false);
cacheMarkerCtx.fill();
cacheMarkerCtx.lineWidth = 2;
cacheMarkerCtx.strokeStyle = '#ccc';
cacheMarkerCtx.stroke();
this.isShowing = true;
};
Catalog.createShape = function (shapeName, color, sourceSize) {
if (shapeName instanceof Image || shapeName instanceof HTMLCanvasElement) {
// in this case, the shape is already created
return shapeName;
}
var c = document.createElement('canvas');
c.width = c.height = sourceSize;
var ctx = c.getContext('2d');
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 2.0;
if (shapeName === "plus") {
ctx.moveTo(sourceSize / 2., 0);
ctx.lineTo(sourceSize / 2., sourceSize);
ctx.stroke();
ctx.moveTo(0, sourceSize / 2.);
ctx.lineTo(sourceSize, sourceSize / 2.);
ctx.stroke();
} else if (shapeName === "cross") {
ctx.moveTo(0, 0);
ctx.lineTo(sourceSize - 1, sourceSize - 1);
ctx.stroke();
ctx.moveTo(sourceSize - 1, 0);
ctx.lineTo(0, sourceSize - 1);
ctx.stroke();
} else if (shapeName === "rhomb") {
ctx.moveTo(sourceSize / 2, 0);
ctx.lineTo(0, sourceSize / 2);
ctx.lineTo(sourceSize / 2, sourceSize);
ctx.lineTo(sourceSize, sourceSize / 2);
ctx.lineTo(sourceSize / 2, 0);
ctx.stroke();
} else if (shapeName === "triangle") {
ctx.moveTo(sourceSize / 2, 0);
ctx.lineTo(0, sourceSize - 1);
ctx.lineTo(sourceSize - 1, sourceSize - 1);
ctx.lineTo(sourceSize / 2, 0);
ctx.stroke();
} else if (shapeName === "circle") {
ctx.arc(sourceSize / 2, sourceSize / 2, sourceSize / 2 - 1, 0, 2 * Math.PI, true);
ctx.stroke();
} else {
// default shape: square
ctx.moveTo(1, 0);
ctx.lineTo(1, sourceSize - 1);
ctx.lineTo(sourceSize - 1, sourceSize - 1);
ctx.lineTo(sourceSize - 1, 1);
ctx.lineTo(1, 1);
ctx.stroke();
}
return c;
}; // find RA, Dec fields among the given fields
//
// @param fields: list of objects with ucd, unit, ID, name attributes
// @param raField: index or name of right ascension column (might be undefined)
// @param decField: index or name of declination column (might be undefined)
//
function findRADecFields(fields, raField, decField) {
var raFieldIdx, decFieldIdx;
raFieldIdx = decFieldIdx = null; // first, look if RA/DEC fields have been already given
if (raField) {
// ID or name of RA field given at catalogue creation
for (var l = 0, len = fields.length; l < len; l++) {
var field = fields[l];
if (Utils.isInt(raField) && raField < fields.length) {
// raField can be given as an index
raFieldIdx = raField;
break;
}
if (field.ID && field.ID === raField || field.name && field.name === raField) {
raFieldIdx = l;
break;
}
}
}
if (decField) {
// ID or name of dec field given at catalogue creation
for (var _l = 0, _len = fields.length; _l < _len; _l++) {
var _field = fields[_l];
if (Utils.isInt(decField) && decField < fields.length) {
// decField can be given as an index
decFieldIdx = decField;
break;
}
if (_field.ID && _field.ID === decField || _field.name && _field.name === decField) {
decFieldIdx = _l;
break;
}
}
} // if not already given, let's guess position columns on the basis of UCDs
for (var _l2 = 0, _len2 = fields.length; _l2 < _len2; _l2++) {
if (raFieldIdx != null && decFieldIdx != null) {
break;
}
var _field2 = fields[_l2];
if (!raFieldIdx) {
if (_field2.ucd) {
var ucd = _field2.ucd.toLowerCase().trim();
if (ucd.indexOf('pos.eq.ra') === 0 || ucd.indexOf('pos_eq_ra') === 0) {
raFieldIdx = _l2;
continue;
}
}
}
if (!decFieldIdx) {
if (_field2.ucd) {
var _ucd = _field2.ucd.toLowerCase().trim();
if (_ucd.indexOf('pos.eq.dec') === 0 || _ucd.indexOf('pos_eq_dec') === 0) {
decFieldIdx = _l2;
continue;
}
}
}
} // still not found ? try some common names for RA and Dec columns
if (raFieldIdx == null && decFieldIdx == null) {
for (var _l3 = 0, _len3 = fields.length; _l3 < _len3; _l3++) {
var _field3 = fields[_l3];
var name = _field3.name || _field3.ID || '';
name = name.toLowerCase();
if (!raFieldIdx) {
if (name.indexOf('ra') === 0 || name.indexOf('_ra') === 0 || name.indexOf('ra(icrs)') === 0 || name.indexOf('_ra') === 0 || name.indexOf('alpha') === 0) {
raFieldIdx = _l3;
continue;
}
}
if (!decFieldIdx) {
if (name.indexOf('dej2000') === 0 || name.indexOf('_dej2000') === 0 || name.indexOf('de') === 0 || name.indexOf('de(icrs)') === 0 || name.indexOf('_de') === 0 || name.indexOf('delta') === 0) {
decFieldIdx = _l3;
continue;
}
}
}
} // last resort: take two first fieds
if (raFieldIdx == null || decFieldIdx == null) {
raFieldIdx = 0;
decFieldIdx = 1;
}
return [raFieldIdx, decFieldIdx];
} // return an array of Source(s) from a VOTable url
// callback function is called each time a TABLE element has been parsed
Catalog.parseVOTable = function (url, callback, maxNbSources, useProxy, raField, decField) {
// adapted from votable.js
function getPrefix(xml) {
var prefix; // If Webkit chrome/safari/... (no need prefix)
if (xml.querySelectorAll('RESOURCE').length > 0) {
prefix = '';
} else {
// Select all data in the document
prefix = xml.querySelector("*");
if (prefix.length === 0) {
return '';
}
prefix = prefix.tagName;
var idx = prefix.indexOf(':');
prefix = prefix.substring(0, idx) + "\\:";
}
return prefix;
}
function doParseVOTable(xml, callback) {
xml = xml.replace(/^\s+/g, ''); // we need to trim whitespaces at start of document
var attributes = ["name", "ID", "ucd", "utype", "unit", "datatype", "arraysize", "width", "precision"];
var fields = [];
var k = 0;
var parser = new DOMParser();
var xmlDoc = parser.parseFromString(xml, "text/xml");
var prefix = getPrefix(xmlDoc);
xmlDoc.querySelectorAll(prefix + "FIELD").forEach(function (item) {
var f = {};
for (var i = 0; i < attributes.length; i++) {
var attribute = attributes[i];
if (item.getAttribute(attribute)) {
f[attribute] = item.getAttribute(attribute);
}
}
if (!f.ID) {
f.ID = "col_" + k;
}
fields.push(f);
k++;
});
var raDecFieldIdxes = findRADecFields(fields, raField, decField);
var raFieldIdx, decFieldIdx;
raFieldIdx = raDecFieldIdxes[0];
decFieldIdx = raDecFieldIdxes[1];
var sources = [];
var coo = new Coo();
var ra, dec;
xmlDoc.querySelectorAll(prefix + "TR").forEach(function (item) {
var mesures = {};
var k = 0;
item.querySelectorAll(prefix + "TD").forEach(function (item) {
var key = fields[k].name ? fields[k].name : fields[k].id;
mesures[key] = item.textContent;
k++;
});
var keyRa = fields[raFieldIdx].name ? fields[raFieldIdx].name : fields[raFieldIdx].id;
var keyDec = fields[decFieldIdx].name ? fields[decFieldIdx].name : fields[decFieldIdx].id;
if (Utils.isNumber(mesures[keyRa]) && Utils.isNumber(mesures[keyDec])) {
ra = parseFloat(mesures[keyRa]);
dec = parseFloat(mesures[keyDec]);
} else {
coo.parse(mesures[keyRa] + " " + mesures[keyDec]);
ra = coo.lon;
dec = coo.lat;
}
sources.push(new Source(ra, dec, mesures));
if (maxNbSources && sources.length === maxNbSources) {
return false; // break the .each loop
}
});
if (callback) {
callback(sources);
}
}
fetch(url).then(xml => xml.text()).then(xml => doParseVOTable(xml, callback));
}; // API
Catalog.prototype.updateShape = function (options) {
options = options || {};
this.color = options.color || this.color || Color.getNextColor();
this.sourceSize = options.sourceSize || this.sourceSize || 6;
this.shape = options.shape || this.shape || "square";
this.selectSize = this.sourceSize + 2;
this.cacheCanvas = Catalog.createShape(this.shape, this.color, this.sourceSize);
this.cacheSelectCanvas = Catalog.createShape('square', this.selectionColor, this.selectSize);
this.reportChange();
}; // API
Catalog.prototype.addSources = function (sourcesToAdd) {
sourcesToAdd = [].concat(sourcesToAdd); // make sure we have an array and not an individual source
this.sources = this.sources.concat(sourcesToAdd);
for (var k = 0, len = sourcesToAdd.length; k < len; k++) {
sourcesToAdd[k].setCatalog(this);
}
this.reportChange();
}; // API
//
// create sources from a 2d array and add them to the catalog
//
// @param columnNames: array with names of the columns
// @array: 2D-array, each item being a 1d-array with the same number of items as columnNames
Catalog.prototype.addSourcesAsArray = function (columnNames, array) {
var fields = [];
for (var colIdx = 0; colIdx < columnNames.length; colIdx++) {
fields.push({
name: columnNames[colIdx]
});
}
var raDecFieldIdxes = findRADecFields(fields, this.raField, this.decField);
var raFieldIdx, decFieldIdx;
raFieldIdx = raDecFieldIdxes[0];
decFieldIdx = raDecFieldIdxes[1];
var newSources = [];
var coo = new Coo();
var ra, dec, row, dataDict;
for (var rowIdx = 0; rowIdx < array.length; rowIdx++) {
row = array[rowIdx];
if (Utils.isNumber(row[raFieldIdx]) && Utils.isNumber(row[decFieldIdx])) {
ra = parseFloat(row[raFieldIdx]);
dec = parseFloat(row[decFieldIdx]);
} else {
coo.parse(row[raFieldIdx] + " " + row[decFieldIdx]);
ra = coo.lon;
dec = coo.lat;
}
dataDict = {};
for (var _colIdx = 0; _colIdx < columnNames.length; _colIdx++) {
dataDict[columnNames[_colIdx]] = row[_colIdx];
}
newSources.push(source(ra, dec, dataDict));
}
this.addSources(newSources);
}; // return the current list of Source objects
Catalog.prototype.getSources = function () {
return this.sources;
}; // TODO : fonction générique traversant la liste des sources
Catalog.prototype.selectAll = function () {
if (!this.sources) {
return;
}
for (var k = 0; k < this.sources.length; k++) {
this.sources[k].select();
}
};
Catalog.prototype.deselectAll = function () {
if (!this.sources) {
return;
}
for (var k = 0; k < this.sources.length; k++) {
this.sources[k].deselect();
}
}; // return a source by index
Catalog.prototype.getSource = function (idx) {
if (idx < this.sources.length) {
return this.sources[idx];
} else {
return null;
}
};
Catalog.prototype.setView = function (view) {
this.view = view;
this.reportChange();
}; // remove a source
Catalog.prototype.remove = function (source) {
var idx = this.sources.indexOf(source);
if (idx < 0) {
return;
}
this.sources[idx].deselect();
this.sources.splice(idx, 1);
this.reportChange();
};
Catalog.prototype.removeAll = Catalog.prototype.clear = function () {
// TODO : RAZ de l'index
this.sources = [];
};
Catalog.prototype.draw = function (ctx, projection, frame, width, height, largestDim, zoomFactor) {
if (!this.isShowing) {
return;
} // tracé simple
//ctx.strokeStyle= this.color;
//ctx.lineWidth = 1;
//ctx.beginPath();
if (this._shapeIsFunction) {
ctx.save();
}
var sourcesInView = [];
for (var k = 0, len = this.sources.length; k < len; k++) {
var inView = Catalog.drawSource(this, this.sources[k], ctx, projection, frame, width, height, largestDim, zoomFactor);
if (inView) {
sourcesInView.push(this.sources[k]);
}
}
if (this._shapeIsFunction) {
ctx.restore();
} //ctx.stroke();
// tracé sélection
ctx.strokeStyle = this.selectionColor; //ctx.beginPath();
var source;
for (var _k = 0, _len4 = sourcesInView.length; _k < _len4; _k++) {
source = sourcesInView[_k];
if (!source.isSelected) {
continue;
}
Catalog.drawSourceSelection(this, source, ctx);
} // NEEDED ?
//ctx.stroke();
// tracé label
if (this.displayLabel) {
ctx.fillStyle = this.labelColor;
ctx.font = this.labelFont;
for (var _k2 = 0, _len5 = sourcesInView.length; _k2 < _len5; _k2++) {
Catalog.drawSourceLabel(this, sourcesInView[_k2], ctx);
}
}
};
Catalog.drawSource = function (catalogInstance, s, ctx, projection, frame, width, height, largestDim, zoomFactor) {
if (!s.isShowing) {
return false;
}
var sourceSize = catalogInstance.sourceSize; // TODO : we could factorize this code with Aladin.world2pix
var xy;
if (frame.system !== CooFrameEnum.SYSTEMS.J2000) {
var lonlat = CooConversion.J2000ToGalactic([s.ra, s.dec]);
xy = projection.project(lonlat[0], lonlat[1]);
} else {
xy = projection.project(s.ra, s.dec);
}
if (xy) {
var xyview = AladinUtils.xyToView(xy.X, xy.Y, width, height, largestDim, zoomFactor, true);
var max = s.popup ? 100 : s.sourceSize;
if (xyview) {
// TODO : index sources by HEALPix cells at level 3, 4 ?
// check if source is visible in view
if (xyview.vx > width + max || xyview.vx < 0 - max || xyview.vy > height + max || xyview.vy < 0 - max) {
s.x = s.y = undefined;
return false;
}
s.x = xyview.vx;
s.y = xyview.vy;
if (catalogInstance._shapeIsFunction) {
catalogInstance.shape(s, ctx, catalogInstance.view.getViewParams());
} else if (s.marker && s.useMarkerDefaultIcon) {
ctx.drawImage(catalogInstance.cacheMarkerCanvas, s.x - sourceSize / 2, s.y - sourceSize / 2);
} else {
ctx.drawImage(catalogInstance.cacheCanvas, s.x - catalogInstance.cacheCanvas.width / 2, s.y - catalogInstance.cacheCanvas.height / 2);
} // has associated popup ?
if (s.popup) {
s.popup.setPosition(s.x, s.y);
}
}
return true;
} else {
return false;
}
};
Catalog.drawSourceSelection = function (catalogInstance, s, ctx) {
if (!s || !s.isShowing || !s.x || !s.y) {
return;
}
var sourceSize = catalogInstance.selectSize;
ctx.drawImage(catalogInstance.cacheSelectCanvas, s.x - sourceSize / 2, s.y - sourceSize / 2);
};
Catalog.drawSourceLabel = function (catalogInstance, s, ctx) {
if (!s || !s.isShowing || !s.x || !s.y) {
return;
}
var label = s.data[catalogInstance.labelColumn];
if (!label) {
return;
}
ctx.fillText(label, s.x, s.y);
}; // callback function to be called when the status of one of the sources has changed
Catalog.prototype.reportChange = function () {
this.view && this.view.requestRedraw();
};
Catalog.prototype.show = function () {
if (this.isShowing) {
return;
}
this.isShowing = true;
this.reportChange();
};
Catalog.prototype.hide = function () {
if (!this.isShowing) {
return;
}
this.isShowing = false;
if (this.view && this.view.popup && this.view.popup.source && this.view.popup.source.catalog === this) {
this.view.popup.hide();
}
this.reportChange();
};
return Catalog;
}();
export default Catalog;