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