GMultiPoint.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.Polygon;
import java.awt.geom.Area;
import java.util.Arrays;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Represents an abstract array of (x, y) coordinates.<br>
 * <i>Note:</i> even though this class extends from {@link GFillableE} it's the
 * subclass option to enforce the fill methods
 *
 * @author Federico Vera {@literal<[email protected]>}
 */
public abstract class GMultiPoint extends GFillableE
                               implements Iterable<GPoint> {
    protected final AtomicInteger modCount = new AtomicInteger(0);
    protected final Lock mutex = new ReentrantLock();
    protected int size;
    protected int[] xs;
    protected int[] ys;

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

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

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

    /**
     * @param initial initial reserved size
     * @throws NegativeArraySizeException if the size is less than zero
     */
    protected GMultiPoint(final int initial) {
        this(new int[initial], new int[initial]);
        size = 0;
    }

    /**
     * @param xs list of X coordinates
     * @param ys list of Y coordinates
     * @throws IllegalArgumentException if either array is {@code null} or if
     * the array size doesn't match
     */
    protected GMultiPoint(final int[] xs, final int[] ys) {
        if (xs == null || ys == null){
            throw new IllegalArgumentException("Neither array can be null");
        }

        if (xs.length != ys.length){
            String msg = "Both arrays MUST have the same size";
            throw new InvalidArgumentException(msg);
        }

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

        System.arraycopy(xs, 0, this.xs, 0, size);
        System.arraycopy(ys, 0, this.ys, 0, size);
    }

    /**
     * Retrieves the number of vertices
     *
     * @return number of vertices
     */
    public int size() {
        mutex.lock();
        try{
            return size;
        } finally {
            mutex.unlock();
        }
    }

    /**
     * Retrieves the index of the first occurrence of this point
     *
     * @param point point to check
     * @return first index of the point or -1 if the point is not one of the
     * path's vertices
     * @throws IllegalArgumentException if {@code point} is {@code null}
     */
    public int indexOf(final GPoint point) {
        if (point == null){
            throw new IllegalArgumentException("Point can't be null");
        }

        return indexOf(point.x(), point.y(), 0);
    }

    /**
     * Retrieves the index of the first occurrence of this point after the
     * starting point
     *
     * @param point point to check
     * @param start starting point of the search
     * @return first index of the point or -1 if the point is not one of the
     * path's vertices
     * @throws IllegalArgumentException if {@code point} is {@code null}
     */
    public int indexOf(final GPoint point, final int start) {
        if (point == null){
            throw new IllegalArgumentException("Point can't be null");
        }

        return indexOf(point.x(), point.y(), start);
    }

    /**
     * Retrieves the index of the first occurrence of this point
     *
     * @param x y coordinate of the point
     * @param y y coordinate of the point
     * @return first index of the point or -1 if the point is not one of the
     * path's vertices
     */
    public int indexOf(final int x, final int y) {
        return indexOf(x, y, 0);
    }

    /**
     * Retrieves the index of the first occurrence of this point after the
     * starting point
     *
     * @param x y coordinate of the point
     * @param y y coordinate of the point
     * @param start starting point of the search
     * @return first index of the point or -1 if the point is not one of the
     * path's vertices
     */
    public int indexOf(
            final int x,
            final int y,
            final int start)
    {
        mutex.lock();
        try{
            for (int i = start; i < size; i++){
                if (xs[i] == x & ys[i] == y){
                    return i;
                }
            }

            return -1;
        } finally {
            mutex.unlock();
        }
    }

    /**
     * Clears the object
     */
    public void clear() {
        size = 0;
    }

    /**
     * Removes the first occurrence of this point in the array
     *
     * @param point the point to check
     * @return {@code true} if the point was contained and {@code false}
     * otherwise
     * @throws IllegalArgumentException if {@code point} is {@code null}
     */
    public boolean remove(final GPoint point) {
        if (point == null){
            throw new IllegalArgumentException("The point can't be null");
        }

        return remove(point.x(), point.y());
    }

    /**
     * Removes the element at the specified index
     *
     * @param idx Index of the element
     * @return {@code true} if the point was contained and {@code false}
     * otherwise
     * @throws IndexOutOfBoundsException if {@code idx < 0 | idx >= size}
     */
    public boolean remove(final int idx) {
        if (idx < 0 || idx >= size){
            String msg = "The index must be contained in [0, %d) current '%d'";
            msg = String.format(msg, size, idx);
            throw new IndexOutOfBoundsException(msg);
        }

        mutex.lock();
        try {
            final int nm = size - idx - 1;

            System.arraycopy(xs, idx + 1, xs, idx, nm);
            System.arraycopy(ys, idx + 1, ys, idx, nm);

            size--;

            modCount.incrementAndGet();

            return true;
        } finally {
            mutex.unlock();
        }
    }

    /**
     * Removes the first occurrence of this point in the array
     *
     * @param x x coordinate of the point
     * @param y y coordinate of the point
     * @return {@code true} if the point was contained and {@code false}
     * otherwise
     */
    public boolean remove(final int x, final int y) {
        mutex.lock();
        try{
            final int idx = indexOf(x, y);

            if (idx < 0){
                return false;
            }

            modCount.incrementAndGet();
            return remove(idx);
        } finally {
            mutex.unlock();
        }
    }

    /**
     * Retrieves a given {@link GPoint} from the path
     *
     * @param idx the index of the point to retrieve
     * @return {@link GPoint} at the specified position
     * @throws ArrayIndexOutOfBoundsException if
     * {@code (idx < 0 | idx > numberOfVertices)}
     */
    public GPoint getPointAt(final int idx) {
        if (idx < 0 || idx >= size){
            throw new ArrayIndexOutOfBoundsException(idx);
        }

        return new GPoint(xs[idx], ys[idx]);
    }

    /**
     * Retrieves a COPY of the {@link GPoint}'s in the path
     *
     * @return array of points
     */
    public GPoint[] getPoints() {
        mutex.lock();
        try{
            final GPoint[] points = new GPoint[size];

            for (int i = 0; i < size; i++){
                points[i] = new GPoint(xs[i], ys[i]);
            }

            return points;
        } finally {
            mutex.unlock();
        }
    }

    /**
     * Appends a new point to the path. If the path has run out of space then
     * it will call {@code ensureCapacity(size + 5)}
     *
     * @param x X coordinate of the point
     * @param y Y coordinate of the point
     * @see GMultiPoint#ensureCapacity(int)
     */
    public void append(final int x, final int y) {
        mutex.lock();
        if (size == xs.length){
            ensureCapacity(size + 5);
        }

        try{
            xs[size] = x;
            ys[size] = y;

            size++;

            modCount.incrementAndGet();
        } finally {
            mutex.unlock();
        }
    }

    /**
     * Appends a new point to the path if and only if this point doesn't exist
     * within the array. If the path has run out of space then
     * it will call {@code ensureCapacity(size + 5)}
     *
     * @param x X coordinate of the point
     * @param y Y coordinate of the point
     * @see GMultiPoint#ensureCapacity(int)
     */
    public void appendNR(final int x, final int y) {
        if (indexOf(x, y) >= 0){
            return;
        }

        mutex.lock();
        if (size == xs.length){
            ensureCapacity(size + 5);
        }

        try{
            xs[size] = x;
            ys[size] = y;

            size++;

            modCount.incrementAndGet();
        } finally {
            mutex.unlock();
        }
    }

    /**
     * Appends a new point to the path
     *
     * @param p the {@link GPoint} to append
     * @throws IllegalArgumentException if {@code p} is {@code null}
     * @see GMultiPoint#ensureCapacity(int)
     */
    public void append(final GPoint p) {
        if (p == null){
            throw new IllegalArgumentException("The point can't be null");
        }

        append(p.x(), p.y());
    }

    /**
     * Allocates enough space for the specified number of elements
     *
     * @param nElements the capacity the array must be able to hold
     * @throws InvalidArgumentException if {@code nElements} is less than 0
     */
    public void ensureCapacity(final int nElements) {
        if (nElements < 0){
            throw new InvalidArgumentException("The size must be positive");
        }
        if (nElements < xs.length){
            return;
        }

        mutex.lock();
        try{
            final int[] fx = new int[nElements];
            final int[] fy = new int[nElements];

            System.arraycopy(xs, 0, fx, 0, size);
            System.arraycopy(ys, 0, fy, 0, size);

            xs = fx;
            ys = fy;
        } finally {
            mutex.unlock();
        }
    }

    /**
     * Tells if the object doesn't contain any points
     *
     * @return {@code true} if the object is empty and {@code false} otherwise
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * Removes all the extra space used by the object
     */
    public void trimToSize() {
        mutex.lock();
        try{
            if (size != xs.length){
                final int[] fx = new int[size];
                final int[] fy = new int[size];

                System.arraycopy(xs, 0, fx, 0, size);
                System.arraycopy(ys, 0, fy, 0, size);

                xs = fx;
                ys = fy;
            }
        } finally {
            mutex.unlock();
        }
    }

    @Override
    public void traslate(final int x, final int y) {
        mutex.lock();
        try{
            for (int i = 0; i < size; i++){
                xs[i] += x;
                ys[i] += y;
            }

            modCount.incrementAndGet();
        } finally {
            mutex.unlock();
        }
    }

    /**
     * Sorts all the points in this array by it's {@code X} value, and breaks
     * ties with the {@code Y} value
     */
    public void sortByX() {
        sort((n1, n2) -> {
            final int status = Integer.compare(xs[n1], xs[n2]);
            return status == 0 ? Integer.compare(ys[n1], ys[n2]) : status;
        });
    }

    private void sort(final Comparator<Integer> comparator) {
        final Integer[] idxs = new Integer[size];
        for( int i = 0 ; i < size; i++ ) {
            idxs[i] = i;
        }

        mutex.lock();
        try{
            Arrays.sort(idxs, comparator);

            //@FIXME this can be done without generating a new array
            final int[] xxs = new int[xs.length];
            final int[] yys = new int[xs.length];

            for (int i = 0; i < size; i++){
                xxs[i] = xs[idxs[i]];
                yys[i] = ys[idxs[i]];
            }

            xs = xxs;
            ys = yys;
        } finally {
            mutex.unlock();
        }

        modCount.incrementAndGet();
    }

    @Override
    public Iterator<GPoint> iterator() {
        return new Iterator<>() {
            private final int modifications = modCount.get();

            //The cursor must be BEFORE the first item
            private int idx = -1;

            @Override
            public boolean hasNext() {
                if (modifications != modCount.get()) {
                    throw new ConcurrentModificationException();
                }

                return idx < size - 1;
            }

            @Override
            public GPoint next() {
                if (idx >= size - 1) {
                    throw new NoSuchElementException();
                }

                if (modifications != modCount.get()) {
                    throw new ConcurrentModificationException();
                }

                return getPointAt(++idx);
            }

            @Override
            public void remove() {
                //Unless this is the last item on the loop, this will always
                //end in a ConcurrentModificationException
                //Should we change modifications to modCount?
                if (modifications != modCount.get()) {
                    throw new ConcurrentModificationException();
                }

                GMultiPoint.this.remove(getPointAt(idx));
            }
        };
    }

    @Override
    public int hashCode() {
        int hash = super.hashCode();
        hash = 53 * hash + size;
        for (int i = 0; i < size(); i++) {
            hash = 53 * hash + xs[i];
            hash = 53 * hash + ys[i];
        }
        return hash;
    }

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

        final GMultiPoint other = (GMultiPoint) obj;
        if (size != other.size) {
            return false;
        }

        for (int i = 0; i < size; i++) {
            if (xs[i] != other.xs[i] ||
                ys[i] != other.ys[i]) {
                return false;
            }
        }

        return true;
    }

    @Override
    public Area getShape() {
        return new Area(new Polygon(xs, ys, size()));
    }
}