View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.geometry.io.euclidean.threed.txt;
18  
19  import java.io.StringWriter;
20  import java.text.DecimalFormat;
21  import java.text.DecimalFormatSymbols;
22  import java.util.Arrays;
23  import java.util.Collections;
24  import java.util.List;
25  import java.util.Locale;
26  
27  import org.apache.commons.geometry.core.GeometryTestUtils;
28  import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
29  import org.apache.commons.geometry.euclidean.threed.ConvexPolygon3D;
30  import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
31  import org.apache.commons.geometry.euclidean.threed.Planes;
32  import org.apache.commons.geometry.euclidean.threed.Vector3D;
33  import org.apache.commons.geometry.io.euclidean.threed.SimpleFacetDefinition;
34  import org.apache.commons.numbers.core.Precision;
35  import org.junit.jupiter.api.Assertions;
36  import org.junit.jupiter.api.BeforeEach;
37  import org.junit.jupiter.api.Test;
38  
39  class TextFacetDefinitionWriterTest {
40  
41      private static final double TEST_EPS = 1e-10;
42  
43      private static final Precision.DoubleEquivalence TEST_PRECISION =
44              Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
45  
46      private StringWriter writer;
47  
48      private TextFacetDefinitionWriter fdWriter;
49  
50      @BeforeEach
51      public void setup() {
52          writer = new StringWriter();
53          fdWriter = new TextFacetDefinitionWriter(writer);
54      }
55  
56      @Test
57      void testPropertyDefaults() {
58          // act/assert
59          Assertions.assertEquals("\n", fdWriter.getLineSeparator());
60          Assertions.assertNotNull(fdWriter.getDoubleFormat());
61          Assertions.assertEquals(" ", fdWriter.getVertexComponentSeparator());
62          Assertions.assertEquals("; ", fdWriter.getVertexSeparator());
63          Assertions.assertEquals(-1, fdWriter.getFacetVertexCount());
64          Assertions.assertEquals("# ", fdWriter.getCommentToken());
65      }
66  
67      @Test
68      void testSetFacetVertexCount_normalizesToMinusOne() {
69          // act
70          fdWriter.setFacetVertexCount(-10);
71  
72          // assert
73          Assertions.assertEquals(-1, fdWriter.getFacetVertexCount());
74      }
75  
76      @Test
77      void testSetFacetVertexCount_invalidArgs() {
78          // arrange
79          final String baseMsg = "Facet vertex count must be less than 0 or greater than 2; was ";
80  
81          // act
82          GeometryTestUtils.assertThrowsWithMessage(() -> {
83              fdWriter.setFacetVertexCount(0);
84          }, IllegalArgumentException.class, baseMsg + "0");
85  
86          GeometryTestUtils.assertThrowsWithMessage(() -> {
87              fdWriter.setFacetVertexCount(1);
88          }, IllegalArgumentException.class, baseMsg + "1");
89  
90          GeometryTestUtils.assertThrowsWithMessage(() -> {
91              fdWriter.setFacetVertexCount(2);
92          }, IllegalArgumentException.class, baseMsg + "2");
93      }
94  
95      @Test
96      void testSetCommentToken_invalidArgs() {
97          // act/assert
98          GeometryTestUtils.assertThrowsWithMessage(() -> {
99              fdWriter.setCommentToken("");
100         }, IllegalArgumentException.class, "Comment token cannot be empty");
101 
102         GeometryTestUtils.assertThrowsWithMessage(() -> {
103             fdWriter.setCommentToken(" ");
104         }, IllegalArgumentException.class, "Comment token cannot begin with whitespace");
105 
106         GeometryTestUtils.assertThrowsWithMessage(() -> {
107             fdWriter.setCommentToken("\n \t");
108         }, IllegalArgumentException.class, "Comment token cannot begin with whitespace");
109     }
110 
111     @Test
112     void testWriteComment() {
113         // arrange
114         fdWriter.setCommentToken("-- ");
115         fdWriter.setLineSeparator("\r\n");
116 
117         // act
118         fdWriter.writeComment("first line");
119         fdWriter.writeComment(null);
120         fdWriter.writeComment("second line \n third line \r\nfourth line");
121 
122         // assert
123         Assertions.assertEquals(
124                 "-- first line\r\n" +
125                 "-- second line \r\n" +
126                 "--  third line \r\n" +
127                 "-- fourth line\r\n", writer.toString());
128     }
129 
130     @Test
131     void testWriteComment_noCommentToken() {
132         // arrange
133         fdWriter.setCommentToken(null);
134 
135         // act/assert
136         GeometryTestUtils.assertThrowsWithMessage(() -> {
137             fdWriter.writeComment("comment");
138         }, IllegalStateException.class, "Cannot write comment: no comment token configured");
139     }
140 
141     @Test
142     void testWriteBlankLine() {
143         // act
144         fdWriter.writeBlankLine();
145         fdWriter.setLineSeparator("\r");
146         fdWriter.writeBlankLine();
147 
148         // assert
149         Assertions.assertEquals("\n\r", writer.toString());
150     }
151 
152     @Test
153     void testWriteVertices() {
154         // arrange
155         final List<Vector3D> vertices1 = Arrays.asList(
156                 Vector3D.ZERO, Vector3D.of(0.5, 0, 0), Vector3D.of(0, -0.5, 0));
157         final List<Vector3D> vertices2 = Arrays.asList(
158                 Vector3D.of(0.5, 0.7, 1.2), Vector3D.of(10.01, -4, 2), Vector3D.of(-10.0 / 3.0, 0, 0), Vector3D.ZERO);
159 
160         // act
161         fdWriter.write(vertices1);
162         fdWriter.write(vertices2);
163 
164         // assert
165         Assertions.assertEquals(
166                 "0.0 0.0 0.0; 0.5 0.0 0.0; 0.0 -0.5 0.0\n" +
167                 "0.5 0.7 1.2; 10.01 -4.0 2.0; -3.3333333333333335 0.0 0.0; 0.0 0.0 0.0\n", writer.toString());
168     }
169 
170     @Test
171     void testWriteVertices_invalidCount() {
172         // arrange
173         fdWriter.setFacetVertexCount(4);
174         final List<Vector3D> notEnough = Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X);
175         final List<Vector3D> tooMany = Arrays.asList(
176                 Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y,
177                 Vector3D.Unit.MINUS_X, Vector3D.Unit.MINUS_Y);
178 
179         // act/assert
180         GeometryTestUtils.assertThrowsWithMessage(() -> {
181             fdWriter.write(notEnough);
182         }, IllegalArgumentException.class, "At least 3 vertices are required per facet; found 2");
183 
184         GeometryTestUtils.assertThrowsWithMessage(() -> {
185             fdWriter.write(tooMany);
186         }, IllegalArgumentException.class, "Writer requires 4 vertices per facet; found 5");
187     }
188 
189     @Test
190     void testWriteFacetDefinition() {
191         // arrange
192         final DecimalFormat fmt =
193                 new DecimalFormat("0.0##", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
194 
195         final SimpleFacetDefinition f1 = new SimpleFacetDefinition(Arrays.asList(
196                 Vector3D.ZERO, Vector3D.of(0.5, 0, 0), Vector3D.of(0, -0.5, 0)));
197         final SimpleFacetDefinition f2 = new SimpleFacetDefinition(Arrays.asList(
198                 Vector3D.of(0.5, 0.7, 1.2), Vector3D.of(10.01, -4, 2), Vector3D.of(-10.0 / 3.0, 0, 0), Vector3D.ZERO));
199 
200         fdWriter.setDoubleFormat(fmt::format);
201 
202         // act
203         fdWriter.write(f1);
204         fdWriter.write(f2);
205 
206         // assert
207         Assertions.assertEquals(
208                 "0.0 0.0 0.0; 0.5 0.0 0.0; 0.0 -0.5 0.0\n" +
209                 "0.5 0.7 1.2; 10.01 -4.0 2.0; -3.333 0.0 0.0; 0.0 0.0 0.0\n", writer.toString());
210     }
211 
212     @Test
213     void testWriteFacetDefinition_invalidCount() {
214         // arrange
215         fdWriter.setFacetVertexCount(4);
216         final SimpleFacetDefinition tooMany = new SimpleFacetDefinition(Arrays.asList(
217                 Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y,
218                 Vector3D.Unit.MINUS_X, Vector3D.Unit.MINUS_Y));
219 
220         // act/assert
221         GeometryTestUtils.assertThrowsWithMessage(() -> {
222             fdWriter.write(tooMany);
223         }, IllegalArgumentException.class, "Writer requires 4 vertices per facet; found 5");
224     }
225 
226     @Test
227     void testWritePlaneConvexSubset() {
228         // arrange
229         final ConvexPolygon3D poly1 = Planes.convexPolygonFromVertices(Arrays.asList(
230                     Vector3D.ZERO, Vector3D.of(0, 0, -0.5), Vector3D.of(0, -0.5, 0)
231                 ), TEST_PRECISION);
232         final ConvexPolygon3D poly2 = Planes.convexPolygonFromVertices(Arrays.asList(
233                 Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 1, 0)
234             ), TEST_PRECISION);
235 
236         // act
237         fdWriter.write(poly1);
238         fdWriter.write(poly2);
239 
240         // assert
241         Assertions.assertEquals(
242                 "0.0 0.0 0.0; 0.0 0.0 -0.5; 0.0 -0.5 0.0\n" +
243                 "0.0 0.0 0.0; 1.0 0.0 0.0; 1.0 1.0 0.0; 0.0 1.0 0.0\n", writer.toString());
244     }
245 
246     @Test
247     void testWritePlaneConvexSubset_convertsToTriangles() {
248         // arrange
249         final ConvexPolygon3D poly = Planes.convexPolygonFromVertices(Arrays.asList(
250                     Vector3D.ZERO, Vector3D.of(0, 1, 0), Vector3D.of(0, 1, 1), Vector3D.of(0, 0, 1)
251                 ), TEST_PRECISION);
252 
253         fdWriter.setFacetVertexCount(3);
254 
255         // act
256         fdWriter.write(poly);
257 
258         // assert
259         Assertions.assertEquals(
260                 "0.0 0.0 0.0; 0.0 1.0 0.0; 0.0 1.0 1.0\n" +
261                 "0.0 0.0 0.0; 0.0 1.0 1.0; 0.0 0.0 1.0\n", writer.toString());
262     }
263 
264     @Test
265     void testWritePlaneConvexSubset_infinite() {
266         // arrange
267         final PlaneConvexSubset inf = Planes.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION).span();
268 
269         // act/assert
270         GeometryTestUtils.assertThrowsWithMessage(() -> {
271             fdWriter.write(inf);
272         }, IllegalArgumentException.class, "Cannot write infinite convex subset");
273     }
274 
275     @Test
276     void testWriteBoundarySource() {
277         // arrange
278         final ConvexPolygon3D poly1 = Planes.convexPolygonFromVertices(Arrays.asList(
279                 Vector3D.ZERO, Vector3D.of(0, 0, -0.5), Vector3D.of(0, -0.5, 0)
280             ), TEST_PRECISION);
281         final ConvexPolygon3D poly2 = Planes.convexPolygonFromVertices(Arrays.asList(
282                 Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 1, 0)
283             ), TEST_PRECISION);
284 
285         // act
286         fdWriter.write(BoundarySource3D.of(poly1, poly2));
287 
288         // assert
289         Assertions.assertEquals(
290                 "0.0 0.0 0.0; 0.0 0.0 -0.5; 0.0 -0.5 0.0\n" +
291                 "0.0 0.0 0.0; 1.0 0.0 0.0; 1.0 1.0 0.0; 0.0 1.0 0.0\n", writer.toString());
292     }
293 
294     @Test
295     void testWriteBoundarySource_empty() {
296         // act
297         fdWriter.write(BoundarySource3D.of(Collections.emptyList()));
298 
299         // assert
300         Assertions.assertEquals("", writer.toString());
301     }
302 
303     @Test
304     void testWriteBoundarySource_alternativeFormatting() {
305         // arrange
306         final DecimalFormat fmt =
307                 new DecimalFormat("0.0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
308         final ConvexPolygon3D poly1 = Planes.convexPolygonFromVertices(Arrays.asList(
309                 Vector3D.ZERO, Vector3D.of(0, 0, -0.5901), Vector3D.of(0, -0.501, 0)
310             ), TEST_PRECISION);
311         final ConvexPolygon3D poly2 = Planes.convexPolygonFromVertices(Arrays.asList(
312                 Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 1, 0)
313             ), TEST_PRECISION);
314 
315         fdWriter.setDoubleFormat(fmt::format);
316 
317         fdWriter.setFacetVertexCount(3);
318         fdWriter.setLineSeparator("\r\n");
319         fdWriter.setVertexComponentSeparator(",");
320         fdWriter.setVertexSeparator(" | ");
321 
322         // act
323         fdWriter.writeComment("Test boundary source");
324         fdWriter.writeBlankLine();
325         fdWriter.write(BoundarySource3D.of(poly1, poly2));
326 
327         // assert
328         Assertions.assertEquals(
329                 "# Test boundary source\r\n" +
330                 "\r\n" +
331                 "0.0,0.0,0.0 | 0.0,0.0,-0.6 | 0.0,-0.5,0.0\r\n" +
332                 "0.0,0.0,0.0 | 1.0,0.0,0.0 | 1.0,1.0,0.0\r\n" +
333                 "0.0,0.0,0.0 | 1.0,1.0,0.0 | 0.0,1.0,0.0\r\n", writer.toString());
334     }
335 
336     @Test
337     void testCsvFormat() {
338         // arrange
339         final ConvexPolygon3D poly1 = Planes.convexPolygonFromVertices(Arrays.asList(
340                 Vector3D.ZERO, Vector3D.of(0, 0, -0.5901), Vector3D.of(0, -0.501, 0)
341             ), TEST_PRECISION);
342         final ConvexPolygon3D poly2 = Planes.convexPolygonFromVertices(Arrays.asList(
343                 Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 1, 0)
344             ), TEST_PRECISION);
345 
346         final TextFacetDefinitionWriter csvWriter = TextFacetDefinitionWriter.csvFormat(writer);
347 
348         // act
349         csvWriter.write(BoundarySource3D.of(poly1, poly2));
350 
351         // assert
352         Assertions.assertEquals(
353                 "0.0,0.0,0.0,0.0,0.0,-0.5901,0.0,-0.501,0.0\n" +
354                 "0.0,0.0,0.0,1.0,0.0,0.0,1.0,1.0,0.0\n" +
355                 "0.0,0.0,0.0,1.0,1.0,0.0,0.0,1.0,0.0\n", writer.toString());
356     }
357 
358     @Test
359     void testCsvFormat_properties() {
360         // act
361         final TextFacetDefinitionWriter csvWriter = TextFacetDefinitionWriter.csvFormat(writer);
362 
363         // act/assert
364         Assertions.assertEquals(",", csvWriter.getVertexComponentSeparator());
365         Assertions.assertEquals(",", csvWriter.getVertexSeparator());
366         Assertions.assertNull(csvWriter.getCommentToken());
367     }
368 }