Graphic.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 com.dkt.graphics.exceptions.InvalidArgumentException;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.util.Iterator;
import java.util.Objects;

/**
 * This class represents a Graphic made by a collection of {@link GraphicE}
 * components.<br>
 * <i>Note:</i> this class is a {@link GraphicE} on itself, so you can add it to
 * Graphics.
 *
 * @author Federico Vera {@literal<[email protected]>}
 */
public class Graphic extends GraphicE implements Iterable<GraphicE> {
    private final UnsafeList components;
    private boolean visible = true;
    private int xOff;
    private int yOff;

    public Graphic() {
        this(20);
    }

    public Graphic(int initialSize) {
        components = new UnsafeList(initialSize);
    }

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

        xOff = e.xOff;
        yOff = e.yOff;

        components = new UnsafeList(e.components.size());
        synchronized (e.components){
            for (final GraphicE ge : e.components){
                components.add(ge.clone());
            }
        }
    }

    /**
     * Retrieves the number of {@link GraphicE} components that compose this
     * {@code Graphic}. This method will not count recursively.
     *
     * @return component count
     */
    public int getCount() {
        return components.size();
    }

    /**
     * Adds a new {@link GraphicE} to this Graphic<br>
     * <i>Note:</i> all the elements that are added will be automatically
     * traslated the same amount as the sum of the traslations up to this
     * moment
     * <i>Note 2:</i>This method doesn't check if the element is already
     * contained on the {@code Graphic}, this mean, that you can add elements
     * twice, the <i>downside</i> (or amazing feature, according to a couple of
     * comments) is that the elements that are added twice will be traslated
     * twice as much as the other elements.
     *
     * @param e element to add
     * @throws IllegalArgumentException if {@code e} is {@code null}
     * @throws InvalidArgumentException if {@code e} is this same object
     * @see Graphic#traslate(int, int)
     */
    public void add(final GraphicE e) {
        if (e == null){
            throw new IllegalArgumentException("The element can't be null");
        }
        if (e == this){
            final String msg = "Graphics can't be added to themselves";
            throw new InvalidArgumentException(msg);
        }

        synchronized (components){
            components.add(e);
            e.traslate(xOff, yOff);
        }
    }

    /**
     * Tells if a given {@link GraphicE} is contained on this {@link Graphic}.
     *
     * @param e element to test
     * @return {@code true} if the element is contained and {@code false}
     * otherwise
     * @throws IllegalArgumentException if {@code e} is {@code null}
     * @see Graphic#flatten()
     */
    public boolean contains(final GraphicE e) {
        if (e == null){
            throw new IllegalArgumentException("The element can't be null");
        }

        synchronized (components){
            return components.contains(e);
        }
    }

    /**
     * Removes a given {@link GraphicE} from this {@link Graphic}
     *
     * @param e element to remove
     * @return {@code true} if the element was contained and {@code false}
     * otherwise
     * @throws IllegalArgumentException if {@code e} is {@code null}
     * @see Graphic#flatten()
     */
    public boolean remove(final GraphicE e) {
        if (e == null){
            throw new IllegalArgumentException("The element can't be null");
        }

        synchronized (components){
            return components.remove(e);
        }
    }

    /**
     * Retrieves the index of a given element on the {@code Graphic}.
     *
     * @param e element
     * @return The index number of the first occurrence of the element, or
     * {@code -1} if the element wasn't found
     * @throws IllegalArgumentException if {@code e} is {@code null}
     * @see Graphic#flatten()
     */
    public int indexOf(final GraphicE e) {
        if (e == null){
            throw new IllegalArgumentException("The element can't be null");
        }

        synchronized (components){
            return components.indexOf(e);
        }
    }

    /**
     * Clears all the components from this {@link Graphic}
     */
    public void removeAll() {
        synchronized (components){
            components.clear();
        }
    }

    @Override
    public void draw(final Graphics2D g) {
        if (!isVisible()) {
            return;
        }

        final AffineTransform at = g.getTransform();
        final Shape clip = g.getClip();

        synchronized (components){
            for (final GraphicE e : components){
                e.draw(g);
            }
        }

        g.setClip(clip);
        g.setTransform(at);
    }

    @Override
    public void traslate(final int x, final int y) {
        synchronized (components){
            for (final GraphicE e : components){
                e.traslate(x, y);
            }

            xOff += x;
        }
    }

    /**
     * Moves all the elements to a given location.<br>
     * This method tends to ruin graphics... use it with care.
     *
     * @param x new X coordinate
     * @param y new Y coordinate
     */
    public void move(final int x, final int y) {
        synchronized (components){
            for (final GraphicE e : components){
                e.traslate(-xOff, -yOff);
                e.traslate(x, y);
            }

            xOff = x;
            yOff = y;
        }
    }

    /**
     * This method adds all of the elements of a given set of {@code Graphic}
     * components into the current element.<br>
     *
     * @param graphics array of graphics to merge
     * @see Graphic#flatten()
     */
    public void addAll(final Graphic... graphics) {
        synchronized (components){
            for (final Graphic graph : graphics){
                components.addAll(graph.components);
            }
        }
    }

    /**
     * This method adds all of the elements of a given set of {@code Graphic}
     * components into the current element.<br>
     *
     * @param graphics array of graphics to merge
     * @see Graphic#flatten()
     */
    public void addCopyOfAll(final Graphic... graphics) {
        synchronized (components){
            for (final Graphic graph : graphics){
                components.addAll(graph.clone().components);
            }
        }
    }

    /**
     * Tells if {@code Graphic} will be drawn in the canvas
     *
     * @return {@code true} if the {@code Graphic} is visible and {@code flase}
     * otherwise
     */
    public boolean isVisible() {
        return visible;
    }

    /**
     *
     * @param v {@code true} if the {@code Graphic} will visible and
     * {@code flase} otherwise
     */
    public void setVisible(boolean v) {
        visible = v;
    }

    /**
     * Flattens the structure of a Graphic, this method is very slow, but it will
     * increase the performance of draw(). <br>
     *
     * This method is specially useful when you use the GraphicCreator, or when
     * combining several {@code Graphic}. Consider trying to search for a specific
     * element when having multiple {@code Graphic} objects... basically, you'll
     * never be able to find them if you need to enter each and every
     * {@code Graphic} object to check.
     */
    public void flatten() {
        synchronized (components){
            Graphic foo = new Graphic(this.getCount());
            flatten(this, foo);
            removeAll();
            components.addAll(foo.components);
            foo.removeAll();
        }
    }

    private void flatten(Graphic src, Graphic dest) {
        synchronized (src.components){
            for (GraphicE component : src.components) {
                if (component instanceof Graphic) {
                    flatten((Graphic)component, dest);
                    ((Graphic)component).removeAll();
                } else {
                    dest.add(component);
                }
            }
        }
    }

    @Override
    public Iterator<GraphicE> iterator() {
        return components.iterator();
    }

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

    @Override
    public int hashCode() {
        int hash = super.hashCode();
        for (int i = 0; i < components.size(); i++) {
            hash = 47 * hash + components.get(i).hashCode();
        }
        hash = 47 * hash + xOff;
        hash = 47 * hash + yOff;
        hash = 47 * hash + (visible ? 1 : 0);
        return hash;
    }

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

        final Graphic other = (Graphic) obj;

        for (int i = 0; i < components.size(); i++) {
            if (!Objects.equals(components.get(i), other.components.get(i))) {
                return false;
            }
        }

        return !(
            visible != other.visible |
            xOff != other.xOff |
            yOff != other.yOff
        );
    }

}