TextFacetDefinitionWriter.java

  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. import java.io.Writer;
  19. import java.util.Iterator;
  20. import java.util.List;
  21. import java.util.stream.Stream;

  22. import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
  23. import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
  24. import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
  25. import org.apache.commons.geometry.euclidean.threed.Triangle3D;
  26. import org.apache.commons.geometry.euclidean.threed.Vector3D;
  27. import org.apache.commons.geometry.io.core.utils.AbstractTextFormatWriter;
  28. import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;

  29. /** Class for writing 3D facet geometry in a simple human-readable text format. The
  30.  * format simply consists of sequences of decimal numbers defining the vertices of each
  31.  * facet, with one facet defined per line. Facet vertices are defined by listing their
  32.  * {@code x}, {@code y}, and {@code z} components in that order. At least 3 vertices are
  33.  * required for each facet but more can be specified. The facet normal is defined implicitly
  34.  * from the facet vertices using the right-hand rule (i.e. vertices are arranged counter-clockwise).
  35.  *
  36.  * <p>Delimiters can be configured for both {@link #getVertexComponentSeparator() vertex components} and
  37.  * {@link #getVertexSeparator() vertices}. This allows a wide range of outputs to be configured, from standard
  38.  * {@link #csvFormat(Writer) CSV format} to formats designed for easy human readability.</p>
  39.  *
  40.  * <p><strong>Examples</strong></p>
  41.  * <p>The examples below demonstrate output from two square facets using different writer
  42.  * configurations.</p>
  43.  *
  44.  * <p><em>Default</em></p>
  45.  * <p>The default writer configuration uses distinct vertex and vertex component separators to make it
  46.  * easier to visually distinguish vertices. Comments are supported and facets are allowed to have
  47.  * any geometrically valid number of vertices. This format is designed for human readability and ease
  48.  * of editing.</p>
  49.  * <pre>
  50.  * # two square facets
  51.  * 0 0 0; 1 0 0; 1 1 0; 0 1 0
  52.  * 0 0 0; 0 1 0; 0 1 1; 0 0 1
  53.  * </pre>
  54.  *
  55.  * <p><em>CSV</em></p>
  56.  * <p>The example below uses a comma as both the vertex and vertex component separators to produce
  57.  * a standard CSV format. The facet vertex count is set to 3 to ensure that each row has the same number
  58.  * of columns and all numbers are written with at least a single fraction digit to ensure proper interpretation
  59.  * as floating point data. Comments are not supported. This configuration is produced by the
  60.  * {@link #csvFormat(Writer)} factory method.</p>
  61.  * <pre>
  62.  * 0.0,0.0,0.0,1.0,0.0,0.0,1.0,1.0,0.0
  63.  * 0.0,0.0,0.0,1.0,1.0,0.0,0.0,1.0,0.0
  64.  * 0.0,0.0,0.0,0.0,1.0,0.0,0.0,1.0,1.0
  65.  * 0.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,1.0
  66.  * </pre>
  67.  *
  68.  * @see TextFacetDefinitionReader
  69.  */
  70. public class TextFacetDefinitionWriter extends AbstractTextFormatWriter {

  71.     /** Vertex and vertex component separator used in the CSV format. */
  72.     static final String CSV_SEPARATOR = ",";

  73.     /** Number of vertices required per facet in the CSV format. */
  74.     static final int CSV_FACET_VERTEX_COUNT = 3;

  75.     /** Default vertex component separator. */
  76.     static final String DEFAULT_VERTEX_COMPONENT_SEPARATOR = " ";

  77.     /** Default vertex separator. */
  78.     static final String DEFAULT_VERTEX_SEPARATOR = "; ";

  79.     /** Default facet vertex count. */
  80.     static final int DEFAULT_FACET_VERTEX_COUNT = -1;

  81.     /** Default comment token. */
  82.     private static final String DEFAULT_COMMENT_TOKEN = "# ";

  83.     /** String used to separate vertex components, ie, x, y, z values. */
  84.     private String vertexComponentSeparator = DEFAULT_VERTEX_COMPONENT_SEPARATOR;

  85.     /** String used to separate vertices. */
  86.     private String vertexSeparator = DEFAULT_VERTEX_SEPARATOR;

  87.     /** Number of vertices required per facet; will be -1 if disabled. */
  88.     private int facetVertexCount = DEFAULT_FACET_VERTEX_COUNT;

  89.     /** Comment start token; may be null. */
  90.     private String commentToken = DEFAULT_COMMENT_TOKEN;

  91.     /** Construct a new instance that writes facet information to the given writer.
  92.      * @param writer writer to write output to
  93.      */
  94.     public TextFacetDefinitionWriter(final Writer writer) {
  95.         super(writer);
  96.     }

  97.     /** Get the string used to separate vertex components (ie, individual x, y, z values).
  98.      * The default value is {@value #DEFAULT_VERTEX_COMPONENT_SEPARATOR}.
  99.      * @return string used to separate vertex components
  100.      */
  101.     public String getVertexComponentSeparator() {
  102.         return vertexComponentSeparator;
  103.     }

  104.     /** Set the string used to separate vertex components (ie, individual x, y, z values).
  105.      * @param sep string used to separate vertex components
  106.      */
  107.     public void setVertexComponentSeparator(final String sep) {
  108.         this.vertexComponentSeparator = sep;
  109.     }

  110.     /** Get the string used to separate facet vertices. The default value is {@value #DEFAULT_VERTEX_SEPARATOR}.
  111.      * @return string used to separate facet vertices
  112.      */
  113.     public String getVertexSeparator() {
  114.         return vertexSeparator;
  115.     }

  116.     /** Set the string used to separate facet vertices.
  117.      * @param sep string used to separate facet vertices
  118.      */
  119.     public void setVertexSeparator(final String sep) {
  120.         this.vertexSeparator = sep;
  121.     }

  122.     /** Get the number of vertices required per facet or {@code -1} if no specific
  123.      * number is required. The default value is {@value #DEFAULT_FACET_VERTEX_COUNT}.
  124.      * @return the number of vertices required per facet or {@code -1} if any geometricallly
  125.      *      valid number is allowed (ie, any number greater than or equal to 3)
  126.      */
  127.     public int getFacetVertexCount() {
  128.         return facetVertexCount;
  129.     }

  130.     /** Set the number of vertices required per facet. This can be used to enforce a consistent
  131.      * format in the output. Set to {@code -1} to allow any geometrically valid number of vertices
  132.      * (ie, any number greater than or equal to 3).
  133.      * @param vertexCount number of vertices required per facet or {@code -1} to allow any number
  134.      * @throws IllegalArgumentException if the argument would produce invalid geometries (ie, is
  135.      *      greater than -1 and less than 3)
  136.      */
  137.     public void setFacetVertexCount(final int vertexCount) {
  138.         if (vertexCount > -1 &&  vertexCount < 3) {
  139.             throw new IllegalArgumentException("Facet vertex count must be less than 0 or greater than 2; was " +
  140.                     vertexCount);
  141.         }

  142.         this.facetVertexCount = Math.max(-1, vertexCount);
  143.     }

  144.     /** Get the string used to begin comment lines in the output.
  145.      * The default value is {@value #DEFAULT_COMMENT_TOKEN}
  146.      * @return the string used to begin comment lines in the output; may be null
  147.      */
  148.     public String getCommentToken() {
  149.         return commentToken;
  150.     }

  151.     /** Set the string used to begin comment lines in the output. Set to null to disable the
  152.      * use of comments.
  153.      * @param commentToken comment token string
  154.      * @throws IllegalArgumentException if the argument is empty or begins with whitespace
  155.      */
  156.     public void setCommentToken(final String commentToken) {
  157.         if (commentToken != null) {
  158.             if (commentToken.isEmpty()) {
  159.                 throw new IllegalArgumentException("Comment token cannot be empty");
  160.             } else if (Character.isWhitespace(commentToken.charAt(0))) {
  161.                 throw new IllegalArgumentException("Comment token cannot begin with whitespace");
  162.             }

  163.         }

  164.         this.commentToken = commentToken;
  165.     }

  166.     /** Write a comment to the output.
  167.      * @param comment comment string to write
  168.      * @throws IllegalStateException if the configured {@link #getCommentToken() comment token} is null
  169.      * @throws java.io.UncheckedIOException if an I/O error occurs
  170.      */
  171.     public void writeComment(final String comment) {
  172.         if (commentToken == null) {
  173.             throw new IllegalStateException("Cannot write comment: no comment token configured");
  174.         }

  175.         if (comment != null) {
  176.             for (final String line : comment.split("\\R")) {
  177.                 write(commentToken + line);
  178.                 writeNewLine();
  179.             }
  180.         }
  181.     }

  182.     /** Write a blank line to the output.
  183.      * @throws java.io.UncheckedIOException if an I/O error occurs
  184.      */
  185.     public void writeBlankLine() {
  186.         writeNewLine();
  187.     }

  188.     /** Write all boundaries in the argument to the output. If the
  189.      * {@link #getFacetVertexCount() facet vertex count} has been set to {@code 3}, then each
  190.      * boundary is converted to triangles before being written. Otherwise, the boundaries are
  191.      * written as-is.
  192.      * @param src object providing the boundaries to write
  193.      * @throws IllegalArgumentException if any boundary has infinite size or a
  194.      *      {@link #getFacetVertexCount() facet vertex count} has been configured and a boundary
  195.      *      cannot be represented using the required number of vertices
  196.      * @throws java.io.UncheckedIOException if an I/O error occurs
  197.      */
  198.     public void write(final BoundarySource3D src) {
  199.         try (Stream<PlaneConvexSubset> stream = src.boundaryStream()) {
  200.             final Iterator<PlaneConvexSubset> it = stream.iterator();
  201.             while (it.hasNext()) {
  202.                 write(it.next());
  203.             }
  204.         }
  205.     }

  206.     /** Write the vertices defining the argument to the output. If the
  207.      * {@link #getFacetVertexCount() facet vertex count} has been set to {@code 3}, then the convex subset
  208.      * is converted to triangles before being written to the output. Otherwise, the argument
  209.      * vertices are written as-is.
  210.      * @param convexSubset convex subset to write
  211.      * @throws IllegalArgumentException if the argument has infinite size or a
  212.      *      {@link #getFacetVertexCount() facet vertex count} has been configured and the number of required
  213.      *      vertices does not match the number present in the argument
  214.      * @throws java.io.UncheckedIOException if an I/O error occurs
  215.      */
  216.     public void write(final PlaneConvexSubset convexSubset) {
  217.         if (convexSubset.isInfinite()) {
  218.             throw new IllegalArgumentException("Cannot write infinite convex subset");
  219.         }

  220.         if (facetVertexCount == EuclideanUtils.TRIANGLE_VERTEX_COUNT) {
  221.             // force conversion to triangles
  222.             for (final Triangle3D tri : convexSubset.toTriangles()) {
  223.                 write(tri.getVertices());
  224.             }
  225.         } else {
  226.             // write as-is; callers are responsible for making sure that the number of
  227.             // vertices matches the required number for the writer
  228.             write(convexSubset.getVertices());
  229.         }
  230.     }

  231.     /** Write the vertices in the argument to the output.
  232.      * @param facet facet containing the vertices to write
  233.      * @throws IllegalArgumentException if a {@link #getFacetVertexCount() facet vertex count}
  234.      *      has been configured and the number of required vertices does not match the number
  235.      *      present in the argument
  236.      * @throws java.io.UncheckedIOException if an I/O error occurs
  237.      */
  238.     public void write(final FacetDefinition facet) {
  239.         write(facet.getVertices());
  240.     }

  241.     /** Write a list of vertices defining a facet as a single line of text to the output. Vertex components
  242.      * (ie, individual x, y, z values) are separated with the configured
  243.      * {@link #getVertexComponentSeparator() vertex component separator} and vertices are separated with the
  244.      * configured {@link #getVertexSeparator() vertex separator}.
  245.      * @param vertices vertices to write
  246.      * @throws IllegalArgumentException if the vertex list contains less than 3 vertices or a
  247.      *      {@link #getFacetVertexCount() facet vertex count} has been configured and the number of required
  248.      *      vertices does not match the number given
  249.      * @throws java.io.UncheckedIOException if an I/O error occurs
  250.      * @see #getVertexComponentSeparator()
  251.      * @see #getVertexSeparator()
  252.      * @see #getFacetVertexCount()
  253.      */
  254.     public void write(final List<Vector3D> vertices) {
  255.         final int size = vertices.size();
  256.         if (size < EuclideanUtils.TRIANGLE_VERTEX_COUNT) {
  257.             throw new IllegalArgumentException("At least " + EuclideanUtils.TRIANGLE_VERTEX_COUNT +
  258.                     " vertices are required per facet; found " + size);
  259.         } else if (facetVertexCount > -1 && size != facetVertexCount) {
  260.             throw new IllegalArgumentException("Writer requires " + facetVertexCount +
  261.                     " vertices per facet; found " + size);
  262.         }

  263.         final Iterator<Vector3D> it = vertices.iterator();

  264.         write(it.next());
  265.         while (it.hasNext()) {
  266.             write(vertexSeparator);
  267.             write(it.next());
  268.         }

  269.         writeNewLine();
  270.     }

  271.     /** Write a single vertex to the output.
  272.      * @param vertex vertex to write
  273.      * @throws java.io.UncheckedIOException if an I/O error occurs
  274.      */
  275.     private void write(final Vector3D vertex) {
  276.         write(vertex.getX());
  277.         write(vertexComponentSeparator);
  278.         write(vertex.getY());
  279.         write(vertexComponentSeparator);
  280.         write(vertex.getZ());
  281.     }

  282.     /** Construct a new instance configured to write CSV output to the given writer.
  283.      * The returned instance has the following configuration:
  284.      * <ul>
  285.      *  <li>Vertex separator and vertex components separator are set to the "," string.</li>
  286.      *  <li>Comments are disabled (i.e., comment token is set to null).</li>
  287.      *  <li>Facet vertex count is set to 3 to ensure a consistent number of columns.</li>
  288.      * </ul>
  289.      * This configuration produces output similar to the following:
  290.      * <pre>
  291.      * 0.0,0.0,0.0,1.0,0.0,0.0,1.0,1.0,0.0
  292.      * 0.0,0.0,0.0,1.0,1.0,0.0,0.0,1.0,0.0
  293.      * </pre>
  294.      *
  295.      * @param writer writer to write output to
  296.      * @return a new facet definition writer configured to produce CSV output
  297.      */
  298.     public static TextFacetDefinitionWriter csvFormat(final Writer writer) {
  299.         final TextFacetDefinitionWriter fdWriter = new TextFacetDefinitionWriter(writer);

  300.         fdWriter.setVertexComponentSeparator(CSV_SEPARATOR);
  301.         fdWriter.setVertexSeparator(CSV_SEPARATOR);
  302.         fdWriter.setFacetVertexCount(CSV_FACET_VERTEX_COUNT);
  303.         fdWriter.setCommentToken(null);

  304.         return fdWriter;
  305.     }
  306. }