001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.data.hydra;
030
031import edu.wisc.ssec.mcidasv.Constants;
032import edu.wisc.ssec.mcidasv.McIDASV;
033import edu.wisc.ssec.mcidasv.PersistenceManager;
034import edu.wisc.ssec.mcidasv.data.HydraDataSource;
035import edu.wisc.ssec.mcidasv.data.PreviewSelection;
036import edu.wisc.ssec.mcidasv.data.QualityFlag;
037
038import java.io.ByteArrayInputStream;
039import java.io.File;
040import java.io.FilenameFilter;
041import java.rmi.RemoteException;
042import java.text.SimpleDateFormat;
043import java.util.ArrayList;
044import java.util.Collections;
045import java.util.Date;
046import java.util.Enumeration;
047import java.util.HashMap;
048import java.util.Hashtable;
049import java.util.Iterator;
050import java.util.LinkedHashMap;
051import java.util.LinkedHashSet;
052import java.util.List;
053import java.util.Map;
054import java.util.Set;
055import java.util.SimpleTimeZone;
056import java.util.StringTokenizer;
057
058import org.jdom2.Document;
059import org.jdom2.Element;
060import org.jdom2.Namespace;
061import org.jdom2.output.XMLOutputter;
062import org.slf4j.Logger;
063import org.slf4j.LoggerFactory;
064
065import ucar.ma2.Array;
066import ucar.ma2.ArrayFloat;
067import ucar.ma2.DataType;
068import ucar.nc2.Attribute;
069import ucar.nc2.Dimension;
070import ucar.nc2.Group;
071import ucar.nc2.NetcdfFile;
072import ucar.nc2.Variable;
073import ucar.nc2.dataset.VariableDS;
074import ucar.unidata.data.DataCategory;
075import ucar.unidata.data.DataChoice;
076import ucar.unidata.data.DataSelection;
077import ucar.unidata.data.DataSelectionComponent;
078import ucar.unidata.data.DataSourceDescriptor;
079import ucar.unidata.data.DerivedDataChoice;
080import ucar.unidata.data.DirectDataChoice;
081import ucar.unidata.data.GeoLocationInfo;
082import ucar.unidata.data.GeoSelection;
083import ucar.unidata.data.grid.GridUtil;
084import ucar.unidata.idv.IdvPersistenceManager;
085import ucar.unidata.util.Misc;
086import visad.Data;
087import visad.DateTime;
088import visad.DerivedUnit;
089import visad.FieldImpl;
090import visad.FlatField;
091import visad.FunctionType;
092import visad.RealType;
093import visad.SampledSet;
094import visad.Unit;
095import visad.VisADException;
096import visad.data.units.NoSuchUnitException;
097import visad.data.units.ParseException;
098import visad.data.units.Parser;
099import visad.util.Util;
100
101/**
102 * A data source for NPOESS Preparatory Project (Suomi NPP) data
103 * and JPSS data (JPSS-1 is officially NOAA-20).
104 * 
105 * This should probably move, but we are placing it here for now
106 * since we are leveraging some existing code used for HYDRA.
107 */
108
109public class SuomiNPPDataSource extends HydraDataSource {
110
111        private static final Logger logger = LoggerFactory.getLogger(SuomiNPPDataSource.class);
112        
113        /** Sources file */
114    protected String filename;
115    
116    // for loading bundles, store granule lists and geo lists here
117    protected List<String> oldSources = new ArrayList<>();
118    protected List<String> geoSources = new ArrayList<>();
119    
120    // integrity map for grouping sets/aggregations of selected products
121    Map<String, List<String>> filenameMap = null;
122
123    protected MultiDimensionReader nppAggReader;
124
125    protected MultiDimensionAdapter[] adapters = null;
126    
127    private List<MultiSpectralData> msd_CrIS = new ArrayList<>();
128    private List<MultiSpectralData> multiSpectralData = new ArrayList<>();
129    private Map<String, MultiSpectralData> msdMap = new HashMap<>();
130    private Map<String, QualityFlag> qfMap = new HashMap<>();
131    private Map<String, float[]> lutMap = new HashMap<>();
132
133    private static final String DATA_DESCRIPTION = "JPSS Data";
134    
135    // instrument related variables and flags
136    Attribute instrumentName = null;
137    private String productName = null;
138    
139    // product related variables and flags
140    String whichEDR = "";
141    
142    // for now, we are only handling CrIS variables that match this filter and SCAN dimensions
143    private String crisFilter = "ES_Real";
144
145    private Map<String, double[]> defaultSubset;
146    public TrackAdapter track_adapter;
147
148    private List<DataCategory> categories;
149    private boolean isCombinedProduct = false;
150    private boolean isDerived = false;
151    private boolean nameHasBeenSet = false;
152
153    private boolean isNOAA;
154    private boolean isEnterprise;
155
156    // need our own separator char since it's always Unix-style in the Suomi NPP files
157    private static final String SEPARATOR_CHAR = "/";
158    
159    // date formatter for NASA L1B data, ex 2016-02-07T00:06:00.000Z
160    SimpleDateFormat sdfNASA = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
161    SimpleDateFormat sdfEnterprise = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
162    
163    // LUTs for NASA L1B data
164    float[] m12LUT = null;
165    float[] m13LUT = null;
166    float[] m14LUT = null;
167    float[] m15LUT = null;
168    float[] m16LUT = null;
169    float[] i04LUT = null;
170    float[] i05LUT = null;
171    
172    // Map to match NASA variables to units (XML Product Profiles used for NOAA)
173    Map<String, String> unitsNASA = new HashMap<String, String>();
174    
175    // Map to match Enterprise EDR variables to units
176    Map<String, String> unitsEnterprise = new HashMap<String, String>();
177
178    // date formatter for converting Suomi NPP day/time to something we can use
179    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss.SSS");
180    
181    // date formatter for how we want to show granule day/time on display
182    SimpleDateFormat sdfOut = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
183    
184    // MJH keep track of date to add time dim to FieldImpl
185    Date theDate;
186
187    // TJJ set different subset with full res stride for derived products
188    Map<String, double[]> subset = null;
189    private Map<String, double[]> derivedSubset = null;
190    private boolean derivedInit = false;
191
192    /**
193     * Zero-argument constructor for construction via unpersistence.
194     */
195    
196    public SuomiNPPDataSource() {
197    }
198    
199    public SuomiNPPDataSource(String fileName) throws VisADException {
200        this(null, Misc.newList(fileName), null);
201        logger.debug("filename only constructor call..");
202    }
203
204    /**
205     * Construct a new Suomi NPP HDF5 data source.
206     * @param  descriptor  descriptor for this {@code DataSource}
207     * @param  fileName  name of the hdf file to read
208     * @param  properties  hashtable of properties
209     *
210     * @throws VisADException problem creating data
211     */
212    
213    public SuomiNPPDataSource(DataSourceDescriptor descriptor,
214                                 String fileName, Hashtable properties)
215            throws VisADException {
216        this(descriptor, Misc.newList(fileName), properties);
217        logger.debug("SuomiNPPDataSource called, single file selected: " + fileName);
218    }
219
220    /**
221     * Construct a new Suomi NPP HDF5 data source.
222     *
223     * @param descriptor Descriptor for this {@code DataSource}.
224     * @param newSources List of filenames.
225     * @param properties Hashtable of properties.
226     *
227     * @throws VisADException problem creating data
228     */
229    
230    public SuomiNPPDataSource(DataSourceDescriptor descriptor,
231                                 List<String> newSources, Hashtable properties)
232            throws VisADException {
233        super(descriptor, newSources, DATA_DESCRIPTION, properties);
234        logger.debug("SuomiNPPDataSource constructor called, file count: " + sources.size());
235
236        filename = (String) sources.get(0);
237        setDescription(DATA_DESCRIPTION);
238        
239        // NASA data is UTC, pre-set time zone
240        SimpleTimeZone stz = new SimpleTimeZone(0, "UTC");
241        sdfNASA.setTimeZone(stz);
242        sdfEnterprise.setTimeZone(stz);
243        
244        // build the filename map - matches each product to set of files for that product
245        filenameMap = new HashMap<>();
246        
247        // Pass 1, populate the list of products selected
248        for (Object o : sources) {
249                String filename = (String) o;
250                // first five characters of any product go together
251                int lastSeparator = filename.lastIndexOf(File.separatorChar);
252                int firstUnderscore = filename.indexOf("_", lastSeparator + 1);
253                String prodStr = filename.substring(lastSeparator + 1, firstUnderscore);
254                if (! filenameMap.containsKey(prodStr)) {
255                                List<String> l = new ArrayList<String>();
256                                filenameMap.put(prodStr, l);
257                }
258        }
259        
260        // Pass 2, create a list of files for each product in this data source
261        for (Object o : sources) {
262                String filename = (String) o;
263                // first five characters of any product go together
264                int lastSeparator = filename.lastIndexOf(File.separatorChar);
265                int firstUnderscore = filename.indexOf("_", lastSeparator + 1);
266                String prodStr = filename.substring(lastSeparator + 1, firstUnderscore);
267                List<String> l = filenameMap.get(prodStr);
268                l.add(filename);
269                filenameMap.put(prodStr, l);
270        }
271        
272        setup();
273        initQfTranslations();
274    }
275    
276
277        public void setup() throws VisADException {
278
279                // which format, NASA, NOAA, or NOAA Enterprise?
280                isNOAA = false;
281                isEnterprise = false;
282                nameHasBeenSet = false;
283                
284        // store filenames for possible bundle unpersistence
285        for (Object o : sources) {
286                oldSources.add((String) o);
287        }
288        
289        // time zone for product labels
290        SimpleTimeZone stz = new SimpleTimeZone(0, "GMT");
291        sdf.setTimeZone(stz);
292        sdfOut.setTimeZone(stz);
293        
294        // looking to populate 3 things - path to lat, path to lon, path to relevant products
295        String pathToLat = null;
296        String pathToLon = null;
297        Set<String> pathToProducts = new LinkedHashSet<>();
298        Map<String, String> prodToDesc = new HashMap<>();
299        
300        // flag to differentiate VIIRS from one of the other Suomi sensors
301        boolean isVIIRS = true;
302        
303        // check source filenames to see if this is a combined product. everything
304        // from last file separator to first underscore should be product info
305        int lastSeparator = filename.lastIndexOf(File.separatorChar);
306        int firstUnderscore = filename.indexOf("_", lastSeparator + 1);
307        String prodStr = filename.substring(lastSeparator + 1, firstUnderscore);
308        // only do this check for NOAA data
309        if (filename.endsWith(".h5")) {
310                isNOAA = true;
311                StringTokenizer st = new StringTokenizer(prodStr, "-");
312                logger.debug("SNPPDS check for embedded GEO, tokenizing: " + prodStr);
313                while (st.hasMoreTokens()) {
314                        String singleProd = st.nextToken();
315                        for (int i = 0; i < JPSSUtilities.geoProductIDs.length; i++) {
316                                if (singleProd.equals(JPSSUtilities.geoProductIDs[i])) {
317                                        logger.debug("Setting isCombinedProduct true, Found embedded GEO: " + singleProd);
318                                        isCombinedProduct = true;
319                                        break;
320                                }
321                        }
322                }
323        }
324        
325        lastSeparator = filename.lastIndexOf(File.separatorChar);
326        String enterpriseTest = filename.substring(lastSeparator + 1);
327        if (enterpriseTest.matches(JPSSUtilities.JPSS_REGEX_ENTERPRISE_EDR)) {
328           isEnterprise = true;
329        }
330
331        // various metatdata we'll need to gather on a per-product basis
332        Map<String, String> unsignedFlags = new LinkedHashMap<>();
333        Map<String, String> unpackFlags = new LinkedHashMap<>();
334        
335        // geo product IDs for each granule
336        Set<String> geoProductIDs = new LinkedHashSet<>();
337        
338        // aggregations will use sets of NetCDFFile readers
339        List<NetCDFFile> ncdfal = new ArrayList<>();
340        
341        // we should be able to find an XML Product Profile for each data/product type
342                Map<String, SuomiNPPProductProfile> profiles = new HashMap<>();
343
344        // and also Profile metadata for geolocation variables
345        boolean haveGeoMetaData = false;
346        
347        // number of source granules which make up the data source
348        int granuleCount = 1;
349                
350        try {
351                
352                // for each source file provided, find the appropriate geolocation,
353                // get the nominal time and various other granule-level metadata
354                Iterator keyIterator = filenameMap.keySet().iterator();
355                while (keyIterator.hasNext()) {
356                        String keyStr = (String) keyIterator.next();
357                        List fileNames = (List) filenameMap.get(keyStr);
358                        granuleCount = fileNames.size();
359                        setProperty(Constants.PROP_GRANULE_COUNT, granuleCount + " Granule");
360                        for (int fileCount = 0; fileCount < granuleCount; fileCount++) {
361                                // need to open the main NetCDF file to determine the geolocation product
362                                NetcdfFile ncfile = null;
363                                String fileAbsPath = null;
364                                try {
365                                        fileAbsPath = (String) fileNames.get(fileCount);
366                                        logger.debug("Trying to open file: " + fileAbsPath);
367                                        ncfile = NetcdfFile.open(fileAbsPath);
368                                        if (! isCombinedProduct) {
369                                                if (isNOAA) {
370                                                        Attribute a = ncfile.findGlobalAttribute("N_GEO_Ref");
371                                                        logger.debug("Value of GEO global attribute: " + a.getStringValue());
372                                                        String tmpGeoProductID = a.getStringValue();
373                                                        geoProductIDs.add(tmpGeoProductID);
374                                                } else {
375                                                        geoProductIDs.add(keyStr.replace("L1B", "GEO"));
376                                                }
377                                        }
378                                        Group rg = ncfile.getRootGroup();
379
380                        // Since no sub-groups for Enterprise EDRs, need to set date and instrument here
381                        if (isEnterprise) {
382                            if (! nameHasBeenSet) {
383                                Attribute coverageStartTime = ncfile.findGlobalAttribute("time_coverage_start");
384                                Date d = new Date();
385                                if (coverageStartTime != null) {
386                                    d = sdfEnterprise.parse(coverageStartTime.getStringValue());
387                                } else {
388                                    logger.error("Warning: unable to retrieve granule start time");
389                                }
390                                theDate = d;
391                                instrumentName = ncfile.findGlobalAttribute("instrument_name");
392                                setName(instrumentName.getStringValue() + " " + sdfOut.format(theDate));
393                            }
394                            nameHasBeenSet = true;
395                        }
396
397                                        List<Group> gl = rg.getGroups();
398                                        if (gl != null) {
399                                                for (Group g : gl) {
400                                                        logger.trace("Group name: " + g.getFullName());
401                                                        if (isNOAA) {
402                                                                        // when we find the Data_Products group, go down another group level and pull out 
403                                                                        // what we will use for nominal day and time (for now anyway).
404                                                                        // XXX TJJ fileCount check is so we don't count the GEO file in time array!
405                                                                        if (g.getFullName().contains("Data_Products")
406                                                                                        && (fileCount != fileNames.size())) {
407                                                                                List<Group> dpg = g.getGroups();
408
409                                                                                // cycle through once looking for XML Product Profiles
410                                                                                for (Group subG : dpg) {
411
412                                                                                        String subName = subG.getFullName();
413                                                                                        // use actual product, not geolocation, to id XML Product Profile
414                                                                                        if (!subName.contains("-GEO")) {
415                                                                                                // determine the instrument name (VIIRS, ATMS, CrIS, OMPS)
416                                                                                                instrumentName = subG.findAttribute("Instrument_Short_Name");
417
418                                                                                                // note any EDR products, will need to check for and remove
419                                                                                                // fill scans later
420                                                                                                Attribute adtt = subG.findAttribute("N_Dataset_Type_Tag");
421                                                                                                if (adtt != null) {
422                                                                                                        String baseName = adtt.getStringValue();
423                                                                                                        if ((baseName != null) && (baseName.equals("EDR"))) {
424                                                                                                                // have to loop through sub groups variables to determine band
425                                                                                                                List<Variable> tmpVar = subG.getVariables();
426                                                                                                                for (Variable v : tmpVar) {
427                                                                                                                        // if Imagery EDR attribute for band is specified, save it
428                                                                                                                        Attribute mBand = v.findAttribute("Band_ID");
429                                                                                                                        if (mBand != null) {
430                                                                                                                                whichEDR = mBand.getStringValue();
431                                                                                                                        }
432                                                                                                                }
433                                                                                                        }
434                                                                                                }
435
436                                                                                                // This is also where we find the attribute which tells us which
437                                                                                                // XML Product Profile to use!
438                                                                                                Attribute axpp = subG.findAttribute("N_Collection_Short_Name");
439                                                                                                if (axpp != null) {
440                                                                                                        String baseName = axpp.getStringValue();
441                                                                                                        productName = baseName;
442                                                                                                        
443                                                                                                        // TJJ Apr 2018
444                                                                                                        // Hack so we can look at CrIS Full Spectrum, until we can
445                                                                                                        // track down existence of an official Product Profile for it.
446                                                                                                        // http://mcidas.ssec.wisc.edu/inquiry-v/?inquiry=2634
447                                                                                                        // The regular SDR profile lets us visualize it.
448
449                                                                                                        SuomiNPPProductProfile profile = new SuomiNPPProductProfile();
450                                                                                                        String productProfileFileName = null;
451                                                                                                        if (productName.equals("CrIS-FS-SDR")) {
452                                                                                                                productProfileFileName = profile.getProfileFileName("CrIS-SDR");
453                                                                                                        } else {
454                                                                                                                productProfileFileName = profile.getProfileFileName(productName);
455                                                                                                        }
456
457                                                                                                        logger.info("Found profile: " + productProfileFileName + " for prod: " + productName);
458                                                                                                        profiles.put(productName, profile);
459                                                                                                        if (productProfileFileName == null) {
460                                                                                                                throw new Exception("XML Product Profile not found in catalog for: " + productName);
461                                                                                                        }
462                                                                                                        try {
463                                                                                                                profile.addMetaDataFromFile(productProfileFileName);
464                                                                                                        } catch (Exception nppppe) {
465                                                                                                                logger.error("Error parsing XML Product Profile: "
466                                                                                                                                + productProfileFileName);
467                                                                                                                throw new Exception("XML Product Profile Error", nppppe);
468                                                                                                        }
469                                                                                                }
470                                                                                        }
471                                                                                }
472
473                                                                                // 2nd pass through sub-group to extract date/time for aggregation
474                                                                                for (Group subG : dpg) {
475                                                                                        List<Variable> vl = subG.getVariables();
476                                                                                        for (Variable v : vl) {
477                                                                                                Attribute aDate = v.findAttribute("AggregateBeginningDate");
478                                                                                                Attribute aTime = v.findAttribute("AggregateBeginningTime");
479                                                                                                // did we find the attributes we are looking for?
480                                                                                                if ((aDate != null) && (aTime != null)) {
481                                                                                                        // set time for display to day/time of 1st granule examined
482                                                                                                    if (! nameHasBeenSet) {
483                                                                                                        String sDate = aDate.getStringValue();
484                                                                                                        String sTime = aTime.getStringValue();
485                                                                                                        logger.debug("For day/time, using: " + sDate
486                                                                                                                + sTime.substring(0, sTime.indexOf('Z') - 3));
487                                                                                                        Date d = sdf.parse(sDate
488                                                                                                                + sTime.substring(0, sTime.indexOf('Z') - 3));
489                                                                                                        theDate = d;
490                                                                                                        setName(instrumentName.getStringValue() + " "
491                                                                                                                + sdfOut.format(d));
492                                                                                                        nameHasBeenSet = true;
493                                                                                                    }
494                                                                                                        break;
495                                                                                                }
496                                                                                        }
497                                                                                }
498                                                                                if (! nameHasBeenSet) {
499                                                                                        throw new VisADException(
500                                                                                                        "No date time found in Suomi NPP granule");
501                                                                                }
502                                                                        } 
503                                } else {
504                                                                        // NASA data - date/time from global attribute
505                                                                        // set time for display to day/time of 1st granule examined
506                                                                        Attribute timeStartNASA = ncfile.findGlobalAttribute("time_coverage_start");
507                                                                        Date d = sdfNASA.parse(timeStartNASA.getStringValue());
508                                                                        theDate = d;
509                                                                        if (! nameHasBeenSet) {
510                                                                                instrumentName = ncfile.findGlobalAttribute("instrument");
511                                                                                setName(instrumentName.getStringValue() + " " + sdfOut.format(d));
512                                                                                nameHasBeenSet = true;
513                                                                        }
514                                                                }                               
515                                                }
516                                        }
517                                } catch (Exception e) {
518                                        logger.warn("Exception during processing of file: " + fileAbsPath);
519                                        throw (e);
520                                } finally {
521                                        ncfile.close();
522                                }
523                        }
524
525                }
526                
527                // build each union aggregation element
528                Iterator<String> iterator = geoProductIDs.iterator();
529                for (int elementNum = 0; elementNum < granuleCount; elementNum++) {
530                        
531                        String s = null;
532                        
533                        // build an XML (NCML actually) representation of the union aggregation of these two files
534                        Namespace ns = Namespace.getNamespace("http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2");
535                        Element root = new Element("netcdf", ns);
536                        Document document = new Document(root);
537
538                        Element agg = new Element("aggregation", ns);
539                        agg.setAttribute("type", "union");
540                        
541                        // TJJ - Loop over filename map, could be several products that need to be aggregated
542                Set set = filenameMap.keySet();
543                Iterator mapIter = set.iterator();
544                while (mapIter.hasNext()) {
545                        String key = (String) mapIter.next();
546                        List l = (List) filenameMap.get(key);
547                                Element fData = new Element("netcdf", ns);
548                                fData.setAttribute("location", (String) l.get(elementNum));
549                                agg.addContent(fData);
550                                s = (String) l.get(elementNum);
551                }
552                        
553                        String geoFilename = null;
554                        Element fGeo = new Element("netcdf", ns);;
555                        
556                        if (! isCombinedProduct) {
557        
558                                if (isNOAA) {
559                                                geoFilename = s.substring(0, s.lastIndexOf(File.separatorChar) + 1);
560                                                // check if we have the whole file name or just the prefix
561                                                String geoProductID = iterator.next();
562                                                if (geoProductID.endsWith("h5")) {
563                                                        geoFilename += geoProductID;
564                                                } else {
565                                                        geoFilename += geoProductID;
566                                                        geoFilename += s.substring(s
567                                                                        .lastIndexOf(File.separatorChar) + 6);
568                                                }
569                                                // Be sure file as specified by N_GEO_Ref global attribute really is there.
570                                                File tmpGeo = new File(geoFilename);
571                                                if (!tmpGeo.exists()) {
572                                                        // Ok, the expected file defined (supposedly) exactly by a global att is not there...
573                                                        // We need to check for similar geo files with different creation dates
574                                                        String geoFileRelative = geoFilename
575                                                                        .substring(geoFilename
576                                                                                        .lastIndexOf(File.separatorChar) + 1);
577                                                        // also check for Terrain Corrected version of geo
578                                                        String geoTerrainCorrected = geoFileRelative;
579                                                        geoTerrainCorrected = geoTerrainCorrected.replace(
580                                                                        "OD", "TC");
581                                                        geoTerrainCorrected = geoTerrainCorrected.replace(
582                                                                        "MG", "TC");
583
584                                                        // now we make a file filter, and see if a matching geo file is present
585                                                        File fList = new File(
586                                                                        geoFilename.substring(
587                                                                                        0,
588                                                                                        geoFilename
589                                                                                                        .lastIndexOf(File.separatorChar) + 1)); // current directory
590
591                                                        FilenameFilter geoFilter = new FilenameFilter() {
592                                                                public boolean accept(File dir, String name) {
593                                                                        if (name.matches(JPSSUtilities.SUOMI_GEO_REGEX_NOAA)) {
594                                                                                return true;
595                                                                        } else {
596                                                                                return false;
597                                                                        }
598                                                                }
599                                                        };
600
601                                                        File[] files = fList.listFiles(geoFilter);
602                                                        for (File file : files) {
603                                                                if (file.isDirectory()) {
604                                                                        continue;
605                                                                }
606                                                                // get the file name for convenience
607                                                                String fName = file.getName();
608                                                                // is it one of the standard Ellipsoid geo types we are looking for?
609                                                                if (fName.substring(0, 5).equals(
610                                                                                geoFileRelative.substring(0, 5))) {
611                                                                        int geoStartIdx = geoFileRelative
612                                                                                        .indexOf("_d");
613                                                                        int prdStartIdx = fName.indexOf("_d");
614                                                                        String s1 = geoFileRelative.substring(
615                                                                                        geoStartIdx, geoStartIdx + JPSSUtilities.NOAA_CREATION_DATE_INDEX);
616                                                                        String s2 = fName.substring(prdStartIdx,
617                                                                                        prdStartIdx + JPSSUtilities.NOAA_CREATION_DATE_INDEX);
618                                                                        if (s1.equals(s2)) {
619                                                                                geoFilename = s.substring(0, s.lastIndexOf(File.separatorChar) + 1) + fName;
620                                                                                break;
621                                                                        }
622                                                                }
623                                                                // same check, but for Terrain Corrected version
624                                                                if (fName.substring(0, 5).equals(
625                                                                                geoTerrainCorrected.substring(0, 5))) {
626                                                                        int geoStartIdx = geoTerrainCorrected
627                                                                                        .indexOf("_d");
628                                                                        int prdStartIdx = fName.indexOf("_d");
629                                                                        String s1 = geoTerrainCorrected.substring(
630                                                                                        geoStartIdx, geoStartIdx + JPSSUtilities.NOAA_CREATION_DATE_INDEX);
631                                                                        String s2 = fName.substring(prdStartIdx,
632                                                                                        prdStartIdx + JPSSUtilities.NOAA_CREATION_DATE_INDEX);
633                                                                        if (s1.equals(s2)) {
634                                                                                geoFilename = s.substring(0, s.lastIndexOf(File.separatorChar) + 1) + fName;
635                                                                                break;
636                                                                        }
637                                                                }
638                                                        }
639                                                } 
640                                        } else if (! isEnterprise) {
641                                                // NASA format
642                                                geoFilename = JPSSUtilities.replaceLast(s, "L1B", "GEO");
643                                                // get list of files in current directory
644                                                File fList = 
645                                                        new File(geoFilename.substring(0, geoFilename.lastIndexOf(File.separatorChar) + 1)); 
646                                                // make a NASA style file filter, and see if a matching geo file is present
647                                                FilenameFilter geoFilter = new FilenameFilter() {
648                                                        public boolean accept(File dir, String name) {
649                                                                if (name.matches(JPSSUtilities.SUOMI_GEO_REGEX_NASA)) {
650                                                                        return true;
651                                                                } else {
652                                                                        return false;
653                                                                }
654                                                        }
655                                                };
656                                                File[] files = fList.listFiles(geoFilter);
657                                                for (File file : files) {
658                                                        if (file.isDirectory()) {
659                                                                continue;
660                                                        }
661                                                        // get the file name for convenience
662                                                        String fName = file.getName();
663                                                        String tmpStr = geoFilename.substring(s.lastIndexOf(File.separatorChar) + 1,
664                                                                        s.lastIndexOf(File.separatorChar) + (JPSSUtilities.NASA_CREATION_DATE_INDEX + 1));
665                                                        if (fName.substring(0, JPSSUtilities.NASA_CREATION_DATE_INDEX).equals(tmpStr.substring(0, JPSSUtilities.NASA_CREATION_DATE_INDEX))) {
666                                                                geoFilename = s.substring(0, s.lastIndexOf(File.separatorChar) + 1) + fName;
667                                                                break;
668                                                        }
669                                                }
670                                        }
671                    if (! isEnterprise) {
672                        logger.debug("Determined GEO file name should be: " + geoFilename);
673                        fGeo.setAttribute("location", geoFilename);
674                        // add this to list used if we create a zipped bundle
675                        geoSources.add(geoFilename);
676                        agg.addContent(fGeo);
677                    }
678                        }
679
680                        root.addContent(agg);    
681                    XMLOutputter xmlOut = new XMLOutputter();
682                    String ncmlStr = xmlOut.outputString(document);
683                    ByteArrayInputStream is = new ByteArrayInputStream(ncmlStr.getBytes());                     
684                    MultiDimensionReader netCDFReader = new NetCDFFile(is);
685                    
686                // let's try and look through the NetCDF reader and see what we can learn...
687                NetcdfFile ncdff = ((NetCDFFile) netCDFReader).getNetCDFFile();
688                
689                Group rg = ncdff.getRootGroup();
690                // this is a list filled with unpacked qflag products, if any
691                ArrayList<VariableDS> qfProds = new ArrayList<VariableDS>();
692                
693                // this is a list filled with pseudo Brightness Temp variables converted from Radiance
694                ArrayList<VariableDS> btProds = new ArrayList<VariableDS>();
695
696                // Enterprise EDRs - no groups, just scan root level
697                if (isEnterprise) {
698                    List<Variable> vl = rg.getVariables();
699                    int xDimEnt = -1;
700                    int yDimEnt = -1;
701                    for (Variable v : vl) {
702                        if (v.getShortName().equals("Latitude")) {
703                            pathToLat = v.getFullName();
704                            pathToProducts.add(v.getFullName());
705                            prodToDesc.put(v.getFullName(), v.getDescription());
706                            xDimEnt = v.getDimension(0).getLength();
707                            yDimEnt = v.getDimension(1).getLength();
708                        }
709                        if (v.getShortName().equals("Longitude")) {
710                            pathToLon = v.getFullName();
711                            pathToProducts.add(v.getFullName());
712                            prodToDesc.put(v.getFullName(), v.getDescription());
713                        }
714                        // store units in a map for later
715                        Attribute unitAtt = v.findAttribute("units");
716                        if (unitAtt != null) {
717                            unitsEnterprise.put(v.getShortName(), unitAtt.getStringValue());
718                        } else {
719                            unitsEnterprise.put(v.getShortName(), "Unknown");
720                        }
721                    }
722
723                    for (Variable v : vl) {
724                        if (v.getShortName().equals("Latitude")) continue;
725                        if (v.getShortName().equals("Longitude")) continue;
726                        // keep any data which matches geolocation dimensions
727                        if (v.getRank() != 2) continue;
728                        if (v.getDimension(0).getLength() == xDimEnt &&
729                            v.getDimension(1).getLength() == yDimEnt) {
730                            pathToProducts.add(v.getFullName());
731                            prodToDesc.put(v.getFullName(), v.getDescription());
732                        }
733                        // store units in a map for later
734                        Attribute unitAtt = v.findAttribute("units");
735                        if (unitAtt != null) {
736                            unitsEnterprise.put(v.getShortName(), unitAtt.getStringValue());
737                        } else {
738                            unitsEnterprise.put(v.getShortName(), "Unknown");
739                        }
740                    }
741                }
742
743                List<Group> gl = rg.getGroups();
744                if (gl != null) {
745                                int xDimNASA = -1;
746                                int yDimNASA = -1;
747                        // Make a first pass to determine the shape of the geolocation data
748                        for (Group g : gl) {
749                                if (g.getFullName().contains("geolocation_data")) {
750                                        List<Variable> vl = g.getVariables();
751                                                for (Variable v : vl) {
752                                                        if (v.getShortName().equals("latitude")) {
753                                                                // XXX TJJ Nov 2015
754                                                                // Hack because fill value in attribute does not match
755                                                                // what I am seeing in the data.
756                                                                Attribute fillAtt = new Attribute("_FillValue", -999.0);
757                                                                v.addAttribute(fillAtt);
758                                                                pathToLat = v.getFullName();
759                                                                pathToProducts.add(v.getFullName());
760                                                                prodToDesc.put(v.getFullName(), v.getDescription());
761                                                                xDimNASA = v.getDimension(0).getLength();
762                                                                yDimNASA = v.getDimension(1).getLength();
763                                                        }
764                                                        if (v.getShortName().equals("longitude")) {
765                                                                // XXX TJJ Nov 2015
766                                                                // Hack because fill value in attribute does not match
767                                                                // what I am seeing in the data.
768                                                                Attribute fillAtt = new Attribute("_FillValue", -999.0);
769                                                                v.addAttribute(fillAtt);
770                                                                pathToLon = v.getFullName();
771                                                                pathToProducts.add(v.getFullName());
772                                                                        prodToDesc.put(v.getFullName(), v.getDescription());
773                                                        }
774                                                }
775                                }
776                        }
777                        for (Group g : gl) {
778                                logger.debug("Group name: " + g.getFullName());
779                                // NASA only - looking through observation_data and geolocation_data
780                                if (g.getFullName().contains("observation_data")) {
781                                        List<Variable> vl = g.getVariables();
782                                                for (Variable v : vl) {
783                                                        // keep any data which matches geolocation dimensions
784                                                        if (v.getDimension(0).getLength() == xDimNASA &&
785                                                                v.getDimension(1).getLength() == yDimNASA) {
786                                                                logger.debug("Adding product: " + v.getFullName());
787                                                                pathToProducts.add(v.getFullName());
788                                                                        prodToDesc.put(v.getFullName(), v.getDescription());
789                                                                Attribute aUnsigned = v.findAttribute("_Unsigned");
790                                                                if (aUnsigned != null) {
791                                                                        unsignedFlags.put(v.getFullName(), aUnsigned.getStringValue());
792                                                                } else {
793                                                                        unsignedFlags.put(v.getFullName(), "false");
794                                                                }
795                                                                
796                                                                // store units in a map for later
797                                                                Attribute unitAtt = v.findAttribute("units");
798                                                                if (unitAtt != null) {
799                                                                        unitsNASA.put(v.getShortName(), unitAtt.getStringValue());
800                                                                } else {
801                                                                        unitsNASA.put(v.getShortName(), "Unknown");
802                                                                }
803                                                                
804                                                                // TJJ Nov 2018 - SIPS V2+ mods
805                                                                // Regridding with bow-tie interpolation wasn't working since there are
806                                                                // now multiple fill value categories and we need to look specifically
807                                                                // for the bowtie deletion flag
808                                                                
809                                                                Attribute longNameAtt = v.findAttribute("long_name");
810                                                                String longName = "empty";
811                                                                if (longNameAtt != null) longName = longNameAtt.getStringValue();
812                                                                if (longName.contains("reflectance") || longName.contains("radiance")) {
813                                                                    
814                                                                    Attribute flagMeanings = v.findAttribute(JPSSUtilities.SIPS_FLAG_MEANINGS_ATTRIBUTE);
815                                                                    // If this is not null, we must be v2.0.0 or higher
816                                                                    if (flagMeanings != null) {
817                                                                        String meanings = flagMeanings.getStringValue();
818                                                                        // Tokenize meanings string, multiple flags defined there
819                                                                        StringTokenizer st = new StringTokenizer(meanings);
820                                                                        int bowtieIdx = -1;
821                                                                        boolean foundBowTieAttribute = false;
822                                                                        String tokStr = null;
823                                                                        while (st.hasMoreTokens()) {
824                                                                            tokStr = st.nextToken();
825                                                                            bowtieIdx++;
826                                                                            if (tokStr.equals(JPSSUtilities.SIPS_BOWTIE_DELETED_FLAG)) {
827                                                                                foundBowTieAttribute = true;
828                                                                                break;
829                                                                            }
830                                                                        }
831
832                                                                        if (foundBowTieAttribute) {
833                                                                            Attribute flagValues = v.findAttribute(JPSSUtilities.SIPS_FLAG_VALUES_ATTRIBUTE);
834                                                                            Array flagValsArr = flagValues.getValues();
835                                                                            int bowTieVal = (int) flagValsArr.getInt(bowtieIdx);
836                                                                            Attribute a1 = new Attribute("_FillValue", bowTieVal);
837                                                                            v.addAttribute(a1);
838                                                                        }
839                                                                    }
840
841                                                                }
842                                                                
843                                                                // TJJ Feb 2016 - Create BT variables where applicable
844                                                                if ((v.getShortName().matches("M12|M13|M14|M15|M16")) ||
845                                                                        (v.getShortName().matches("I04|I05"))) {
846                                                                        
847                                                                        // Get the LUT variable, load into primitive array
848                                                                        Variable lut = g.findVariable(v.getShortName() + "_brightness_temperature_lut");
849                                                                        int [] lutShape = lut.getShape();
850                                                                        logger.debug("Handling NASA LUT Variable, LUT size: " + lutShape[0]);
851                                                                        
852                                                                        // pull out valid min, max - these will be used for our new VariableDS
853                                                                        Attribute aVMin = lut.findAttribute("valid_min");
854                                                                        Attribute aVMax = lut.findAttribute("valid_max");
855                                                                        Attribute fillAtt = lut.findAttribute("_FillValue");
856                                                                        logger.debug("valid_min from LUT: " + aVMin.getNumericValue());
857                                                                        logger.debug("valid_max from LUT: " + aVMax.getNumericValue());
858                                                                        
859                                                                        // A little hacky, but at this point the class is such a mess
860                                                                        // that what's a little more, right? Load M12-M16, I4-I5 LUTS
861                                                                        
862                                                                        if (v.getShortName().matches("M12")) {
863                                                                                m12LUT = new float[lutShape[0]];
864                                                                                ArrayFloat.D1 lutArray = (ArrayFloat.D1) lut.read();
865                                                                                for (int lutIdx = 0; lutIdx < lutShape[0]; lutIdx++) {
866                                                                                        m12LUT[lutIdx] = lutArray.get(lutIdx);
867                                                                                }
868                                                                        }
869                                                                        
870                                                                        if (v.getShortName().matches("M13")) {
871                                                                                m13LUT = new float[lutShape[0]];
872                                                                                ArrayFloat.D1 lutArray = (ArrayFloat.D1) lut.read();
873                                                                                for (int lutIdx = 0; lutIdx < lutShape[0]; lutIdx++) {
874                                                                                        m13LUT[lutIdx] = lutArray.get(lutIdx);
875                                                                                }
876                                                                        }
877                                                                        
878                                                                        if (v.getShortName().matches("M14")) {
879                                                                                m14LUT = new float[lutShape[0]];
880                                                                                ArrayFloat.D1 lutArray = (ArrayFloat.D1) lut.read();
881                                                                                for (int lutIdx = 0; lutIdx < lutShape[0]; lutIdx++) {
882                                                                                        m14LUT[lutIdx] = lutArray.get(lutIdx);
883                                                                                }
884                                                                        }
885                                                                        
886                                                                        if (v.getShortName().matches("M15")) {
887                                                                                m15LUT = new float[lutShape[0]];
888                                                                                ArrayFloat.D1 lutArray = (ArrayFloat.D1) lut.read();
889                                                                                for (int lutIdx = 0; lutIdx < lutShape[0]; lutIdx++) {
890                                                                                        m15LUT[lutIdx] = lutArray.get(lutIdx);
891                                                                                }
892                                                                        }
893                                                                        
894                                                                        if (v.getShortName().matches("M16")) {
895                                                                                m16LUT = new float[lutShape[0]];
896                                                                                ArrayFloat.D1 lutArray = (ArrayFloat.D1) lut.read();
897                                                                                for (int lutIdx = 0; lutIdx < lutShape[0]; lutIdx++) {
898                                                                                        m16LUT[lutIdx] = lutArray.get(lutIdx);
899                                                                                }
900                                                                        }
901                                                                        
902                                                                        if (v.getShortName().matches("I04")) {
903                                                                                i04LUT = new float[lutShape[0]];
904                                                                                ArrayFloat.D1 lutArray = (ArrayFloat.D1) lut.read();
905                                                                                for (int lutIdx = 0; lutIdx < lutShape[0]; lutIdx++) {
906                                                                                        i04LUT[lutIdx] = lutArray.get(lutIdx);
907                                                                                }
908                                                                        }
909                                                                        
910                                                                        if (v.getShortName().matches("I05")) {
911                                                                                i05LUT = new float[lutShape[0]];
912                                                                                ArrayFloat.D1 lutArray = (ArrayFloat.D1) lut.read();
913                                                                                for (int lutIdx = 0; lutIdx < lutShape[0]; lutIdx++) {
914                                                                                        i05LUT[lutIdx] = lutArray.get(lutIdx);
915                                                                                }
916                                                                        }
917                                                                        
918                                                                        // Create a pseudo-variable, fill using LUT
919                                                                        // make a copy of the source variable
920                                                                        // NOTE: by using a VariableDS here, the original
921                                                                        // variable is used for the I/O, this matters!
922                                                                        VariableDS vBT = new VariableDS(g, v, false);
923
924                                                                        // Name is orig name plus suffix
925                                                                        vBT.setShortName(v.getShortName() + "_BT");
926
927                                                                        vBT.addAttribute(fillAtt);
928                                                                        vBT.addAttribute(aVMin);
929                                                                        vBT.addAttribute(aVMax);
930
931                                                                        if (v.getShortName().matches("M12")) {
932                                                                                lutMap.put(vBT.getFullName(), m12LUT);
933                                                                        }
934                                                                        if (v.getShortName().matches("M13")) {
935                                                                                lutMap.put(vBT.getFullName(), m13LUT);
936                                                                        }
937                                                                        if (v.getShortName().matches("M14")) {
938                                                                                lutMap.put(vBT.getFullName(), m14LUT);
939                                                                        }
940                                                                        if (v.getShortName().matches("M15")) {
941                                                                                lutMap.put(vBT.getFullName(), m15LUT);
942                                                                        }
943                                                                        if (v.getShortName().matches("M16")) {
944                                                                                lutMap.put(vBT.getFullName(), m16LUT);
945                                                                        }
946                                                                        if (v.getShortName().matches("I04")) {
947                                                                                lutMap.put(vBT.getFullName(), i04LUT);
948                                                                        }
949                                                                        if (v.getShortName().matches("I05")) {
950                                                                                lutMap.put(vBT.getFullName(), i05LUT);
951                                                                        }
952                                                                        pathToProducts.add(vBT.getFullName());
953                                                                        String newName = vBT.getDescription().replace("radiance", "brightness temperature");
954                                                                                prodToDesc.put(vBT.getFullName(), newName);
955                                                                        btProds.add(vBT);
956                                                                }
957                                                        }
958                                                }
959                                }
960                                if (g.getFullName().contains("geolocation_data")) {
961                                        List<Variable> vl = g.getVariables();
962                                                for (Variable v : vl) {
963                                                        // keep any data which matches geolocation dimensions
964                                                        if (v.getDimension(0).getLength() == xDimNASA &&
965                                                                v.getDimension(1).getLength() == yDimNASA) {
966                                                                // except we already found Lat and Lon, skip those 
967                                                                if ((v.getShortName().equals("latitude")) ||
968                                                                    (v.getShortName().equals("latitude"))) continue;
969                                                                logger.debug("Adding product: " + v.getFullName());
970                                                                pathToProducts.add(v.getFullName());
971                                                                        prodToDesc.put(v.getFullName(), v.getDescription());
972                                                        }
973                                                }
974                                }
975                                
976                                // NOAA only - we are looking through All_Data, finding displayable data
977                                if (g.getFullName().contains("All_Data")) {
978                                        List<Group> adg = g.getGroups();
979                                        int xDim = -1;
980                                        int yDim = -1;
981
982                                        // two sub-iterations, first one to find geolocation and product dimensions
983                                        for (Group subG : adg) {
984                                                logger.debug("Sub group name: " + subG.getFullName());
985                                                String subName = subG.getFullName();
986                                                if (subName.contains("-GEO")) {
987                                                        // this is the geolocation data
988                                                        String geoBaseName = subG.getShortName();
989                                                        geoBaseName = geoBaseName.substring(0, geoBaseName.indexOf('_'));
990                                                        if (! haveGeoMetaData) {
991                                                                                SuomiNPPProductProfile profile = new SuomiNPPProductProfile();
992                                        String geoProfileFileName = profile.getProfileFileName(geoBaseName);
993                                                                                logger.info("Found profile: " + geoProfileFileName + " for prod: " + geoBaseName);
994                                                                                if (geoProfileFileName == null) {
995                                                                                        throw new Exception("XML Product Profile not found in catalog for: " + geoBaseName);
996                                                                                }
997                                                                        // also add meta data from geolocation profile
998                                                                                profile.addMetaDataFromFile(geoProfileFileName);
999                                                                                profiles.put(geoBaseName, profile);
1000                                                                haveGeoMetaData = true;
1001                                                        }
1002                                                        List<Variable> vl = subG.getVariables();
1003                                                        for (Variable v : vl) {
1004                                                                if (v.getFullName().endsWith(SEPARATOR_CHAR + "Latitude")) {
1005                                                                        pathToLat = v.getFullName();
1006                                                                        logger.debug("Ellipsoid Lat/Lon Variable: " + v.getFullName());
1007                                                                        // get the dimensions of the lat variable
1008                                                                        Dimension dAlongTrack = v.getDimension(0);
1009                                                                        yDim = dAlongTrack.getLength();
1010                                                                        Dimension dAcrossTrack = v.getDimension(1);
1011                                                                        xDim = dAcrossTrack.getLength();
1012                                                                        logger.debug("Lat across track dim: " + dAcrossTrack.getLength());
1013                                                                }
1014                                                                if (v.getFullName().endsWith(SEPARATOR_CHAR + "Longitude")) {
1015                                                                        // we got dimensions from lat, don't need 'em twice, but need path
1016                                                                        pathToLon = v.getFullName();
1017                                                                }
1018                                                        } 
1019                                                        // one more pass in case there is terrain-corrected Lat/Lon
1020                                                        for (Variable v : vl) {
1021                                                                if (v.getFullName().endsWith(SEPARATOR_CHAR + "Latitude_TC")) {
1022                                                                        pathToLat = v.getFullName();
1023                                                                        logger.debug("Switched Lat/Lon Variable to TC: " + v.getFullName());
1024                                                                        // get the dimensions of the lat variable
1025                                                                        Dimension dAlongTrack = v.getDimension(0);
1026                                                                        yDim = dAlongTrack.getLength();
1027                                                                        Dimension dAcrossTrack = v.getDimension(1);
1028                                                                        xDim = dAcrossTrack.getLength();
1029                                                                        logger.debug("Lat across track dim: " + dAcrossTrack.getLength());
1030                                                                }
1031                                                                if (v.getFullName().endsWith(SEPARATOR_CHAR + "Longitude_TC")) {
1032                                                                        // we got dimensions from lat, don't need 'em twice, but need path
1033                                                                        pathToLon = v.getFullName();
1034                                                                }
1035                                                        }
1036                                                }
1037                                        }
1038
1039                                        // second to identify displayable products
1040                                        for (Group subG : adg) {
1041                                                // this is the product data
1042                                                List<Variable> vl = subG.getVariables();
1043                                                for (Variable v : vl) {
1044                                                        boolean useThis = false;
1045                                                        String vName = v.getFullName();
1046                                                        logger.trace("Variable: " + vName);
1047                                                        String varShortName = vName.substring(vName.lastIndexOf(SEPARATOR_CHAR) + 1);
1048
1049                                                                        // Pull out the profile pertaining to this variable
1050                                                                        SuomiNPPProductProfile matchingProfile = null;
1051                                                                        Set<String> keys = profiles.keySet();
1052                                                                        for (String key : keys) {
1053                                                                                if (v.getFullName().contains(key)) {
1054                                                                                        matchingProfile = profiles.get(key);
1055                                                                                        break;
1056                                                                                }
1057                                                                        }
1058
1059                                                                        if (matchingProfile == null) {
1060                                                                                throw new VisADException("No profile found for " + varShortName);
1061                                                                        }
1062
1063                                                        // Special code to handle quality flags. We throw out anything
1064                                                        // that does not match bounds of the geolocation data
1065                                                        
1066                                                        if (varShortName.startsWith("QF")) {
1067                                                                
1068                                                                logger.trace("Handling Quality Flag: " + varShortName);
1069                                                                
1070                                                                // this check is done later for ALL variables, but we need
1071                                                                // it early here to weed out those quality flags that are 
1072                                                                // simply a small set of data w/no granule geo nbounds
1073                                                                boolean xScanOk = false;
1074                                                                boolean yScanOk = false;
1075                                                                List<Dimension> dl = v.getDimensions();
1076                                                                
1077                                                                // toss out > 2D Quality Flags 
1078                                                                if (dl.size() > 2) {
1079                                                                        logger.trace("SKIPPING QF, > 2D: " + varShortName);
1080                                                                        continue;
1081                                                                }
1082                                                                
1083                                                                for (Dimension d : dl) {
1084                                                                        // in order to consider this a displayable product, make sure
1085                                                                        // both scan direction dimensions are present and look like a granule
1086                                                                        if (d.getLength() == xDim) {
1087                                                                                xScanOk = true;
1088                                                                        }
1089                                                                        if (d.getLength() == yDim) {
1090                                                                                yScanOk = true;
1091                                                                        }
1092                                                                }
1093                                                                
1094                                                                if (! (xScanOk && yScanOk)) {
1095                                                                        logger.trace("SKIPPING QF, does not match geo bounds: " + varShortName);
1096                                                                        continue;
1097                                                                }
1098
1099                                        ArrayList<QualityFlag> qfal = matchingProfile.getQualityFlags(varShortName);
1100                                                                if (qfal != null) {
1101                                                                        for (QualityFlag qf : qfal) {
1102                                                                                qf.setPackedName(vName);
1103                                                                                // make a copy of the qflag variable
1104                                                                                // NOTE: by using a VariableDS here, the original
1105                                                                                // variable is used for the I/O, this matters!
1106                                                                                VariableDS vqf = new VariableDS(subG, v, false);
1107                                                                                // prefix with QF num to help guarantee uniqueness across groups
1108                                                                                // this will cover most cases, but could still be dupe names
1109                                                                                // within a single QF.  This is handled when fetching XMLPP metadata
1110                                                                                vqf.setShortName(
1111                                                                                                varShortName.substring(0, 3) + "_" + qf.getName()
1112                                                                                );
1113                                                                                logger.debug("New QF var full name: " + vqf.getFullName());
1114                                                                        qfProds.add(vqf);
1115                                                                        qfMap.put(vqf.getFullName(), qf);
1116                                                                        }
1117                                                                }
1118                                                        }
1119
1120                                                        // for CrIS instrument, first find dimensions of var matching
1121                                                        // CrIS filter, then throw out all variables which don't match 
1122                                                        // those dimensions
1123                                                        
1124                                                        if (instrumentName.getStringValue().equals("CrIS")) {
1125                                                                if (! vName.contains("GEO")) {
1126                                                                        if (! varShortName.startsWith(crisFilter)) {
1127                                                                                logger.trace("Skipping variable: " + varShortName);
1128                                                                                continue;
1129                                                                        }
1130                                                                } else {
1131                                                                        // these variables are all GEO-related
1132                                                                        // if they match lat/lon bounds, keep them
1133                                                                        List<Dimension> dl = v.getDimensions();
1134                                                                        if (dl.size() == 3) {
1135                                                                                boolean isDisplayableCrIS = true;
1136                                                                                for (Dimension d : dl) {
1137                                                                                        if ((d.getLength() != xDim) && (d.getLength() != yDim) && (d.getLength() != 9)) {
1138                                                                                                isDisplayableCrIS = false;
1139                                                                                        }
1140                                                                                }
1141                                                                                if (! isDisplayableCrIS) {
1142                                                                                        continue;
1143                                                                                }
1144                                                                        }
1145                                                                }
1146                                                        }
1147
1148                                                        DataType dt = v.getDataType();
1149                                                        if ((dt.getSize() != 4) && (dt.getSize() != 2) && (dt.getSize() != 1)) {
1150                                                                continue;
1151                                                        }
1152
1153                                                        List<Dimension> dl = v.getDimensions();
1154                                                        if (dl.size() > 4) {
1155                                                                continue;
1156                                                        }
1157
1158                                                        // for now, skip any 3D VIIRS data
1159                                                        if (instrumentName.getStringValue().equals("VIIRS")) {
1160                                                                if (dl.size() == 3) {
1161                                                                        continue;
1162                                                                }
1163                                                        }
1164
1165                                                        boolean xScanOk = false;
1166                                                        boolean yScanOk = false;
1167                                                        for (Dimension d : dl) {
1168                                                                // in order to consider this a displayable product, make sure
1169                                                                // both scan direction dimensions are present and look like a granule
1170                                                                if (d.getLength() == xDim) {
1171                                                                        xScanOk = true;
1172                                                                }
1173                                                                if (d.getLength() == yDim) {
1174                                                                        yScanOk = true;
1175                                                                }
1176                                                        }
1177
1178                                                        if (xScanOk && yScanOk) {
1179                                                                useThis = true;
1180                                                        }
1181
1182                                                        // For ATMS, only 3-D variable we pass through is BrightnessTemperature
1183                                                        // Dimensions for BT are (lon, lat, channel)
1184                                                        if (instrumentName.getStringValue().equals("ATMS")) {
1185                                                                if (dl.size() == 3) {
1186                                                                        boolean isDisplayableATMS = false;
1187                                                                        for (Dimension d : dl) {
1188                                                                                if (d.getLength() == JPSSUtilities.ATMSChannelCenterFrequencies.length) {
1189                                                                                        isDisplayableATMS = true;
1190                                                                                        logger.trace("This variable has a dimension matching num ATMS channels");
1191                                                                                        break;
1192                                                                                }
1193                                                                        }
1194                                                                        if (! isDisplayableATMS) useThis = false;
1195                                                                }
1196                                                        }
1197
1198                                                        // sensor data with a channel dimension
1199                                                        if (useThis) {
1200                                                                if ((instrumentName.getStringValue().equals("CrIS")) ||
1201                                                                                (instrumentName.getStringValue().equals("ATMS")) || 
1202                                                                                (instrumentName.getStringValue().contains("OMPS"))) {
1203                                                                        isVIIRS = false;
1204                                                                        logger.debug("Handling non-VIIRS data source...");
1205                                                                }
1206                                                        }
1207
1208                                                        if (useThis) { 
1209                                                                // loop through the variable list again, looking for a corresponding "Factors"
1210                                                                float scaleVal = 1f;
1211                                                                float offsetVal = 0f;
1212                                                                boolean unpackFlag = false;
1213
1214                                                                //   if the granule has an entry for this variable name
1215                                                                //     get the data, data1 = scale, data2 = offset
1216                                                                //     create and poke attributes with this data
1217                                                                //   endif
1218
1219                                        String factorsVarName = matchingProfile.getScaleFactorName(varShortName);
1220                                                                if (factorsVarName != null) {
1221                                            logger.debug("Mapping: " + varShortName + " to: " + factorsVarName);
1222                                                                        for (Variable fV : vl) {
1223                                                                                if (fV.getShortName().equals(factorsVarName)) {
1224                                                                                        logger.trace("Pulling scale and offset values from variable: " + fV.getShortName());
1225                                                                                        ucar.ma2.Array a = fV.read();
1226                                                                                        float[] so = (float[]) a.copyTo1DJavaArray();
1227                                                                                        scaleVal = so[0];
1228                                                                                        offsetVal = so[1];
1229                                                                                        logger.trace("Scale value: " + scaleVal + ", Offset value: " + offsetVal);
1230                                                                                        unpackFlag = true;
1231                                                                                        break;
1232                                                                                }
1233                                                                        }
1234                                                                }
1235
1236                                                                // poke in scale/offset attributes for now
1237
1238                                                                Attribute a1 = new Attribute("scale_factor", scaleVal);
1239                                                                v.addAttribute(a1);
1240                                                                Attribute a2 = new Attribute("add_offset", offsetVal);
1241                                        v.addAttribute(a2);
1242
1243                                                                // add valid range and fill value attributes here
1244                                                                // try to fill in valid range
1245                                        if (matchingProfile.hasNameAndMetaData(varShortName)) {
1246                                            String rangeMin = matchingProfile.getRangeMin(varShortName);
1247                                            String rangeMax = matchingProfile.getRangeMax(varShortName);
1248                                            logger.trace("range min: " + rangeMin + ", range max: " + rangeMax);
1249                                                                        // only store range attribute if VALID range found
1250                                                                        if ((rangeMin != null) && (rangeMax != null)) {
1251                                                                                int [] shapeArr = new int [] { 2 };
1252                                                                                ArrayFloat af = new ArrayFloat(shapeArr);
1253                                                                                try {
1254                                                                                        af.setFloat(0, Float.parseFloat(rangeMin));
1255                                                                                } catch (NumberFormatException nfe) {
1256                                                                                        af.setFloat(0, new Float(Integer.MIN_VALUE));
1257                                                                                }
1258                                                                                try {
1259                                                                                        af.setFloat(1, Float.parseFloat(rangeMax));
1260                                                                                } catch (NumberFormatException nfe) {
1261                                                                                        af.setFloat(1, new Float(Integer.MAX_VALUE));
1262                                                                                }
1263                                                                                Attribute rangeAtt = new Attribute("valid_range", af);
1264                                                                                v.addAttribute(rangeAtt);
1265                                                                        }
1266
1267                                                                        // check for and load fill values too...
1268
1269                                                                        // we need to check two places, first, the XML product profile
1270                                            ArrayList<Float> fval = matchingProfile.getFillValues(varShortName);
1271
1272                                                                        // 2nd, does the variable already have one defined?
1273                                                                        // if there was already a fill value associated with this variable, make
1274                                                                        // sure we bring that along for the ride too...
1275                                                                        Attribute aFill = v.findAttribute("_FillValue");
1276
1277                                                                        // determine size of our fill value array
1278                                                                        int fvArraySize = 0;
1279                                                                        if (aFill != null) fvArraySize++;
1280                                                                        if (! fval.isEmpty()) fvArraySize += fval.size();
1281                                                                        int [] fillShape = new int [] { fvArraySize };
1282
1283                                                                        // allocate the array
1284                                                                        ArrayFloat afFill = new ArrayFloat(fillShape);
1285
1286                                                                        // and FINALLY, fill it!
1287                                                                        if (! fval.isEmpty()) {
1288                                                                                for (int fillIdx = 0; fillIdx < fval.size(); fillIdx++) {
1289                                                                                        afFill.setFloat(fillIdx, fval.get(fillIdx));
1290                                                                                        logger.trace("Adding fill value (from XML): " + fval.get(fillIdx));
1291                                                                                }
1292                                                                        }
1293
1294                                                                        if (aFill != null) {
1295                                                                                Number n = aFill.getNumericValue();
1296                                                                                // is the data unsigned?
1297                                                                                Attribute aUnsigned = v.findAttribute("_Unsigned");
1298                                                                                float fillValAsFloat = Float.NaN;
1299                                                                                if (aUnsigned != null) {
1300                                                                                        if (aUnsigned.getStringValue().equals("true")) {
1301                                                                                                DataType fvdt = aFill.getDataType();
1302                                                                                                logger.trace("Data String: " + aFill.toString());
1303                                                                                                logger.trace("DataType primitive type: " + fvdt.getPrimitiveClassType());
1304                                                                                                // signed byte that needs conversion?
1305                                                                                                if (fvdt.getPrimitiveClassType() == byte.class) {
1306                                                                                                        fillValAsFloat = (float) Util.unsignedByteToInt(n.byteValue());
1307                                                                                                }
1308                                                                                                else if (fvdt.getPrimitiveClassType() == short.class) {
1309                                                                                                        fillValAsFloat = (float) Util.unsignedShortToInt(n.shortValue());
1310                                                                                                } else {
1311                                                                                                        fillValAsFloat = n.floatValue();
1312                                                                                                }
1313                                                                                        }
1314                                                                                }
1315                                                                                afFill.setFloat(fvArraySize - 1, fillValAsFloat);
1316                                                                                logger.trace("Adding fill value (from variable): " + fillValAsFloat);
1317                                                                        }
1318                                                                        Attribute fillAtt = new Attribute("_FillValue", afFill);
1319                                                                        v.addAttribute(fillAtt);
1320                                                                }
1321
1322                                                                Attribute aUnsigned = v.findAttribute("_Unsigned");
1323                                                                if (aUnsigned != null) {
1324                                                                        unsignedFlags.put(v.getFullName(), aUnsigned.getStringValue());
1325                                                                } else {
1326                                                                        unsignedFlags.put(v.getFullName(), "false");
1327                                                                }
1328
1329                                                                if (unpackFlag) {
1330                                                                        unpackFlags.put(v.getFullName(), "true");
1331                                                                } else {
1332                                                                        unpackFlags.put(v.getFullName(), "false");
1333                                                                }
1334
1335                                                                logger.debug("Adding product: " + v.getFullName());
1336                                                                pathToProducts.add(v.getFullName());
1337                                                                                prodToDesc.put(v.getFullName(), v.getDescription());
1338                                                        }
1339                                                }
1340                                        }
1341                                }
1342                        }
1343                }
1344                
1345                // add in any unpacked qflag products
1346                for (VariableDS qfV: qfProds) {
1347                        // skip the spares - they are reserved for future use
1348                        if (qfV.getFullName().endsWith("Spare")) {
1349                                continue;
1350                        }
1351                        // String.endsWith is case sensitive so gotta check both cases
1352                        if (qfV.getFullName().endsWith("spare")) {
1353                                continue;
1354                        }
1355                        ncdff.addVariable(qfV.getGroup(), qfV);
1356                        logger.trace("Adding QF product: " + qfV.getFullName());
1357                        pathToProducts.add(qfV.getFullName());
1358                                        prodToDesc.put(qfV.getFullName(), qfV.getDescription());
1359                        unsignedFlags.put(qfV.getFullName(), "true");
1360                        unpackFlags.put(qfV.getFullName(), "false");
1361                }
1362                
1363                // add in any pseudo BT products from NASA data
1364                for (Variable vBT: btProds) {
1365                        logger.trace("Adding BT product: " + vBT.getFullName());
1366                                        ncdff.addVariable(vBT.getGroup(), vBT);
1367                                        unsignedFlags.put(vBT.getFullName(), "true");
1368                                        unpackFlags.put(vBT.getFullName(), "false");
1369                }
1370                
1371                    ncdfal.add((NetCDFFile) netCDFReader);
1372                }
1373                
1374        } catch (Exception e) {
1375                logger.error("cannot create NetCDF reader for files selected", e);
1376                if (e.getMessage() != null && e.getMessage().equals("XML Product Profile Error")) {
1377                        throw new VisADException("Unable to extract metadata from required XML Product Profile", e);
1378                }
1379        }
1380        
1381        // TJJ Feb 2018 
1382        // Doing a reorder of variable names here, as per HP's request from
1383        // http://mcidas.ssec.wisc.edu/inquiry-v/?inquiry=2613
1384
1385        if (isVIIRS) {
1386            // Copy the variable Set to a sortable List
1387            List<String> sortedList = new ArrayList(pathToProducts);
1388            Collections.sort(sortedList, new VIIRSSort());
1389
1390            // Clear the original data structure which retains insert order
1391            // (it's a LinkedHashSet)
1392            pathToProducts.clear();
1393
1394            // Re-add the variables in corrected order
1395            for (String s : sortedList) {
1396                pathToProducts.add(s);
1397            }
1398        }
1399        
1400        // initialize the aggregation reader object
1401        try {
1402                if (isNOAA) {
1403                    nppAggReader = new GranuleAggregation(ncdfal, pathToProducts, "Track", "XTrack", isVIIRS);
1404                    ((GranuleAggregation) nppAggReader).setQfMap(qfMap);
1405            } else if (isEnterprise) {
1406                nppAggReader = new GranuleAggregation(ncdfal, pathToProducts, "Rows", "Columns", isVIIRS);
1407                ((GranuleAggregation) nppAggReader).setQfMap(qfMap);
1408                } else {
1409                        nppAggReader = new GranuleAggregation(ncdfal, pathToProducts, "number_of_lines", "number_of_pixels", isVIIRS);
1410                    ((GranuleAggregation) nppAggReader).setLUTMap(lutMap);
1411                }
1412        } catch (Exception e) {
1413                throw new VisADException("Unable to initialize aggregation reader", e);
1414        }
1415
1416        // make sure we found valid data
1417        if (pathToProducts.size() == 0) {
1418                throw new VisADException("No data found in files selected");
1419        }
1420        
1421        logger.debug("Number of adapters needed: " + pathToProducts.size());
1422        adapters = new MultiDimensionAdapter[pathToProducts.size()];
1423        Hashtable<String, String[]> properties = new Hashtable<>();
1424        
1425        Iterator<String> iterator = pathToProducts.iterator();
1426        int pIdx = 0;
1427        boolean adapterCreated = false;
1428        while (iterator.hasNext()) {
1429                String pStr = iterator.next();
1430                logger.debug("Working on adapter number " + (pIdx + 1) + ": " + pStr);
1431                Map<String, Object> swathTable = SwathAdapter.getEmptyMetadataTable();
1432                Map<String, Object> spectTable = SpectrumAdapter.getEmptyMetadataTable();
1433                swathTable.put("array_name", pStr);
1434                swathTable.put("lon_array_name", pathToLon);
1435                swathTable.put("lat_array_name", pathToLat);
1436                swathTable.put("XTrack", "XTrack");
1437                swathTable.put("Track", "Track");
1438                swathTable.put("geo_Track", "Track");
1439                swathTable.put("geo_XTrack", "XTrack");
1440                // TJJ is this even needed?  Is product_name used anywhere?
1441                if (productName == null) productName = pStr.substring(pStr.indexOf(SEPARATOR_CHAR) + 1);
1442                swathTable.put("product_name", productName);
1443                        swathTable.put("_mapping", prodToDesc);
1444                // array_name common to spectrum table
1445                spectTable.put("array_name", pStr);
1446                spectTable.put("product_name", productName);
1447                        spectTable.put("_mapping", prodToDesc);
1448                
1449                if (! isVIIRS) {
1450
1451                        // 3D data is either ATMS, OMPS, or CrIS
1452                        if ((instrumentName.getShortName() != null) && (instrumentName.getStringValue().equals("ATMS"))) {
1453
1454                                spectTable.put(SpectrumAdapter.channelIndex_name, "Channel");
1455                        swathTable.put(SpectrumAdapter.channelIndex_name, "Channel");
1456                        
1457                        swathTable.put("array_dimension_names", new String[] {"Track", "XTrack", "Channel"});
1458                        swathTable.put("lon_array_dimension_names", new String[] {"Track", "XTrack"});
1459                        swathTable.put("lat_array_dimension_names", new String[] {"Track", "XTrack"});
1460                        spectTable.put("array_dimension_names", new String[] {"Track", "XTrack", "Channel"});
1461                        spectTable.put("lon_array_dimension_names", new String[] {"Track", "XTrack"});
1462                        spectTable.put("lat_array_dimension_names", new String[] {"Track", "XTrack"});
1463                        
1464                        spectTable.put(SpectrumAdapter.channelType, "wavelength");
1465                        spectTable.put(SpectrumAdapter.channels_name, "Channel");
1466                    spectTable.put(SpectrumAdapter.x_dim_name, "XTrack");
1467                    spectTable.put(SpectrumAdapter.y_dim_name, "Track");
1468                    
1469                                int numChannels = JPSSUtilities.ATMSChannelCenterFrequencies.length;
1470                        float[] bandArray = new float[numChannels];
1471                        String[] bandNames = new String[numChannels];
1472                        for (int bIdx = 0; bIdx < numChannels; bIdx++) {
1473                                bandArray[bIdx] = JPSSUtilities.ATMSChannelCenterFrequencies[bIdx];
1474                                bandNames[bIdx] = "Channel " + (bIdx + 1);
1475                        }
1476                        spectTable.put(SpectrumAdapter.channelValues, bandArray);
1477                        spectTable.put(SpectrumAdapter.bandNames, bandNames);
1478
1479                        } else {
1480                                if (instrumentName.getStringValue().equals("CrIS")) {
1481                                        
1482                                        swathTable.put("XTrack", "dim1");
1483                                        swathTable.put("Track", "dim0");
1484                                        swathTable.put("geo_XTrack", "dim1");
1485                                        swathTable.put("geo_Track", "dim0");
1486                                        swathTable.put("product_name", "CrIS_SDR");
1487                                        swathTable.put(SpectrumAdapter.channelIndex_name, "dim3");
1488                                        swathTable.put(SpectrumAdapter.FOVindex_name, "dim2"); 
1489                                         
1490                                        spectTable.put(SpectrumAdapter.channelIndex_name, "dim3");
1491                                        spectTable.put(SpectrumAdapter.FOVindex_name, "dim2");
1492                                spectTable.put(SpectrumAdapter.x_dim_name, "dim1");
1493                                spectTable.put(SpectrumAdapter.y_dim_name, "dim0");
1494                                
1495                                } else if (instrumentName.getStringValue().contains("OMPS")) {
1496                                        
1497                                        spectTable.put(SpectrumAdapter.channelIndex_name, "Channel");
1498                                swathTable.put(SpectrumAdapter.channelIndex_name, "Channel");
1499                                
1500                                swathTable.put("array_dimension_names", new String[] {"Track", "XTrack", "Channel"});
1501                                swathTable.put("lon_array_dimension_names", new String[] {"Track", "XTrack"});
1502                                swathTable.put("lat_array_dimension_names", new String[] {"Track", "XTrack"});
1503                                spectTable.put("array_dimension_names", new String[] {"Track", "XTrack", "Channel"});
1504                                spectTable.put("lon_array_dimension_names", new String[] {"Track", "XTrack"});
1505                                spectTable.put("lat_array_dimension_names", new String[] {"Track", "XTrack"});
1506                                
1507                                spectTable.put(SpectrumAdapter.channelType, "wavelength");
1508                                spectTable.put(SpectrumAdapter.channels_name, "Channel");
1509                        spectTable.put(SpectrumAdapter.x_dim_name, "XTrack");
1510                        spectTable.put(SpectrumAdapter.y_dim_name, "Track");
1511                        
1512                                int numChannels = 200;
1513                                if (instrumentName.getStringValue().equals("OMPS-TC")) {
1514                                        numChannels = 260;
1515                                }
1516                                logger.debug("Setting up OMPS adapter, num channels: " + numChannels);
1517                                float[] bandArray = new float[numChannels];
1518                                String[] bandNames = new String[numChannels];
1519                                for (int bIdx = 0; bIdx < numChannels; bIdx++) {
1520                                        bandArray[bIdx] = bIdx;
1521                                        bandNames[bIdx] = "Channel " + (bIdx + 1);
1522                                }
1523                                spectTable.put(SpectrumAdapter.channelValues, bandArray);
1524                                spectTable.put(SpectrumAdapter.bandNames, bandNames);
1525                                
1526                                } else {
1527                                        // sorry, if we can't id the instrument, we can't display the data!
1528                                        throw new VisADException("Unable to determine instrument name");
1529                                }
1530                        }
1531
1532                } else {
1533                        swathTable.put("array_dimension_names", new String[] {"Track", "XTrack"});
1534                        swathTable.put("lon_array_dimension_names", new String[] {"Track", "XTrack"});
1535                        swathTable.put("lat_array_dimension_names", new String[] {"Track", "XTrack"});
1536                }
1537                
1538                swathTable.put("scale_name", "scale_factor");
1539                swathTable.put("offset_name", "add_offset");
1540                swathTable.put("fill_value_name", "_FillValue");
1541                swathTable.put("range_name", pStr.substring(pStr.indexOf(SEPARATOR_CHAR) + 1));
1542                spectTable.put("range_name", pStr.substring(pStr.indexOf(SEPARATOR_CHAR) + 1));
1543                
1544                // set the valid range hash if data is available
1545                        // Pull out the profile pertaining to this variable
1546                        SuomiNPPProductProfile stProfile = null;
1547                        Set<String> keys = profiles.keySet();
1548                        for (String key : keys) {
1549                                if (productName.contains(key)) {
1550                                        stProfile = profiles.get(key);
1551                                        break;
1552                                }
1553                        }
1554
1555                        if (stProfile == null) {
1556                                throw new VisADException("No profile found for " + productName);
1557                        }
1558
1559            if (stProfile != null) {
1560                if (stProfile.getRangeMin(pStr.substring(pStr.lastIndexOf(SEPARATOR_CHAR) + 1)) != null) {
1561                                swathTable.put("valid_range", "valid_range");
1562                        }
1563                }
1564                
1565                String unsignedAttributeStr = unsignedFlags.get(pStr);
1566                if ((unsignedAttributeStr != null) && (unsignedAttributeStr.equals("true"))) {
1567                        swathTable.put("unsigned", unsignedAttributeStr);
1568                }
1569                
1570                String unpackFlagStr = unpackFlags.get(pStr);
1571                if ((unpackFlagStr != null) && (unpackFlagStr.equals("true"))) {
1572                        swathTable.put("unpack", "true");
1573                }
1574                
1575                // For Suomi NPP data, do valid range check AFTER applying scale/offset
1576                swathTable.put("range_check_after_scaling", "true");
1577                
1578                // pass in a GranuleAggregation reader...
1579                if (! isVIIRS) {
1580                if (instrumentName.getStringValue().equals("ATMS")) {
1581                        adapters[pIdx] = new SwathAdapter(nppAggReader, swathTable);
1582                        adapterCreated = true;
1583                        SpectrumAdapter sa = new SpectrumAdapter(nppAggReader, spectTable);
1584                    DataCategory.createCategory("MultiSpectral");
1585                    categories = DataCategory.parseCategories("MultiSpectral;MultiSpectral;IMAGE");
1586                        MultiSpectralData msd = new MultiSpectralData((SwathAdapter) adapters[pIdx], sa, 
1587                                "BrightnessTemperature", "BrightnessTemperature", "SuomiNPP", "ATMS");
1588                        msd.setInitialWavenumber(JPSSUtilities.ATMSChannelCenterFrequencies[0]);
1589                        multiSpectralData.add(msd);
1590                } 
1591                if (instrumentName.getStringValue().equals("CrIS")) {
1592                        if (pStr.contains(crisFilter)) {
1593                                adapters[pIdx] = new CrIS_SDR_SwathAdapter(nppAggReader, swathTable);
1594                                adapterCreated = true;
1595                                CrIS_SDR_Spectrum csa = new CrIS_SDR_Spectrum(nppAggReader, spectTable);
1596                            DataCategory.createCategory("MultiSpectral");
1597                            categories = DataCategory.parseCategories("MultiSpectral;MultiSpectral;IMAGE");
1598                                MultiSpectralData msd = new CrIS_SDR_MultiSpectralData((CrIS_SDR_SwathAdapter) adapters[pIdx], csa); 
1599                            msd.setInitialWavenumber(csa.getInitialWavenumber());
1600                            msd_CrIS.add(msd);
1601                        }
1602                }
1603                if (instrumentName.getStringValue().contains("OMPS")) {
1604                        adapters[pIdx] = new SwathAdapter(nppAggReader, swathTable);
1605                        adapterCreated = true;
1606                        SpectrumAdapter sa = new SpectrumAdapter(nppAggReader, spectTable);
1607                    DataCategory.createCategory("MultiSpectral");
1608                    categories = DataCategory.parseCategories("MultiSpectral;MultiSpectral;IMAGE");
1609                        MultiSpectralData msd = new MultiSpectralData((SwathAdapter) adapters[pIdx], sa, 
1610                                "RadianceEarth", "RadianceEarth", "SuomiNPP", "OMPS");
1611                        // TJJ Jul 2023 - below numbers are just a first guess to get OMPS from J2 working
1612                        msd.setInitialWavenumber(85);
1613                        float[] ompsRange = new float[2];
1614                        ompsRange[0] = 0;
1615                        ompsRange[1] = 400;
1616                        msd.setDataRange(ompsRange);
1617                        multiSpectralData.add(msd);
1618                } 
1619                if (pIdx == 0) {
1620                        // generate default subset for ATMS and OMPS
1621                        if (! instrumentName.getStringValue().equals("CrIS")) {
1622                                defaultSubset = multiSpectralData.get(pIdx).getDefaultSubset();
1623                        }
1624                }
1625                
1626                } else {
1627                        // setting NOAA-format units
1628                        String varName = pStr.substring(pStr.indexOf(SEPARATOR_CHAR) + 1);
1629                        String varShortName = pStr.substring(pStr.lastIndexOf(SEPARATOR_CHAR) + 1);
1630                String units = stProfile.getUnits(varShortName);
1631
1632                // setting NASA-format and Enterprise EDR units
1633                        if (! isNOAA) {
1634                    if (isEnterprise) {
1635                        units = unitsEnterprise.get(varShortName);
1636                    } else {
1637                        units = unitsNASA.get(varShortName);
1638                        // Need to set _BT variables manually, since they are created on the fly
1639                        if (varShortName.endsWith("_BT")) units = "Kelvin";
1640                    }
1641                        }
1642                        if (units == null) units = "Unknown";
1643                        Unit u = null;
1644                        try {
1645                                        u = Parser.parse(units);
1646                    logger.debug("Found units: " + units);
1647                                } catch (NoSuchUnitException e) {
1648                                        u = new DerivedUnit(units);
1649                                        logger.debug("Unknown units: " + units);
1650                                } catch (ParseException e) {
1651                                        u = new DerivedUnit(units);
1652                                        logger.debug("Unparseable units: " + units);
1653                                }
1654                        // associate this variable with these units, if not done already
1655                        RealType.getRealType(varName, u);
1656                        adapters[pIdx] = new SwathAdapter(nppAggReader, swathTable);
1657                        adapterCreated = true;
1658                        if (pIdx == 0) {
1659                                defaultSubset = adapters[pIdx].getDefaultSubset();
1660                        }
1661                        categories = DataCategory.parseCategories("IMAGE");
1662                }
1663                // only increment count if we created an adapter, some products are skipped
1664                if (adapterCreated) pIdx++;
1665                adapterCreated = false;
1666        }
1667
1668        if (msd_CrIS.size() > 0) {
1669                try {
1670                        MultiSpectralAggr aggr = new MultiSpectralAggr(msd_CrIS.toArray(new MultiSpectralData[msd_CrIS.size()]));
1671                        aggr.setInitialWavenumber(902.25f);
1672                        multiSpectralData.add(aggr);
1673                        defaultSubset = ((MultiSpectralData) msd_CrIS.get(0)).getDefaultSubset();
1674                } catch (Exception e) {
1675                        logger.error("Exception: ", e);
1676                }
1677        }
1678
1679        // Merge with pre-set properties
1680        Hashtable tmpHt = getProperties();
1681        tmpHt.putAll(properties);
1682        setProperties(tmpHt);
1683    }
1684
1685    /* (non-Javadoc)
1686     * @see ucar.unidata.data.DataSourceImpl#initDataChoice(ucar.unidata.data.DataChoice)
1687     */
1688    @Override
1689    public void initDataChoice(DataChoice dataChoice) {
1690        super.initDataChoice(dataChoice);
1691        // Note here if we are a derived data choice
1692        if (dataChoice instanceof DerivedDataChoice) {
1693            isDerived = true;
1694        }
1695    }
1696
1697    public void initAfterUnpersistence() {
1698        try {
1699            String zidvPath = 
1700                    McIDASV.getStaticMcv().getStateManager().
1701                    getProperty(IdvPersistenceManager.PROP_ZIDVPATH, "");
1702            if (getTmpPaths() != null) {
1703                // New code for zipped bundles-
1704                // we want 'sources' to point to wherever the zipped data was unpacked.
1705                sources.clear();
1706                // following PersistenceManager.fixBulkDataSources, get temporary data location
1707                for (Object o : getTmpPaths()) {
1708                    String tempPath = (String) o;
1709                    // replace macro string with actual path
1710                    String expandedPath = tempPath.replace(PersistenceManager.MACRO_ZIDVPATH, zidvPath);
1711                    // we don't want to add nav files to this list!:
1712                    File f = new File(expandedPath);
1713                    if (!f.getName().matches(JPSSUtilities.SUOMI_GEO_REGEX_NOAA)) {
1714                        sources.add(expandedPath);
1715                    }
1716                }
1717
1718                // mjh fix absolute paths in filenameMap
1719                logger.debug("original filenameMap: {}", filenameMap);
1720                Iterator<String> keyIterator = filenameMap.keySet().iterator();
1721                while (keyIterator.hasNext()) {
1722                    String keyStr = (String) keyIterator.next();
1723                    List<String> fileNames = (List<String>) filenameMap.get(keyStr);
1724                    for (int i = 0; i < fileNames.size(); i++) {
1725                        String name = fileNames.get(i);
1726                        int lastSeparator = name.lastIndexOf(File.separatorChar);
1727                        String sub = name.substring(0, lastSeparator);
1728                        name = name.replace(sub, zidvPath);
1729                        fileNames.set(i, name);
1730                    }
1731                }
1732                logger.debug("filenameMap with zidvPath: {}", filenameMap);
1733            } else {
1734                // leave in original unpersistence code - this will get run for unzipped bundles.
1735                // TODO: do we need to handle the "Save with relative paths" case specially?
1736                    if (! oldSources.isEmpty()) {
1737                    sources.clear();
1738                    for (Object o : oldSources) {
1739                        sources.add((String) o);
1740                    }
1741                }
1742            }
1743            oldSources.clear();
1744                setup();
1745        } catch (Exception e) {
1746                logger.error("Exception: ", e);
1747        }
1748    }
1749
1750    /* (non-Javadoc)
1751         * @see edu.wisc.ssec.mcidasv.data.HydraDataSource#canSaveDataToLocalDisk()
1752         */
1753        @Override
1754        public boolean canSaveDataToLocalDisk() {
1755                // At present, Suomi data is always data granules on disk
1756                return true;
1757        }
1758
1759        /* (non-Javadoc)
1760         * @see ucar.unidata.data.DataSourceImpl#saveDataToLocalDisk(java.lang.String, java.lang.Object, boolean)
1761         */
1762        @Override
1763        protected List saveDataToLocalDisk(String filePrefix, Object loadId,
1764                        boolean changeLinks) throws Exception {
1765                // need to make a list of all data granule files
1766                // PLUS all geolocation granule files, but only if accessed separate!
1767                List<String> fileList = new ArrayList<String>();
1768                for (Object o : sources) {
1769                        fileList.add((String) o);
1770                }
1771                for (String s : geoSources) {
1772                        fileList.add(s);
1773                }
1774                return fileList;
1775        }
1776
1777        public List<String> getOldSources() {
1778                return oldSources;
1779        }
1780
1781        public void setOldSources(List<String> oldSources) {
1782                this.oldSources = oldSources;
1783        }
1784    
1785    public Map<String, List<String>> getFilenameMap() {
1786        return filenameMap;
1787    }
1788
1789    public void setFilenameMap(Map<String, List<String>> filenameMap) {
1790        this.filenameMap = filenameMap;
1791    }
1792
1793        /**
1794     * Make and insert the {@link DataChoice DataChoices} for this
1795     * {@code DataSource}.
1796     */
1797    
1798    public void doMakeDataChoices() {
1799        
1800        // special loop for CrIS, ATMS, and OMPS data
1801        if (multiSpectralData.size() > 0) {
1802                for (int k = 0; k < multiSpectralData.size(); k++) {
1803                        MultiSpectralData adapter = multiSpectralData.get(k);
1804                        DataChoice choice = null;
1805                                try {
1806                                        choice = doMakeDataChoice(k, adapter);
1807                                        choice.setObjectProperty(Constants.PROP_GRANULE_COUNT, 
1808                                                        getProperty(Constants.PROP_GRANULE_COUNT, "1 Granule"));
1809                                msdMap.put(choice.getName(), adapter);
1810                                addDataChoice(choice);
1811                                } catch (Exception e) {
1812                                        logger.error("Exception: ", e);
1813                                }
1814                }
1815                return;
1816        }
1817                
1818        // all other data (VIIRS and 2D EDRs)
1819        if (adapters != null) {
1820                for (int idx = 0; idx < adapters.length; idx++) {
1821                        DataChoice choice = null;
1822                        try {
1823                                Map<String, Object> metadata = adapters[idx].getMetadata();
1824                                        String description = null;
1825                                if (metadata.containsKey("_mapping")) {
1826                                                String arrayName = metadata.get("array_name").toString();
1827                                        Map<String, String> mapping =
1828                                                        (Map<String, String>)metadata.get("_mapping");
1829                                                description = mapping.get(arrayName);
1830                                        }
1831                                        choice = doMakeDataChoice(idx, adapters[idx].getArrayName(), description);
1832                                choice.setObjectProperty(Constants.PROP_GRANULE_COUNT, 
1833                                                        getProperty(Constants.PROP_GRANULE_COUNT, "1 Granule")); 
1834                        } 
1835                        catch (Exception e) {
1836                                logger.error("doMakeDataChoice failed", e);
1837                        }
1838
1839                        if (choice != null) {
1840                                addDataChoice(choice);
1841                        }
1842                }
1843        }
1844    }
1845
1846    private DataChoice doMakeDataChoice(int idx, String var, String description) throws Exception {
1847        String name = var;
1848        if (description == null) {
1849            description = name;
1850        }
1851        DataSelection dataSel = new MultiDimensionSubset(defaultSubset);
1852        Hashtable dcSubset = new Hashtable();
1853        dcSubset.put(new MultiDimensionSubset(), dataSel);
1854        // TJJ Hack check for uber-odd case of data type varies for same variable
1855        // If it's M12 - M16, it's a BrightnessTemperature, otherwise Reflectance
1856        if (name.endsWith("BrightnessTemperatureOrReflectance")) {
1857                name = name.substring(0, name.length() - "BrightnessTemperatureOrReflectance".length());
1858                if (whichEDR.matches("M12|M13|M14|M15|M16")) {
1859                        name = name + "BrightnessTemperature";
1860                } else {
1861                        name = name + "Reflectance";
1862                }
1863        }
1864        DirectDataChoice ddc = new DirectDataChoice(this, idx, name, description, categories, dcSubset);
1865        return ddc;
1866    }
1867    
1868    private DataChoice doMakeDataChoice(int idx, MultiSpectralData adapter) throws Exception {
1869        String name = adapter.getName();
1870        DataSelection dataSel = new MultiDimensionSubset(defaultSubset);
1871        Hashtable dcSubset = new Hashtable();
1872        dcSubset.put(MultiDimensionSubset.key, dataSel);
1873        dcSubset.put(MultiSpectralDataSource.paramKey, adapter.getParameter());
1874        // TJJ Hack check for uber-odd case of data type varies for same variable
1875        // If it's M12 - M16, it's a BrightnessTemperature, otherwise Reflectance
1876        if (name.endsWith("BrightnessTemperatureOrReflectance")) {
1877                name = name.substring(0, name.length() - "BrightnessTemperatureOrReflectance".length());
1878                if (whichEDR.matches("M12|M13|M14|M15|M16")) {
1879                        name = name + "BrightnessTemperature";
1880                } else {
1881                        name = name + "Reflectance";
1882                }
1883        }
1884        DirectDataChoice ddc = new DirectDataChoice(this, new Integer(idx), name, name, categories, dcSubset);
1885        ddc.setProperties(dcSubset);
1886        return ddc;
1887    }
1888
1889    /**
1890     * Check to see if this {@code SuomiNPPDataSource} is equal to the object
1891     * in question.
1892     * @param o  object in question
1893     * @return true if they are the same or equivalent objects
1894     */
1895    
1896    public boolean equals(Object o) {
1897        if ( !(o instanceof SuomiNPPDataSource)) {
1898            return false;
1899        }
1900        return (this == (SuomiNPPDataSource) o);
1901    }
1902
1903    public MultiSpectralData getMultiSpectralData() {
1904        return multiSpectralData.get(0);
1905    }
1906    
1907    public MultiSpectralData getMultiSpectralData(DataChoice choice) {
1908        return msdMap.get(choice.getName());
1909    }
1910
1911    public String getDatasetName() {
1912      return filename;
1913    }
1914
1915        /**
1916         * @return the qfMap
1917         */
1918        public Map<String, QualityFlag> getQfMap() {
1919                return qfMap;
1920        }
1921
1922        public void setDatasetName(String name) {
1923      filename = name;
1924    }
1925
1926    /**
1927     * Determine if this data source originated from a
1928         * {@literal "NOAA file"}.
1929     *
1930     * @return {@code true} if file came from NOAA, {@code false} otherwise.
1931     */
1932    public boolean isNOAA() {
1933        return isNOAA;
1934    }
1935
1936    public Map<String, double[]> getSubsetFromLonLatRect(MultiDimensionSubset select, GeoSelection geoSelection) {
1937      GeoLocationInfo ginfo = geoSelection.getBoundingBox();
1938      return adapters[0].getSubsetFromLonLatRect(select.getSubset(), ginfo.getMinLat(), ginfo.getMaxLat(),
1939                                        ginfo.getMinLon(), ginfo.getMaxLon());
1940    }
1941
1942    public synchronized Data getData(DataChoice dataChoice, DataCategory category,
1943                                DataSelection dataSelection, Hashtable requestProperties)
1944                                throws VisADException, RemoteException {
1945       return this.getDataInner(dataChoice, category, dataSelection, requestProperties);
1946    }
1947
1948
1949    protected Data getDataInner(DataChoice dataChoice, DataCategory category,
1950                                DataSelection dataSelection, Hashtable requestProperties)
1951                                throws VisADException, RemoteException {
1952
1953        //- this hack keeps the HydraImageProbe from doing a getData()
1954        //- TODO: need to use categories?
1955        if (requestProperties != null) {
1956          if ((requestProperties.toString()).equals("{prop.requester=MultiSpectral}")) {
1957            return null;
1958          }
1959        }
1960
1961        GeoLocationInfo ginfo = null;
1962        GeoSelection geoSelection = null;
1963        
1964        if ((dataSelection != null) && (dataSelection.getGeoSelection() != null)) {
1965            if (! isDerived) {
1966                geoSelection = (dataSelection.getGeoSelection().getBoundingBox() != null) ?
1967                    dataSelection.getGeoSelection() :
1968                    dataChoice.getDataSelection().getGeoSelection();
1969            } else {
1970                geoSelection = dataSelection.getGeoSelection();
1971            }
1972        }
1973
1974        if (geoSelection != null) {
1975           ginfo = geoSelection.getBoundingBox();
1976        }
1977
1978        Data data = null;
1979        if (adapters == null) {
1980          return data;
1981        }
1982
1983        MultiDimensionAdapter adapter = null;
1984        
1985        // pick the adapter with the same index as the current data choice
1986        int aIdx = 0;
1987        List<DataChoice> dcl = getDataChoices();
1988        for (DataChoice dc : dcl) {
1989                if (dc.getName().equals(dataChoice.getName())) {
1990                        aIdx = dcl.indexOf(dc);
1991                        break;
1992                }
1993        }
1994
1995        adapter = adapters[aIdx];
1996
1997        try {
1998
1999            if (ginfo != null) {
2000                subset = adapter.getSubsetFromLonLatRect(ginfo.getMinLat(), ginfo.getMaxLat(),
2001                                ginfo.getMinLon(), ginfo.getMaxLon(),
2002                                geoSelection.getXStride(),
2003                                geoSelection.getYStride(),
2004                                geoSelection.getZStride());
2005            }
2006            else {
2007
2008              MultiDimensionSubset select = null;
2009              Hashtable table = null;
2010              if (dataChoice instanceof DerivedDataChoice) {
2011                  List<DataChoice> children = ((DerivedDataChoice) dataChoice).getChoices();
2012                  DataChoice dc = children.get(0);
2013                  table = dc.getProperties();
2014              } else {
2015                  table = dataChoice.getProperties();
2016              }
2017              Enumeration keys = table.keys();
2018              while (keys.hasMoreElements()) {
2019                Object key = keys.nextElement();
2020                // TJJ - need a better way here to determine when a new, non-derived product has been selected
2021                if (key.toString().equals("Use_Display_Driver_Times")) {
2022                    isDerived = false;
2023                    derivedInit = false;
2024                }
2025                if (key instanceof MultiDimensionSubset) {
2026                  select = (MultiDimensionSubset) table.get(key);
2027                }
2028              }  
2029              subset = select.getSubset();
2030              if ((dataSelection != null) && (dataSelection.getGeoSelection() != null)) {
2031                 if (isDerived) {
2032                    // Only need to set this once
2033                    if (! derivedInit) {
2034                       derivedSubset = subset;
2035                       derivedInit = true;
2036                    } else {
2037                       subset = derivedSubset;
2038                    }
2039                 }
2040              }
2041
2042              if (dataSelection != null) {
2043                Hashtable props = dataSelection.getProperties();
2044                if (props != null) {
2045                  if (props.containsKey(SpectrumAdapter.channelIndex_name)) {
2046                          logger.debug("Props contains channel index key...");
2047                    double[] coords = subset.get(SpectrumAdapter.channelIndex_name);
2048                    int idx = ((Integer) props.get(SpectrumAdapter.channelIndex_name)).intValue();
2049                    coords[0] = (double) idx;
2050                    coords[1] = (double) idx;
2051                    coords[2] = (double) 1;
2052                  }
2053                }
2054              }
2055            }
2056
2057            if (subset != null) {
2058                // TJJ Feb 2021 - For derived products, we want to use the same subset for all
2059                // contributing bands
2060                data = adapter.getData(subset);
2061                data = applyProperties(data, requestProperties, subset, aIdx);
2062            }
2063        } catch (Exception e) {
2064            logger.error("getData Exception: ", e);
2065        }
2066        ////////// inq1429 return FieldImpl with time dim /////////////////
2067        if (data != null) {
2068                List dateTimes = new ArrayList();
2069                dateTimes.add(new DateTime(theDate));
2070                SampledSet timeSet = (SampledSet) ucar.visad.Util.makeTimeSet(dateTimes);
2071                FunctionType ftype = new FunctionType(RealType.Time, data.getType());
2072                FieldImpl fi = new FieldImpl(ftype, timeSet);
2073                fi.setSample(0, data);
2074                data = fi;
2075        }
2076        //////////////////////////////////////////////////////////////////
2077        return data;
2078    }
2079
2080    protected Data applyProperties(Data data, Hashtable requestProperties, Map<String, double[]> subset, int adapterIndex)
2081          throws VisADException, RemoteException {
2082      Data new_data = data;
2083
2084      if (requestProperties == null) {
2085        new_data = data;
2086        return new_data;
2087      }
2088
2089      return new_data;
2090    }
2091
2092    protected void initDataSelectionComponents(
2093         List<DataSelectionComponent> components,
2094             final DataChoice dataChoice) {
2095      
2096                  try {
2097                          // inq1429: need to handle FieldImpl here
2098              FieldImpl thing = null;
2099              if (dataChoice instanceof DerivedDataChoice) {
2100                  List<DataChoice> children = ((DerivedDataChoice) dataChoice).getChoices();
2101                  DataChoice dc = children.get(0);
2102                  thing = (FieldImpl) dc.getData(null);
2103              } else {
2104                  thing = (FieldImpl) dataChoice.getData(null);
2105              }
2106                          FlatField image;
2107                          if (GridUtil.isTimeSequence(thing)) {
2108                                  image = (FlatField) thing.getSample(0);
2109                          } else {
2110                                  image = (FlatField) thing;
2111                          }
2112                          if (image != null) {
2113                              // For derived data choices, pull out 1st in list for preview
2114                              PreviewSelection ps = null;
2115                              if (dataChoice instanceof DerivedDataChoice) {
2116                                  isDerived = true;
2117                                  List<DataChoice> children = ((DerivedDataChoice) dataChoice).getChoices();
2118                                  DataChoice dc = children.get(0);
2119                                  ps = new PreviewSelection(dc, image, null);
2120                              } else {
2121                                  ps = new PreviewSelection(dataChoice, image, null);
2122                              }
2123                                  // Region subsetting not yet implemented for CrIS data
2124                                  if (instrumentName.getStringValue().equals("CrIS")) {
2125                                          ps.enableSubsetting(false);
2126                                  }
2127                                  components.add(ps);
2128                          }
2129                  } catch (Exception e) {
2130                          logger.error("Can't make PreviewSelection: ", e);
2131                  }
2132      
2133    }
2134    
2135    /**
2136     * Add {@code Integer->String} translations to IDV's
2137     * {@literal "translations"} resource, so they will be made available to
2138     * the data probe of Image Display's.
2139     */
2140    public void initQfTranslations() {
2141        
2142        Map<String, Map<Integer, String>> translations =
2143                getIdv().getResourceManager().
2144                getTranslationsHashtable();
2145        
2146        for (String qfKey : qfMap.keySet()) {
2147                // This string needs to match up with the data choice name:
2148                String qfKeySubstr = qfKey.replace("All_Data/", "");
2149                // check if we've already added map for this QF
2150                if (!translations.containsKey(qfKeySubstr)) {
2151                        Map<String, String> hm = qfMap.get(qfKey).getHm();
2152                        Map<Integer, String> newMap = 
2153                                        new HashMap<Integer, String>(hm.size());
2154                        for (String dataValueKey : hm.keySet()) {
2155                                // convert Map<String, String> to Map<Integer, String>
2156                                Integer intKey = Integer.parseInt(dataValueKey);
2157                                newMap.put(intKey, hm.get(dataValueKey));
2158                        }
2159                        translations.put(qfKeySubstr, newMap);
2160                }
2161        }
2162    }
2163    
2164}