VisiOmatic web client

source

vector/EllipseMarker.js

/**
 #	This file part of:	VisiOmatic
 * @file Provide an ellipse marker.

 * @copyright (c) 2015-2023 CNRS/IAP/CFHT/SorbonneU
 * @author Emmanuel Bertin <bertin@cfht.hawaii.edu>
*/
import {
	Bounds,
	Canvas,
	Path,
	SVG,
	Util,
	latLng
} from 'leaflet';


/**
 * Ellipse shape parameters.
 * @typedef ellipseShape
 * @property {number} [majAxis]
   Length of the ellipse major axis in pixels.
 * @property {number} [minAxis]
   Length of the ellipse minor axis in pixels.
 * @property {number} [posAngle]
   Ellipse position angle in degrees (CCW from x axis).
 */


export const EllipseMarker = Path.extend( /** @lends EllipseMarker */ {
	CANVAS: true,
	SVG: false,

	options: {
		majAxis: 10,
		minAxis: 10,
		posAngle: 0,
		fill: true
	},

	/**
	 * Create a new ellipse marker.
	 * @extends leaflet.Path
	 * @memberof module:vector/EllipseMarker.js
	 * @constructs
	 * @param {leaflet.LatLng} latlng
	   World coordinates of the center of the marker.
	 * @param {object} [options]
	   Options.

	 * @param {number} [options.majAxis=10]
	   Length of the ellipse major axis in pixels.

	 * @param {number} [options.minAxis=10]
	   Length of the ellipse minor axis in pixels.

	 * @param {number} [options.posAngle=0]
	   Ellipse position angle in degrees (CCW from x axis).

	 * @param {boolean} [options.fill=true]
	   Fill the ellipse?

	 * @returns {EllipseMarker} Instance of an ellipse marker.
	 */
	initialize: function (latlng, options) {
		Util.setOptions(this, options);

		this._majAxis = this.options.majAxis;
		this._minAxis = this.options.majAxis;
		this._posAngle = this.options.posAngle;
		this._latlng = latLng(latlng);

		const	deg = Math.PI / 180.0,
			cpa = Math.cos(this._posAngle * deg),
			spa = Math.sin(this._posAngle * deg),
			cpa2 = cpa * cpa,
			spa2 = spa * spa,
			a2 = this._majAxis * this._majAxis,
			b2 = this._minAxis * this._minAxis;
		let	mx2 = a2 * cpa2 + b2 * spa2,
			my2 = a2 * spa2 + b2 * cpa2,
			mxy = (a2 - b2) * cpa * spa,
			c = mx2 * my2 - mxy * mxy;

		this._limX = Math.sqrt(mx2);
		this._limY = Math.sqrt(my2);
		// Manage ellipses with minor axis = 0
		if (c <= 0.0) {
			mx2 += 1.0;
			my2 += 1.0;
			c = mx2 * my2 - mxy * mxy;
		}
		// Necessary for computing the exact ellipse boundaries
		this._cXX = my2 / c;
		this._cYY = mx2 / c;
		this._cXY = -2.0 * mxy / c;
	},

	/**
	 * Set/update the world coordinates of the ellipse marker.
	 * @param {leaflet.LatLng} latlng
	   World coordinates of the marker.
	 * @returns {EllipseMarker} this.
	 */
	setLatLng: function (latlng) {
		this._latlng = latLng(latlng);
		this.redraw();
		return this.fire('move', {latlng: this._latlng});
	},

	/**
	 * Return the world coordinates of the ellipse marker.
	 * @returns {leaflet.LatLng} World coordinates of the marker.
	 */
	getLatLng: function () {
		return this._latlng;
	},

	/**
	 * Set/update the shape parameters of the ellipse marker.
	 * @param {ellipseShape} ellipseParams
	   Ellipse shape parameters.
	 * @returns {EllipseMarker} this.
	 */
	setParams: function (ellipseParams) {
		this.options.majAxis = this._majAxis = ellipseParams.majAxis;
		this.options.minAxis = this._minAxis = ellipseParams.minAxis;
		this.options.posAngle = this._posAngle = ellipseParams.posAngle;
		return this.redraw();
	},

	/**
	 * Set/update the shape parameters of the ellipse marker.
	 * @returns {ellipseShape} Ellipse shape parameters.
	 */
	getParams: function () {
		const	ellipseParams = {

			majAxis: this._majAxis,
			minAxis: this._minAxis,
			posAngle: this._posAngle
		};
		return ellipseParams;
	},

	// setStyle: Path.prototype.setStyle,


	/**
	 * Project the marker to pixel coordinates.
	 * @private
	 */
	_project: function () {
		this._point = this._map.latLngToLayerPoint(this._latlng);
		this._updateBounds();
	},

	/**
	 * Project the boundaries of the marker.
	 * @private
	 */
	_updateBounds: function () {
		const	w = this._clickTolerance(),
			p = [this._limX + w, this._limY + w];
		this._pxBounds = new Bounds(this._point.subtract(p), this._point.add(p));
	},

	/**
	 * Update the marker.
	 * @private
	 */
	_update: function () {
		if (this._map) {
			this._updatePath();
		}
	},

	/**
	 * Update the rendering of the marker.
	 * @private
	 */
	_updatePath: function () {
		this._renderer._updateEllipse(this);
	},

	_empty: function () {
		return this._majAxis &&
			!this._renderer._bounds.intersects(this._pxBounds);
	},

	/**
	 * Test if the marker contains the given  point.
	 * @param {leaflet.Point} p
	   Coordinates of the point.
	 * @returns {boolean}
	   True if the marker contains the point, false otherwise.
	 */
	_containsPoint: function (p) {
		const	dp = p.subtract(this._point),
			ct = this._clickTolerance(),
			dx = Math.abs(dp.x) - ct,
			dy = Math.abs(dp.y) - ct;

		return this._cXX * (dx > 0.0 ? dx * dx : 0.0) +
		  this._cYY * (dy > 0.0 ? dy * dy : 0.0) +
		  this._cXY * (dp.x * dp.y) <= 1.0;
	}
});

