GVector.java

/*
 *                      ..::jDrawingLib::..
 *
 * Copyright (C) Federico Vera 2012 - 2023 <[email protected]>
 *
 * This program 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, either version 3 of the License, or any later
 * version.
 *
 * This program 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.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.dkt.graphics.elements;

import java.awt.Graphics2D;
import java.util.Arrays;

/**
 * This class represents a Vector, basically a line with a triangular tip.<br>
 * If the modulus of the vector is {@code 0} then not even the triangle is
 * drawn.
 *
 * @author Federico Vera {@literal<[email protected]>}
 */
public class GVector extends GraphicE {
    private int[] xs = new int[3];
    private int[] ys = new int[3];

    //It would be awesome if this could handle arrow angles >= 45°
    private double aa = Math.toRadians(25);
    private int x1;
    private int y1;
    private int x2;
    private int y2;

    private double l;
    private double a;

    private int aw = 10;
    private int n = 3;

    /**
     * Copy constructor
     *
     * @param e {@code GVector} to copy
     * @throws IllegalArgumentException if {@code e} is {@code null}
     */
    public GVector(GVector e) {
        super(e);

        aa = e.aa;
        aw = e.aw;
        x1 = e.x1;
        x2 = e.x2;
        y1 = e.y1;
        y2 = e.y2;
        a = e.a;
        l = e.l;
        n = e.n;

        xs = new int[xs.length];
        ys = new int[ys.length];

        System.arraycopy(e.xs, 0, xs, 0, xs.length);
        System.arraycopy(e.ys, 0, ys, 0, ys.length);
    }

    /**
     * Basic Vector constructor
     *
     * @param x x coordinate of the application point of the vector
     * @param y y coordinate of the application point of the vector
     * @param l vector modulus
     * @param a vector angle (in degrees)
     */
    public GVector(
            final int x,
            final int y,
            final double l,
            final double a)
    {
        this.a = Math.toRadians(a % 360);
        this.l = l;

        this.x1 = x;
        this.y1 = y;

        calc();
    }

    /**
     * Creates a new vector from a given {@link GLine}, using the start point as
     * the application point.
     *
     * @param line the line to use as base for this {@code GVector}
     * @throws IllegalArgumentException if {@code line} is {@code null}
     */
    public GVector(final GLine line) throws IllegalArgumentException {
        if (line == null) {
            throw new IllegalArgumentException("The line can't be null");
        }

        this.a = line.getRadArgument();
        this.l = line.modulus();

        final GPoint start = line.getStartPoint();
        this.x1 = start.x();
        this.y1 = start.y();

        calc();
    }

    /**
     * Basic Vector constructor
     *
     * @param x1 x coordinate of the application point of the vector
     * @param y1 y coordinate of the application point of the vector
     * @param x2 x coordinate of the end of the vector
     * @param y2 y coordinate of the end of the vector
     *
     */
    public GVector(
            final int x1,
            final int y1,
            final int x2,
            final int y2)
    {
        this.x1 = x1;
        this.y1 = y1;

        this.a = Math.atan2(y2 - y1, x2 - x1);
        this.l = Math.hypot(x2 - x1, y2 - y1);

        calc();
    }

    /**
     * Multiplies this vector by a scalar
     *
     * @param scalar scale
     */
    public void scalarMultiplication(final double scalar) {
        l *= scalar;
        calc();
    }

    /**
     * Calculates the dot product between this vector and the one passed as an
     * argument
     *
     * @param v other vector
     * @return dot product
     * @throws IllegalArgumentException if {@code v} is {@code null}
     */
    public double dot(final GVector v) {
        if (v == null){
            throw new IllegalArgumentException("The vector can't be null");
        }

        final int xx1 =   getXComponent();
        final int yy1 =   getYComponent();
        final int xx2 = v.getXComponent();
        final int yy2 = v.getYComponent();

        return (xx1 * xx2) + (yy1 * yy2);
    }

    /**
     * Calculates the module of the orthogonal vector resulting of the cross
     * product between this vector and the one passed as an argument,
     * considering the {@code z} coordinate of both vectors equal to zero
     *
     * @param v other vector
     * @return module of the cross product vector
     * @throws IllegalArgumentException if {@code v} is {@code null}
     */
    public double cross(final GVector v) {
        if (v == null){
            throw new IllegalArgumentException("The vector can't be null");
        }

        final int xx1 =   getXComponent();
        final int yy1 =   getYComponent();
        final int xx2 = v.getXComponent();
        final int yy2 = v.getYComponent();

        return (xx1 * yy2) - (yy1 * xx2);
    }

