001/**
002 * TREETOPSDataReader -- Reads data from NASA TREETOPS/CLVTOPS simulation data files.
003 *
004 * Copyright (C) 2017-2024, by 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 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 Library 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 jahuwaldt.js.datareader;
019
020import jahuwaldt.io.FileUtils;
021import jahuwaldt.js.unit.EditUnitSetDialog;
022import jahuwaldt.js.unit.UnitSet;
023import jahuwaldt.swing.AppUtilities;
024import java.awt.Frame;
025import java.io.*;
026import java.lang.reflect.InvocationTargetException;
027import java.lang.reflect.Method;
028import java.text.MessageFormat;
029import java.util.*;
030import javax.measure.quantity.AngularVelocity;
031import javax.measure.quantity.Dimensionless;
032import javax.measure.unit.SI;
033import javax.measure.unit.Unit;
034
035/**
036 * This class reads data from a NASA TREETOPS/CLVTOPS *.dat and matching *.crf file and
037 * returns a list of DataSet data structures containing the simulation data.
038 *
039 * <p> Modified by: Joseph A. Huwaldt </p>
040 *
041 * @author Joseph A. Huwaldt, Date: March 17, 2017
042 * @version March 27, 2024
043 */
044public class TREETOPSDataReader implements DataReader {
045
046    //  A brief description of the data read by this reader.
047    private static final String DESCRIPTION = RESOURCES.getString("treetopsDesc");
048
049    //  The preferred file extension for files of this reader's type.
050    public static final String EXTENSION = RESOURCES.getString("treetopsDefExt");
051
052    //  Define constants for each element type.
053    private static final String BODY = "Body";
054    private static final String HINGE = "Hinge";
055    private static final String SENSOR = "Sensor";
056    private static final String FUNCTION = "Function";
057    private static final String ACTUATOR = "Actuator";
058    private static final String DEVICE = "Device";
059    private static final String CONTROLLER_OUT = "Controller Output";
060    private static final String CONTROLLER_IN = "Controller Input";
061    
062    //  Define dictionaries containing the output variable names, descriptions and units.
063    private static final HashMap<String,String[]> OUT_VARS = new HashMap();
064    private static final HashMap<String,String[]> OUT_DESC = new HashMap();
065    private static final HashMap<String,String[]> OUT_UNITS = new HashMap();
066    
067    //  Define a dictionary mapping the element type to it's abbreviation.
068    private static final HashMap<String,String> IDABR = new HashMap();
069
070    static {
071        //  Define the body output variable names, descriptions and units.
072        String[] bodyOutputVars = RESOURCES.getString("ttBodyOutputVars").split(",");
073        String[] bodyOutputDesc = RESOURCES.getString("ttBodyOutputDesc").split(",");
074        String[] bodyOutputUnits = RESOURCES.getString("ttBodyOutputUnits").split(",");
075        
076        //  Store in the appropriate dictionaries.
077        OUT_VARS.put(BODY, bodyOutputVars);
078        OUT_DESC.put(BODY, bodyOutputDesc);
079        OUT_UNITS.put(BODY, bodyOutputUnits);
080
081        //  Define the hinge output variable names, descriptions and units.
082        String[] hingeOutputVars = RESOURCES.getString("ttHingeOutputVars").split(",");
083        String[] hingeOutputDesc = RESOURCES.getString("ttHingeOutputDesc").split(",");
084        String[] hingeOutputUnits = RESOURCES.getString("ttHingeOutputUnits").split(",");
085
086        //  Store in the appropriate dictionaries.
087        OUT_VARS.put(HINGE, hingeOutputVars);
088        OUT_DESC.put(HINGE, hingeOutputDesc);
089        OUT_UNITS.put(HINGE, hingeOutputUnits);
090
091        //  Define the element type ID abbreviations.
092        IDABR.put(BODY, "BO");
093        IDABR.put(HINGE, "HI");
094        IDABR.put(SENSOR, "SE");
095        IDABR.put(FUNCTION, "FU");
096        IDABR.put(ACTUATOR, "AC");
097        IDABR.put(DEVICE, "DEMAG");
098        IDABR.put(CONTROLLER_OUT, "RC");
099        IDABR.put(CONTROLLER_IN, "UC");
100    }
101
102    //  Since the TREETOPS data is stored in a space delimited array, we can just use
103    //  The tab data reader to process the *.dat file.
104    private final DataReader datReader = new TabDataReader();
105
106    /**
107     * Returns a string representation of the object. This will return a brief description
108     * of the format read by this reader.
109     */
110    @Override
111    public String toString() {
112        return DESCRIPTION;
113    }
114
115    /**
116     * Returns the preferred file extension (not including the ".") for files of this
117     * DataReader's type.
118     *
119     * @return The preferred file extension for this file's type.
120     */
121    @Override
122    public String getExtension() {
123        return EXTENSION;
124    }
125
126    /**
127     * Compares this object with the specified object for order based on the
128     * <code>toString().compareTo(o.toString())</code> method. Returns a negative integer,
129     * zero, or a positive integer as this object is less than, equal to, or greater than
130     * the specified object.
131     */
132    @Override
133    public int compareTo(DataReader o) {
134        return this.toString().compareTo(o.toString());
135    }
136
137    /**
138     * Method that determines if this reader can read data from the specified input
139     * stream.
140     *
141     * @param pathName The path to the file to be read.
142     * @param input    An input stream containing the data to be read. Any methods that
143     *                 read from this stream must first set a mark and then reset back to
144     *                 that mark before the method returns (even if it returns with an
145     *                 exception).
146     * @return DataReader.NO if the file is not recognized at all or DataReader.MAYBE if
147     * the file has an appropriate extension.
148     * @throws java.io.IOException If the input stream could not be read from.
149     */
150    @Override
151    public int canReadData(String pathName, BufferedInputStream input) throws IOException {
152        int answer = DataReader.NO;
153
154        pathName = pathName.trim();
155        String prefix = FileUtils.getFileNameWithoutExtension(pathName);
156        pathName = pathName.toLowerCase();
157        
158        File crfFile = new File(prefix + ".crf");
159        if (crfFile.exists()) {
160            if (pathName.endsWith(".dat")) {
161                answer = DataReader.YES;
162            }
163        }
164        
165        return answer;
166    }
167
168    /**
169     * Returns false. This class can not write data to a CLVTOPS formatted file stream.
170     *
171     * @return Always returns false.
172     */
173    @Override
174    public boolean canWriteData() {
175        return false;
176    }
177
178    /**
179     * Method that reads in CLVTOPS formatted simulation data from the specified
180     * input stream and returns that data as a list of {@link DataSet} objects.
181     *
182     * @param pathName The path to the file to be read.  In addition to the *.dat file that
183     * this should point to, there must be a *.crf file with the same prefix in the same
184     * directory that will also be read in.
185     * @param input    An input stream containing the space delimited array data file (*.dat).
186     * @return A list of DataSet objects that contains the data read in from the
187     * specified stream (will contain a single data set which will contain a single case
188     * with ArrayParam objects for each column).
189     * @throws IOException If there is a problem reading the specified stream.
190     */
191    @Override
192    public List<DataSet> read(String pathName, InputStream input) throws IOException {
193        
194        //  Define the units for the data.
195        UnitSet nativeUnits = askForUnitSet();
196        if (nativeUnits == null)
197            return null;
198        
199        List<DataSet> data = read(pathName, input, nativeUnits);
200        
201        return data;
202    }
203
204   /**
205     * Method that reads in CLVTOPS formatted simulation data from the specified
206     * input stream and returns that data as a list of {@link DataSet} objects.
207     *
208     * @param pathName The path to the file to be read.  In addition to the *.dat file that
209     * this should point to, there must be a *.crf file with the same prefix in the same
210     * directory that will also be read in.
211     * @param input    An input stream containing the space delimited array data file (*.dat).
212     * @param nativeUnits The units for the data in the *.dat file.
213     * @return A list of DataSet objects that contains the data read in from the
214     * specified stream (will contain a single data set which will contain a single case
215     * with ArrayParam objects for each column).
216     * @throws IOException If there is a problem reading the specified stream.
217     */
218    public List<DataSet> read(String pathName, InputStream input, UnitSet nativeUnits) throws IOException {
219        Objects.requireNonNull(nativeUnits);
220        
221        //  Start by reading in the tabular data from the input stream pointing to the
222        //  *.dat file.
223        List<DataSet> data = datReader.read(pathName, input);
224        
225        //  Now parse the CRF file to define the names for each column in the table.
226        File crfFile = new File(FileUtils.getFileNameWithoutExtension(pathName) + ".crf");
227        read(crfFile, data.get(0).get(0), nativeUnits);
228        
229        return data;
230    }
231
232    /**
233     * Method that asks the user to select the units for the data being read in from the file.
234     * 
235     * @return The unit set for the data in the file.
236     */
237    private static UnitSet askForUnitSet() {
238        
239        //  Define the default unit set for the locale.
240        UnitSet nativeUnits = new UnitSet(UnitSet.UnitSystem.SI_MKS);
241        if (Locale.getDefault().equals(Locale.US))
242            nativeUnits = new UnitSet(UnitSet.UnitSystem.US_FSS);
243        
244        //  Ask the user to select the units for the data in the file.
245        EditUnitSetDialog dialog = new EditUnitSetDialog(null, RESOURCES.getString("editUnitSetTitle"),
246                RESOURCES.getString("editUnitSetMsg"), nativeUnits);
247        dialog.setLocation(AppUtilities.dialogPosition(dialog));
248        dialog.setVisible(true);
249        nativeUnits = dialog.getUnits();
250        dialog.dispose();
251        
252        return nativeUnits;
253    }
254    
255    /**
256     * Read in the CLVTOPS CRF file and parse its contents.  The CRF file defines what
257     * each column in the output data file is.
258     * 
259     * @param crf A reference to the *.crf file for the data file (*.dat) being read in.
260     * @param rawData   The raw data read in from the *.dat file.
261     * @param nativeUnits The units for the data in the *.dat file.
262     * @return A reference to the input rawData after the names of each array have been
263     * changed to reflect the data in the CRF file.
264     * @throws IOException 
265     */
266    private static DataCase read(File crf, DataCase rawData, UnitSet nativeUnits) throws IOException {
267        //  Open the CRF file and get a line number reader to it.
268        try (FileInputStream is = new FileInputStream(crf)) {
269            LineNumberReader reader = new LineNumberReader(new InputStreamReader(is));
270            
271            //  Parse the meta-data from the CRF file.
272            parseCRFStream(reader, rawData, nativeUnits);
273        }
274        
275        return rawData;
276    }
277
278    /**
279     * Method that actually parses the CRF file.
280     *
281     * @param in          A reader that reads in the characters from the CRF file.
282     * @param data        A list of all the ArrayParams as read from the tabular data file
283     *                    (one ArrayParam per column in the CLVTOPS array). These
284     *                    parameters will be modified by this method by adding names,
285     *                    units, and reference data.
286     * @param nativeUnits The system of units used by the data.
287     */
288    private static void parseCRFStream(LineNumberReader in, DataCase data, UnitSet nativeUnits) throws IOException {
289        
290        try {
291            //  Skip down to the start of the array data.
292            String aLine = in.readLine();
293            while ((aLine != null) && (!aLine.startsWith("    1     TIME"))) {
294                aLine = in.readLine();
295            }
296            if (aLine == null)
297                throw new IOException("Could not find the 1st data line in CRF file.");
298            
299            //  Time is always the 1st parameter.
300            ArrayParam arr = (ArrayParam)data.get(0);
301            arr.setName("TIME");
302            arr.setUserObject(RESOURCES.getString("ttSimTimeDesc"));
303            arr = arr.changeTo(nativeUnits.time());
304            data.set(0, arr);
305 
306            //  Define a set of dictionaries for each type of data that could be in the DAT file.
307            HashMap<String,List<String>> sensorData = new HashMap();
308            HashMap<String,List<String>> functionData = new HashMap();
309            HashMap<String,List<String>> actuatorData = new HashMap();
310            HashMap<String,List<String>> deviceData = new HashMap();
311            HashMap<String,List<String>> controllerOutData = new HashMap();
312            HashMap<String,List<String>> controllerInData = new HashMap();
313            HashMap<String,List<String>> thetaData = new HashMap();
314            HashMap<String,List<String>> thDotData = new HashMap();
315            HashMap<String,List<String>> htransData = new HashMap();
316            HashMap<String,List<String>> hvelData = new HashMap();
317            HashMap<String,List<String>> hcontFData = new HashMap();
318            HashMap<String,List<String>> hcontMData = new HashMap();
319            HashMap<String,List<String>> hconstFData = new HashMap();
320            HashMap<String,List<String>> hconstMData = new HashMap();
321            HashMap<String,List<String>> wjiData = new HashMap();
322            HashMap<String,List<String>> qbodyData = new HashMap();
323            HashMap<String,List<String>> rvectData = new HashMap();
324            HashMap<String,List<String>> massData = new HashMap();
325            HashMap<String,List<String>> cmData = new HashMap();
326            HashMap<String,List<String>> moipoiData = new HashMap();
327            HashMap<String,List<String>> etaData = new HashMap();
328            HashMap<String,List<String>> etaDotData = new HashMap();
329            HashMap<String,List<String>> udjbjData = new HashMap();
330            HashMap<String,List<String>> udotjData = new HashMap();
331            HashMap<String,List<String>> psiEtaData = new HashMap();
332            HashMap<String,List<String>> psiEtaDotData = new HashMap();
333            
334            //  Loop over all the columns defined in the CRF file.
335            int col = 1;
336            aLine = in.readLine();
337            while (aLine != null && !"".equals(aLine)) {
338                //  Split the line using repeated spaces as a delimiter.
339                String[] parts = aLine.trim().split("\\s+");
340                
341                //  Parse out the column number.
342                try {
343                    String colStr = parts[0];
344                    col = Integer.parseInt(colStr) - 1;
345                } catch (NumberFormatException ex) {
346                    throw new NoColumnNumberException();
347                }
348                
349                //  Get the output type string.
350                String type = parts[1];
351                
352                //  Get the array for the column in the DAT file we are working with.
353                arr = (ArrayParam)data.get(col);
354                switch (type) {
355                    case "RSG":
356                        //  We have a function generator output.
357                        arr = parseCRFElementOutput(parts, arr, 0, functionData, FUNCTION, nativeUnits);
358                        break;
359                    
360                    case "RC":
361                        //  We have a controller output.
362                        arr = parseCRFElementOutput(parts, arr, 0, controllerOutData, CONTROLLER_OUT, nativeUnits);
363                        break;
364                        
365                    case "UC":
366                        //  We have a controller input column.
367                        arr = parseCRFElementOutput(parts, arr, 0, controllerInData, CONTROLLER_IN, nativeUnits);
368                        break;
369                        
370                    case "RP":
371                        //  We have a sensor output.
372                        arr = parseCRFElementOutput(parts, arr, 0, sensorData, SENSOR, nativeUnits);
373                        break;
374                    
375                    case "UP":
376                        //  We have an actuator output.
377                        arr = parseCRFDestInput(parts, arr, actuatorData, ACTUATOR);
378                        break;
379                        
380                    case "DEMAG":
381                        //  We have device output.
382                        arr = parseCRFElementOutput(parts, arr, 0, deviceData, DEVICE, nativeUnits);
383                        break;
384                    
385                    case "THETA":
386                        //  We have hinge angle output.
387                        arr = parseCRFElementOutput(parts, arr, 0, thetaData, HINGE, nativeUnits);
388                        break;
389                        
390                    case "THDOT":
391                        //  We have hinge angular rate output.
392                        arr = parseCRFElementOutput(parts, arr, 3, thDotData, HINGE, nativeUnits);
393                        break;
394                        
395                    case "WJI":
396                        //  We have angular body rate outputs.
397                        arr = parseCRFElementOutput(parts, arr, 0, wjiData, BODY, nativeUnits);
398                        break;
399                        
400                    case "YJI":
401                        //  We have hinge translation outputs.
402                        arr = parseCRFElementOutput(parts, arr, 6, htransData, HINGE, nativeUnits);
403                        break;
404                        
405                    case "YDOTJI":
406                    case "YDJI":
407                        //  We have hinge velocity outputs.
408                        arr = parseCRFElementOutput(parts, arr, 9, hvelData, HINGE, nativeUnits);
409                        break;
410                        
411                    case "ETA":
412                        //  We have generalized modal coordinate outputs.
413                        arr = parseCRFElementOutput(parts, arr, 3, etaData, "ETA", nativeUnits);
414                        break;
415                        
416                    case "ETADOT":
417                        //  We have generalized modal velocity outputs.
418                        arr = parseCRFElementOutput(parts, arr, 3, etaDotData, "ETADOT", nativeUnits);
419                        break;
420                        
421                    case "UDJBJ":
422                        //  We have flex body deformation outputs.
423                        arr = parseCRFElementOutput(parts, arr, 3, udjbjData, "UDJBJ", nativeUnits);
424                        break;
425                        
426                    case "UDOTJ":
427                        //  We have flex body deformation rate outputs.
428                        arr = parseCRFElementOutput(parts, arr, 3, udotjData, "UDOTJ", nativeUnits);
429                        break;
430                        
431                    case "PSIETA":
432                        //  We have flex body rotational deformation outputs.
433                        arr = parseCRFElementOutput(parts, arr, 3, psiEtaData, "PSIETA", nativeUnits);
434                        break;
435                        
436                    case "PSETAD":
437                        //  We have flex body rotation rate outputs.
438                        arr = parseCRFElementOutput(parts, arr, 3, psiEtaDotData, "PSETAD", nativeUnits);
439                        break;
440                        
441                    case "QBODY":
442                        //  We have a TREETOPS quaternion output.
443                        arr = parseCRFElementOutput(parts, arr, 3, qbodyData, BODY, nativeUnits);
444                        break;
445                        
446                    case "Q_BODY":
447                        //  We have a CLVTOPS quaternion output.
448                        arr = parseCRFElementOutput(parts, arr, 7, qbodyData, BODY, nativeUnits);
449                        break;
450                        
451                    case "RHJ":
452                        //  We have hinge location vector output.
453                        arr = parseCRFElementOutput(parts, arr, 11, rvectData, BODY, nativeUnits);
454                        break;
455                        
456                    case "MASS":
457                        //  We have body mass output.
458                        arr = parseCRFElementOutput(parts, arr, 14, massData, BODY, nativeUnits);
459                        break;
460                       
461                    case "CM":
462                        //  We have body center of mass outputs.
463                        arr = parseCRFElementOutput(parts, arr, 15, cmData, BODY, nativeUnits);
464                        break;
465                       
466                    case "MOIPOI":
467                        //  We have body inertia outputs.
468                        arr = parseCRFElementOutput(parts, arr, 18, moipoiData, BODY, nativeUnits);
469                        break;
470                        
471                    case "FCONI":
472                        //  We have contact force outputs.
473                        arr = parseCRFElementOutput(parts, arr, 12, hcontFData, HINGE, nativeUnits);
474                        break;
475                        
476                    case "MCONI":
477                        //  We have contact moment outputs.
478                        arr = parseCRFElementOutput(parts, arr, 15, hcontMData, HINGE, nativeUnits);
479                        break;
480                        
481                    case "FCJI":
482                        //  We have constraint force outputs.
483                        arr = parseCRFElementOutput(parts, arr, 18, hconstFData, HINGE, nativeUnits);
484                        break;
485                        
486                    case "MCHJI":
487                        //  We have constraint moment outputs.
488                        arr = parseCRFElementOutput(parts, arr, 21, hconstMData, HINGE, nativeUnits);
489                        break;
490                        
491                    case "NSTEPS":
492                        //  We have the number of steps completed column.
493                        arr.setName("NSteps");
494                        arr.setUserObject(RESOURCES.getString("ttNStepsDesc"));
495                        break;
496                        
497                    case "UNSUN":
498                        //  We have solar direction unit vector components.
499                        parseCRFSunUnitVector(parts, arr);
500                        break;
501                        
502                    default:
503                        //  We have an unknown type.
504                        aLine = aLine.trim();
505                        aLine = aLine.substring(aLine.indexOf(" ")).trim();
506                        arr.setUserObject(aLine);
507                        break;
508                }
509                
510                //  Replace the array in the DataSet with the one with updated units.
511                data.set(col, arr);
512                
513                //  Read in the next line.
514                aLine = in.readLine();
515            }
516            
517        } catch (Throwable ex) {
518            throw new IOException("Error parsing CRF file on line #" + in.getLineNumber() + ".", ex);
519        }
520    }
521 
522    /**
523     * Parse model element output line from the CRF file to get the element ID number and
524     * output index.
525     *
526     * @param tokens       An array of tokens parsed from a line of CRF file input.
527     * @param arr          The array of data from the data file for this line of input.
528     * @param idxOffset    The offset into the list of outputs from the element for this
529     *                     "type" of output.
530     * @param existingData A dictionary containing data on the elements of a particular type
531     *                     that have already been read in.
532     * @param type         A constant representing the type of the data.
533     * @param nativeUnits  The unit set representing the native units for this TreeTops
534     *                     model.
535     */
536    private static ArrayParam parseCRFElementOutput(String[] tokens, ArrayParam arr, int idxOffset,
537            HashMap<String, List<String>> existingData, String type, UnitSet nativeUnits) throws IOException, NumberFormatException {
538        //  Get the output index number or element type of this type of item.
539        String outIdxStr = tokens[2];
540        
541        //  Get the element ID.
542        String idStr = "?";
543        if (tokens.length > 3) {
544            idStr = tokens[3];
545            
546            //  Make sure it is actually a number (an exception is thrown if it is not).
547            Integer.parseInt(idStr);
548        }
549        
550        //  Determine the index of the output in this column.
551        int outputIdx;
552        if (!"?".equals(idStr)) {
553            List<String> existing = existingData.get(idStr);
554            if (existing == null) {
555                existing = new ArrayList();
556                existingData.put(idStr, existing);
557            }
558            existing.add(idStr);
559            outputIdx = existing.size() - 1 + idxOffset;    //  Make the index 0 offset.
560        } else
561            outputIdx = Integer.parseInt(outIdxStr) - 1;    //  Make index 0 offset.
562        
563        String desc, var;
564        Unit unit = Dimensionless.UNIT;
565        switch (type) {
566            case BODY:
567            case HINGE: {
568                //  Get the output description for this output.
569                desc = OUT_DESC.get(type)[outputIdx];
570                //  Get the output variable name.
571                var = OUT_VARS.get(type)[outputIdx];
572                //  Get the output unit type.
573                String unitType = OUT_UNITS.get(type)[outputIdx];
574                //  Get the output units from the unit type and this model's native units.
575                unit = getUnitFromType(nativeUnits, unitType);
576                break;
577            }
578            case "ETA": {
579                //  Skip the MODE column (idx = 4)
580                //  Get the MODE ID #.
581                String mIDStr = tokens[5].trim();
582                desc = MessageFormat.format(RESOURCES.getString("ttFlexBodyModeIDDesc"),mIDStr);
583                var = "ETA_" + mIDStr;
584                type = BODY;
585                break;
586            }
587            case "UDJBJ": {
588                //  Skip the NODE column (idx = 4).
589                //  Get the NODE ID #.
590                String nIDStr = tokens[5].trim();
591                //  Skip the AXIS column (idx = 6).
592                //  Get the Axis ID#.
593                String axisIDStr = tokens[7].trim();
594                int axis = Integer.parseInt(axisIDStr);
595                desc = MessageFormat.format(RESOURCES.getString("ttFlexBodyDispDesc"),nIDStr);
596                var = "n" + nIDStr + ":UDJBJ_" + (axis == 1 ? "X" : (axis == 2 ? "Y" : "Z"));
597                unit = nativeUnits.length();
598                type = BODY;
599                break;
600            }
601            case "UDOTJ": {
602                //  Skip the NODE column (idx = 4).
603                //  Get the NODE ID #.
604                String nIDStr = tokens[5].trim();
605                //  Skip the AXIS column (idx = 6).
606                //  Get the AXIS ID #.
607                String axisIDStr = tokens[7].trim();
608                int axis = Integer.parseInt(axisIDStr);
609                desc = MessageFormat.format(RESOURCES.getString("ttFlexBodyDispRateDesc"),nIDStr);
610                var = "n" + nIDStr + ":UDOTJ_" + (axis == 1 ? "X" : (axis == 2 ? "Y" : "Z"));
611                unit = nativeUnits.velocity();
612                type = BODY;
613                break;
614            }
615            case "PSIETA": {
616                //  Skip the NODE column (idx = 4).
617                //  Get the NODE ID #.
618                String nIDStr = tokens[5].trim();
619                //  Skip the AXIS column (idx = 6).
620                //  Get the AXIS ID #.
621                String axisIDStr = tokens[7].trim();
622                int axis = Integer.parseInt(axisIDStr);
623                desc = MessageFormat.format(RESOURCES.getString("ttFlexBodyRotDesc"),nIDStr);
624                var = "n" + nIDStr + ":PSIETA_" + (axis == 1 ? "X" : (axis == 2 ? "Y" : "Z"));
625                unit = SI.RADIAN;
626                type = BODY;
627                break;
628            }
629            case "PSETAD": {
630                //  Skip the NODE column (idx = 4).
631                //  Get the NODE ID #.
632                String nIDStr = tokens[5].trim();
633                //  Skip the AXIS column (idx = 6).
634                //  Get the AXIS ID #.
635                String axisIDStr = tokens[7].trim();
636                int axis = Integer.parseInt(axisIDStr);
637                desc = MessageFormat.format(RESOURCES.getString("ttFlexBodyRotRateDesc"),nIDStr);
638                var = "n" + nIDStr + ":PSETAD_" + (axis == 1 ? "X" : (axis == 2 ? "Y" : "Z"));
639                unit = AngularVelocity.UNIT;
640                type = BODY;
641                break;
642            }
643            case CONTROLLER_IN:
644                desc = MessageFormat.format(RESOURCES.getString("ttControllerInputDesc"), outputIdx + 1);
645                var = "Input" + (outputIdx + 1);
646                break;
647            case CONTROLLER_OUT:
648                desc = MessageFormat.format(RESOURCES.getString("ttControllerOutputDesc"), outputIdx + 1);
649                var = "Output" + (outputIdx + 1);
650                break;
651            default:
652                desc = MessageFormat.format(RESOURCES.getString("ttOutputDesc"), outputIdx + 1);
653                var = "Output" + (outputIdx + 1);
654                break;
655        }
656        
657        //  Construct a name from the information about this column.
658        StringBuilder name = new StringBuilder();
659        name.append(IDABR.get(type));
660        name.append(":");
661        name.append(idStr);
662        name.append(":");
663        name.append(var);
664        
665        //  Set the name, units, and reference data.
666        arr.setName(name.toString());
667        arr = arr.changeTo(unit);
668        arr.setUserObject(desc);
669        
670        return arr;
671    }
672
673    /**
674     * Parse an interconnect destination input line from the CRF file to get the element
675     * ID number and output index.
676     *
677     * @param tokens       An array of tokens parsed from a line of CRF file input.
678     * @param arr          The array of data from the DAT file for this line of input.
679     * @param existingData A dictionary containing data on the destinations that have
680     *                     already been read in.
681     * @param type         A constant representing the type of the data.
682     */
683    private static ArrayParam parseCRFDestInput(String[] tokens, ArrayParam arr, 
684            HashMap<String,List<String>> existingData, String type) {
685
686        //  The output index number or element type.
687        String inputIdxStr = tokens[2].trim();
688
689        //  Get the element ID.
690        String idStr = "?";
691        if (tokens.length > 3) {
692            idStr = tokens[3].trim();
693            
694            //  Make sure it is actually a number (an exception thrown if it is not).
695            Integer.parseInt(idStr);
696        }
697
698        //  Determine the index of the input in this column.
699        int inputIdx;
700        if (!"?".equals(idStr)) {
701            List<String> existing = existingData.get(idStr);
702            if (existing == null) {
703                existing = new ArrayList();
704                existingData.put(idStr, existing);
705            }
706            existing.add(idStr);
707            inputIdx = existing.size() - 1; //  Make index 0 offset.
708
709        } else
710            inputIdx = Integer.parseInt(inputIdxStr) - 1;   //  Make index 0 offset.
711
712        //  Get the description for this input.
713        String desc = MessageFormat.format(RESOURCES.getString("ttInputParDesc"), inputIdx + 1);
714
715        //  Get the input variable name.
716        String var = "Input" + (inputIdx + 1);
717
718        //  Get the input units from the unit type and this model's native units.
719        Unit unit = Dimensionless.UNIT;
720
721        //  Construct a name from the information about this column.
722        StringBuilder name = new StringBuilder();
723        name.append(IDABR.get(type));
724        name.append(":");
725        name.append(idStr);
726        name.append(":");
727        name.append(var);
728
729        //  Set the name, units, and reference data.
730        arr.setName(name.toString());
731        arr = arr.changeTo(unit);
732        arr.setUserObject(desc);
733        
734        return arr;
735    }
736
737    /**
738     * Parse a Sun unit vector line from the CRF file.
739     *
740     * @param tokens An array of tokens parsed from a line of CRF file input.
741     * @param arr    The array of data from the DAT file for this line of input.
742     */
743    private static void parseCRFSunUnitVector(String[] tokens, ArrayParam arr)
744            throws NoSuchElementException, NumberFormatException {
745        //  Skip the "INERTIAL", "COMPONENTS," and "AXIS" tokens (idx = 2, 3 & 4).
746
747        //  Get the axis index number.
748        String axisStr = tokens[5].trim();
749        int axis = Integer.parseInt(axisStr) - 1;       //  Make zero offset index.
750
751        String[] axisOptions = {"USX", "USY", "USZ"};
752
753        //  Construct a list of information about this column.
754        StringBuilder name = new StringBuilder();
755        name.append("UNSUN");
756        name.append(":");
757        name.append(axisOptions[axis]);
758
759        //  Set the name and reference data.
760        arr.setName(name.toString());
761        arr.setUserObject(RESOURCES.getString("ttSunUVDesc"));
762    }
763
764    /**
765     * Method that returns the unit from the provided unit set of the specified type.
766     */
767    private static Unit getUnitFromType(UnitSet unitSet, String type) throws IOException {
768        //  Deal with some special cases first.
769        if (null == type)
770            return Dimensionless.UNIT;
771        else switch (type) {
772            case "nd":
773                return Dimensionless.UNIT;
774            case "angularVelocityR":
775                return AngularVelocity.UNIT;
776            case "angleR":
777                return SI.RADIAN;
778            default:
779                break;
780        }
781        
782        //  Use reflection to get the unit type from the unit set.
783        //  "type" is the name of the method in unitSet to call.
784        Unit unit = Dimensionless.UNIT;
785        try {
786            //  Get the appropriate method from unitSet.
787            Method method = unitSet.getClass().getMethod( type, (Class[])null );
788            
789            //  Get the unit of the specified type.
790            unit = (Unit)method.invoke(unitSet, (Object[])null);
791            
792        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
793            throw new IOException(e);
794        }
795        
796        return unit;
797    }
798    
799    /**
800     * Sets the default set name to use.
801     * 
802     * @param name The name to use as the default set name.
803     */
804    @Override
805    public void setDefaultSetName(CharSequence name) {
806        datReader.setDefaultSetName(name);
807    }
808
809    /**
810     * This class can not write to the CLVTOPS format file(s), so this method always throws
811     * an exception.
812     * 
813     * @param parent the parent frame
814     * @param data the data to be selected from.
815     * @return the selected data.
816     * @throws UnsupportedOperationException always thrown
817     */
818    @Override
819    public List<DataSet> selectDataForSaving(Frame parent, List<DataSet> data) {
820        throw new UnsupportedOperationException("Not supported yet.");
821    }
822
823    /**
824     * Method that writes out the data stored in the specified {@link DataSet} object to
825     * the specified output stream in CLVTOPS format. This method will throw an exception
826     * as writing to this format is not supported.
827     *
828     * @param output The output stream to which the data is to be written.
829     * @param data   A list of {@link DataSet} objects containing data to be written out.
830     * @throws IOException If there is a problem writing to the specified stream.
831     * @throws UnsupportedOperationException always thrown
832     */
833    @Override
834    public void write(OutputStream output, List<DataSet> data) throws IOException {
835        throw new UnsupportedOperationException("Not supported yet.");
836    }
837
838    /**
839     * A simple exception to catch the case of the column number missing in the CRF file.
840     */
841    @SuppressWarnings("serial")
842    private static class NoColumnNumberException extends Exception {
843
844        public NoColumnNumberException() {
845        }
846    }
847
848}