/**
 * Instantiate an ellipse marker.
 *
 * @function
 * @param {leaflet.LatLng} latlng
   World coordinates of the center of the marker.
 * @param {object} [options]
   Options: see {@link EllipseMarker}.
 * @returns {EllipseMarker} Instance of an ellipse marker.

 * @example
   ...
   const marker = ellipseMarker(latLng(30.0, 24.0), {
				majAxis: 24.7,
				minAxis: 12.1,
				posAngle: 47.3
	});
 */
export const ellipseMarker = function (latlng, options) {
	return new EllipseMarker(latlng, options);
};

/**
 * Ellipse marker rendering recipe for Canvas.
 * @memberof Canvas
 * @mixin
 * @param [EllipseMarker] layer - ellipse marker layer.
*/
Canvas.include({
	_updateEllipse: function (layer) {

		if (layer._empty()) { return; }

		var p = layer._point,
		    ctx = this._ctx,
		    r = layer._minAxis,
		    s = layer._majAxis / layer._minAxis;

		ctx.save();
		ctx.translate(p.x, p.y);
		ctx.rotate(layer._posAngle * Math.PI / 180.0);
		ctx.scale(1, s);

		ctx.beginPath();
		ctx.arc(0, 0, r, 0, Math.PI * 2, false);
		ctx.restore();

		this._fillStroke(ctx, layer);
	}
});

/**
 * Ellipse marker rendering recipe for SVG.
 * @memberof SVG
 * @mixin
 * @param [EllipseMarker] layer - ellipse marker layer.
*/
SVG.include({
	_updateEllipse: function (layer) {
		const	deg = Math.PI / 180.0,
			p = layer._point,
			r = layer._minAxis,
			r2 = layer._majAxis,
			dx = r * Math.cos(layer._posAngle * deg),
			dy = r * Math.sin(layer._posAngle * deg),
			arc = 'a' + r + ',' + r2 + ' ' + layer._posAngle + ' 1,0 ';

		// drawing a circle with two half-arcs
		const	d = layer._empty() ? 'M0 0' :
			'M' + (p.x - dx) + ',' + (p.y - dy) +
			arc + (dx * 2) + ',' + (dy * 2) + ' ' +
			arc + (-dx * 2) + ',' + (-dy * 2) + ' ';

		this._setPath(layer, d);
	}
});