    /**
     * Retrieves the angle between this vector and the one passed as an argument
     *
     * @param v other vector
     * @return Angle between vectors (in degrees)
     * @throws IllegalArgumentException if {@code v} is {@code null}
     */
    public double angleBetween(final GVector v) {
        if (v == null){
            throw new IllegalArgumentException("The vector can't be null");
        }

        return Math.toDegrees(radAngleBetween(v));
    }

    /**
     * Retrieves the angle between this vector and the one passed as an argument
     *
     * @param v other vector
     * @return Angle between vectors (in radians)
     * @throws IllegalArgumentException if {@code v} is {@code null}
     */
    public double radAngleBetween(final GVector v) {
        if (v == null){
            throw new IllegalArgumentException("The vector can't be null");
        }

        return Math.asin(cross(v) / (modulus() * v.modulus()));
    }

    /**
     * Retrieves a vector with the same direction and application point but
     * with a modulus equal to 1
     *
     * @return normalized vector
     */
    public GVector normalized() {
        final GVector v = new GVector(this);
        v.scalarMultiplication(1. / modulus());
        return v;
    }

    /**
     * Changes the modulus of this vector
     *
     * @param modulus new modulus of the vector
     */
    public void setModulus(final double modulus) {
        l = modulus;
        calc();
    }

    /**
     * Changes the argument of this vector (in radians)
     *
     * @param arg new argument of the vector
     */
    public void setRadArgument(final double arg) {
        a = arg % (2 * Math.PI);
        calc();
    }

    /**
     * Changes the argument of this vector (in degrees)
     *
     * @param arg new argument of the vector
     */
    public void setArgument(final double arg) {
        a = Math.toRadians(arg % 360);
        calc();
    }

    /**
     * Changes the arrow weight. <br>
     * The arrow weight is defined as the length of the arrow, by default the
     * arrow will be appended to the line, but if {@code arrowWeight} is
     * negative then the arrow will end at the end of the line.
     *
     * @param arrowWeight new arrow weight
     */
    public void setArrowWeight(final int arrowWeight) {
        aw = arrowWeight;
        calc();
    }

    /**
     * Sets the new angle for the tip of the arrow (in degrees).
     *
     * @param arrowAngle angle in degrees.
     */
    public void setArrowTipAngle(final double arrowAngle) {
        aa = Math.toRadians(arrowAngle / 2);
        calc();
    }

    /**
     * Retrieves the X coordinate of the point of application of the vector
     *
     * @return x coordinate
     */
    public int x() {
        return x1;
    }

    /**
     * Retrieves the Y coordinate of the point of application of the vector
     *
     * @return y coordinate
     */
    public int y() {
        return y1;
    }

    /**
     * Retrieves the X coordinate of the tip of the vector
     *
     * @return X coordinate
     */
    public int xf() {
        return x2;
    }

    /**
     * Retrieves the Y coordinate of the tip of the vector
     *
     * @return Y coordinate
     */
    public int yf() {
        return y2;
    }

    /**
     * Retrieves the X projection of this vector
     *
     * @return x component
     */
    public int getXComponent() {
        return x2 - x1;
    }

    /**
     * Retrieves the Y projection of this vector
     *
     * @return y component
     */
    public int getYComponent() {
        return y2 - y1;
    }

    /**
     * Retrieves the modulus of the vector
     *
     * @return modulus
     */
    public double modulus() {
        return l;
    }

    /**
     * Retrieves the argument of the vector (in degrees)
     *
     * @return argument
     */
    public double argument() {
        return Math.toDegrees(a);
    }

    /**
     * Retrieves the argument of the vector (in radians)
     *
     * @return argument
     */
    public double radArgument() {
        return a;
    }

    public void rotate(final double a) {
        radRotate(Math.toRadians(a));
    }

    public void radRotate(final double a) {
        this.a += a;
        this.a %= 2 * Math.PI;

        calc();
    }

