GAxis.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.extras;

import com.dkt.graphics.canvas.Canvas;
import com.dkt.graphics.elements.GLine;
import com.dkt.graphics.elements.Graphic;
import com.dkt.graphics.elements.GraphicE;
import com.dkt.graphics.exceptions.IntervalException;
import com.dkt.graphics.exceptions.InvalidArgumentException;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.util.Objects;

/**
 * This class represents a pair of Cartesian axis.
 *
 * @author Federico Vera {@literal<[email protected]>}
 */
public class GAxis extends GraphicE {
    private final Graphic axis = new Graphic();
    private final int xs;
    private final int xf;
    private final int ys;
    private final int yf;
    
    private int mtx = 10;
    private int Mtx = 50;
    private int mty = 10;
    private int Mty = 50;
    
    private boolean drawLinesH;
    private boolean drawLinesV;
    
    private Paint gridPaint = Color.LIGHT_GRAY;
    
    private int mTS = 2;
    private int MTS = 5;

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

        drawLinesH = e.drawLinesH;
        drawLinesV = e.drawLinesV;
        gridPaint  = e.gridPaint;
        mTS = e.mTS;
        MTS = e.MTS;
        Mtx = e.Mtx;
        Mty = e.Mty;
        mtx = e.mtx;
        mty = e.mty;
        xs = e.xs;
        xf = e.xf;
        ys = e.ys;
        yf = e.xf;
    }

    /**
     * Main constructor of the Axis.<br>
     * This constructor asks for the dimensions of the axis, but if you want it
     * centered in the drawable area of the canvas, you'll need to set the
     * origin of the canvas in the correct position.
     *
     * @param xs The starting point of the X axis
     * @param xf The ending point of the X axis
     * @param ys The starting point of the Y axis
     * @param yf The ending point of the Y axis
     * @throws IntervalException if either starting point is bigger than the
     * corresponding end point
     * @see GAxis#setOrigin(Canvas)
     */
    public GAxis(
            final int xs,
            final int xf,
            final int ys,
            final int yf)
    {
        if (xf <= xs){
            String msg = "The starting point can't be bigger than the end "
                       + "point (X Axis)";
            throw new IntervalException(msg, xs, xf);
        }

        if (yf <= ys){
            String msg = "The starting point can't be bigger than the end "
                       + "point (Y Axis)";
            throw new IntervalException(msg, ys, yf);
        }

        this.xs = xs;
        this.xf = xf;
        this.ys = ys;
        this.yf = yf;
        calc();
    }

    /**
     * Tells the axis to draw the minor ticks of the horizontal axis with a
     * distance of 'h'
     *
     * @param h the vertical
     * @throws InvalidArgumentException if {@code h} is smaller than 1
     */
    public void minorTicksH(final int h) {
        if (h != 0 && h < 1){
            String msg = "The minor tick space must be bigger than 1";
            throw new InvalidArgumentException(msg);
        }

        this.mtx = h;
        calc();
    }

    /**
     * Tells the axis to draw the mayor ticks of the horizontal axis with a
     * distance of 'h'.<br>
     * <i>Note:</i> The mayor ticks MUST correspond to a minor tick. What does
     * this mean? that the minor tick 'h' and the mayor tick 'h' must be
     * perfect integer multiples. <pre>
     *                  MTickH = mTickH * i (i = 2,3,4,5,6,....)</pre>
     *
     * @param h the new distance between mayor horizontal ticks
     * @throws InvalidArgumentException if 'h' is smaller than the minor tick
     * space or if 'h' is not a perfect multiple of mtx
     */
    public void mayorTicksH(final int h) {
        if (h != 0){
            if (h <= mtx){
                String msg = "The mayor tick must be bigger than the minor tick"
                           + " (mtx: " + mtx + ")";
                throw new InvalidArgumentException(msg);
            }
            if (mtx != 0 && h % mtx != 0){
                String msg = "The mayor tick must be a perfect multiple of the "
                           + "minor tick (i.e. " + mtx * 2 + ", " + mtx * 3
                            + ", " + mtx * 4 + ", ...)";
                throw new InvalidArgumentException(msg);
            }
        }

        this.Mtx = h;
        calc();
    }

    /**
     * Tells the axis to draw the minor ticks of the vertical axis with a
     * distance of 'v'
     *
     * @param v the vertical
     * @throws InvalidArgumentException if {@code v} is smaller than 1
     */
    public void minorTicksV(final int v) {
        if (v != 0 && v < 1){
            String msg = "The minor tick space must be bigger than 1";
            throw new InvalidArgumentException(msg);
        }

        this.mty = v;
        calc();
    }

    /**
     * Tells the axis to draw the mayor ticks of the vertical axis with a
     * distance of 'v'.<br>
     * <i>Note:</i> The mayor ticks MUST correspond to a minor tick. What does
     * this mean? that the minor tick 'v' and the mayor tick 'v' must be
     * perfect integer multiples. <pre>
     *                  MTickV = mTickV * i (i = 2,3,4,5,6,....)</pre>
     *
     * @param v the new distance between mayor horizontal ticks
     * @throws InvalidArgumentException if 'v' is smaller than the minor tick
     * space or if 'v' is not a perfect multiple of mty
     */
    public void mayorTicksV(final int v) {
        if (v != 0){
            if (v <= mty){
                String msg = "The mayor tick must be bigger than the minor tick"
                           + " (mtx: " + mty + ")";
                throw new InvalidArgumentException(msg);
            }
            if (mty != 0 && v % mty != 0){
                String msg = "The mayor tick must be a perfect multiple of the "
                           + "minor tick (i.e. " + mty * 2 + ", " + mty * 3
                            + ", " + mty * 4 + ", ...)";
                throw new InvalidArgumentException(msg);
            }
        }

        this.Mty = v;
        calc();
    }

    /**
     * Tells the Axis to draw the horizontal lines for the grid
     *
     * @param drawLinesH {@code true} if you want to see the horizontal lines
     * and {@code false} otherwise
     */
    public void drawLinesH(final boolean drawLinesH) {
        this.drawLinesH = drawLinesH;
        calc();
    }

    /**
     * Tells the Axis to draw the vertical lines for the grid
     *
     * @param drawLinesV {@code true} if you want to see the vertical lines
     * and {@code false} otherwise
     */
    public void drawLinesV(final boolean drawLinesV) {
        this.drawLinesV = drawLinesV;
        calc();
    }

    /**
     * Sets the color for the grid (if shown).<br>
     * The default value is {@link Color#LIGHT_GRAY}
     *
     * @param paint The new {@link Paint} for the grid
     * @throws IllegalArgumentException if the paint is {@code null}
     */
    public void setGridColor(final Paint paint) {
        if (paint == null){
            throw new IllegalArgumentException("Grid paint can't be null");
        }

        this.gridPaint = paint;
        calc();
    }

    /**
     * This method set's the origin of the {@link Canvas} on the correct place
     * in order for the Axis to be completely displayed.
     *
     * @param canvas canvas you want to set
     * @throws IllegalArgumentException if the canvas is {@code null}
     */
    public void setOrigin(final Canvas canvas) {
        if (canvas == null){
            throw new IllegalArgumentException("Canvas can't be null");
        }

        canvas.setCenterOrigin(false);
        canvas.moveOrigin(-xs, yf);
    }

    /**
     * Sets the minor tick size<br>
     * <i>Note:</i> This size is used at both sides of the axis so the final
     * length of the tick will be two times this value
     *
     * @param mTS minor tick size
     */
    public void setMinorTickSize(final int mTS) {
        this.mTS = mTS;
        calc();
    }

    /**
     * Retrieves the minor tick size. The default value is 2.
     *
     * @return minor tick size
     */
    public int getMinorTickSize() {
        return mTS;
    }

    /**
     * Sets the mayor tick size<br>
     * <i>Note:</i> This size is used at both sides of the axis so the final
     * length of the tick will be two times this value
     *
     * @param MTS mayor tick size
     */
    public void setMayorTickSize(final int MTS) {
        this.MTS = MTS;
        calc();
    }

    /**
     * Retrieves the mayor tick size. The default value is 5.
     *
     * @return mayor tick size
     */
    public int getMayorTickSize() {
        return MTS;
    }

    private void calc() {
        axis.removeAll();

        //Draw the grid on the background
        if (drawLinesV && Mtx >= 1){
            dLLTZ(Mtx, xs, ys, yf, true, gridPaint);
            dLBTZ(Mtx, xf, ys, yf, true, gridPaint);
        }
        if (drawLinesH && Mty >= 1){
            dLLTZ(Mty, ys, xs, xf, false, gridPaint);
            dLBTZ(Mty, yf, xs, xf, false, gridPaint);
        }

        //Draw the axis lines
        axis.add(new GLine(xs, 0 , xf, 0 ));
        axis.add(new GLine(0 , ys, 0 , yf));

        //Draw tics
        //Minor horizontal ticks
        dLLTZ(mtx, xs, -mTS, mTS, true, getPaint());
        dLBTZ(mtx, xf, -mTS, mTS, true, getPaint());
        //Mayor horizontal ticks
        dLLTZ(Mtx, xs, -MTS, MTS, true, getPaint());
        dLBTZ(Mtx, xf, -MTS, MTS, true, getPaint());
        //Minor vertical ticks
        dLLTZ(mty, ys, -mTS, mTS, false, getPaint());
        dLBTZ(mty, yf, -mTS, mTS, false, getPaint());
        //Mayor vertical ticks
        dLLTZ(Mty, ys, -MTS, MTS, false, getPaint());
        dLBTZ(Mty, yf, -MTS, MTS, false, getPaint());
    }

    private void dLLTZ(
            final int s,
            final int e,
            final int n1,
            final int n2,
            final boolean f,
            final Paint p)
    {
        if (n1 == n2) {
            return;
        }

        for (int xx = -s; xx >= e; xx -= s){
            dLAZ(xx, n1, n2, f, p);
        }
    }

    private void dLBTZ(
            final int s,
            final int e,
            final int n1,
            final int n2,
            final boolean f,
            final Paint p)
    {
        if (n1 == n2) {
            return;
        }

        for (int xx = s; xx < e; xx += s){
            dLAZ(xx, n1, n2, f, p);
        }
    }

    private void dLAZ(
            final int xx,
            final int n1,
            final int n2,
            final boolean f,
            final Paint p)
    {
        final GLine line;

        if (f){
            line = new GLine(xx, n1, xx, n2);
        } else {
            line = new GLine(n1, xx, n2, xx);
        }

        line.setPaint(p);
        axis.add(line);
    }

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

    @Override
    public int hashCode() {
        int hash = super.hashCode();
        hash = 23 * hash + xs;
        hash = 23 * hash + xf;
        hash = 23 * hash + ys;
        hash = 23 * hash + yf;
        hash = 23 * hash + mtx;
        hash = 23 * hash + Mtx;
        hash = 23 * hash + mty;
        hash = 23 * hash + Mty;
        hash = 23 * hash + (drawLinesH ? 1 : 0);
        hash = 23 * hash + (drawLinesV ? 1 : 0);
        hash = 23 * hash + Objects.hashCode(gridPaint);
        hash = 23 * hash + mTS;
        hash = 23 * hash + MTS;
        return hash;
    }

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

        final GAxis other = (GAxis) obj;
        if (!Objects.equals(gridPaint, other.gridPaint)) {
            return false;
        }

        return !(
            xs  != other.xs  |
            xf  != other.xf  |
            ys  != other.ys  |
            yf  != other.yf  |
            mtx != other.mtx |
            Mtx != other.Mtx |
            mty != other.mty |
            Mty != other.Mty |
            mTS != other.mTS |
            MTS != other.MTS |
            drawLinesH != other.drawLinesH |
            drawLinesV != other.drawLinesV
        );
    }

    @Override
    public void traslate(int x, int y) {
        axis.traslate(x, y);
    }

    @Override
    public void draw(Graphics2D g) {
        axis.draw(g);
    }

}