shapes/Circle.js

// 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 Circle
 *
 * Author: Thomas Boch[CDS]
 *
 *****************************************************************************/

import { Utils } from "./../Utils";
import { GraphicOverlay } from "./../Overlay.js";

export let Circle = (function() {
    /**
     * Constructor function for creating a new circle.
     *
     * @class
     * @constructs Circle
     * @param {number[]} centerRaDec - right-ascension/declination 2-tuple of the circle's center in degrees
     * @param {number} radius - radius in degrees
     * @param {ShapeOptions} options - Configuration options for the circle
     * 
     * @returns {Circle} - The circle shape object
     */
    let Circle = function(centerRaDec, radius, options) {
        options = options || {};

        this.color     = options['color']     || undefined;
        this.fillColor = options['fillColor'] || undefined;
        this.lineWidth = options["lineWidth"] || 2;
        this.selectionColor = options["selectionColor"] || '#00ff00';
        this.hoverColor = options["hoverColor"] || undefined;

        // TODO : all graphic overlays should have an id
        this.id = 'circle-' + Utils.uuidv4();

        this.setCenter(centerRaDec);
        this.setRadius(radius);
    	this.overlay = null;

    	this.isShowing = true;
    	this.isSelected = false;
        this.isHovered = false;
        this.frame = options.frame || "icrs";
    };

    Circle.prototype.setColor = function(color) {
        if (!color || this.color == color) {
            return;
        }
        this.color = color;
        if (this.overlay) {
            this.overlay.reportChange();
        }
    };

    Circle.prototype.setSelectionColor = function(color) {
        if (!color || this.selectionColor == color) {
            return;
        }
        this.selectionColor = color;
        if (this.overlay) {
            this.overlay.reportChange();
        }
    };

    Circle.prototype.setHoverColor = function(color) {
        if (!color || this.hoverColor == color) {
            return;
        }
        this.hoverColor = color;
        if (this.overlay) {
            this.overlay.reportChange();
        }
    };

    Circle.prototype.setLineWidth = function(lineWidth) {
        if (this.lineWidth == lineWidth) {
            return;
        }
        this.lineWidth = lineWidth;
        if (this.overlay) {
            this.overlay.reportChange();
        }
    };

    Circle.prototype.getLineWidth = function() {
        return this.lineWidth;
    };

    Circle.prototype.setOverlay = function(overlay) {
        this.overlay = overlay;
    };

    Circle.prototype.show = function() {
        if (this.isShowing) {
            return;
        }
        this.isShowing = true;
        if (this.overlay) {
            this.overlay.reportChange();
        }
    };

    Circle.prototype.hide = function() {
        if (! this.isShowing) {
            return;
        }
        this.isShowing = false;
        if (this.overlay) {
            this.overlay.reportChange();
        }
    };

    Circle.prototype.select = function() {
        if (this.isSelected) {
            return;
        }
        this.isSelected = true;
        if (this.overlay) {
            this.overlay.reportChange();
        }
    };

    Circle.prototype.deselect = function() {
        if (! this.isSelected) {
            return;
        }
        this.isSelected = false;
        if (this.overlay) {
            this.overlay.reportChange();
        }
    };

    Circle.prototype.hover = function() {
        if (this.isHovered) {
            return;
        }
        this.isHovered = true;
        if (this.overlay) {
            this.overlay.reportChange();
        }
    }

    Circle.prototype.unhover = function() {
        if (! this.isHovered) {
            return;
        }
        this.isHovered = false;
        if (this.overlay) {
            this.overlay.reportChange();
        }
    }

    Circle.prototype.isFootprint = function() {
        return true;
    }

    Circle.prototype.setCenter = function(centerRaDec) {
        this.centerRaDec = centerRaDec;
        if (this.overlay) {
            this.overlay.reportChange();
        }
    };

    Circle.prototype.setRadius = function(radiusDegrees) {
        this.radiusDegrees = radiusDegrees;
        if (this.overlay) {
            this.overlay.reportChange();
        }
    };

    // TODO
    Circle.prototype.draw = function(ctx, view, noStroke, noSmallCheck) {
        if (! this.isShowing) {
            return false;
        }

        noSmallCheck = noSmallCheck===true || false;
        if (!noSmallCheck) {
            const px_per_deg = view.width / view.fov;
            this.isTooSmall = this.radiusDegrees * 2 * px_per_deg < this.lineWidth;
            if (this.isTooSmall) {
                return false;
            }
        }

        noStroke = noStroke===true || false;

        var centerXyview = view.aladin.world2pix(this.centerRaDec[0], this.centerRaDec[1]);
        if (!centerXyview) {
            // the center goes out of the projection
            // we do not draw it
            return false;
        }
        this.center = {
            x: centerXyview[0],
            y: centerXyview[1],
        };

        let hidden = true;

        var ra, dec, vertOnCircle, dx, dy;
        this.radius = Number.NEGATIVE_INFINITY;
        
        // Project 4 points lying on the circle and take the minimal dist with the center as radius
        [[-1, 0], [1, 0], [0, -1], [0, 1]].forEach(([cardDirRa, cardDirDec]) => {
            ra = this.centerRaDec[0] + cardDirRa * this.radiusDegrees;
            dec = this.centerRaDec[1] + cardDirDec * this.radiusDegrees;

            vertOnCircle = view.aladin.world2pix(ra, dec);

            if (vertOnCircle) {
                dx = vertOnCircle[0] - this.center.x;
                dy = vertOnCircle[1] - this.center.y;

                this.radius = Math.max(Math.sqrt(dx*dx + dy*dy), this.radius);

                hidden = false;
            }            
        });

        if (hidden) {
            return false;
        }
        // Then we can draw

        var baseColor = this.color;
        if (! baseColor && this.overlay) {
            baseColor = this.overlay.color;
        }
        if (! baseColor) {
            baseColor = '#ff0000';
        }

        if (this.isSelected) {
            if(this.selectionColor) {
                ctx.strokeStyle = this.selectionColor;
            } else {
                ctx.strokeStyle = GraphicOverlay.increaseBrightness(baseColor, 50);
            }
        } else if (this.isHovered) {
            ctx.strokeStyle = this.hoverColor || GraphicOverlay.increaseBrightness(baseColor, 25);
        } else {
            ctx.strokeStyle = baseColor;
        }

        ctx.lineWidth = this.lineWidth;
        ctx.beginPath();
        ctx.arc(this.center.x, this.center.y, this.radius, 0, 2*Math.PI, false);
        if (!noStroke) {
            if (this.fillColor) {
                ctx.fillStyle = this.fillColor;
                ctx.fill();
            }
            ctx.stroke();
        }

        return true;
    };

    Circle.prototype.isInStroke = function(ctx, view, x, y) {
        this.draw(ctx, view, true);
        return ctx.isPointInStroke(x, y);
    };

    // From StackOverflow: https://stackoverflow.com/questions/401847/circle-rectangle-collision-detection-intersection
    Circle.prototype.intersectsBBox = function(x, y, w, h) {
        const circleDistance = {
            x: Math.abs(this.center.x - x),
            y: Math.abs(this.center.y - y)
        };

        if (circleDistance.x > (w/2 + this.radius)) { return false; }
        if (circleDistance.y > (h/2 + this.radius)) { return false; }

        if (circleDistance.x <= (w/2)) { return true; } 
        if (circleDistance.y <= (h/2)) { return true; }

        const dx = circleDistance.x - w/2;
        const dy = circleDistance.y - h/2;

        const cornerDistanceSquared = dx*dx + dy*dy;
        return (cornerDistanceSquared <= (this.radius*this.radius));
    }

    return Circle;
})();