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 }