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