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.Writer;
20 import java.util.Iterator;
21 import java.util.List;
22 import java.util.stream.Stream;
23
24 import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
25 import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
26 import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
27 import org.apache.commons.geometry.euclidean.threed.Triangle3D;
28 import org.apache.commons.geometry.euclidean.threed.Vector3D;
29 import org.apache.commons.geometry.io.core.utils.AbstractTextFormatWriter;
30 import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
31
32 /** Class for writing 3D facet geometry in a simple human-readable text format. The
33 * format simply consists of sequences of decimal numbers defining the vertices of each
34 * facet, with one facet defined per line. Facet vertices are defined by listing their
35 * {@code x}, {@code y}, and {@code z} components in that order. At least 3 vertices are
36 * required for each facet but more can be specified. The facet normal is defined implicitly
37 * from the facet vertices using the right-hand rule (i.e. vertices are arranged counter-clockwise).
38 *
39 * <p>Delimiters can be configured for both {@link #getVertexComponentSeparator() vertex components} and
40 * {@link #getVertexSeparator() vertices}. This allows a wide range of outputs to be configured, from standard
41 * {@link #csvFormat(Writer) CSV format} to formats designed for easy human readability.</p>
42 *
43 * <p><strong>Examples</strong></p>
44 * <p>The examples below demonstrate output from two square facets using different writer
45 * configurations.</p>
46 *
47 * <p><em>Default</em></p>
48 * <p>The default writer configuration uses distinct vertex and vertex component separators to make it
49 * easier to visually distinguish vertices. Comments are supported and facets are allowed to have
50 * any geometrically valid number of vertices. This format is designed for human readability and ease
51 * of editing.</p>
52 * <pre>
53 * # two square facets
54 * 0 0 0; 1 0 0; 1 1 0; 0 1 0
55 * 0 0 0; 0 1 0; 0 1 1; 0 0 1
56 * </pre>
57 *
58 * <p><em>CSV</em></p>
59 * <p>The example below uses a comma as both the vertex and vertex component separators to produce
60 * a standard CSV format. The facet vertex count is set to 3 to ensure that each row has the same number
61 * of columns and all numbers are written with at least a single fraction digit to ensure proper interpretation
62 * as floating point data. Comments are not supported. This configuration is produced by the
63 * {@link #csvFormat(Writer)} factory method.</p>
64 * <pre>
65 * 0.0,0.0,0.0,1.0,0.0,0.0,1.0,1.0,0.0
66 * 0.0,0.0,0.0,1.0,1.0,0.0,0.0,1.0,0.0
67 * 0.0,0.0,0.0,0.0,1.0,0.0,0.0,1.0,1.0
68 * 0.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,1.0
69 * </pre>
70 *
71 * @see TextFacetDefinitionReader
72 */
73 public class TextFacetDefinitionWriter extends AbstractTextFormatWriter {
74
75 /** Vertex and vertex component separator used in the CSV format. */
76 static final String CSV_SEPARATOR = ",";
77
78 /** Number of vertices required per facet in the CSV format. */
79 static final int CSV_FACET_VERTEX_COUNT = 3;
80
81 /** Default vertex component separator. */
82 static final String DEFAULT_VERTEX_COMPONENT_SEPARATOR = " ";
83
84 /** Default vertex separator. */
85 static final String DEFAULT_VERTEX_SEPARATOR = "; ";
86
87 /** Default facet vertex count. */
88 static final int DEFAULT_FACET_VERTEX_COUNT = -1;
89
90 /** Default comment token. */
91 private static final String DEFAULT_COMMENT_TOKEN = "# ";
92
93 /** String used to separate vertex components, ie, x, y, z values. */
94 private String vertexComponentSeparator = DEFAULT_VERTEX_COMPONENT_SEPARATOR;
95
96 /** String used to separate vertices. */
97 private String vertexSeparator = DEFAULT_VERTEX_SEPARATOR;
98
99 /** Number of vertices required per facet; will be -1 if disabled. */
100 private int facetVertexCount = DEFAULT_FACET_VERTEX_COUNT;
101
102 /** Comment start token; may be null. */
103 private String commentToken = DEFAULT_COMMENT_TOKEN;
104
105 /** Construct a new instance that writes facet information to the given writer.
106 * @param writer writer to write output to
107 */
108 public TextFacetDefinitionWriter(final Writer writer) {
109 super(writer);
110 }
111
112 /** Get the string used to separate vertex components (ie, individual x, y, z values).
113 * The default value is {@value #DEFAULT_VERTEX_COMPONENT_SEPARATOR}.
114 * @return string used to separate vertex components
115 */
116 public String getVertexComponentSeparator() {
117 return vertexComponentSeparator;
118 }
119
120 /** Set the string used to separate vertex components (ie, individual x, y, z values).
121 * @param sep string used to separate vertex components
122 */
123 public void setVertexComponentSeparator(final String sep) {
124 this.vertexComponentSeparator = sep;
125 }
126
127 /** Get the string used to separate facet vertices. The default value is {@value #DEFAULT_VERTEX_SEPARATOR}.
128 * @return string used to separate facet vertices
129 */
130 public String getVertexSeparator() {
131 return vertexSeparator;
132 }
133
134 /** Set the string used to separate facet vertices.
135 * @param sep string used to separate facet vertices
136 */
137 public void setVertexSeparator(final String sep) {
138 this.vertexSeparator = sep;
139 }
140
141 /** Get the number of vertices required per facet or {@code -1} if no specific
142 * number is required. The default value is {@value #DEFAULT_FACET_VERTEX_COUNT}.
143 * @return the number of vertices required per facet or {@code -1} if any geometricallly
144 * valid number is allowed (ie, any number greater than or equal to 3)
145 */
146 public int getFacetVertexCount() {
147 return facetVertexCount;
148 }
149
150 /** Set the number of vertices required per facet. This can be used to enforce a consistent
151 * format in the output. Set to {@code -1} to allow any geometrically valid number of vertices
152 * (ie, any number greater than or equal to 3).
153 * @param vertexCount number of vertices required per facet or {@code -1} to allow any number
154 * @throws IllegalArgumentException if the argument would produce invalid geometries (ie, is
155 * greater than -1 and less than 3)
156 */
157 public void setFacetVertexCount(final int vertexCount) {
158 if (vertexCount > -1 && vertexCount < 3) {
159 throw new IllegalArgumentException("Facet vertex count must be less than 0 or greater than 2; was " +
160 vertexCount);
161 }
162
163 this.facetVertexCount = Math.max(-1, vertexCount);
164 }
165
166 /** Get the string used to begin comment lines in the output.
167 * The default value is {@value #DEFAULT_COMMENT_TOKEN}
168 * @return the string used to begin comment lines in the output; may be null
169 */
170 public String getCommentToken() {
171 return commentToken;
172 }
173
174 /** Set the string used to begin comment lines in the output. Set to null to disable the
175 * use of comments.
176 * @param commentToken comment token string
177 * @throws IllegalArgumentException if the argument is empty or begins with whitespace
178 */
179 public void setCommentToken(final String commentToken) {
180 if (commentToken != null) {
181 if (commentToken.isEmpty()) {
182 throw new IllegalArgumentException("Comment token cannot be empty");
183 } else if (Character.isWhitespace(commentToken.charAt(0))) {
184 throw new IllegalArgumentException("Comment token cannot begin with whitespace");
185 }
186
187 }
188
189 this.commentToken = commentToken;
190 }
191
192 /** Write a comment to the output.
193 * @param comment comment string to write
194 * @throws IllegalStateException if the configured {@link #getCommentToken() comment token} is null
195 * @throws java.io.UncheckedIOException if an I/O error occurs
196 */
197 public void writeComment(final String comment) {
198 if (commentToken == null) {
199 throw new IllegalStateException("Cannot write comment: no comment token configured");
200 }
201
202 if (comment != null) {
203 for (final String line : comment.split("\\R")) {
204 write(commentToken + line);
205 writeNewLine();
206 }
207 }
208 }
209
210 /** Write a blank line to the output.
211 * @throws java.io.UncheckedIOException if an I/O error occurs
212 */
213 public void writeBlankLine() {
214 writeNewLine();
215 }
216
217 /** Write all boundaries in the argument to the output. If the
218 * {@link #getFacetVertexCount() facet vertex count} has been set to {@code 3}, then each
219 * boundary is converted to triangles before being written. Otherwise, the boundaries are
220 * written as-is.
221 * @param src object providing the boundaries to write
222 * @throws IllegalArgumentException if any boundary has infinite size or a
223 * {@link #getFacetVertexCount() facet vertex count} has been configured and a boundary
224 * cannot be represented using the required number of vertices
225 * @throws java.io.UncheckedIOException if an I/O error occurs
226 */
227 public void write(final BoundarySource3D src) {
228 try (Stream<PlaneConvexSubset> stream = src.boundaryStream()) {
229 final Iterator<PlaneConvexSubset> it = stream.iterator();
230 while (it.hasNext()) {
231 write(it.next());
232 }
233 }
234 }
235
236 /** Write the vertices defining the argument to the output. If the
237 * {@link #getFacetVertexCount() facet vertex count} has been set to {@code 3}, then the convex subset
238 * is converted to triangles before being written to the output. Otherwise, the argument
239 * vertices are written as-is.
240 * @param convexSubset convex subset to write
241 * @throws IllegalArgumentException if the argument has infinite size or a
242 * {@link #getFacetVertexCount() facet vertex count} has been configured and the number of required
243 * vertices does not match the number present in the argument
244 * @throws java.io.UncheckedIOException if an I/O error occurs
245 */
246 public void write(final PlaneConvexSubset convexSubset) {
247 if (convexSubset.isInfinite()) {
248 throw new IllegalArgumentException("Cannot write infinite convex subset");
249 }
250
251 if (facetVertexCount == EuclideanUtils.TRIANGLE_VERTEX_COUNT) {
252 // force conversion to triangles
253 for (final Triangle3D tri : convexSubset.toTriangles()) {
254 write(tri.getVertices());
255 }
256 } else {
257 // write as-is; callers are responsible for making sure that the number of
258 // vertices matches the required number for the writer
259 write(convexSubset.getVertices());
260 }
261 }
262
263 /** Write the vertices in the argument to the output.
264 * @param facet facet containing the vertices to write
265 * @throws IllegalArgumentException if a {@link #getFacetVertexCount() facet vertex count}
266 * has been configured and the number of required vertices does not match the number
267 * present in the argument
268 * @throws java.io.UncheckedIOException if an I/O error occurs
269 */
270 public void write(final FacetDefinition facet) {
271 write(facet.getVertices());
272 }
273
274 /** Write a list of vertices defining a facet as a single line of text to the output. Vertex components
275 * (ie, individual x, y, z values) are separated with the configured
276 * {@link #getVertexComponentSeparator() vertex component separator} and vertices are separated with the
277 * configured {@link #getVertexSeparator() vertex separator}.
278 * @param vertices vertices to write
279 * @throws IllegalArgumentException if the vertex list contains less than 3 vertices or a
280 * {@link #getFacetVertexCount() facet vertex count} has been configured and the number of required
281 * vertices does not match the number given
282 * @throws java.io.UncheckedIOException if an I/O error occurs
283 * @see #getVertexComponentSeparator()
284 * @see #getVertexSeparator()
285 * @see #getFacetVertexCount()
286 */
287 public void write(final List<Vector3D> vertices) {
288 final int size = vertices.size();
289 if (size < EuclideanUtils.TRIANGLE_VERTEX_COUNT) {
290 throw new IllegalArgumentException("At least " + EuclideanUtils.TRIANGLE_VERTEX_COUNT +
291 " vertices are required per facet; found " + size);
292 } else if (facetVertexCount > -1 && size != facetVertexCount) {
293 throw new IllegalArgumentException("Writer requires " + facetVertexCount +
294 " vertices per facet; found " + size);
295 }
296
297 final Iterator<Vector3D> it = vertices.iterator();
298
299 write(it.next());
300 while (it.hasNext()) {
301 write(vertexSeparator);
302 write(it.next());
303 }
304
305 writeNewLine();
306 }
307
308 /** Write a single vertex to the output.
309 * @param vertex vertex to write
310 * @throws java.io.UncheckedIOException if an I/O error occurs
311 */
312 private void write(final Vector3D vertex) {
313 write(vertex.getX());
314 write(vertexComponentSeparator);
315 write(vertex.getY());
316 write(vertexComponentSeparator);
317 write(vertex.getZ());
318 }
319
320 /** Construct a new instance configured to write CSV output to the given writer.
321 * The returned instance has the following configuration:
322 * <ul>
323 * <li>Vertex separator and vertex components separator are set to the "," string.</li>
324 * <li>Comments are disabled (i.e., comment token is set to null).</li>
325 * <li>Facet vertex count is set to 3 to ensure a consistent number of columns.</li>
326 * </ul>
327 * This configuration produces output similar to the following:
328 * <pre>
329 * 0.0,0.0,0.0,1.0,0.0,0.0,1.0,1.0,0.0
330 * 0.0,0.0,0.0,1.0,1.0,0.0,0.0,1.0,0.0
331 * </pre>
332 *
333 * @param writer writer to write output to
334 * @return a new facet definition writer configured to produce CSV output
335 */
336 public static TextFacetDefinitionWriter csvFormat(final Writer writer) {
337 final TextFacetDefinitionWriter fdWriter = new TextFacetDefinitionWriter(writer);
338
339 fdWriter.setVertexComponentSeparator(CSV_SEPARATOR);
340 fdWriter.setVertexSeparator(CSV_SEPARATOR);
341 fdWriter.setFacetVertexCount(CSV_FACET_VERTEX_COUNT);
342 fdWriter.setCommentToken(null);
343
344 return fdWriter;
345 }
346 }