VisiOmatic web client

source

catalog/Catalog.js

/**
 #	This file part of:	VisiOmatic
 * @file Catalog settings and conversion tools.
 *
 * @copyright (c) 2014-2023 CNRS/IAP/CFHT/SorbonneU
 * @author Emmanuel Bertin <bertin@cfht.hawaii.edu>
*/
import {Class, Util, circleMarker, extend} from 'leaflet';

// Callback definitions
/**
 * Callback for drawing catalog objects.
 * @callback Catalog~drawCallback
 * @param {object} feature - Feature property of the source.
 * @param {leaflet.LatLng} latlng - World coordinates of the source.
 * @return {leaflet.Path} Path.
 */

export const Catalog = Class.extend( /** @lends Catalog */ {
	options: {
		name: 'A catalog',
		attribution: '',
		color: 'yellow',
		properties: ['mag'],
		propertyMask: undefined,
		units: [''],
		magLim: 20.0,
		magIndex: 0,
		magScaleType: 'mag',
		regionType: 'box',
		service: 'Vizier@CDS',
		className: 'logo-catalog-vizier',
		serviceURL: 'https://vizier.unistra.fr/viz-bin',
		catalogURL: '/',
		objectURL: '/',
		authenticate: 'false',
		nmax: 10000,
		format: 'text',
		draw: undefined
	},

	/**
	 * Create a catalog.
	 *
	 * @extends leaflet.Class
	 * @memberof module:catalog/Catalog.js
	 * @constructs
	 * @param {object} [options] - Options.

	 * @param {string} [options.name='A catalog']
	   Catalog name.

	 * @param {string} [options.attribution='']
	   Reference or copyright.

	 * @param {RGB} [options.color='yellow']
	   Default display color. Currently unused.

	 * @param {string[]} [options.properties=['mag']]
	   Names of catalog object properties.
	 * @param {string[]} [options.propertyMask]
	   Property display mask: only properties with propertyMask element set to
	   ``true`` are displayed. Defaults to all properties being displayed.

	 * @param {string[]} [options.units=['']]
	   Property units.

	 * @param {number} [options.magLim=20.0]
	   Reference magnitude limit (for scaling symbols).

	 * @param {number} [options.magIndex=0]
	   Index of the property member that stores the reference magnitude.

	 * @param {'mag' | 'log' | 'linear'} [options.magScaleType='mag']
	   Scale type for the reference "magnitude".

	 * @param {'box'|'cone'} [options.regionType='box']
	   Geometry of the query region.

	 * @param {string} [options.service='Vizier@CDS']
	   Name of the catalog web service.

	 * @param {string} [options.className='logo-catalog-vizier'],
	   Class name for the catalog or service logo.

	 * @param {string} [options.serviceURL='https://vizier.unistra.fr/viz-bin']
	   Root web service query URL.

	 * @param {string} [options.catalogURL='/']
	   _Relative_ catalog query URL.

	 * @param {string} [options.objectURL='/']
	   _Relative_ object query URL.

	 * @param {boolean} [options.authenticate=false]
	   Catalog requires authentication?

	 * @param {number} [options.nmax=10000]
	   Maximum number of sources per query.

	 * @param {string} [options.format='csv']
	   Data format ('csv' or 'json')

	 * @param {Catalog~drawCallback} [options.draw]
	   Callback function called for drawing object. Defaults to a circle marker.

	 * @returns {Catalog} Instance of a catalog.
	 */
	initialize: function (options) {
		Util.setOptions(this, options);
		// Stupid copy all option properties to this (FIXME).
		for (var key in this.options) {
			if (this.options[key] !== undefined) {
				this[key] = this.options[key];
			}
		}
		this.url = this.serviceURL + this.catalogURL;
		if (this.objectURL) {
			this.objURL = this.objectURL.startsWith('http')?
				this.objectURL : this.serviceURL + this.objectURL;
		}		
	},

	/**
	 * Convert CSV data to [GeoJSON]{@link https://geojson.org/}.
	 * @private
	 * @param {string} str - CSV data.
	 * @return {object} GeoJSON object.
	 */
	_csvToGeoJSON: function (str) {
		// Check to see if the delimiter is defined. If not, then default to comma.
		const	badreg = new RegExp('#|--|objName|string|^$'),
			lines = str.split('\n'),
			geo = {type: 'FeatureCollection', features: []};

		for (var i in lines) {
			var line = lines[i];
			if (badreg.test(line) === false) {
				var feature = {
						type: 'Feature',
						id: '',
						properties: {
							items: []
						},
						geometry: {
							type: 'Point',
							coordinates: [0.0, 0.0]
						}
					},
					geometry = feature.geometry,
					properties = feature.properties;

				const	cell = line.split(/[,;\t]/);
				feature.id = cell[0];
				geometry.coordinates[0] = parseFloat(cell[1]);
				geometry.coordinates[1] = parseFloat(cell[2]);
				const	items = cell.slice(3);
				for (var j in items) {
					properties.items.push(this.readProperty(items[j]));
				}
				geo.features.push(feature);
			}
		}
		return geo;
	},

	/**
	 * Read number in a cell from a
	   [Vizier]{@link https://vizier.cds.unistra.fr/} ASCII output.
	 * @param {string} item - Cell content.
	 * @return {number} Value in the cell.
	 */
	readProperty: function (item) {
		const	fitem = parseFloat(item);
		return isNaN(fitem) ? '--' : fitem;
	},

	/**
	 * @summary Convert catalog data to [GeoJSON]{@link https://geojson.org/}.
	 * @desc Defaults to a wrapper around private method
	   [_csvToGeoJSON]{@link Catalog._csvToGeoJSON}.
	 * @param {string|object} data - catalog data.
	 * @return {object} GeoJSON object.
	 */
	toGeoJSON: function (data) {
		return this._csvToGeoJSON(data);
	},

	/**
	 * Generate HTML content for popups.
	 * @override
	 * @param {object} feature - Feature property of the source.
	 * @return {string} HTML content.
	 */
	popup: function (feature) {
		var str = '<div>';
		if (this.objURL) {
			str += 'ID: <a href=\"' + Util.template(this.objURL, extend({
				id: feature.id,
				ra: feature.geometry.coordinates[0].toFixed(6),
				dec: feature.geometry.coordinates[1].toFixed(6)
			})) + '\" target=\"_blank\">' + feature.id + '</a></div>';
		} else {
			str += 'ID: ' + feature.id + '</div>';
		}
		str += '<TABLE style="margin:auto;">' +
		       '<TBODY style="vertical-align:top;text-align:left;">';
		var	items = feature.properties.items;
		for (var i in this.properties) {
			if (this.propertyMask === undefined ||
				this.propertyMask[i] === true) {
				str += '<TR><TD>' + this.properties[i] + ':</TD>' +
					'<TD>' + (typeof(items[i]) === 'number' ?
					items[i].toPrecision(4) : items[i].toString()) + ' ';
				if (this.units[i]) {
					str += this.units[i];
				}
				str += '</TD></TR>';
			}
		}
		str += '</TBODY></TABLE>';
		return str;
	},

	/**
	 * Draw a circle at the current catalog source world coordinates.
	 * @override
	 * @param {object} feature - Feature property of the source.
	 * @param {leaflet.LatLng} latlng - World coordinates of the source.
	 * @return {leaflet.circleMarker} Circle marker.
	 */
	draw: function (feature, latlng) {
		var refmag = feature.properties.items[this.magIndex];
		return circleMarker(latlng, {
			radius: refmag ? 5. + (
				this.magScaleType === 'mag' ?
					this.magLim - refmag : 2.5 * (
						this.magScaleType === 'log' ? refmag - this.magLim :
							Math.log(refmag / this.magLim + 1.)
					)
			) : 8.
		});
	},

	/**
	 * Return drawing style for sources.
	 * @override
	 * @param {object} feature - Feature property of the source.
	 * @return {leaflet.Path.options} Drawing style options.
	 */
	style: function (feature) {
		return {color: this.color, weight: 2};
	},

	/**
	 * Filter out a source based on its feature property.
	 * @override
	 * @param {object} feature - Feature property of the source.
	 * @return {boolean} ``false`` if filtered out, ``true`` otherwise.
	 */
	filter: function (feature) {
		return true;
	}

});