001/** 002 * TRIGeomReader -- A class that can read and write an ASCII *.tri formatted geometry file. 003 * 004 * Copyright (C) 2015-2016, Joseph A. Huwaldt. All rights reserved. 005 * 006 * This library is free software; you can redistribute it and/or modify it under the terms 007 * of the GNU Lesser General Public License as published by the Free Software Foundation; 008 * either version 2.1 of the License, or (at your option) any later version. 009 * 010 * This library is distributed in the hope that it will be useful, but WITHOUT ANY 011 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 012 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 013 * 014 * You should have received a copy of the GNU Lesser General Public License along with 015 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - 016 * Suite 330, Boston, MA 02111-1307, USA. Or visit: http://www.gnu.org/licenses/lgpl.html 017 */ 018package geomss.geom.reader; 019 020import geomss.geom.*; 021import jahuwaldt.js.param.Parameter; 022import java.io.*; 023import java.nio.charset.StandardCharsets; 024import java.nio.file.Files; 025import java.text.MessageFormat; 026import java.util.Arrays; 027import java.util.List; 028import java.util.Locale; 029import static java.util.Objects.isNull; 030import static java.util.Objects.requireNonNull; 031import javax.measure.quantity.Length; 032import javax.measure.unit.Unit; 033import javolution.context.StackContext; 034import javolution.text.TypeFormat; 035import javolution.util.FastTable; 036 037/** 038 * A {@link GeomReader} for reading and writing geometry from/to an Cart3D ASCII 039 * configuration TRI (TRIangulation) formatted geometry file. 040 * <p> 041 * Reference: 042 * <a href="http://docs.desktop.aero/docs/cart3d/index.php/Surface_Geometry#Cart3D_Triangulation_Format"> 043 * http://docs.desktop.aero/docs/cart3d/index.php/Surface_Geometry</a> 044 * </p> 045 * 046 * <p> Modified by: Joseph A. Huwaldt </p> 047 * 048 * @author Joseph A. Huwaldt, Date: September 4, 2015 049 * @version September 9, 2016 050 */ 051public class TRIGeomReader extends AbstractGeomReader { 052 053 // Debug output flag. 054 //private static final boolean DEBUG = false; 055 056 // A brief description of the data read by this reaader. 057 private static final String DESCRIPTION = RESOURCES.getString("triDescription"); 058 059 // The preferred file extension for files of this reader's type. 060 public static final String EXTENSION = "tri"; 061 062 // Used to split strings on white space. 063 private static final String WHITESPACE = "\\s+"; 064 065 /** 066 * Returns a string representation of the object. This will return a brief description 067 * of the format read by this reader. 068 * 069 * @return A brief description of the format read by this reader. 070 */ 071 @Override 072 public String toString() { 073 return DESCRIPTION; 074 } 075 076 /** 077 * Returns the preferred file extension (not including the ".") for files of this 078 * GeomReader's type. 079 * 080 * @return The preferred file extension for files of this readers type. 081 */ 082 @Override 083 public String getExtension() { 084 return EXTENSION; 085 } 086 087 /** 088 * Method that determines if this reader can read geometry from the specified input 089 * file. 090 * 091 * @param inputFile The input file containing the geometry to be read in. 092 * @return GeomReader.NO if the file format is not recognized by this reader. 093 * GeomReader.YES if the file has the extension ".geo" or ".mk5". 094 * GeomReader.MAYBE if the file has the extension ".lib". 095 * @throws java.io.IOException If there is a problem reading from the specified file. 096 */ 097 @Override 098 public int canReadData(File inputFile) throws IOException { 099 100 String name = inputFile.getName(); 101 name = name.toLowerCase().trim(); 102 if (name.endsWith(".tri")) { 103 // Try reading the number of vertices and triangles from the file. 104 try (BufferedReader reader = Files.newBufferedReader(inputFile.toPath())) { 105 106 String aLine = reader.readLine(); 107 if (isNull(aLine)) 108 return NO; 109 String[] tokens = aLine.trim().split(WHITESPACE); 110 if (tokens.length > 2) 111 return NO; 112 int numVerts = TypeFormat.parseInt(tokens[0]); 113 int numTris = TypeFormat.parseInt(tokens[1]); 114 if (numVerts < 3 || numTris < 1) 115 return NO; 116 117 return YES; 118 119 } catch (NumberFormatException e) { 120 return NO; 121 } 122 } 123 124 return NO; 125 } 126 127 /** 128 * Returns true. This class can write triangle and quad-paneled point geometry data to 129 * an ASCII *.tri formatted file. 130 * 131 * @return this method always returns true 132 */ 133 @Override 134 public boolean canWriteData() { 135 return true; 136 } 137 138 /** 139 * Reads in a Cart3D ASCII Configuration *.tri formatted geometry file from the 140 * specified input file and returns a {@link GeomList} object that contains a set of 141 * {@link TriangleList} objects (1 for each "configuration" in the *.tri file or a 142 * single one if the *.tri file is a component file only). 143 * <p> 144 * WARNING: This file format is not unit aware. You must set the units 145 * to be used by calling "setFileUnits()" before calling this method! 146 * </p> 147 * 148 * @param inputFile The input file containing the geometry to be read in. May not be 149 * null. 150 * @return A {@link GeomList} object containing the geometry read in from the file. If 151 * the file has no geometry in it, then this list will have no triangles in it 152 * (will have a size() of zero). 153 * @throws IOException If there is a problem reading the specified file. 154 * @see #setFileUnits(javax.measure.unit.Unit) 155 */ 156 @Override 157 public GeomList<TriangleList> read(File inputFile) throws IOException { 158 requireNonNull(inputFile); 159 _warnings.clear(); 160 161 // Create an initially empty geometry list for output. 162 GeomList<TriangleList> output = GeomList.newInstance(); 163 164 // Read in all the lines of the file. 165 List<String> lines = Files.readAllLines(inputFile.toPath(), StandardCharsets.US_ASCII); 166 167 Unit<Length> units = this.getFileUnits(); 168 int line = 0; 169 try { 170 // Read in the number of verticies and triangles in the file. 171 String[] tokens = lines.get(line++).trim().split(WHITESPACE); 172 int numVerts = TypeFormat.parseInt(tokens[0]); 173 int numTris = TypeFormat.parseInt(tokens[1]); 174 175 // Read in all the vertices. 176 FastTable<Point> vertices = FastTable.newInstance(); 177 for (int i = 0; i < numVerts; ++i) { 178 tokens = lines.get(line++).trim().split(WHITESPACE); 179 double x = TypeFormat.parseDouble(tokens[0]); 180 double y = TypeFormat.parseDouble(tokens[1]); 181 double z = TypeFormat.parseDouble(tokens[2]); 182 Point p = Point.valueOf(x, y, z, units); 183 vertices.add(p); 184 } 185 186 // Read in the array of indexes. 187 int numIndexes = numTris * 3; 188 int[] indexes = new int[numIndexes]; 189 int pos = 0; 190 for (int i = 0; i < numTris; ++i) { 191 tokens = lines.get(line++).trim().split(WHITESPACE); 192 for (int j = 0; j < 3; ++j) { 193 int idx = TypeFormat.parseInt(tokens[j]) - 1; // Convert to 0 offset indexes. 194 indexes[pos++] = idx; 195 } 196 } 197 198 // Define the configuration ID list and initially set them all to 1. 199 int[] configIDs = new int[numTris]; 200 Arrays.fill(configIDs, 1); 201 202 // Read in the configuration codes. 203 // Could be 1 per line or all on one line space delimited. 204 int maxConfigID = 1; 205 pos = 0; 206 while (line < lines.size() && pos < numTris) { 207 String aLine = lines.get(line++).trim(); 208 if (aLine.contains(" ") || aLine.contains("\t")) { 209 // Multiple IDs on one line. 210 tokens = aLine.split(WHITESPACE); 211 for (int i = 0; i < tokens.length; ++i) { 212 int id = TypeFormat.parseInt(tokens[i]); 213 configIDs[pos++] = id; 214 if (id > maxConfigID) 215 maxConfigID = id; 216 } 217 } else { 218 // A single ID per line. 219 int id = TypeFormat.parseInt(aLine); 220 configIDs[pos++] = id; 221 if (id > maxConfigID) 222 maxConfigID = id; 223 } 224 } 225 226 // Break up the output into TriangleList objects based on the configuration IDs. 227 if (maxConfigID == 1) { 228 // Only one configuration. 229 TriangleList<Triangle> tris = TriangleList.valueOf(vertices, indexes); 230 output.add(tris); 231 232 } else { 233 // Multiple configurations in the file. 234 235 // Loop over each configuration ID. 236 for (int cID = 1; cID <= maxConfigID; ++cID) { 237 // Count how many triangles have this configuration ID. 238 int num = 0; 239 for (int i = numTris - 1; i >= 0; --i) { 240 if (configIDs[i] == cID) 241 ++num; 242 } 243 244 // Create an array of indexes for this configuration. 245 int[] configIndexes = new int[num * 3]; 246 247 // Find all the triangles with this set of configuration indexes. 248 pos = 0; 249 for (int i = numTris - 1; i >= 0; --i) { 250 if (configIDs[i] == cID) { 251 int idx = i * 3; 252 configIndexes[pos++] = indexes[idx++]; 253 configIndexes[pos++] = indexes[idx++]; 254 configIndexes[pos++] = indexes[idx]; 255 } 256 } 257 258 // Create a list of triangles using this array of indexes. 259 TriangleList<Triangle> tris = TriangleList.valueOf(vertices, configIndexes); 260 tris.setName("Config #" + cID); 261 output.add(tris); 262 } 263 264 } 265 266 } catch (NumberFormatException e) { 267 throw new IOException(MessageFormat.format( 268 RESOURCES.getString("parseErrMsg"), DESCRIPTION, line + 1)); 269 } 270 271 return output; 272 } 273 274 /** 275 * Writes out a Cart3D ASCII Configuration *.tri formatted geometry file for the 276 * geometry contained in the supplied geometry list. If the list contains only 277 * triangles, they are written out as a "Component TRI file". If the list contains 278 * lists of triangles, each are written out as separate configurations in a 279 * "Configuration TRI file". If the list contains a PointVehicle, then each 280 * PointComponent is triangulated and written out as a separate configuration in a 281 * "Configuration TRI file". If no triangle or point array based geometry can be found 282 * in the geometry list, then nothing will happen (the output file will not be 283 * created). 284 * <p> 285 * The user data key "TRI_TOL" can optionally be set to a Parameter containing the 286 * tolerance to use when determining if vertices are coincident. If not supplied, a 287 * default tolerance will be used that essentially requires identical vertex 288 * coordinates in order to be coincident. 289 * </p> 290 * <p> 291 * WARNING: This format is not unit aware. The geometry will be written out in 292 * whatever its current units are! Make sure to convert to the desired units for the 293 * file before calling this method. 294 * </p> 295 * 296 * @param outputFile The output file to which the geometry is to be written. May not 297 * be null. 298 * @param geometry The list of triangle or point array geometry to be written out. 299 * May not be null. 300 * @throws IOException If there is a problem writing to the specified file. 301 */ 302 @Override 303 public void write(File outputFile, GeometryList geometry) throws IOException { 304 requireNonNull(outputFile); 305 _warnings.clear(); 306 if (!geometry.containsGeometry()) { 307 _warnings.add(RESOURCES.getString("noGeometryWarning")); 308 return; 309 } 310 311 StackContext.enter(); 312 try { 313 // Convert the input into a list of TriangleList objects. 314 GeomList<TriangleList> configs = convertGeometry(geometry); 315 int numConfigs = configs.size(); 316 if (numConfigs == 0) { 317 _warnings.add(RESOURCES.getString("noGeometryWarning")); 318 return; 319 } 320 321 // Convert all the triangles to the same units. 322 Unit<Length> units = geometry.getUnit(); 323 324 // Create a single triangle list with all the triangles in it. 325 TriangleList triLst = TriangleList.newInstance(); 326 for (int i = 0; i < numConfigs; ++i) 327 triLst.addAll(configs.get(i)); 328 329 // Convert the Triangle objects into a list of unique vertices and indexes. 330 Parameter<Length> tol = (Parameter<Length>)geometry.getUserData("TRI_TOL"); 331 if (isNull(tol)) 332 tol = Parameter.valueOf(Parameter.SQRT_EPS, triLst.getUnit()); 333 TriangleVertData vd = triLst.toVertData(tol); 334 int numVerts = vd.vertices.size(); 335 int numTris = vd.numTris; 336 337 // Get a writer to the file. 338 Locale US = Locale.US; 339 try (PrintWriter writer = new PrintWriter(outputFile, StandardCharsets.US_ASCII.name())) { 340 341 // Write out the number of vertices and triangles. 342 writer.printf(US, "%12d%12d", numVerts, numTris); 343 writer.println(); 344 345 // Lookp over all the vertices and write them out. 346 for (int i = 0; i < numVerts; ++i) { 347 GeomPoint p = vd.vertices.get(i); 348 double x = p.getValue(Point.X, units); 349 double y = p.getValue(Point.Y, units); 350 double z = p.getValue(Point.Z, units); 351 writer.printf(US, "%18.8E%18.8E%18.8E", x, y, z); 352 writer.println(); 353 } 354 355 // Loop over all the triangle vertex indexes and write them out 3 at a time. 356 int numIndexes = numTris * 3; 357 for (int i = 0; i < numIndexes;) { 358 int idx1 = vd.tris[i++] + 1; // Convert to unit offset indexes. 359 int idx2 = vd.tris[i++] + 1; 360 int idx3 = vd.tris[i++] + 1; 361 writer.printf(US, "%12d%12d%12d", idx1, idx2, idx3); 362 writer.println(); 363 } 364 365 // Write out the configuration ID tags. 366 if (numConfigs > 1) { 367 for (int cID = 1; cID <= numConfigs; ++cID) { 368 int num = configs.get(cID - 1).size(); 369 for (int i = 0; i < num; ++i) { 370 writer.printf(US, "%8d", cID); 371 writer.println(); 372 } 373 } 374 } 375 } 376 } finally { 377 StackContext.exit(); 378 } 379 380 } 381 382 /** 383 * Convert the input list of geometry into a list of TriangleList objects if possible. 384 * 385 * @param geom The list of geometry to be processed. 386 * @param output The list that the triangles are all added to. 387 */ 388 private static GeomList<TriangleList> convertGeometry(GeometryList geometry) { 389 // Sort out what the top-level list is. 390 391 GeomList<TriangleList> geom = GeomList.newInstance(); 392 if (geometry instanceof TriangleList) { 393 geom.add((TriangleList)geometry); 394 395 } else if (geometry instanceof PointVehicle) { 396 // A PointVehicle is output as a series of TriangleList objects. 397 PointVehicle veh = (PointVehicle)geometry; 398 399 // Convert each PointComponent into a separate list of triangles. 400 for (PointComponent comp : veh) { 401 TriangleList tris = TriangleList.newInstance(); 402 403 // Combine all the arrays in this component into one triangle list. 404 for (PointArray arr : comp) 405 tris.addAll(arr.triangulate()); 406 407 // Add the triangle list to the output list. 408 geom.add(tris); 409 410 } 411 412 } else if (geometry instanceof PointComponent) { 413 // A PointComponent is output as a series of TriangleList objects. 414 PointComponent comp = (PointComponent)geometry; 415 416 // Combine all the arrays in this component into one triangle list. 417 TriangleList tris = TriangleList.newInstance(); 418 for (PointArray arr : comp) 419 tris.addAll(arr.triangulate()); 420 421 // Add the triangle list to the output list. 422 geom.add(tris); 423 424 } else if (geometry instanceof PointArray) { 425 // A PointComponent is output as a series of TriangleList objects. 426 PointArray arr = (PointArray)geometry; 427 TriangleList tris = arr.triangulate(); 428 geom.add(tris); 429 430 } else if (geometry instanceof GeomList) { 431 int size = geometry.size(); 432 if (geometry.get(0) instanceof TriangleList) { 433 // All the elements must be TriangleList. 434 for (int i=0; i < size; ++i) { 435 if (!(geometry.get(i) instanceof TriangleList)) 436 return geom; 437 } 438 geom.addAll(geometry); 439 440 } else if (geometry.get(0) instanceof GeomTriangle) { 441 // All the elements must be triangles. 442 for (int i=0; i < size; ++i) { 443 if (!(geometry.get(i) instanceof GeomTriangle)) 444 return geom; 445 } 446 TriangleList tris = TriangleList.newInstance(); 447 tris.addAll(geometry); 448 geom.add(tris); 449 450 } 451 452 } 453 454 return geom; 455 } 456 457 /** 458 * This method always returns <code>false</code> as TRI files do not encode the units 459 * that are being used. You must call <code>setFileUnits</code> to set the units being 460 * used before reading from a file of this format. 461 * 462 * @return This implementation always returns false. 463 * @see #setFileUnits(javax.measure.unit.Unit) 464 */ 465 @Override 466 public boolean isUnitAware() { 467 return false; 468 } 469 470}