001/*
002 *   GGPGeomReader  -- A class that can read a GGP formatted geometry file.
003 *
004 *   Copyright (C) 2000-2016, Joseph A. Huwaldt
005 *   All rights reserved.
006 *   
007 *   This library is free software; you can redistribute it and/or
008 *   modify it under the terms of the GNU Lesser General Public
009 *   License as published by the Free Software Foundation; either
010 *   version 2.1 of the License, or (at your option) any later version.
011 *   
012 *   This library is distributed in the hope that it will be useful,
013 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
014 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 *   Lesser General Public License for more details.
016 *
017 *   You should have received a copy of the GNU Lesser General Public License
018 *   along with this program; if not, write to the Free Software
019 *   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
020 *   Or visit:  http://www.gnu.org/licenses/lgpl.html
021 */
022package geomss.geom.reader;
023
024import geomss.geom.*;
025import jahuwaldt.js.util.TextTokenizer;
026import java.io.File;
027import java.io.FileReader;
028import java.io.IOException;
029import java.io.LineNumberReader;
030import java.text.MessageFormat;
031import java.util.ArrayList;
032import java.util.List;
033import java.util.Locale;
034import static java.util.Objects.isNull;
035import static java.util.Objects.nonNull;
036import static java.util.Objects.requireNonNull;
037import javolution.context.StackContext;
038import javolution.text.Text;
039import javolution.text.TypeFormat;
040import javolution.util.FastTable;
041
042/**
043 * A {@link GeomReader} for reading vehicle geometry from a GGP formatted geometry file.
044 * This is a geometry file format often used in conjunction with solution output from A502
045 * (PANAIR) and A633 (TRANAIR). This class will only read in parameters from the GGP file
046 * with the names "X", "Y", and "Z". All other parameters are ignored. This class also
047 * assumes that all the parameters are contained in a single line and that the 1st format
048 * is duplicated throughout the file.
049 *
050 * <p> Modified by: Joseph A. Huwaldt </p>
051 *
052 * @author Joseph A. Huwaldt, Date: May 11, 2000
053 * @version July 3, 2019
054 */
055public class GGPGeomReader extends AbstractGeomReader {
056
057    //  A brief description of the data read by this reaader.
058    private static final String DESCRIPTION = RESOURCES.getString("ggpDescription");
059    
060    //  The preferred file extension for files of this reader's type.
061    public static final String EXTENSION = "ggp";
062    
063    // Define the comment characters that are possible.
064    private static final String[] COMMENT_CHARACTERS = {"*", "$", "("};
065    
066    /**
067     * A description of the format of the arrays in the GGP file.
068     */
069    private final FastTable<Integer> formatLst = FastTable.newInstance();
070
071    /**
072     * Returns a string representation of the object. This will return a brief description
073     * of the format read by this reader.
074     *
075     * @return A brief description of the format read by this reader.
076     */
077    @Override
078    public String toString() {
079        return DESCRIPTION;
080    }
081
082    /**
083     * Returns the preferred file extension (not including the ".") for files of this
084     * GeomReader's type.
085     *
086     * @return The preferred file extension for files of this readers type.
087     */
088    @Override
089    public String getExtension() {
090        return EXTENSION;
091    }
092
093    /**
094     * Method that determines if this reader can read geometry from the specified input
095     * file.
096     *
097     * @param inputFile The input file containing the geometry to be read in. May not be
098     *                  null.
099     * @return GeomReader.NO if the file format is not recognized by this reader.
100     *         GeomReader.YES if the file format is definitely recognized by this reader.
101     *         GeomReader.MAYBE if the file format might be readable by this reader, but
102     *         that can't easily be determined without actually reading the file.
103     * @throws java.io.IOException If there is a problem reading from the specified
104     * file.
105     */
106    @Override
107    public int canReadData(File inputFile) throws IOException {
108
109        int response = NO;
110        String name = inputFile.getName();
111        name = name.toLowerCase().trim();
112        if (name.endsWith(".ggp"))
113            response = MAYBE;
114
115        return response;
116    }
117
118    /**
119     * Reads in a GGP formatted geometry file from the specified input file and returns a
120     * {@link PointVehicle} object that contains the geometry from the GGP file.
121     * <p>
122     * A GGP file does not support multiple components, therefore, the vehicle returned
123     * will always contain a single component (or none at all if there was no data in the
124     * file).
125     * </p>
126     * <p>
127     * WARNING: This file format is not unit aware. You must set the units to be used by
128     * calling "setFileUnits()" before calling this method!
129     * </p>
130     *
131     * @param inputFile The input file containing the geometry to be read in. May not be
132     *                  null.
133     * @return A {@link PointVehicle} object containing the geometry read in from the
134     *         file. If the file has no geometry in it, then this list will have no
135     *         components in it (will have a size() of zero).
136     * @throws IOException If there is a problem reading the specified file.
137     * @see #setFileUnits(javax.measure.unit.Unit) 
138     */
139    @Override
140    public PointVehicle read(File inputFile) throws IOException {
141        requireNonNull(inputFile);
142        _warnings.clear();
143
144        PointVehicle vehicle = PointVehicle.newInstance(inputFile.getName());
145
146        //  GGP files are required to be in ASCII with U.S. style number formatting.
147        //  Get the default locale and save it.  Then change the default locale to US.
148        Locale defLocale = Locale.getDefault();
149        Locale.setDefault(Locale.US);
150
151        // Create a reader to access the ASCII file.
152        try (LineNumberReader reader = new LineNumberReader(new FileReader(inputFile))) {
153            reader.mark(8192);
154
155            // Start by parsing the "format" line (must be 1st line if it exists).
156            String aLine = readLine(reader);
157            parseFormatString(aLine);
158            reader.reset();
159
160            // Read in the networks.
161            PointComponent comp = readNetworks(reader);
162
163            // Add the component to the vehicle.
164            if (!comp.isEmpty())
165                vehicle.add(comp);
166
167        } finally {
168            //  Restore the default locale.
169            Locale.setDefault(defLocale);
170        }
171
172        return vehicle;
173    }
174
175    /**
176     * This method always returns <code>false</code> as GGP files do not encode the units
177     * that are being used. You must call <code>setFileUnits</code> to set the units being
178     * used before reading from a file of this format.
179     *
180     * @return true if this reader (and its format) is unit aware.
181     * @see #setFileUnits(javax.measure.unit.Unit) 
182     */
183    @Override
184    public boolean isUnitAware() {
185        return false;
186    }
187
188    /**
189     * Parse the format line of the GGP file. The format line is always the 1st line and
190     * must contain a FORTRAN style number format declaration such as (3G15.7) or
191     * (F5.0,7F13.4). All that this reader cares about is how many columns there are and
192     * how many characters are contained in each column. This method sets a class variable
193     * "formatLst" such that each entry corresponds to a column and each entry records how
194     * many characters are in each column.
195     *
196     * @param formatStr A string containing the format statement.
197     * @throws IOException if unable to parse the format line.
198     */
199    private void parseFormatString(String formatStr) throws IOException {
200        formatLst.clear();
201        formatStr = formatStr.trim();
202
203        if (formatStr.startsWith("(")) {
204            try {
205
206                // Search for commas that separate different formats
207                int start = 1;
208                int end = formatStr.indexOf(",", start);
209                while (end != -1) {
210                    String subStr = formatStr.substring(start, end);
211                    parseSingleFormat(subStr);
212                    start = end + 1;
213                    end = formatStr.indexOf(",", start);
214                }
215                parseSingleFormat(formatStr.substring(start, formatStr.length() - 1));
216
217            } catch (NumberFormatException e) {
218                throw new IOException(RESOURCES.getString("ggpformatParseErr"));
219            }
220
221        } else {
222
223            // Create the default format list if one isn't provided.
224            for (int i = 0; i < 3; ++i) {
225                formatLst.add(15);
226            }
227        }
228    }
229
230    /**
231     * Parse a single FORTRAN style number format declaration such as "3G15.7" or "F5.0"
232     * or "7F13.4". All that this reader cares about is how many columns there are and how
233     * many characters are contained in each column. This method sets a class variable
234     * "formatLst" such that each entry corresponds to a column and each entry records how
235     * many characters are in each column.
236     *
237     * @param formatStr A string containing the format statement.
238     * @throws NumberFormatException if unable to parse the format declaration.
239     */
240    private void parseSingleFormat(String formatStr) throws NumberFormatException {
241        formatStr = formatStr.trim();
242
243        // Convert string to a character array.
244        char[] array = formatStr.toCharArray();
245        
246        //  Check for a line feed.
247        if (array[0] == '/') {
248            formatLst.add(-1);
249            return;
250        }
251
252        // Extract the number of columns using this format.
253        int pos1 = 0;
254        int pos2 = 0;
255        while (Character.isDigit(array[pos2])) {
256            ++pos2;
257        }
258
259        int nCol;
260        if (pos2 == pos1)
261            nCol = 1;
262        else
263            nCol = Integer.parseInt(formatStr.substring(pos1, pos2));
264
265        // Skip the format type (F,G, or E).
266        pos1 = pos2 + 1;
267
268        // Extract the number of characters per column.
269        pos2 = pos1;
270        while (Character.isDigit(array[pos2])) {
271            ++pos2;
272        }
273
274        if (pos2 < pos1 + 1)
275            throw new NumberFormatException();
276
277        Integer nChar = Integer.valueOf(formatStr.substring(pos1, pos2));
278
279        // Create list of formats, one for each column.
280        for (int i = 0; i < nCol; ++i) {
281            formatLst.add(nChar);
282        }
283    }
284
285    /**
286     * Read in all the networks in this geometry file.
287     *
288     */
289    private PointComponent readNetworks(LineNumberReader reader) throws IOException {
290
291        // Create a component object to contain the networks read in.
292        PointComponent comp = PointComponent.newInstance();
293
294        // Read until we get something that is not a comment.
295        String aLine = readSkippingComments(reader);
296
297        // Read in the network ID and optional name.
298        String netID = parseNetID(aLine);
299        reader.mark(8192);
300
301        // Parse the parameter name line.
302        FastTable<Text> paramNames = parseParamNames(reader);
303        reader.reset();
304
305        // Determine which columns contain the X,Y,Z parameters.
306        int[] columns = new int[3];
307        columns[0] = findParam(paramNames, "X");
308        columns[1] = findParam(paramNames, "Y");
309        columns[2] = findParam(paramNames, "Z");
310        if (columns[0] < 0 || columns[1] < 0 || columns[2] < 0) {
311            throw new IOException(
312                    MessageFormat.format(RESOURCES.getString("ggpMissingParameter"),
313                            reader.getLineNumber()));
314        }
315
316        // Keep reading until we reach the end of the file.
317        while (nonNull(netID)) {
318
319            // Parse the optional network name.
320            String netName = parseOptionalNetName(aLine);
321
322            //  Skip the header line.
323            parseParamNames(reader);
324            
325            // Read in a single list of data points.
326            PointString dataStr = readString(reader, columns);
327
328            // Create a list of strings (lists of data points) and add the
329            // 1st one to it.
330            PointArray strList = PointArray.newInstance();
331            strList.setName(netName);
332            strList.add(dataStr);
333
334            // Read in the remaining strings of points in this network/array.
335            String oldNetID = netID;
336            aLine = readSkippingComments(reader);
337            netID = parseNetID(aLine);
338            while (nonNull(netID) && netID.equals(oldNetID)) {
339
340                //  Skip header line.
341                parseParamNames(reader);
342
343                // Read in the string of points.
344                dataStr = readString(reader, columns);
345
346                // Add it to the list of strings of points (array).
347                strList.add(dataStr);
348
349                //  Check for the end of the file.
350                reader.mark(64);
351                aLine = reader.readLine();
352                reader.reset();
353                if (isNull(aLine)) {
354                    //  End-of-File encountered.
355                    netID = null;
356                    break;
357                }
358                aLine = readSkippingComments(reader);
359                netID = parseNetID(aLine);
360            }
361
362            // Add the new network to the component.
363            comp.add(strList);
364
365        }
366
367        return comp;
368    }
369
370    /**
371     * Determines if this line is a comment line.
372     *
373     * @param aLine The line to be tested.
374     * @return true if the line is a comment line, false if it is not. If aLine is null,
375     *         false is returned.
376     */
377    private boolean isComment(String aLine) {
378        boolean retVal = false;
379
380        aLine = aLine.trim();
381        int length = COMMENT_CHARACTERS.length;
382        for (int i = 0; i < length; ++i) {
383            if (aLine.startsWith(COMMENT_CHARACTERS[i]) && !aLine.startsWith("*EOD")
384                    && !aLine.startsWith("*EOF")) {
385                retVal = true;
386                break;
387            }
388        }
389
390        return retVal;
391    }
392
393    /**
394     * Read in lines from the GGP file while skipping comment lines.
395     *
396     * @param reader The reader for the GGP file.
397     * @return The 1st line that is not a comment line or null if the end of file is
398     *         reached.
399     */
400    private String readSkippingComments(LineNumberReader reader) throws IOException {
401        String aLine = readLine(reader);
402        while (isComment(aLine)) {
403            aLine = readLine(reader);
404        }
405        return aLine;
406    }
407
408    /**
409     * Parse the network ID from the string name line. The network ID is a letter/number
410     * combination that uniquely identifies the network that this string of points belongs
411     * to. Immediately following the network ID is the string ID which this program
412     * ignores.
413     *
414     * @param aLine The line containing the net/string ID.
415     * @param The   network ID is returned.
416     */
417    private String parseNetID(String aLine) {
418
419        aLine = aLine.trim();
420        char[] array = aLine.toCharArray();
421        int length = array.length;
422
423        // Find end of letter part of ID.
424        int pos = 0;
425        while (pos < length) {
426            if (Character.isDigit(array[pos]))
427                break;
428            ++pos;
429        }
430
431        // Find end of number part of ID.
432        ++pos;
433        while (pos < length) {
434            if (!Character.isDigit(array[pos])) {
435                ++pos;
436                break;
437            }
438            ++pos;
439        }
440
441        // Extract the ID string.
442        return aLine.substring(0, pos - 1);
443    }
444
445    /**
446     * Parse the list of parameters.
447     *
448     * @param reader The reader for our GGP file.
449     * @return A list of strings where each element is a parameter in this GGP file.
450     */
451    private FastTable<Text> parseParamNames(LineNumberReader reader) throws IOException {
452
453        String aLine = readSkippingComments(reader);
454
455        int nParams = formatLst.size();
456        FastTable<Text> array = FastTable.newInstance();
457        TextTokenizer tokenizer = TextTokenizer.valueOf(aLine, " ");
458
459        // Extract one token (param name) for each element identified
460        // in the format line.
461        for (int i = 0; i < nParams; ++i) {
462            if (formatLst.get(i) == -1) {
463                //  Read the next line.
464                aLine = readSkippingComments(reader);
465                tokenizer.setText(aLine);
466                continue;
467            }
468
469            Text token = tokenizer.nextToken();
470            array.add(token);
471        }
472
473        return array;
474    }
475
476    /**
477     * Identify which parameter in the parameter string matches the target parameter.
478     *
479     * @param nameList The list of parameter names.
480     * @param target   The target parameter that we are looking for.
481     * @return The data column corresponding the the target parameter. If the parameter
482     *         could not be found, -1 is returned.
483     */
484    private int findParam(List<Text> nameList, CharSequence target) {
485        Text targetText = Text.valueOf(target);
486
487        int length = nameList.size();
488        int pos = 0;
489        while (pos < length) {
490            if (nameList.get(pos).equals(targetText))
491                break;
492            ++pos;
493        }
494
495        if (pos == length)
496            pos = -1;
497
498        return pos;
499    }
500
501    /**
502     * Parse the optional network name from the string name line. The network name is
503     * different from the network ID. The optional network name comes after the network ID
504     * and string ID and is separated from them by at least one space.
505     *
506     * @param aLine The line containing the string & network name.
507     * @return A string containing the optional network name or null if that name is not
508     *         found.
509     */
510    private String parseOptionalNetName(String aLine) {
511        aLine = aLine.trim();
512        int pos = aLine.lastIndexOf(" ");
513
514        String retVal = null;
515        if (pos > 0)
516            retVal = aLine.substring(pos);
517
518        // Extract the ID string.
519        return retVal;
520    }
521
522    /**
523     * Read a string of points from the GGP file. A string of points consists of the
524     * parameters read in, 1 point per line, until the *EOD record is encountered. This
525     * version of this method does not require the string to be of any length. Data is
526     * read in until the *EOD card is reached.
527     *
528     * @param reader  The reader for the GGP file.
529     * @param columns An array containing the indices of the data columns (parameters) to
530     *                be read in.
531     * @return A list of PointString objects.
532     *
533     */
534    private PointString readString(LineNumberReader reader, int[] columns)
535            throws IOException {
536
537        //  A string of 3D points.
538        ArrayList<Point> string = new ArrayList();
539
540        StackContext.enter();
541        try {
542            FastTable<Text> coordinates = FastTable.newInstance();
543            int nParams = formatLst.size();
544
545            // Read a line of data.
546            String aLine = readSkippingComments(reader);
547
548            // If *EOD, then the string is finished.
549            String trimLine = aLine.trim();
550            while (!trimLine.equals("*EOD") && !trimLine.equals("*EOF")) {
551                TextTokenizer tokenizer = TextTokenizer.valueOf(trimLine, " ");
552
553                // Loop over the parameters in the file.
554                for (int pos=0; pos < nParams; ++pos) {
555                    if (formatLst.get(pos) == -1) {
556                        //  Read in the next line.
557                        aLine = readSkippingComments(reader);
558                        trimLine = aLine.trim();
559                        tokenizer = TextTokenizer.valueOf(trimLine, " ");
560                        continue;
561                    }
562                    Text token = tokenizer.nextToken();
563
564                    // Check if this parameter is one of the columns requested.
565                    for (int i = 0; i < columns.length; ++i) {
566                        if (columns[i] == pos) {
567
568                            // This one is requested, so store it.
569                            coordinates.add(token);
570                            break;
571                        }
572                    }
573                }
574
575                // Parse the stored coordinates.
576                double xValue = TypeFormat.parseDouble(coordinates.get(0));
577                double yValue = TypeFormat.parseDouble(coordinates.get(1));
578                double zValue = TypeFormat.parseDouble(coordinates.get(2));
579                coordinates.clear();
580
581                //  Create and store a point.
582                Point point = Point.valueOf(xValue, yValue, zValue, getFileUnits());
583                string.add(StackContext.outerCopy(point));
584
585                // Read the next line.
586                aLine = readSkippingComments(reader);
587                trimLine = aLine.trim();
588            }
589
590        } catch (NumberFormatException e) {
591            e.printStackTrace();
592            throw new IOException(
593                    MessageFormat.format(RESOURCES.getString("parseErrMsg"),
594                            DESCRIPTION, reader.getLineNumber()));
595
596        } finally {
597            StackContext.exit();
598        }
599
600        //  Convert ArrayList to a PointString
601        PointString pstr = PointString.valueOf(null, string);
602        return pstr;
603    }
604}