View Javadoc
1   package org.djutils.draw.surface;
2   
3   import java.util.Arrays;
4   import java.util.Iterator;
5   import java.util.LinkedHashMap;
6   import java.util.Locale;
7   import java.util.Map;
8   import java.util.NoSuchElementException;
9   
10  import org.djutils.draw.DrawRuntimeException;
11  import org.djutils.draw.Drawable2d;
12  import org.djutils.draw.Drawable3d;
13  import org.djutils.draw.bounds.Bounds3d;
14  import org.djutils.draw.point.Point3d;
15  import org.djutils.exceptions.Throw;
16  
17  /**
18   * Surface3d.java. Triangulated surface in 3D space.
19   * <p>
20   * Copyright (c) 2021-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
21   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
22   * </p>
23   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
24   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
25   */
26  public class Surface3d implements Drawable3d
27  {
28      /** */
29      private static final long serialVersionUID = 20210706L;
30  
31      /** X-coordinates of all points used to define the triangulated surface. */
32      private final double[] x;
33  
34      /** Y-coordinates of all points used to define the triangulated surface. */
35      private final double[] y;
36  
37      /** Z-coordinates of all points used to define the triangulated surface. */
38      private final double[] z;
39  
40      /** Indices into the points array for each triangle in succession. */
41      private final int[] indices;
42  
43      /** The bounds of this Surface3d. */
44      private final Bounds3d bounds;
45  
46      /**
47       * Construct a new Surface3d.
48       * @param points Point3d[][]; two dimensional array of points. The first index iterates over the individual triangles; the
49       *            second index iterates over the points of a single triangle. The range of the second index must be 3. It is
50       *            expected that all points appear multiple times in the points array, but never within the same sub-array.
51       * @throws NullPointerException when points is null, or any element in points is null
52       * @throws DrawRuntimeException when points is empty, or any element in points does not contain exactly three different
53       *             points
54       */
55      public Surface3d(final Point3d[][] points) throws NullPointerException, DrawRuntimeException
56      {
57          Throw.whenNull(points, "points");
58          Throw.when(points.length == 0, DrawRuntimeException.class, "points must have at least one element");
59          this.indices = new int[points.length * 3];
60          // Figure out how many points are unique
61          Map<Point3d, Integer> indexMap = new LinkedHashMap<>();
62          for (int triangle = 0; triangle < points.length; triangle++)
63          {
64              Point3d[] trianglePoints = points[triangle];
65              Throw.whenNull(trianglePoints, "Element in trianglePoints may not be null");
66              Throw.when(trianglePoints.length != 3, DrawRuntimeException.class,
67                      "Triangle %d contain wrong number of points (should be 3, got %d", triangle, trianglePoints.length);
68              Point3d prevPoint = trianglePoints[2];
69              for (int i = 0; i < 3; i++)
70              {
71                  Point3d point = trianglePoints[i];
72                  // The next check is not good enough when this constructor can be fed a subclass of Point3d
73                  Throw.when(point.equals(prevPoint), DrawRuntimeException.class, "Triangle %d contains duplicate point",
74                          triangle);
75                  Integer currentIndex = indexMap.get(point);
76                  if (currentIndex == null)
77                  {
78                      indexMap.put(point, indexMap.size());
79                  }
80                  prevPoint = point;
81              }
82          }
83          this.x = new double[indexMap.size()];
84          this.y = new double[indexMap.size()];
85          this.z = new double[indexMap.size()];
86          for (int triangle = 0; triangle < points.length; triangle++)
87          {
88              Point3d[] trianglePoints = points[triangle];
89              for (int i = 0; i < 3; i++)
90              {
91                  Point3d point3d = trianglePoints[i];
92                  Integer index = indexMap.get(point3d);
93                  this.indices[triangle * 3 + i] = index;
94                  this.x[index] = point3d.x;
95                  this.y[index] = point3d.y;
96                  this.z[index] = point3d.z;
97              }
98          }
99          this.bounds = new Bounds3d(getPoints());
100     }
101 
102     @Override
103     public Iterator<? extends Point3d> getPoints()
104     {
105         return new Iterator<Point3d>()
106         {
107             private int current = 0;
108 
109             @Override
110             public boolean hasNext()
111             {
112                 return this.current < size();
113             }
114 
115             @Override
116             public Point3d next()
117             {
118                 Throw.when(this.current >= size(), NoSuchElementException.class, "Iterator has exhausted the input");
119                 int index = this.current++;
120                 return new Point3d(Surface3d.this.x[Surface3d.this.indices[index]],
121                         Surface3d.this.y[Surface3d.this.indices[index]], Surface3d.this.z[Surface3d.this.indices[index]]);
122             }
123         };
124     }
125 
126     @Override
127     public int size()
128     {
129         return this.indices.length;
130     }
131 
132     @Override
133     public Bounds3d getBounds()
134     {
135         return this.bounds;
136     }
137 
138     @Override
139     public Drawable2d project() throws DrawRuntimeException
140     {
141         throw new DrawRuntimeException("Project not implemented because we do not have a class that represents a multitude "
142                 + "of triangles in the 2D plane");
143     }
144 
145     @Override
146     public String toString()
147     {
148         return toString("%f", false);
149     }
150 
151     @Override
152     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
153     {
154         StringBuilder result = new StringBuilder();
155         if (!doNotIncludeClassName)
156         {
157             result.append("Surface3d ");
158         }
159         String format = String.format("%%sx=%1$s, y=%1$s, z=%1$s", doubleFormat);
160         for (int index = 0; index < this.indices.length; index++)
161         {
162             if (index % 3 == 0)
163             {
164                 result.append("[");
165             }
166             int i = this.indices[index];
167             result.append(String.format(Locale.US, format, index == 0 ? "[" : ", ", this.x[i], this.y[i], this.z[i]));
168             if (index % 3 == 2)
169             {
170                 result.append("]");
171             }
172         }
173         result.append("]");
174         return result.toString();
175     }
176 
177     @SuppressWarnings("checkstyle:designforextension")
178     @Override
179     public int hashCode()
180     {
181         final int prime = 31;
182         int result = 1;
183         result = prime * result + Arrays.hashCode(this.indices);
184         result = prime * result + Arrays.hashCode(this.x);
185         result = prime * result + Arrays.hashCode(this.y);
186         result = prime * result + Arrays.hashCode(this.z);
187         return result;
188     }
189 
190     @SuppressWarnings({"checkstyle:designforextension", "checkstyle:needbraces"})
191     @Override
192     public boolean equals(final Object obj)
193     {
194         if (this == obj)
195             return true;
196         if (obj == null)
197             return false;
198         if (getClass() != obj.getClass())
199             return false;
200         Surface3d other = (Surface3d) obj;
201         if (!Arrays.equals(this.indices, other.indices))
202             return false;
203         if (!Arrays.equals(this.x, other.x))
204             return false;
205         if (!Arrays.equals(this.y, other.y))
206             return false;
207         if (!Arrays.equals(this.z, other.z))
208             return false;
209         return true;
210     }
211 
212 }