Surface3d.java

package org.djutils.draw.surface;

import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;

import org.djutils.draw.DrawRuntimeException;
import org.djutils.draw.Drawable2d;
import org.djutils.draw.Drawable3d;
import org.djutils.draw.bounds.Bounds3d;
import org.djutils.draw.point.Point3d;
import org.djutils.exceptions.Throw;

/**
 * Surface3d.java. Triangulated surface in 3D space.
 * <p>
 * Copyright (c) 2021-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
 * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
 * </p>
 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
 * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
 */
public class Surface3d implements Drawable3d
{
    /** */
    private static final long serialVersionUID = 20210706L;

    /** X-coordinates of all points used to define the triangulated surface. */
    private final double[] x;

    /** Y-coordinates of all points used to define the triangulated surface. */
    private final double[] y;

    /** Z-coordinates of all points used to define the triangulated surface. */
    private final double[] z;

    /** Indices into the points array for each triangle in succession. */
    private final int[] indices;

    /** The bounds of this Surface3d. */
    private final Bounds3d bounds;

    /**
     * Construct a new Surface3d.
     * @param points Point3d[][]; two dimensional array of points. The first index iterates over the individual triangles; the
     *            second index iterates over the points of a single triangle. The range of the second index must be 3. It is
     *            expected that all points appear multiple times in the points array, but never within the same sub-array.
     * @throws NullPointerException when points is null, or any element in points is null
     * @throws DrawRuntimeException when points is empty, or any element in points does not contain exactly three different
     *             points
     */
    public Surface3d(final Point3d[][] points) throws NullPointerException, DrawRuntimeException
    {
        Throw.whenNull(points, "points");
        Throw.when(points.length == 0, DrawRuntimeException.class, "points must have at least one element");
        this.indices = new int[points.length * 3];
        // Figure out how many points are unique
        Map<Point3d, Integer> indexMap = new LinkedHashMap<>();
        for (int triangle = 0; triangle < points.length; triangle++)
        {
            Point3d[] trianglePoints = points[triangle];
            Throw.whenNull(trianglePoints, "Element in trianglePoints may not be null");
            Throw.when(trianglePoints.length != 3, DrawRuntimeException.class,
                    "Triangle %d contain wrong number of points (should be 3, got %d", triangle, trianglePoints.length);
            Point3d prevPoint = trianglePoints[2];
            for (int i = 0; i < 3; i++)
            {
                Point3d point = trianglePoints[i];
                // The next check is not good enough when this constructor can be fed a subclass of Point3d
                Throw.when(point.equals(prevPoint), DrawRuntimeException.class, "Triangle %d contains duplicate point",
                        triangle);
                Integer currentIndex = indexMap.get(point);
                if (currentIndex == null)
                {
                    indexMap.put(point, indexMap.size());
                }
                prevPoint = point;
            }
        }
        this.x = new double[indexMap.size()];
        this.y = new double[indexMap.size()];
        this.z = new double[indexMap.size()];
        for (int triangle = 0; triangle < points.length; triangle++)
        {
            Point3d[] trianglePoints = points[triangle];
            for (int i = 0; i < 3; i++)
            {
                Point3d point3d = trianglePoints[i];
                Integer index = indexMap.get(point3d);
                this.indices[triangle * 3 + i] = index;
                this.x[index] = point3d.x;
                this.y[index] = point3d.y;
                this.z[index] = point3d.z;
            }
        }
        this.bounds = new Bounds3d(getPoints());
    }

    @Override
    public Iterator<? extends Point3d> getPoints()
    {
        return new Iterator<Point3d>()
        {
            private int current = 0;

            @Override
            public boolean hasNext()
            {
                return this.current < size();
            }

            @Override
            public Point3d next()
            {
                Throw.when(this.current >= size(), NoSuchElementException.class, "Iterator has exhausted the input");
                int index = this.current++;
                return new Point3d(Surface3d.this.x[Surface3d.this.indices[index]],
                        Surface3d.this.y[Surface3d.this.indices[index]], Surface3d.this.z[Surface3d.this.indices[index]]);
            }
        };
    }

    @Override
    public int size()
    {
        return this.indices.length;
    }

    @Override
    public Bounds3d getBounds()
    {
        return this.bounds;
    }

    @Override
    public Drawable2d project() throws DrawRuntimeException
    {
        throw new DrawRuntimeException("Project not implemented because we do not have a class that represents a multitude "
                + "of triangles in the 2D plane");
    }

    @Override
    public String toString()
    {
        return toString("%f", false);
    }

    @Override
    public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
    {
        StringBuilder result = new StringBuilder();
        if (!doNotIncludeClassName)
        {
            result.append("Surface3d ");
        }
        String format = String.format("%%sx=%1$s, y=%1$s, z=%1$s", doubleFormat);
        for (int index = 0; index < this.indices.length; index++)
        {
            if (index % 3 == 0)
            {
                result.append("[");
            }
            int i = this.indices[index];
            result.append(String.format(Locale.US, format, index == 0 ? "[" : ", ", this.x[i], this.y[i], this.z[i]));
            if (index % 3 == 2)
            {
                result.append("]");
            }
        }
        result.append("]");
        return result.toString();
    }

    @SuppressWarnings("checkstyle:designforextension")
    @Override
    public int hashCode()
    {
        final int prime = 31;
        int result = 1;
        result = prime * result + Arrays.hashCode(this.indices);
        result = prime * result + Arrays.hashCode(this.x);
        result = prime * result + Arrays.hashCode(this.y);
        result = prime * result + Arrays.hashCode(this.z);
        return result;
    }

    @SuppressWarnings({"checkstyle:designforextension", "checkstyle:needbraces"})
    @Override
    public boolean equals(final Object obj)
    {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Surface3d other = (Surface3d) obj;
        if (!Arrays.equals(this.indices, other.indices))
            return false;
        if (!Arrays.equals(this.x, other.x))
            return false;
        if (!Arrays.equals(this.y, other.y))
            return false;
        if (!Arrays.equals(this.z, other.z))
            return false;
        return true;
    }

}