GFormula.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.elements.GPath;
import com.dkt.graphics.elements.GPointArray;
import com.dkt.graphics.elements.GPoly;
import com.dkt.graphics.elements.GRegPoly;
import com.dkt.graphics.elements.Graphic;
import com.dkt.graphics.elements.GraphicE;
import com.dkt.graphics.exceptions.IntervalException;
import com.dkt.graphics.extras.formula.Calculable;
import java.awt.Paint;

/**
 * This class represents a basic Formula.<br>
 * It translates a {@link Calculable} into a Graphic.
 *
 * @author Federico Vera {@literal<[email protected]>}
 */
public class GFormula extends Graphic {
    private final Calculable formula;

    private Paint area;

    /**
     * Creates a new {@code GFormula} for the given {@link Calculable} object
     *
     * @param formula the {@link Calculable} object that contains the relation
     * @throws IllegalArgumentException if formula is {@code null}
     */
    public GFormula(Calculable formula){
        if (formula == null){
            throw new IllegalArgumentException("Formula can't be null");
        }

        this.formula = formula;
    }

    /**
     * Calculates the formula, this method must be called before the method is
     * printed in the canvas, otherwise it will print nothing.
     *
     * @param xs The starting point of the interval
     * @param xf The end point of the interval
     * @param step The step used to f
     * @see GFormula#calculate(double, double, double, GraphicE)
     * @see GFormula#calculateArea(double, double, double)
     * @see GFormula#calculatePath(double, double, double)
     * @throws IntervalException if {@code xs < xf} or {@code step < 0} or
     * {@code step > xf - xs}
     */
    public void calculate(
            final double xs,
            final double xf,
            final double step) throws IntervalException
    {
        checkValues(xs, xf, step);

        removeAll();

        final double sx = formula.scaleX();
        final double sy = formula.scaleY();
        final int size  = getSize(xs, xf, step);

        final GPointArray array = new GPointArray(size);

        int i   = 0;
        int lx  = Integer.MAX_VALUE;
        int lfx = Integer.MAX_VALUE;

        for (double xx = xs; xx < xf; xx = xs + i * sx, i++){
            final int x  = (int)(sx * xx);
            final int fx = (int)(sy * formula.f(xx));

            //if the distance between points is 0px then don't append it
            if (lx != x | lfx != fx){
                array.append(x, fx);
                lfx = fx;
                lx  = x;
            }
        }

        array.setPaint(getPaint());

        add(array);
    }

    /**
     * Calculates the formula, this method must be called before the method is
     * printed in the canvas, otherwise it will print nothing.<br>
     * <i>Note:</i> The element must be in the position {@code (0, 0)}
     * otherwise it the graphics will be in the incorrect position. Of course
     * this will come in handy with {@link Graphic} that have strange shapes.
     * <br><i>Note 2:</i> For some reason that I was unable to figure out yet,
     * this method fails when using {@link GRegPoly}.
     *
     * @param xs The starting point of the interval
     * @param xf The end point of the interval
     * @param step The step used to f
     * @param element The element that will be copied and translated in order
     * to generate this value
     * @see GFormula#calculate(double, double, double)
     * @see GFormula#calculateArea(double, double, double)
     * @see GFormula#calculatePath(double, double, double)
     * @throws IntervalException if {@code xs < xf} or {@code step < 0} or
     * {@code step > xf - xs}
     * @throws IllegalArgumentException if {@code element} is {@code null}
     */
    public void calculate(
            final double xs,
            final double xf,
            final double step,
            final GraphicE element) throws IntervalException,
                                           IllegalArgumentException
    {
        checkValues(xs, xf, step);

        if (element == null){
            throw new IllegalArgumentException("Element can't be null");
        }

        removeAll();

        final double sx = formula.scaleX();
        final double sy = formula.scaleY();

        int i   = 0;
        int lx  = Integer.MAX_VALUE;
        int lfx = Integer.MAX_VALUE;

        for (double xx = xs; xx < xf; xx = xs + i * step, i++){
            final int x  = (int)(sx * xx);
            final int fx = (int)(sy * formula.f(xx));

            //if the distance between points is 0px then don't append it
            if (lx != x | lfx != fx){
                final GraphicE e = element.clone();
                e.traslate(x, fx);
                lfx = fx;
                lx  = x;
                add(e);
            }
        }
    }

