expose Line in the API and add an example with proper motions drawn from a Simbad CS around the LMC

This commit is contained in:
Matthieu Baumann
2024-04-03 16:06:13 +02:00
committed by Matthieu Baumann
parent d56dbd1659
commit 9109c69fc3
8 changed files with 289 additions and 95 deletions

View File

@@ -0,0 +1,49 @@
<!doctype html>
<html>
<head>
</head>
<body>
<div id="aladin-lite-div" style="width: 1024px; height: 768px"></div>
<script>let aladin;</script>
<script type="module">
import A from '../src/js/A.js';
A.init.then(() => {
// Start up Aladin Lite
aladin = A.aladin('#aladin-lite-div', {
target: "LMC",
fov: 2,
showContextMenu: true,
fullScreen: true,
showSimbadPointerControl: true,
showShareControl: true,
showSettingsControl: true,
showStackLayerControl: true,
samp: true,
});
aladin.addCatalog(A.catalogFromSimbad("LMC", 10, {
limit: 10000,
onClick: 'showTable',
orderBy: 'nb_ref',
color: 'yellow',
hoverColor: 'yellow',
shape: (s) => {
let pmra = +s.data.pmra;
let pmdec = +s.data.pmdec;
let mag2 = pmra * pmra + pmdec * pmdec;
if (mag2 > 1000) {
return;
}
let mag = Math.sqrt(mag2)
return A.line(+s.ra, +s.dec, +s.ra + (0.5 * pmra / mag), +s.dec + (0.5 * pmdec / mag), null, {lineWidth: 3, arrow: true, color: +s.data.rvz_radvel < 0 ? 'blue' : 'red'})
}
}));
});
</script>
</body>
</html>

View File

@@ -40,7 +40,7 @@
}
let angle = +s.data.size_angle || 0.0;
return A.ellipse(s.ra, s.dec, a / 60, b / 60, angle, {lineWidth: 3, hoverColor: 'yellow'});
return A.ellipse(s.ra, s.dec, a / 60, b / 60, angle, {lineWidth: 3, color: 'cyan'});
};
var hips = A.catalogHiPS('https://axel.u-strasbg.fr/HiPSCatService/Simbad', {onClick: 'showTable', name: 'Simbad', color: 'cyan', hoverColor: 'red', shape: drawFunctionFootprint});

View File

@@ -33,6 +33,7 @@ import { Overlay } from "./Overlay.js";
import { Circle } from "./Circle.js";
import { Ellipse } from "./Ellipse.js";
import { Polyline } from "./Polyline.js";
import { Line } from "./Line.js";
import { Catalog } from "./Catalog.js";
import { ProgressiveCat } from "./ProgressiveCat.js";
import { Source } from "./Source.js";
@@ -262,6 +263,26 @@ A.ellipse = function (ra, dec, radiusRaDeg, radiusDecDeg, rotationDeg, options)
return new Ellipse([ra, dec], radiusRaDeg, radiusDecDeg, rotationDeg, options);
};
/**
* Creates a ellipse object
*
* @function
* @memberof A
* @name line
*
* @param {number} ra1 - Right Ascension (RA) coordinate of the center in degrees.
* @param {number} dec1 - Declination (Dec) coordinate of the center in degrees.
* @param {number} ra2 - Right Ascension (RA) coordinate of the center in degrees.
* @param {number} dec2 - Declination (Dec) coordinate of the center in degrees.
* @param {CooFrame} [frame] - Right Ascension (RA) coordinate of the center in degrees.
* @param {Object} options - Options for configuring the ellipse.
*
* @returns {Line}
*/
A.line = function (ra1, dec1, ra2, dec2, frame, options) {
return new Line(ra1, dec1, ra2, dec2, frame, options);
};
/**
* Creates a graphic overlay on the Aladin Lite view.
*

View File

@@ -37,6 +37,7 @@ import { VOTable } from "./vo/VOTable.js";
import { ObsCore } from "./vo/ObsCore.js";
import A from "./A.js";
import { Polyline } from "./Polyline.js";
import { Line } from "./Line.js";
import { Ellipse } from "./Ellipse.js";
import { Circle } from "./Circle.js";
import { Footprint } from "./Footprint.js";
@@ -534,7 +535,7 @@ export let Catalog = (function() {
try {
let shape = this.shape(source)
// convert simple shapes to footprints
if (shape instanceof Circle || shape instanceof Polyline || shape instanceof Ellipse) {
if (shape instanceof Circle || shape instanceof Polyline || shape instanceof Ellipse || shape instanceof Line) {
shape = new Footprint(shape, source);
}

View File

@@ -57,9 +57,13 @@ export let Footprint= (function() {
Footprint.prototype.setCatalog = function(catalog) {
if (this.source) {
this.source.setCatalog(catalog);
/*
// Take the color properties of the catalog
this.setColor(catalog.color);
if (this.color === undefined) {
this.setColor(catalog.color);
}*/
this.setSelectionColor(catalog.selectionColor);
this.setHoverColor(catalog.hoverColor);
}

