001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2025
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 java.io.File;
032import java.io.IOException;
033import java.util.ArrayList;
034import java.util.HashMap;
035import java.util.HashSet;
036import java.util.Iterator;
037import java.util.List;
038import java.util.Map;
039import java.util.Set;
040import java.util.StringTokenizer;
041
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044import ucar.nc2.Attribute;
045import ucar.nc2.NetcdfFile;
046
047/**
048 * Utility class to support Joint Polar Satellite System (JPSS) functionality.
049 * Documentation referenced is from Suomi NPP Common Data Format Control Book.
050 * See:
051 * http://npp.gsfc.nasa.gov/documents.html
052 * 
053 * @author tommyj
054 *
055 */
056
057public abstract class JPSSUtilities {
058        
059        private static final Logger logger =
060                LoggerFactory.getLogger(JPSSUtilities.class);
061        
062        public static final String JPSS_FIELD_SEPARATOR = "_";
063        public static final int NASA_CREATION_DATE_INDEX = 28;
064        public static final int NOAA_CREATION_DATE_INDEX = 35;
065        
066        // Flags to check for SIPS data V2.0.0 and higher
067        // Augments single fill value from prior versions
068        public static final String SIPS_BOWTIE_DELETED_FLAG = "Bowtie_Deleted";
069        public static final String SIPS_FLAG_MEANINGS_ATTRIBUTE = "flag_meanings";
070        public static final String SIPS_FLAG_VALUES_ATTRIBUTE = "flag_values";
071        
072        // This regular expression matches a Suomi NPP Data Product as defined by the 
073        // NOAA spec in CDFCB-X Volume 1, Page 21
074        public static final String SUOMI_NPP_REGEX_NOAA =
075                // Product Id, Multiple (ex: VSSTO-GATMO-VSLTO)
076                "(\\w\\w\\w\\w\\w-)*" + 
077                // Product Id, Single (ex: VSSTO)
078                "\\w\\w\\w\\w\\w" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
079                // Spacecraft Id (ex: npp)
080                "\\w\\w\\w" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
081                // Data Start Date (ex: dYYYYMMDD)
082                "d20[0-3]\\d[0-1]\\d[0-3]\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
083                // Data Start Time (ex: tHHMMSSS)
084                "t[0-2]\\d[0-5]\\d[0-6]\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
085                // Data Stop Time (ex: eHHMMSSS)
086                "e[0-2]\\d[0-5]\\d[0-6]\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
087                // Orbit Number (ex: b00015)
088                "b\\d\\d\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
089                // Creation Date (ex: cYYYYMMDDHHMMSSSSSSSS)
090                "c20[0-3]\\d[0-1]\\d[0-3]\\d[0-2]\\d[0-5]\\d[0-6]\\d\\d\\d\\d\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
091                // Origin (ex: navo)
092                "\\w\\w\\w\\w" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
093                // Domain (ex: ops)
094                "\\w\\w\\w" + 
095                // HDF5 suffix
096                ".h5";
097
098            // This regular expression matches a VIIRS Enterprise EDR
099        public static final String JPSS_REGEX_ENTERPRISE_EDR =
100            // Product Id, Single (ex: JRR-CloudPhase)
101            "(\\w|-)*" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
102            // Version (e.g v2r3)
103            "v\\dr\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
104            // Origin (ex: npp)
105            "\\w\\w\\w" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
106            // Data Start Date/Time (ex: sYYYYMMDDHHMMSSS)
107            "s20[1-3]\\d[0-1]\\d[0-3]\\d\\d\\d\\d\\d\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
108            // Data End Date/Time (ex: eYYYYMMDDHHMMSSS)
109            "e20[1-3]\\d[0-1]\\d[0-3]\\d\\d\\d\\d\\d\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
110            // Creation Date/Time (ex: cYYYYMMDDHHMMSSS)
111            "c20[1-3]\\d[0-1]\\d[0-3]\\d\\d\\d\\d\\d\\d\\d\\d" +
112            // NetCDF 4 suffix
113            ".nc";
114
115        // This regular expression matches a Suomi NPP Data Product as defined by the 
116        // NASA spec in TBD (XXX TJJ - find out where is this documented?)
117        public static final String SUOMI_NPP_REGEX_NASA =
118                        // Product Id, Single (ex: VL1BM)
119                        // NOTE: These are the only supported NASA data at this time
120                "(VL1BD|VL1BI|VL1BM)" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
121                // Platform - always Suomi NPP
122                "snpp" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
123                // Data Start Date (ex: dYYYYMMDD)
124                "d20[0-3]\\d[0-1]\\d[0-3]\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
125                // Data Start Time (ex: tHHMM)
126                "t[0-2]\\d[0-5]\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
127                // Creation Date (ex: cYYYYMMDDHHMMSSSSSSSS)
128                "c20[0-3]\\d[0-1]\\d[0-3]\\d\\d\\d\\d\\d\\d\\d" +
129                // NetCDF 4 suffix
130                ".nc";
131        
132        // This regular expression matches a Suomi NPP geolocation file as defined by the 
133        // NASA spec in TBD
134        public static final String SUOMI_GEO_REGEX_NASA =
135                        // Product Id, Single (ex: VGEOM)
136                        // NOTE: This MUST match the list of product ids in static array in this file!
137                "(VGEOD|VGEOI|VGEOM)" + 
138                        JPSSUtilities.JPSS_FIELD_SEPARATOR +
139                // Platform - always Suomi NPP
140                "snpp" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
141                // Data Start Date (ex: dYYYYMMDD)
142                "d20[0-3]\\d[0-1]\\d[0-3]\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
143                // Data Start Time (ex: tHHMM)
144                "t[0-2]\\d[0-5]\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
145                // Creation Date (ex: cYYYYMMDDHHMMSSSSSSSS)
146                "c20[0-3]\\d[0-1]\\d[0-3]\\d\\d\\d\\d\\d\\d\\d" +
147                // NetCDF 4 suffix
148                ".nc";
149        
150        public static String[] validNASAVariableNames = {
151                "M01",
152                "M02",
153                "M03",
154                "M04",
155                "M05",
156                "M06",
157                "M07",
158                "M08",
159                "M09",
160                "M10",
161                "M11",
162                "M12",
163                "M13",
164                "M14",
165                "M15",
166                "M16",
167                "I01",
168                "I02",
169                "I03",
170                "I04",
171                "I05",
172                "DNB_observations"
173        };
174        
175        public static float[] ATMSChannelCenterFrequencies = {
176                23.8f,
177                31.4f,
178                50.3f,
179                51.76f,
180                52.8f,
181                53.596f,
182                54.40f,
183                54.94f,
184                55.50f,
185                57.29032f,
186                57.29033f,
187                57.29034f,
188                57.29035f,
189                57.29036f,
190                57.29037f,
191                88.20f,
192                165.5f,
193                183.3101f,
194                183.3102f,
195                183.3103f,
196                183.3104f,
197                183.3105f
198        };
199        
200        // the list of valid geolocation product ids
201        public static String[] geoProductIDs = {
202        "GATMO",
203        "GCRSO",
204        "GAERO",
205        "GCLDO",
206        "GDNBO",
207        "GNCCO",
208        "GIGTO",
209        "GIMGO",
210        "GITCO",
211        "GMGTO",
212        "GMODO",
213        "GMTCO",
214        "GNHFO",
215        "GOTCO",
216        "GOSCO",
217        "GONPO",
218        "GONCO",
219        "GCRIO",
220        "GATRO",
221        "IVMIM",
222        "VGEOD",
223        "VGEOI",
224        "VGEOM",
225        "VMUGE"
226        };  
227        
228        // This regular expression matches a Suomi NPP geolocation granule, see 
229        // NOAA spec in CDFCB-X Volume 1, Page 21
230        public static final String SUOMI_GEO_REGEX_NOAA =
231                // Geo Id, Single (ex: GMODO)
232                        // NOTE: This MUST match the list of product ids in static array above!
233                "(GATMO|GCRSO|GAERO|GCLDO|GDNBO|GNCCO|GIGTO|GIMGO|GITCO|" + 
234                        "GMGTO|GMODO|GMTCO|GNHFO|GOTCO|GOSCO|GONPO|GONCO|GCRIO|GATRO|IVMIM|VMUGE)" + 
235                        JPSSUtilities.JPSS_FIELD_SEPARATOR +
236                // Spacecraft Id (ex: npp)
237                "\\w\\w\\w" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
238                // Data Start Date (ex: dYYYYMMDD)
239                "d20[0-3]\\d[0-1]\\d[0-3]\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
240                // Data Start Time (ex: tHHMMSSS)
241                "t[0-2]\\d[0-5]\\d[0-6]\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
242                // Data Stop Time (ex: eHHMMSSS)
243                "e[0-2]\\d[0-5]\\d[0-6]\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
244                // Orbit Number (ex: b00015)
245                "b\\d\\d\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
246                // Creation Date (ex: cYYYYMMDDHHMMSSSSSSSS)
247                "c20[0-3]\\d[0-1]\\d[0-3]\\d[0-2]\\d[0-5]\\d[0-6]\\d\\d\\d\\d\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
248                // Origin (ex: navo)
249                "\\w\\w\\w\\w" + JPSSUtilities.JPSS_FIELD_SEPARATOR +
250                // Domain (ex: ops)
251                "\\w\\w\\w" + 
252                // HDF5 suffix
253                ".h5";
254        
255        /**
256         * Determine if the input variable name is a valid NASA product
257         *
258         * @param varName Variable name to validate.
259         *
260         * @return {@code true} if {@code varName} passes checks.
261         */
262        
263        public static boolean isValidNASA(String varName) {
264                boolean isValid = false;
265                if (varName == null) return isValid; 
266                for (String s : validNASAVariableNames) {
267                        if (s.equals(varName)) {
268                                isValid = true;
269                                break;
270                        }
271                }
272                return isValid;
273        }
274
275    /**
276     * An unusual method used to filter out files of type "GCRSO-SCRIF-SCRIS",
277     * where CrIS Science and Full Spectrum data are combined by CLASS into
278     * a single granule.  These are deemed an odd aggregation that does not
279     * make sense combined into a single product, and are disallowed.
280     *
281     * @param fileList List of files to validate.
282     *
283     * @return {@code false} if {@code fileList} passes checks. Note reverse
284     * of typical boolean logic!  True is retuned if data is INVALID.
285     */
286
287    public static boolean isInvalidCris(List fileList) {
288        for (Object o : fileList) {
289            String filename = (String) o;
290            if (filename.contains("SCRIF-SCRIS")) {
291                return true;
292            }
293        }
294        return false;
295    }
296
297        /**
298         * Determine if the set if filenames constitutes contiguous SNPP granules
299         * of the same geographic coverage.
300         *
301         * @param fileList List of files to validate.
302         *
303         * @return {@code true} if {@code fileList} passes checks.
304         */
305        
306        public static boolean isValidSet(List fileList) {
307                
308                // map with filename from start date through orbit will be used for comparisons
309        Map<String, List<String>> metadataMap = new HashMap<String, List<String>>();
310        
311        // Pass 1, populate the list of products selected, and empty maps
312        for (Object o : fileList) {
313                String filename = (String) o;
314                // start at last path separator to clip off absolute paths
315                int lastSeparator = filename.lastIndexOf(File.separatorChar);
316                // NOAA style products
317                if (filename.endsWith(".h5")) {
318                        // products go to first underscore, see regex above for more detail
319                        int firstUnderscore = filename.indexOf("_", lastSeparator + 1);
320                        String prodStr = filename.substring(lastSeparator + 1, firstUnderscore);
321                        if (! metadataMap.containsKey(prodStr)) {
322                                        List<String> l = new ArrayList<String>();
323                                        metadataMap.put(prodStr, l);
324                        }
325                }
326                // NASA style products
327                if (filename.endsWith(".nc")) {
328                        // products end at first underscore, see regex above for more detail
329                        int firstUnderscore = filename.indexOf("_", lastSeparator + 1);
330                        int secondUnderscore = filename.indexOf("_", firstUnderscore + 1);
331                        String prodStr = filename.substring(firstUnderscore + 1, secondUnderscore);
332                        if (! metadataMap.containsKey(prodStr)) {
333                                        List<String> l = new ArrayList<String>();
334                                        metadataMap.put(prodStr, l);
335                        }
336                }
337        }
338        
339        // Pass 2, build up the lists of meta data strings and full filenames
340        for (Object o : fileList) {
341                String filename = (String) o;
342                // start at last path separator to clip off absolute paths
343                int lastSeparator = filename.lastIndexOf(File.separatorChar);
344                
345                // NOAA style products
346                if (filename.endsWith(".h5")) {
347                        // products go to first underscore, see regex above for more detail
348                        int firstUnderscore = filename.indexOf("_", lastSeparator + 1);
349                        // this is the key for the maps
350                        String prodStr = filename.substring(lastSeparator + 1, firstUnderscore);
351                        // this is the value for the meta data map - start time through orbit 
352                        String metaStr = filename.substring(firstUnderscore + 1, firstUnderscore + 39);
353                        // get the appropriate list, add the new value
354                        List<String> l = (List<String>) metadataMap.get(prodStr);
355                        l.add(metaStr);
356                        metadataMap.put(prodStr, l);
357                }
358                
359                // NASA style products
360                if (filename.endsWith(".nc")) {
361                        // products end at first underscore, see regex above for more detail
362                        int firstUnderscore = filename.indexOf("_", lastSeparator + 1);
363                        int secondUnderscore = filename.indexOf("_", firstUnderscore + 1);
364                        // this is the key for the maps
365                        String prodStr = filename.substring(firstUnderscore + 1, secondUnderscore);
366                        // this is the value for the meta data map - date and time 
367                        int dateIndex = filename.indexOf("_d");
368                        int creationIndex = filename.indexOf("_c");
369                        String metaStr = filename.substring(dateIndex + 1, creationIndex);
370                        // get the appropriate list, add the new value
371                        List<String> l = (List<String>) metadataMap.get(prodStr);
372                        l.add(metaStr);
373                        metadataMap.put(prodStr, l);
374                }
375        }
376        
377        // loop over metadata map, every list much match the one for ALL other products
378        Set<String> s = metadataMap.keySet();
379        Iterator iterator = s.iterator();
380        List prvList = null;
381        while (iterator.hasNext()) {
382                String key = (String) iterator.next();
383                List l = (List) metadataMap.get(key);
384                for (int i = 0; i < l.size(); i++) {
385                        if (prvList != null) {
386                                if (! l.equals(prvList)) return false;
387                        }
388                }
389                prvList = l;
390        }
391        
392                return true;
393        }
394
395        /**
396         * Replace last substring within input string which matches the input with the 
397         * provided replacement string.
398         * 
399         * @param string
400         * @param substring
401         * @param replacestr
402         * @return
403         */
404        
405        public static String replaceLast(String string, String substring, String replacestr) {
406                // Sanity check on input
407                if (string == null) return null;
408                if ((substring == null) || (replacestr == null)) return string;
409                
410                int index = string.lastIndexOf(substring);
411                
412                // substring not present
413                if (index == -1)
414                        return string;
415                // it's there, swap it
416                return string.substring(0, index) + replacestr
417                                + string.substring(index + substring.length());
418        }
419        
420        /**
421         * Determine if a set if filenames which constitutes contiguous SNPP
422         * granules of various products all share the same geolocation data type.
423         *
424         * @param fileList List of files to validate.
425         * @param directory Used when {@literal "GEO"} is not embedded within one
426         *                  of {@code fileList}.
427         *
428         * @return {@code true} if {@code fileList} passes checks.
429         */
430        
431        public static boolean hasCommonGeo(List fileList, File directory) {
432                Set<String> s = new HashSet<String>();
433                boolean isCombinedProduct = false;
434                
435                // loop through all filenames provided
436        for (Object o : fileList) {
437                isCombinedProduct = false;
438                String filename = (String) o;
439                
440                // check the case where GEO is embedded in the data granules
441                int lastSeparator = filename.lastIndexOf(File.separatorChar);
442                int firstUnderscore = filename.indexOf("_", lastSeparator + 1);
443                String prodStr = filename.substring(lastSeparator + 1, firstUnderscore);
444            StringTokenizer st = new StringTokenizer(prodStr, "-");
445            while (st.hasMoreTokens()) {
446                String singleProd = st.nextToken();
447                for (int i = 0; i < JPSSUtilities.geoProductIDs.length; i++) {
448                        if (singleProd.equals(JPSSUtilities.geoProductIDs[i])) {
449                                s.add(singleProd);
450                                isCombinedProduct = true;
451                                break;
452                        }
453                }
454            }
455            // GEO not embedded in file, need to see which GEO file is
456                        // referenced in the global attribute
457            if (! isCombinedProduct) {
458                try {
459                        String fileFullPath = directory.getAbsolutePath() + File.separator + filename;
460                                        NetcdfFile ncfile = NetcdfFile.open(fileFullPath);
461                                        Attribute a = ncfile.findGlobalAttribute("N_GEO_Ref");
462                                        if (a != null) {
463                                                String geoFromAttr = a.getStringValue().substring(0, 5);
464                                                s.add(geoFromAttr);
465                                        }
466                                        ncfile.close();
467                                } catch (IOException ioe) {
468                        logger.error("problem reading from attribute", ioe);
469                                }
470            }
471        }
472        
473        // if the products chosen utilize multiple GEO types, fail the selection
474        if (s.size() > 1) return false;
475                return true;
476        }
477        
478}