GPointArray.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.util.Arrays;
import java.util.Comparator;

/**
 *
 * @author Federico Vera {@literal<[email protected]>}
 */
public class GPointArray extends GMultiPoint {
    private int cs;

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

        cs = e.cs;
    }

    /**
     * @param points {@link GPoint} array containing all the points
     * @throws IllegalArgumentException if the array is {@code null}
     */
    public GPointArray(final GPoint[] points) {
        this(points, 0);
    }

    /**
     * @param points {@link GPoint} array containing all the points
     * @param cs cross size
     * @throws IllegalArgumentException if the array is {@code null}
     */
    public GPointArray(final GPoint[] points, final int cs) {
        this(points.length);
        for (final GPoint p : points){
            append(new GPoint(p.x(), p.y(), cs));
        }
    }

    /**
     * Generates a new empty {@code GPointArray}
     */
    public GPointArray() {
        this(new int[0], new int[0]);
    }

    /**
     * @param initial initial reserved size
     * @throws NegativeArraySizeException if the size is less than zero
     */
    public GPointArray(final int initial) {
        super(initial);
    }

    /**
     * @param xs array containing all the x coordinates
     * @param ys array containing all the y coordinates
     * @throws IllegalArgumentException if either array is {@code null}
     * @throws InvalidArgumentException if the array size doesn't match
     */
    public GPointArray(final int[] xs, final int[] ys) {
        this(xs, ys, 0);
    }

    /**
     * @param xs array containing all the x coordinates
     * @param ys array containing all the y coordinates
     * @param cs the cross size of the point
     * @throws IllegalArgumentException if either array is {@code null}
     * @throws InvalidArgumentException if the array size doesn't match
     */
    public GPointArray(
            final int[] xs,
            final int[] ys,
            final int cs)
    {
        super(xs, ys);
        this.cs = cs;
    }

    /**
     * Changes the cross size.<br>
     * We treat points as crosses on the canvas, this method changes the size
     * of that cross.<br>
     * <i>Note:</i> The default value is 0
     *
     * @param cs new cross size
     */
    public void setCrossSize(final int cs){
        this.cs = cs;
    }

    /**
     * Retrieves a new {@code GPointArray} with all the points that are
     * contained in the circle with center {@code p} and radius {@code r}.
     *
     * @param p {@link GPoint} of the center
     * @param r radius
     * @return a new {@code GPointArray} with the points of the intersection
     * @throws IllegalArgumentException if {@code p} is {@code null}
     */
    public GPointArray pointsInRadius(
            final GPoint p,
            final double r)
    {
        if (p == null){
            throw new IllegalArgumentException("The point can't be null");
        }

        return pointsInRadius(p.x(), p.y(), r);
    }

    /**
     * Retrieves a new {@code GPointArray} with all the points that are
     * contained in the circle with center in {@code (x, y)} and radius
     * {@code r}.
     *
     * @param x X coordinate of the center
     * @param y Y coordinate of the center
     * @param r radius
     * @return a new {@code GPointArray} with the points of the intersection
     */
    public GPointArray pointsInRadius(
            final int x,
            final int y,
            final double r)
    {
        final GPointArray array = new GPointArray();
        if (isEmpty()){
            return array;
        }

        mutex.lock();
        try{
            array.ensureCapacity(size);

            final double r2 = r * r;

            for (int i = 0; i < size; i++){
                final int xd = xs[i] - x;
                final int yd = ys[i] - y;

                final double nd = xd * xd + yd * yd;

                if (nd < r2){
                    array.append(xs[i], ys[i]);
                }
            }
        } finally {
            mutex.unlock();
        }

        array.trimToSize();
        return array;
    }

    /**
     * Retrieves a new {@code GPoint} representing the coordinates of the
     * closest point in this array
     *
     * @param p {@link GPoint} that will be used to compare
     * @return a new {@code GPoint} representing the closest to {@code p}
     * @throws IllegalArgumentException if {@code p} is {@code null}
     */
    public GPoint closestPoint(final GPoint p) {
        if (p == null){
            throw new IllegalArgumentException("The point can't be null");
        }

        return closestPoint(p.x(), p.y());
    }

    /**
     * Retrieves a new {@code GPoint} representing the coordinates of the
     * closest point in this array
     *
     * @param x X coordinate of the point
     * @param y Y coordinate of the point
     * @return a new {@code GPoint} representing the closest to {@code (x, y)}
     * or {@code null} if none is found.
     */
    public GPoint closestPoint(final int x, final int y) {
        if (isEmpty()){
            return null;
        }

        double distance = Double.MAX_VALUE;
        int nx = 0;
        int ny = 0;

        mutex.lock();
        try{
            for (int i = 0; i < size; i++){
                final double ndistance = Math.hypot(xs[i] - x, ys[i] - y);

                if (ndistance < distance){
                    distance = ndistance;
                    nx = xs[i];
                    ny = ys[i];

                    if (x == nx && y == ny){
                        break;
                    }
                }
            }
        } finally {
            mutex.unlock();
        }

        return new GPoint(nx, ny);
    }

    /**
     * Retrieves a copy of the highest point in this array at the moment
     *
     * @return highest point
     */
    public GPoint highestPoint() {
        if (isEmpty()){
            return null;
        }

        int nx = 0;
        int ny = Integer.MIN_VALUE;

        mutex.lock();
        try{
            for (int i = 0; i < size; i++){
                if (ny < ys[i]){
                    nx = xs[i];
                    ny = ys[i];
                }
            }
        } finally {
            mutex.unlock();
        }

        return new GPoint(nx, ny);
    }

    /**
     * Retrieves a copy of the lowest point in this array at the moment
     *
     * @return lowest point
     */
    public GPoint lowestPoint() {
        if (isEmpty()){
            return null;
        }

        int nx = 0;
        int ny = Integer.MAX_VALUE;

        mutex.lock();
        try{
            for (int i = 0; i < size; i++){
                if (ny > ys[i]){
                    nx = xs[i];
                    ny = ys[i];
                }
            }
        } finally {
            mutex.unlock();
        }

        return new GPoint(nx, ny);
    }

    /**
     * Retrieves a copy of the leftmost point in this array at the moment
     *
     * @return leftmost point or {@code null} if none is found.
     */
    public GPoint leftmostPoint() {
        if (isEmpty()){
            return null;
        }

        int nx = Integer.MAX_VALUE;
        int ny = 0;

        mutex.lock();
        try{
            for (int i = 0; i < size; i++){
                if (nx > xs[i]){
                    nx = xs[i];
                    ny = ys[i];
                }
            }
        } finally {
            mutex.unlock();
        }

        return new GPoint(nx, ny);
    }

    /**
     * Retrieves a copy of the rightmost point in this array at the moment
     *
     * @return rightmost point or {@code null} if none is found.
     */
    public GPoint rightmostPoint() {
        if (isEmpty()){
            return null;
        }

        int nx = Integer.MIN_VALUE;
        int ny = 0;

        mutex.lock();
        try{
            for (int i = 0; i < size; i++){
                if (nx < xs[i]){
                    nx = xs[i];
                    ny = ys[i];
                }
            }
        } finally {
            mutex.unlock();
        }

        return new GPoint(nx, ny);
    }

    /**
     * Retrieves {@link GRectangle} that contains all the points in this
     * array
     *
     * @return rectangle containing all the points
     */
    public GRectangle getBounds() {
        if (isEmpty()){
            return null;
        }

        int le = Integer.MAX_VALUE;
        int ri = Integer.MIN_VALUE;
        int up = Integer.MIN_VALUE;
        int lo = Integer.MAX_VALUE;

        mutex.lock();
        try{
            for (int i = 0; i < size; i++){
                le = Math.min(le, xs[i]);
                ri = Math.max(ri, xs[i]);
                up = Math.max(up, ys[i]);
                lo = Math.min(lo, ys[i]);
            }
        } finally {
            mutex.unlock();
        }

        final int w = ri - le;
        final int h = up - lo;

        return new GRectangle(le + w / 2, lo + h / 2 + 1, w, h);
    }

    /**
     * Retrieves a new {@code GPointArray} representing all the points that
     * are above {@code p}.<br>
     * Please note that this is done considering the Y axis, graphically it
     * means the higher if and only if you are inverting the Y axis
     *
     * @param p {@link GPoint} that will be used to compare
     * @return {@code GPointArray} with all the higher points
     * @throws IllegalArgumentException if {@code p} is {@code null}
     */
    public GPointArray higherThan(final GPoint p) {
        if (p == null){
            throw new IllegalArgumentException("The point can't be null");
        }

        GPointArray array = new GPointArray();
        if (isEmpty()){
            return array;
        }

        final int y = p.y();
        mutex.lock();
        try{
            array.ensureCapacity(size);

            for (int i = 0; i < size; i++){
                if (y < ys[i]){
                    array.append(xs[i], ys[i]);
                }
            }
        } finally {
            mutex.unlock();
        }

        array.trimToSize();
        return array;
    }

    /**
     * Retrieves a new {@code GPointArray} representing all the points that
     * are below {@code p}.<br>
     * Please note that this is done considering the Y axis, graphically it
     * means the lower if and only if you are inverting the Y axis
     *
     * @param p {@link GPoint} that will be used to compare
     * @return {@code GPointArray} with all the lower points
     * @throws IllegalArgumentException if {@code p} is {@code null}
     */
    public GPointArray lowerThan(final GPoint p) {
        if (p == null){
            throw new IllegalArgumentException("The point can't be null");
        }

        GPointArray array = new GPointArray();
        if (isEmpty()){
            return array;
        }

        final int y = p.y();

        mutex.lock();
        try{
            array.ensureCapacity(size);

            for (int i = 0; i < size; i++){
                if (y > ys[i]){
                    array.append(xs[i], ys[i]);
                }
            }
        } finally {
            mutex.unlock();
        }

        array.trimToSize();
        return array;
    }

    /**
     * Retrieves a new {@code GPointArray} representing all the points that
     * are located to the left of {@code p}.
     *
     * @param p {@link GPoint} that will be used to compare
     * @return {@code GPointArray} with all the left points
     * @throws IllegalArgumentException if {@code p} is {@code null}
     */
    public GPointArray leftThan(final GPoint p) {
        if (p == null){
            throw new IllegalArgumentException("The point can't be null");
        }

        GPointArray array = new GPointArray();
        if (isEmpty()){
            return array;
        }

        final int x = p.x();

        mutex.lock();
        try{
            array.ensureCapacity(size);

            for (int i = 0; i < size; i++){
                if (x > xs[i]){
                    array.append(xs[i], ys[i]);
                }
            }
        } finally {
            mutex.unlock();
        }

        array.trimToSize();
        return array;
    }

    /**
     * Retrieves a new {@code GPointArray} representing all the points that
     * are located to the right of {@code p}.
     *
     * @param p {@link GPoint} that will be used to compare
     * @return {@code GPointArray} with all the right points
     * @throws IllegalArgumentException if {@code p} is {@code null}
     */
    public GPointArray rightThan(final GPoint p) {
        if (p == null){
            throw new IllegalArgumentException("The point can't be null");
        }

        GPointArray array = new GPointArray();
        if (isEmpty()){
            return array;
        }

        final int x = p.x();

        mutex.lock();
        try{
            array.ensureCapacity(size);

            for (int i = 0; i < size; i++){
                if (x < xs[i]){
                    array.append(xs[i], ys[i]);
                }
            }
        } finally {
            mutex.unlock();
        }

        array.trimToSize();
        return array;
    }

    /**
     * Removes all the points that are contained both in this array and in
     * the array passed as an argument
     *
     * @param arr points to remove
     * @throws IllegalArgumentException if {@code array} is {@code null}
     */
    public void removeAll(final GPointArray arr) {
        if (arr == null){
            throw new IllegalArgumentException("The array can't be null");
        }

        mutex.lock();
        try {
            //@FIXME this should be done more gracefully.
            for (int i = 0; i < arr.size; i++){
                int idx = 0;

                while ((idx = indexOf(arr.xs[i], arr.ys[i], idx)) >= 0){
                    remove(idx);
                }
            }
        } finally {
            mutex.unlock();
        }
    }

    /**
     * Appends all the point to this array, regardless if they are already
     * contained
     *
     * @param arr points to add
     * @throws IllegalArgumentException if {@code array} is {@code null}
     */
    public void append(final GPointArray arr) {
        if (arr == null){
            throw new IllegalArgumentException("The array can't be null");
        }

        mutex.lock();
        ensureCapacity(size + arr.size);

        try{
            System.arraycopy(arr.xs, 0, xs, size, arr.size);
            System.arraycopy(arr.ys, 0, ys, size, arr.size);
            size += arr.size;
            modCount.incrementAndGet();
        } finally {
            mutex.unlock();
        }
    }

    /**
     * Retrieves a new {@code GPointArray} containing all the points that are
     * both in this array and in the one passed as an argument
     *
     * @param array points to intersect
     * @return a new {@code GPointArray} with all the points of the intersection
     * @throws IllegalArgumentException if {@code array} is {@code null}
     */
    public GPointArray intersection(final GPointArray array) {
        if (array == null){
            throw new IllegalArgumentException("The array can't be null");
        }

        //@FIXME this should be done more gracefully.
        final GPointArray arr = new GPointArray();
        arr.ensureCapacity(size + array.size);

        mutex.lock();
        try{
            for (int i = 0; i < array.size; i++){
                if (indexOf(array.xs[i], array.ys[i]) >= 0){
                    arr.append(array.xs[i], array.ys[i]);
                }
            }
        } finally {
            mutex.unlock();
        }

        arr.trimToSize();
        return arr;
    }

    /**
     * Retrieves a new {@code GPointArray} containing all the points that are
     * contained both in this array and in the {@link GRectangle} passed as an
     * argument
     *
     * @param r rectangle to intersect
     * @return a new {@code GPointArray} with all the points of the intersection
     * @throws IllegalArgumentException if {@code r} is {@code null}
     */
    public GPointArray intersection(final GRectangle r) {
        if (r == null){
            throw new IllegalArgumentException("The rectangle can't be null");
        }

        final GPointArray arr = new GPointArray();
        arr.ensureCapacity(size);

        mutex.lock();
        try{
            for (int i = 0; i < size; i++){
                if (r.contains(xs[i], ys[i])){
                    arr.append(xs[i], ys[i]);
                }
            }
        } finally {
            mutex.unlock();
        }

        arr.trimToSize();
        return arr;
    }

    /**
     * Retrieves a new {@code GPointArray} containing all the points that are
     * contained both in this array and in the {@link GLine} passed as an
     * argument
     *
     * @param l the line to intersect
     * @return a new {@code GPointArray} with all the points of the intersection
     * @throws IllegalArgumentException if {@code l} is {@code null}
     */
    public GPointArray intersection(final GLine l) {
        if (l == null){
            throw new IllegalArgumentException("The line can't be null");
        }

        final GPointArray arr = new GPointArray();
        arr.ensureCapacity(size);

        mutex.lock();
        try{
            for (int i = 0; i < size; i++){
                if (l.contains(xs[i], ys[i])){
                    arr.append(xs[i], ys[i]);
                }
            }
        } finally {
            mutex.unlock();
        }

        arr.trimToSize();
        return arr;
    }

    /**
     * Retrieves a new {@code GPointArray} containing all the points that are
     * contained both in this array and in the {@link GCircle} passed as an
     * argument
     *
     * @param c the circle to intersect
     * @return a new {@code GPointArray} with all the points of the intersection
     * @throws IllegalArgumentException if {@code c} is {@code null}
     */
    public GPointArray intersection(final GCircle c) {
        if (c == null){
            throw new IllegalArgumentException("The circle can't be null");
        }

        return pointsInRadius(c.x(), c.y(), c.getRadius());
    }

    /**
     * Sorts all the points in this array by it's {@code X} value, and breaks
     * ties with the {@code Y} value
     */
    @Override
    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;
        });
    }

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

    /**
     * Removes <b>ALL</b> duplicate entries in the array.
     */
    public void removeDuplicates() {
        //I guess this can be done more efficiently... but I have no idea how...
        final Integer[] idxs = new Integer[size];
        for( int i = 0 ; i < size; i++ ) {
            idxs[i] = i;
        }

        Arrays.sort(idxs, (n1, n2) -> Integer.compare(xs[n1], xs[n2]));

        final GPointArray parr = new GPointArray();

        mutex.lock();
        try {
            //Worst case scenario is that the array is full of duplicates
            //perhaps this should be changed if size is very big since we'll
            //need a lot of memory
            parr.ensureCapacity(size);

            for (int i = 0; i < size; i++){
                final int ii = idxs[i];
                final int xi = xs[ii];
                final int yi = ys[ii];

                for (int j = i + 1; j < size && xi == xs[idxs[j]]; j++){
                    final int ij = idxs[j];

                    if (xi == xs[ij] & yi == ys[ij]){
                        parr.append(xs[ij], ys[ij]);
                    }
                }
            }
        } finally {
            mutex.unlock();
        }

        removeAll(parr);
    }

    @Override
    public void draw(Graphics2D g) {
        g.setPaint(getPaint());

        mutex.lock();
        try{
            if (cs != 0) {
                g.setStroke(getStroke());
                for (int i = 0; i < size; i++){
                    final int x = xs[i];
                    final int y = ys[i];

                    g.drawLine(x - cs, y     , x + cs, y     );
                    g.drawLine(x     , y - cs, x     , y + cs);
                }
            } else {
                for (int i = 0; i < size; i++){
                    g.drawRect(xs[i], ys[i], 0, 0);
                }
            }
        } finally {
            mutex.unlock();
        }
    }

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

    @Override
    public int hashCode() {
        int hash = super.hashCode();
        hash = 73 * hash + cs;
        return hash;
    }

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

        final GPointArray other = (GPointArray) obj;
        return this.cs == other.cs;
    }

}