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.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 }