    /**
     * Calculates the formula, this method must be called before the method is
     * printed in the canvas, otherwise it will print nothing. The drawing will
     * be based on lines.
     *
     * @param xs The starting point of the interval
     * @param xf The end point of the interval
     * @param step The step used to f
     * @see GFormula#calculate(double, double, double)
     * @see GFormula#calculate(double, double, double, GraphicE)
     * @see GFormula#calculateArea(double, double, double)
     * @throws IntervalException if {@code xs < xf} or {@code step < 0} or
     * {@code step > xf - xs}
     */
    public void calculatePath(
            final double xs,
            final double xf,
            final double step) throws IntervalException
    {
        checkValues(xs, xf, step);

        removeAll();

        final double sx = formula.scaleX();
        final double sy = formula.scaleY();
        final int size  = getSize(xs, xf, step);

        final GPath path = new GPath(size);

        int i   = 0;
        int lx  = Integer.MAX_VALUE;
        int lfx = Integer.MAX_VALUE;

        for (double xx = xs; xx < xf; xx = xs + i * step, i++){
            final int x  = (int)(sx * xx);
            final int fx = (int)(sy * formula.f(xx));

            //if the distance between points is 0px then don't append it
            if (lx != x | lfx != fx){
                path.append(x, fx);

                lfx = fx;
                lx  = x;
            }
        }

        path.setPaint(getPaint());
        path.setStroke(getStroke());

        add(path);
    }

    /**
     * Sets the {@link Paint} used for the area below the curve
     *
     * @param paint The {@link Paint} that will be used to render the area
     * @throws IllegalArgumentException if {@code paint} is {@code null}
     */
    public void setAreaPaint(Paint paint) {
        if (paint == null){
            throw new IllegalArgumentException("Paint can't be null");
        }
        this.area = paint;
    }

    /**
     * Calculates the formula, this method must be called before the method is
     * printed in the canvas, otherwise it will print nothing. The drawing will
     * be based on areas.<br>
     * <i>Note:</i> you must set the area paint first
     *
     * @param xs The starting point of the interval
     * @param xf The end point of the interval
     * @param step The step used to f
     * @see GFormula#calculate(double, double, double)
     * @see GFormula#calculate(double, double, double, GraphicE)
     * @see GFormula#calculateArea(double, double, double)
     * @see GFormula#setAreaPaint(Paint)
     * @throws IntervalException if {@code xs < xf} or {@code step < 0} or
     * {@code step > xf - xs}
     */
    public void calculateArea(
            final double xs,
            final double xf,
            final double step) throws IntervalException
    {
        checkValues(xs, xf, step);

        removeAll();

        final double sx = formula.scaleX();
        final double sy = formula.scaleY();
        final int size  = getSize(xs, xf, step);

        final GPoly poly = new GPoly(size + 2);

        poly.append((int)(sx * xs), 0);

        int i   = 0;
        int lx  = Integer.MAX_VALUE;
        int lfx = Integer.MAX_VALUE;

        for (double xx = xs; xx < xf; xx = xs + i * step, i++){
            final int x  = (int)(sx * xx);
            final int fx = (int)(sy * formula.f(xx));

            //if the distance between points is 0px then don't append it
            if (lx != x | lfx != fx){
                poly.append(x, fx);

                lfx = fx;
                lx  = x;
            }
        }

        poly.append(lx, 0);

        poly.setStroke(getStroke());
        poly.setPaint(getPaint());
        poly.setFillPaint(area == null ? getPaint() : area);
        poly.setFill(true);

        add(poly);
    }

    private static void checkValues(double xs, double xf, double step){
        if (xs >= xf){
            String msg = "The final point must be bigger than the initial point";
            throw new IntervalException(msg, xs, xf, step);
        }

        if (step <= 0.0){
            String msg = "The step must be a non-zero positive real!";
            throw new IntervalException(msg, xs, xf, step);
        }

        if (step > xf - xs){
            String msg = "The step can't be bigger than the interval!";
            throw new IntervalException(msg, xs, xf, step);
        }
    }

    private int getSize(double xs, double xf, double step) {
        //The extra 2% is for good luck... =P
        return (int)((xf - xs) / step * 1.02);
    }

}