View File

@@ -29,77 +29,146 @@
* Author: Matthieu Baumann[CDS]
*
*****************************************************************************/
import { Polyline } from "./Polyline.js";
import { Utils } from './Utils';
import { Overlay } from "./Overlay.js";
export let Line = (function() {
// constructor
let Line = function(x1, y1, x2, y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
let Line = function(ra1, dec1, ra2, dec2, frame, options) {
options = options || {};
this.color = options['color'] || undefined;
this.opacity = options['opacity'] || undefined;
this.lineWidth = options["lineWidth"] || undefined;
this.selectionColor = options["selectionColor"] || '#00ff00';
this.hoverColor = options["hoverColor"] || undefined;
this.arrow = options["arrow"] === undefined ? false : options["arrow"];
// All graphics overlay have an id
this.id = 'line-' + Utils.uuidv4();
this.overlay = null;
this.isShowing = true;
this.isSelected = false;
this.isHovered = false;
this.ra1 = ra1;
this.dec1 = dec1;
this.ra2 = ra2;
this.dec2 = dec2;
this.frame = frame;
};
// Method for testing whether a line is inside the view
// http://www.jeffreythompson.org/collision-detection/line-rect.php
Line.prototype.isInsideView = function(rw, rh) {
if (this.x1 >= 0 && this.x1 <= rw && this.y1 >= 0 && this.y1 <= rh) {
return true;
}
if (this.x2 >= 0 && this.x2 <= rw && this.y2 >= 0 && this.y2 <= rh) {
return true;
}
Line.prototype = {
setOverlay: Polyline.prototype.setOverlay,
isFootprint: Polyline.prototype.isFootprint,
show: Polyline.prototype.show,
hide: Polyline.prototype.hide,
select: Polyline.prototype.select,
deselect: Polyline.prototype.deselect,
hover: Polyline.prototype.hover,
unhover: Polyline.prototype.unhover,
getLineWidth: Polyline.prototype.getLineWidth,
setLineWidth: Polyline.prototype.setLineWidth,
// check if the line has hit any of the rectangle's sides
// uses the Line/Line function below
let left = Line.intersectLine(this.x1, this.y1, this.x2, this.y2, 0, 0, 0, rh);
let right = Line.intersectLine(this.x1, this.y1, this.x2, this.y2, rw, 0, rw, rh);
let top = Line.intersectLine(this.x1, this.y1, this.x2, this.y2, 0, 0, rw, 0);
let bottom = Line.intersectLine(this.x1, this.y1, this.x2, this.y2, 0, rh, rw, rh);
// if ANY of the above are true, the line
// has hit the rectangle
if (left || right || top || bottom) {
return true;
}
setColor: Polyline.prototype.setColor,
setSelectionColor: Polyline.prototype.setSelectionColor,
setHoverColor: Polyline.prototype.setHoverColor,
return false;
};
draw: function(ctx, view, noStroke) {
noStroke = noStroke===true || false;
Line.prototype.isFootprint = function() {
return false;
}
// project
const v1 = view.aladin.world2pix(this.ra1, this.dec1, this.frame);
if (!v1)
return;
const v2 = view.aladin.world2pix(this.ra2, this.dec2, this.frame);
if (!v2)
return;
const xmin = Math.min(v1.x, v2.x);
const xmax = Math.max(v1.x, v2.x);
const ymin = Math.min(v1.y, v2.y);
const ymax = Math.max(v1.y, v2.y);
Line.prototype.draw = function(ctx, noStroke) {
noStroke = noStroke===true || false;
// out of bbox
if (xmax < 0 || xmin > view.width || ymax < 0 || ymin > view.height) {
return;
}
ctx.beginPath();
ctx.moveTo(this.x1, this.y1);
ctx.lineTo(this.x2, this.y2);
var baseColor = this.color;
if (!baseColor && this.overlay) {
baseColor = this.overlay.color;
}
if (!baseColor) {
baseColor = '#ff0000';
}
if (!noStroke) {
ctx.stroke();
}
};
if (!this.lineWidth) {
this.lineWidth = this.overlay.lineWidth || 2;
}
Line.intersectLine = function(x1, y1, x2, y2, x3, y3, x4, y4) {
// Calculate the direction of the lines
let uA = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
let uB = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
// If uA and uB are between 0-1, lines are colliding
if (uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1) {
return true;
}
return false;
};
// too small
if ((xmax - xmin) < this.lineWidth || (ymax - ymin) < this.lineWidth) {
return;
}
Line.prototype.isInStroke = function(ctx, view, x, y) {
this.draw(ctx, view, true);
return ctx.isPointInStroke(x, y);
};
if (this.isSelected) {
ctx.strokeStyle = this.selectionColor || Overlay.increaseBrightness(baseColor, 50);
} else if (this.isHovered) {
ctx.strokeStyle = this.hoverColor || Overlay.increaseBrightness(baseColor, 25);
} else {
ctx.strokeStyle = baseColor;
}
Line.prototype.intersectsBBox = function(x, y, w, h) {
// todo
ctx.lineWidth = this.lineWidth;
ctx.globalAlpha = this.opacity;
ctx.beginPath();
ctx.moveTo(v1[0], v1[1]);
ctx.lineTo(v2[0], v2[1]);
if (this.arrow) {
// draw the arrow
var angle, x, y, xh, yh;
var arrowRad = this.lineWidth * 3;
angle = Math.atan2(v2[1] - v1[1], v2[0] - v1[0])
xh = v2[0];
yh = v2[1];
//ctx.moveTo(xh, yh);
var t = angle + Math.PI * 3 / 4;
x = arrowRad * Math.cos(t) + v2[0];
y = arrowRad * Math.sin(t) + v2[1];
ctx.moveTo(x, y);
ctx.lineTo(xh, yh);
var t = angle - Math.PI * 3 / 4;
x = arrowRad *Math.cos(t) + v2[0];
y = arrowRad *Math.sin(t) + v2[1];
ctx.lineTo(x, y);
}
if (!noStroke) {
ctx.stroke();
}
},
isInStroke: function(ctx, view, x, y) {
this.draw(ctx, view, true);
return ctx.isPointInStroke(x, y);
},
/*Line.prototype.intersectsBBox = function(x, y, w, h) {
// todo
};*/
};
return Line;

View File

@@ -33,24 +33,35 @@
*
*****************************************************************************/
import { AladinUtils } from './AladinUtils.js';
import { Line } from './Line.js';
import { Utils } from './Utils';
import { Overlay } from "./Overlay.js";
import { ProjectionEnum } from "./ProjectionEnum.js";
export let Polyline= (function() {
function _calculateMag2ForNoSinProjections(line, view) {
export let Polyline = (function() {
function _calculateMag2ForNoSinProjections(l, view) {
// check if the line is too big (in the clip space) to be drawn
const [x1, y1] = view.wasm.screenToClip(line.x1, line.y1);
const [x2, y2] = view.wasm.screenToClip(line.x2, line.y2);
const [x1, y1] = view.wasm.screenToClip(l.x1, l.y1);
const [x2, y2] = view.wasm.screenToClip(l.x2, l.y2);
const mag2 = (x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2);
return mag2;
}
function _isAcrossCollignonZoneForHpxProjection(line, view) {
function _drawLine(l, ctx, noStroke) {
noStroke = noStroke===true || false;
ctx.beginPath();
ctx.moveTo(l.x1, l.y1);
ctx.lineTo(l.x2, l.y2);
if (!noStroke) {
ctx.stroke();
}
}
/*function _isAcrossCollignonZoneForHpxProjection(line, view) {
const [x1, y1] = view.wasm.screenToClip(line.x1, line.y1);
const [x2, y2] = view.wasm.screenToClip(line.x2, line.y2);
@@ -73,7 +84,7 @@ export let Polyline= (function() {
}
return false;
}
}*/
// constructor
let Polyline = function(radecArray, options) {
@@ -277,8 +288,12 @@ export let Polyline= (function() {
ymax = Math.max(ymax, xyview[1]);
}
// 2. do not draw the polygon if it lies in less than 1 pixel
if ((xmax - xmin) < 1 || (ymax - ymin) < 1) {
// 2. do not draw the polygon if it lies in less than linewidth pixels
if (xmax < 0 || xmin > view.width || ymax < 0 || ymin > view.height) {
return;
}
if ((xmax - xmin) < this.lineWidth || (ymax - ymin) < this.lineWidth) {
return;
}
@@ -287,21 +302,22 @@ export let Polyline= (function() {
if (view.projection === ProjectionEnum.SIN) {
drawLine = (v0, v1) => {
const line = new Line(v0.x, v0.y, v1.x, v1.y);
const l = {x1: v0.x, y1: v0.y, x2: v1.x, y2: v1.y};
if (line.isInsideView(view.width, view.height)) {
line.draw(ctx);
if (Polyline.isInsideView(l.x1, l.y1, l.x2, l.y2, view.width, view.height)) {
_drawLine(l, ctx);
}
};
if (this.closed && this.fill) {
fillPoly = (v0, v1, index) => {
const line = new Line(v0.x, v0.y, v1.x, v1.y);
const l = {x1: v0.x, y1: v0.y, x2: v1.x, y2: v1.y};
if (index === 0) {
ctx.beginPath();
ctx.moveTo(line.x1, line.y1);
ctx.moveTo(l.x1, l.y1);
} else {
ctx.lineTo(line.x1, line.y1);
ctx.lineTo(l.x1, l.y1);
}
return true;
@@ -350,28 +366,28 @@ export let Polyline= (function() {
}*/
} else {
drawLine = (v0, v1) => {
const line = new Line(v0.x, v0.y, v1.x, v1.y);
const l = {x1: v0.x, y1: v0.y, x2: v1.x, y2: v1.y};
if (line.isInsideView(view.width, view.height)) {
const mag2 = _calculateMag2ForNoSinProjections(line, view);
if (Polyline.isInsideView(l.x1, l.y1, l.x2, l.y2, view.width, view.height)) {
const mag2 = _calculateMag2ForNoSinProjections(l, view);
if (mag2 < 0.1) {
line.draw(ctx);
_drawLine(l, ctx);
}
}
};
if (this.closed && this.fill) {
fillPoly = (v0, v1, index) => {
const line = new Line(v0.x, v0.y, v1.x, v1.y);
const l = {x1: v0.x, y1: v0.y, x2: v1.x, y2: v1.y};
const mag2 = _calculateMag2ForNoSinProjections(line, view);
const mag2 = _calculateMag2ForNoSinProjections(l, view);
if (mag2 < 0.1) {
if (index === 0) {
ctx.beginPath();
ctx.moveTo(line.x1, line.y1);
ctx.moveTo(l.x1, l.y1);
} else {
ctx.lineTo(line.x1, line.y1);
ctx.lineTo(l.x1, l.y1);
}
return true;
@@ -416,7 +432,7 @@ export let Polyline= (function() {
v1 = v1 + 1;
}
ctx.globalAlpha = 1;
//ctx.globalAlpha = 1;
ctx.save();
ctx.fillStyle = this.fillColor;
ctx.globalAlpha = this.opacity;
@@ -442,8 +458,8 @@ export let Polyline= (function() {
const lastPointIdx = pointXY.length - 1;
for (var l = 0; l < lastPointIdx; l++) {
const line = new Line(pointXY[l].x, pointXY[l].y, pointXY[l + 1].x, pointXY[l + 1].y); // new segment
line.draw(ctx, true);
const line = {x1: pointXY[l].x, y1: pointXY[l].y, x2: pointXY[l + 1].x, y2: pointXY[l + 1].y}; // new segment
_drawLine(line, ctx, true);
if (ctx.isPointInStroke(x, y)) { // x,y is on line?
return true;
@@ -451,8 +467,8 @@ export let Polyline= (function() {
}
if(this.closed) {
const line = new Line(pointXY[lastPointIdx].x, pointXY[lastPointIdx].y, pointXY[0].x, pointXY[0].y); // new segment
line.draw(ctx, true);
const line = {x1: pointXY[lastPointIdx].x, y1: pointXY[lastPointIdx].y, x2: pointXY[0].x, y2: pointXY[0].y}; // new segment
_drawLine(line, ctx, true);
if (ctx.isPointInStroke(x, y)) { // x,y is on line?
return true;
@@ -466,5 +482,44 @@ export let Polyline= (function() {
// todo
};
// static methods
// Method for testing whether a line is inside the view
// http://www.jeffreythompson.org/collision-detection/line-rect.php
Polyline.isInsideView = function(x1, y1, x2, y2, rw, rh) {
if (x1 >= 0 && x1 <= rw && y1 >= 0 && y1 <= rh) {
return true;
}
if (x2 >= 0 && x2 <= rw && y2 >= 0 && y2 <= rh) {
return true;
}
// check if the line has hit any of the rectangle's sides
// uses the Line/Line function below
let left = Polyline._intersectLine(x1, y1, x2, y2, 0, 0, 0, rh);
let right = Polyline._intersectLine(x1, y1, x2, y2, rw, 0, rw, rh);
let top = Polyline._intersectLine(x1, y1, x2, y2, 0, 0, rw, 0);
let bottom = Polyline._intersectLine(x1, y1, x2, y2, 0, rh, rw, rh);
// if ANY of the above are true, the line
// has hit the rectangle
if (left || right || top || bottom) {
return true;
}
return false;
};
Polyline._intersectLine = function(x1, y1, x2, y2, x3, y3, x4, y4) {
// Calculate the direction of the lines
let uA = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
let uB = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1));
// If uA and uB are between 0-1, lines are colliding
if (uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1) {
return true;
}
return false;
};
return Polyline;
})();

View File

@@ -33,11 +33,6 @@ import { Color } from "./Color.js";
import { Coo } from "./libs/astro/coo.js";
import { Utils } from "./Utils";
import { CooFrameEnum } from "./CooFrameEnum.js";
import { Footprint } from "./Footprint.js";
import { Circle } from "./Circle.js";
import { Ellipse } from "./Ellipse.js";
import { Line } from "./Line.js";
import { Polyline } from "./Polyline.js";
// TODO: index sources according to their HEALPix ipix
// TODO : merge parsing with class Catalog
export let ProgressiveCat = (function() {