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.Reader; 20 import java.util.ArrayList; 21 import java.util.Arrays; 22 import java.util.List; 23 24 import org.apache.commons.geometry.euclidean.threed.Vector3D; 25 import org.apache.commons.geometry.io.core.internal.GeometryIOUtils; 26 import org.apache.commons.geometry.io.core.internal.SimpleTextParser; 27 import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition; 28 import org.apache.commons.geometry.io.euclidean.threed.FacetDefinitionReader; 29 import org.apache.commons.geometry.io.euclidean.threed.SimpleFacetDefinition; 30 31 /** Facet definition reader implementation that reads an extremely simple 32 * text format. The format simply consists of sequences of decimal numbers 33 * defining the vertices of each facet, with one facet defined per line. 34 * Facet vertices are defined by listing their {@code x}, {@code y}, and {@code z} 35 * components in that order. The format can be described as follows: 36 * <p> 37 * <code> 38 * p1<sub>x</sub> p1<sub>y</sub> p1<sub>z</sub> p2<sub>x</sub> p2<sub>y</sub> p2<sub>z</sub> p3<sub>x</sub> p3<sub>y</sub> p3<sub>z</sub> ... 39 * </code> 40 * </p> 41 * <p>where the <em>p1</em> elements contain the coordinates of the first facet vertex, 42 * <em>p2</em> those of the second, and so on. At least 3 vertices are required for each 43 * facet but more can be specified as long as all {@code x, y, z} components are provided 44 * for each vertex. The facet normal is defined implicitly from the facet vertices using 45 * the right-hand rule (i.e. vertices are arranged counter-clockwise).</p> 46 * 47 * <p><strong>Delimiters</strong></p> 48 * <p>Vertex coordinate values may be separated by any character that is 49 * not a digit, alphabetic, '-' (minus), or '+' (plus). The character does 50 * not need to be consistent between (or even within) lines and does not 51 * need to be configured in the reader. This design provides configuration-free 52 * support for common formats such as CSV as well as other formats designed 53 * for human readability.</p> 54 * 55 * <p><strong>Comments</strong></p> 56 * <p>Comments are supported through use of the {@link #getCommentToken() comment token} 57 * property. Characters from the comment token through the end of the current line are 58 * discarded. Setting the comment token to null or the empty string disables comment parsing. 59 * The default comment token is {@value #DEFAULT_COMMENT_TOKEN}</p> 60 * 61 * <p><strong>Examples</strong></p> 62 * <p>The following examples demonstrate the definition of two facets, 63 * one with 3 vertices and one with 4 vertices, in different formats.</p> 64 * <p><em>CSV</em></p> 65 * <pre> 66 * 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0 67 * 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0 68 * </pre> 69 * <p><em>Whitespace and semicolons</em></p> 70 * <pre> 71 * # line comment 72 * 0 0 0; 1 0 0; 1 1 0 # 3 vertices 73 * 1 0 0; 1 1 0; 1 1 1; 1 0 1 # 4 vertices 74 * </pre> 75 * 76 * @see TextFacetDefinitionWriter 77 */ 78 public class TextFacetDefinitionReader implements FacetDefinitionReader { 79 80 /** Default comment token string. */ 81 public static final String DEFAULT_COMMENT_TOKEN = "#"; 82 83 /** Reader for accessing the character stream. */ 84 private final Reader reader; 85 86 /** Parser used to parse text content. */ 87 private final SimpleTextParser parser; 88 89 /** Comment token string; may be null. */ 90 private String commentToken; 91 92 /** True if the instance has a non-null, non-empty comment token. */ 93 private boolean hasCommentToken; 94 95 /** First character of the comment token. */ 96 private int commentStartChar; 97 98 /** Construct a new instance that reads characters from the argument and uses 99 * the default comment token value of {@value TextFacetDefinitionReader#DEFAULT_COMMENT_TOKEN}. 100 * @param reader reader to read characters from 101 */ 102 public TextFacetDefinitionReader(final Reader reader) { 103 this(reader, DEFAULT_COMMENT_TOKEN); 104 } 105 106 /** Construct a new instance with the given reader and comment token. 107 * @param reader reader to read characters from 108 * @param commentToken comment token string; set to null to disable comment parsing 109 * @throws IllegalArgumentException if {@code commentToken} is non-null and contains whitespace 110 */ 111 public TextFacetDefinitionReader(final Reader reader, final String commentToken) { 112 this.reader = reader; 113 this.parser = new SimpleTextParser(reader); 114 115 setCommentTokenInternal(commentToken); 116 } 117 118 /** Get the comment token string. If not null or empty, any characters from 119 * this token to the end of the current line are discarded during parsing. 120 * @return comment token string; may be null 121 */ 122 public String getCommentToken() { 123 return commentToken; 124 } 125 126 /** Set the comment token string. If not null or empty, any characters from this 127 * token to the end of the current line are discarded during parsing. Set to null 128 * or the empty string to disable comment parsing. Comment tokens may not contain 129 * whitespace. 130 * @param commentToken token to set 131 * @throws IllegalArgumentException if the argument is non-null and contains whitespace 132 */ 133 public void setCommentToken(final String commentToken) { 134 setCommentTokenInternal(commentToken); 135 } 136 137 /** {@inheritDoc} */ 138 @Override 139 public FacetDefinition readFacet() { 140 discardNonDataLines(); 141 if (parser.hasMoreCharacters()) { 142 try { 143 return readFacetInternal(); 144 } finally { 145 // advance to the next line even if parsing failed for the 146 // current line 147 parser.discardLine(); 148 } 149 } 150 return null; 151 } 152 153 /** {@inheritDoc} */ 154 @Override 155 public void close() { 156 GeometryIOUtils.closeUnchecked(reader); 157 } 158 159 /** Internal method to read a facet definition starting from the current parser 160 * position. Empty lines (including lines containing only comments) are discarded. 161 * @return facet definition or null if the end of input is reached 162 * @throws IllegalStateException if a data format error occurs 163 * @throws java.io.UncheckedIOException if an I/O error occurs 164 */ 165 private FacetDefinition readFacetInternal() { 166 final Vector3D p1 = readVector(); 167 discardNonData(); 168 final Vector3D p2 = readVector(); 169 discardNonData(); 170 final Vector3D p3 = readVector(); 171 172 final List<Vector3D> vertices; 173 174 discardNonData(); 175 if (parser.hasMoreCharactersOnLine()) { 176 vertices = new ArrayList<>(); 177 vertices.add(p1); 178 vertices.add(p2); 179 vertices.add(p3); 180 181 do { 182 vertices.add(readVector()); 183 discardNonData(); 184 } while (parser.hasMoreCharactersOnLine()); 185 } else { 186 vertices = Arrays.asList(p1, p2, p3); 187 } 188 189 return new SimpleFacetDefinition(vertices); 190 } 191 192 /** Read a vector starting from the current parser position. 193 * @return vector read from the parser 194 * @throws IllegalStateException if a data format error occurs 195 * @throws java.io.UncheckedIOException if an I/O error occurs 196 */ 197 private Vector3D readVector() { 198 final double x = readDouble(); 199 discardNonData(); 200 final double y = readDouble(); 201 discardNonData(); 202 final double z = readDouble(); 203 204 return Vector3D.of(x, y, z); 205 } 206 207 /** Read a double starting from the current parser position. 208 * @return double value read from the parser 209 * @throws IllegalStateException if a data format error occurs 210 * @throws java.io.UncheckedIOException if an I/O error occurs 211 */ 212 private double readDouble() { 213 return parser 214 .next(TextFacetDefinitionReader::isDataTokenPart) 215 .getCurrentTokenAsDouble(); 216 } 217 218 /** Discard lines that do not contain any data. This includes empty lines 219 * and lines that only contain comments. 220 * @throws IllegalStateException if a data format error occurs 221 * @throws java.io.UncheckedIOException if an I/O error occurs 222 */ 223 private void discardNonDataLines() { 224 parser.discardLineWhitespace(); 225 while (parser.hasMoreCharacters() && 226 (!parser.hasMoreCharactersOnLine() || 227 foundComment())) { 228 229 parser 230 .discardLine() 231 .discardLineWhitespace(); 232 } 233 } 234 235 /** Discard a sequence of non-data characters on the current line starting 236 * from the current parser position. 237 * @throws IllegalStateException if a data format error occurs 238 * @throws java.io.UncheckedIOException if an I/O error occurs 239 */ 240 private void discardNonData() { 241 parser.discard(c -> 242 !SimpleTextParser.isNewLinePart(c) && 243 !isDataTokenPart(c) && 244 c != commentStartChar); 245 246 if (foundComment()) { 247 // discard everything to the end of the line but do 248 // not read the new line sequence 249 parser.discard(SimpleTextParser::isNotNewLinePart); 250 } 251 } 252 253 /** Return true if the parser is positioned at the start of the comment token. 254 * @return true if the parser is positioned at the start of the comment token. 255 * @throws IllegalStateException if a data format error occurs 256 * @throws java.io.UncheckedIOException if an I/O error occurs 257 */ 258 private boolean foundComment() { 259 return hasCommentToken && 260 commentToken.equals(parser.peek(commentToken.length())); 261 } 262 263 /** Internal method called to set the comment token state. 264 * @param commentTokenStr comment token to set 265 * @throws IllegalArgumentException if the argument is non-null and contains whitespace 266 */ 267 private void setCommentTokenInternal(final String commentTokenStr) { 268 if (commentTokenStr != null && containsWhitespace(commentTokenStr)) { 269 throw new IllegalArgumentException("Comment token cannot contain whitespace; was [" + 270 commentTokenStr + "]"); 271 } 272 273 this.commentToken = commentTokenStr; 274 this.hasCommentToken = commentTokenStr != null && commentTokenStr.length() > 0; 275 this.commentStartChar = this.hasCommentToken ? 276 commentTokenStr.charAt(0) : 277 -1; 278 } 279 280 /** Return true if the given character is considered as part of a data token 281 * for this reader. 282 * @param ch character to test 283 * @return true if {@code ch} is part of a data token 284 */ 285 private static boolean isDataTokenPart(final int ch) { 286 // include all alphabetic characters in the data tokens, which will help 287 // to provide better error messages in case of failure (ie, tokens will 288 // be split more naturally) 289 return Character.isAlphabetic(ch) || 290 SimpleTextParser.isDecimalPart(ch); 291 } 292 293 /** Return true if the given string contains any whitespace characters. 294 * @param str string to test 295 * @return true if {@code str} contains any whitespace characters 296 */ 297 private static boolean containsWhitespace(final String str) { 298 for (final char ch : str.toCharArray()) { 299 if (Character.isWhitespace(ch)) { 300 return true; 301 } 302 } 303 304 return false; 305 } 306 }