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}