    private void calc() {
        final double sl =-Math.signum(l) * Math.abs(aw);
        final double ca = Math.cos(a);
        final double sa = Math.sin(a);

        //Ensure the arrow position when l is negative
        //final double ll = aw > 0 ? (l < 0 ? -aw : aw) : 0;

        //Last point of the line
        x2 = x1 + (int)(l * ca);
        y2 = y1 + (int)(l * sa);

        //Calculate triangle
        final int xx = x2;// - (int)(ll * ca);
        final int yy = y2;// - (int)(ll * sa);

        xs[0] = xx;
        xs[1] = xx + (int)(sl * Math.cos(a -  aa));
        xs[2] = xx + (int)(sl * Math.cos(a +  aa));
        ys[0] = yy;
        ys[1] = yy + (int)(sl * Math.sin(a -  aa));
        ys[2] = yy + (int)(sl * Math.sin(a +  aa));

        n = Math.abs(l) < 1e-10 ? 0 : 3;
    }

    /**
     * Adds a finite number of vectors with <b>no</b> overflow check
     *
     * @param vectors the vectors to add
     * @return A new {@link GVector} equal to the resultant
     * @throws IllegalArgumentException if no vector is passed
     */
    public GVector add(final GVector... vectors) {
        if (vectors == null){
            throw new IllegalArgumentException("You must add at least ONE vector");
        }

        int x = x2;
        int y = y2;
        for (GVector vector : vectors){
            x += vector.x2 - vector.x1;
            y += vector.y2 - vector.y1;
        }

        final double mod = Math.hypot(x, y);
        final double arg = Math.toDegrees(Math.atan2(y, x));

        return new GVector(x1, y1, mod, arg);
    }

    public GVector subtract(final GVector... vectors) {
        if (vectors == null){
            throw new IllegalArgumentException("You must add at least ONE vector");
        }

        int x = x2;
        int y = y2;
        for (final GVector vector : vectors){
            x -= vector.x2 - vector.x1;
            y -= vector.y2 - vector.y1;
        }

        final double mod = Math.hypot(x, y);
        final double arg = Math.toDegrees(Math.atan2(y, x));

        return new GVector(x1, y1, mod, arg);
    }

    public double distance(final GVector v) {
        if (v == null){
            throw new IllegalArgumentException("The vector can't be null");
        }
        //@TODO test me
        return subtract(v).modulus();
    }

    @Override
    public void draw(Graphics2D g) {
        g.setPaint(getPaint());
        g.setStroke(getStroke());
        g.drawLine(x1, y1, x2, y2);
        g.drawPolygon(xs, ys, n);
        g.fillPolygon(xs, ys, n);
    }

    @Override
    public void traslate(final int x, final int y) {
        x1 += x;
        y1 += y;
        calc();
    }

    /**
     * Moves the application point of this vector to the given coordinates
     *
     * @param x new x coordinate
     * @param y new y coordinate
     */
    public void move(final int x, final int y) {
        x1 = x;
        y1 = y;
        calc();
    }

    @Override
    public GVector clone() {
        return new GVector(this);
    }

    @Override
    public int hashCode() {
        int hash = super.hashCode();
        hash = 89 * hash + Arrays.hashCode(xs);
        hash = 89 * hash + Arrays.hashCode(ys);
        hash = 89 * hash + x1;
        hash = 89 * hash + y1;
        hash = 89 * hash + x2;
        hash = 89 * hash + y2;
        hash = 89 * hash + aw;
        hash = 89 * hash + n;
        hash = 89 * hash + (int) (Double.doubleToLongBits(l)
                               ^ (Double.doubleToLongBits(l) >>> 32));
        hash = 89 * hash + (int) (Double.doubleToLongBits(a)
                               ^ (Double.doubleToLongBits(a) >>> 32));
        hash = 89 * hash + (int) (Double.doubleToLongBits(aa)
                               ^ (Double.doubleToLongBits(aa) >>> 32));
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (!super.equals(obj)) {
            return false;
        }

        final GVector other = (GVector) obj;
        if (!Arrays.equals(this.xs, other.xs)) {
            return false;
        }
        if (!Arrays.equals(this.ys, other.ys)) {
            return false;
        }

        return !(
            aa != other.aa |
            aw != other.aw |
            x1 != other.x1 |
            y1 != other.y1 |
            x2 != other.x2 |
            y2 != other.y2 |
            n  != other.n  |
            l  != other.l  |
            a  != other.a
        );
    }

}