001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.io.euclidean.threed.txt;
018
019import java.io.Writer;
020import java.util.Iterator;
021import java.util.List;
022import java.util.stream.Stream;
023
024import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
025import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
026import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
027import org.apache.commons.geometry.euclidean.threed.Triangle3D;
028import org.apache.commons.geometry.euclidean.threed.Vector3D;
029import org.apache.commons.geometry.io.core.utils.AbstractTextFormatWriter;
030import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
031
032/** Class for writing 3D facet geometry in a simple human-readable text format. The
033 * format simply consists of sequences of decimal numbers defining the vertices of each
034 * facet, with one facet defined per line. Facet vertices are defined by listing their
035 * {@code x}, {@code y}, and {@code z} components in that order. At least 3 vertices are
036 * required for each facet but more can be specified. The facet normal is defined implicitly
037 * from the facet vertices using the right-hand rule (i.e. vertices are arranged counter-clockwise).
038 *
039 * <p>Delimiters can be configured for both {@link #getVertexComponentSeparator() vertex components} and
040 * {@link #getVertexSeparator() vertices}. This allows a wide range of outputs to be configured, from standard
041 * {@link #csvFormat(Writer) CSV format} to formats designed for easy human readability.</p>
042 *
043 * <p><strong>Examples</strong></p>
044 * <p>The examples below demonstrate output from two square facets using different writer
045 * configurations.</p>
046 *
047 * <p><em>Default</em></p>
048 * <p>The default writer configuration uses distinct vertex and vertex component separators to make it
049 * easier to visually distinguish vertices. Comments are supported and facets are allowed to have
050 * any geometrically valid number of vertices. This format is designed for human readability and ease
051 * of editing.</p>
052 * <pre>
053 * # two square facets
054 * 0 0 0; 1 0 0; 1 1 0; 0 1 0
055 * 0 0 0; 0 1 0; 0 1 1; 0 0 1
056 * </pre>
057 *
058 * <p><em>CSV</em></p>
059 * <p>The example below uses a comma as both the vertex and vertex component separators to produce
060 * a standard CSV format. The facet vertex count is set to 3 to ensure that each row has the same number
061 * of columns and all numbers are written with at least a single fraction digit to ensure proper interpretation
062 * as floating point data. Comments are not supported. This configuration is produced by the
063 * {@link #csvFormat(Writer)} factory method.</p>
064 * <pre>
065 * 0.0,0.0,0.0,1.0,0.0,0.0,1.0,1.0,0.0
066 * 0.0,0.0,0.0,1.0,1.0,0.0,0.0,1.0,0.0
067 * 0.0,0.0,0.0,0.0,1.0,0.0,0.0,1.0,1.0
068 * 0.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,1.0
069 * </pre>
070 *
071 * @see TextFacetDefinitionReader
072 */
073public class TextFacetDefinitionWriter extends AbstractTextFormatWriter {
074
075    /** Vertex and vertex component separator used in the CSV format. */
076    static final String CSV_SEPARATOR = ",";
077
078    /** Number of vertices required per facet in the CSV format. */
079    static final int CSV_FACET_VERTEX_COUNT = 3;
080
081    /** Default vertex component separator. */
082    static final String DEFAULT_VERTEX_COMPONENT_SEPARATOR = " ";
083
084    /** Default vertex separator. */
085    static final String DEFAULT_VERTEX_SEPARATOR = "; ";
086
087    /** Default facet vertex count. */
088    static final int DEFAULT_FACET_VERTEX_COUNT = -1;
089
090    /** Default comment token. */
091    private static final String DEFAULT_COMMENT_TOKEN = "# ";
092
093    /** String used to separate vertex components, ie, x, y, z values. */
094    private String vertexComponentSeparator = DEFAULT_VERTEX_COMPONENT_SEPARATOR;
095
096    /** String used to separate vertices. */
097    private String vertexSeparator = DEFAULT_VERTEX_SEPARATOR;
098
099    /** 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}