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 */
028package edu.wisc.ssec.mcidasv.chooser;
029
030import java.awt.BorderLayout;
031import java.io.File;
032import java.text.ParseException;
033import java.text.SimpleDateFormat;
034import java.util.ArrayList;
035import java.util.Arrays;
036import java.util.Collections;
037import java.util.Date;
038import java.util.Vector;
039
040import javax.swing.JFileChooser;
041import javax.swing.JOptionPane;
042import javax.swing.JPanel;
043import javax.swing.filechooser.FileFilter;
044
045import org.slf4j.Logger;
046import org.slf4j.LoggerFactory;
047
048import edu.wisc.ssec.mcidasv.data.hydra.JPSSUtilities;
049import ucar.unidata.idv.chooser.IdvChooserManager;
050import ucar.unidata.util.StringUtil;
051
052public class SuomiNPPChooser extends FileChooser {
053        
054        private static final long serialVersionUID = 1L;
055        private static final Logger logger = LoggerFactory.getLogger(SuomiNPPChooser.class);
056        // Our consecutive granule "slop"
057        // No granule of any type should be shorter than this
058        // And therefore no gap between consecutive granules could ever be greater. 5 seconds feels safe
059        private static final long CONSECUTIVE_GRANULE_MAX_GAP_MS = 5000;
060        private static final long CONSECUTIVE_GRANULE_MAX_GAP_MS_NASA = 360000;
061        
062        // date formatters for converting Suomi NPP day/time from file name for consecutive granule check
063    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssS");
064    private static final SimpleDateFormat sdfNASA = new SimpleDateFormat("yyyyMMddHHmm");
065
066    /**
067     * Create the chooser with the given manager and xml
068     *
069     * @param mgr The manager
070     * @param root The xml
071     *
072     */
073    
074    public SuomiNPPChooser(IdvChooserManager mgr, org.w3c.dom.Element root) {
075        super(mgr, root);
076    }
077   
078    /**
079     * Make the file chooser
080     *
081     * @param path   the initial path
082     *
083     * @return  the file chooser
084     */
085    
086    protected JFileChooser doMakeFileChooser(String path) {
087        if (fileChooser == null) {
088                logger.debug("Creating Suomi NPP File Chooser...");
089                fileChooser = new SuomiNPPFileChooser(path, this);
090        } else {
091                logger.debug("2nd call to doMakeFileChooser, why?");
092        }
093        return fileChooser;
094    }
095
096    /**
097     * Handle the selection of the set of files
098     *
099     * @param files The files the user chose
100     * @param directory The directory they chose them from
101     * @return True if the file was successful
102     * @throws Exception
103     */
104    
105    protected boolean selectFilesInner(File[] files, File directory)
106            throws Exception {
107        if ((files == null) || (files.length == 0)) {
108            userMessage("Please select a file");
109            return false;
110        }
111
112        // make a list of just the file names
113        ArrayList<String> fileNames = new ArrayList<String>();
114        for (int i = 0; i < files.length; i++) {
115                fileNames.add(files[i].getName());
116        }
117
118        // Kick out attempts to load "GCRSO-SCRIF-SCRIS" products, which for some
119        // reason CLASS will provide, but scientists have told us we do not need
120        // to support.
121        if (JPSSUtilities.isInvalidCris(fileNames)) {
122            JOptionPane.showMessageDialog(null,
123                    "CrIS files containing both Full Spectral Science SDR (SCRIF)\n" +
124                    "and Science SDR (SCRIS) are not supported.");
125            return false;
126        }
127
128        // ensure these files make sense as a set to create a single SNPP data source
129        if (! JPSSUtilities.isValidSet(fileNames)) {
130                JOptionPane.showMessageDialog(null, 
131                        "Unable to group selected data as a single data source.");
132                return false;
133        }
134        
135        // ensure these files make sense as a set to create a single SNPP data source
136        if (! JPSSUtilities.hasCommonGeo(fileNames, directory)) {
137                JOptionPane.showMessageDialog(null, 
138                        "Unable to group selected data as a single data source.");
139                return false;
140        }
141        
142        // At present, Suomi NPP chooser only allows selecting sets of consecutive granules
143        int granulesAreConsecutive = -1;
144        // Consecutive granule check - can only aggregate a contiguous set
145        if (files.length > 1) {
146           granulesAreConsecutive = testConsecutiveGranules(files);
147        }
148        
149        // Need to reverse file list, so granules are increasing time order
150        if (granulesAreConsecutive == 1) {
151           Collections.reverse(Arrays.asList(files));
152        }
153        
154        if ((granulesAreConsecutive >= 0) || (files.length == 1)) {
155                return super.selectFilesInner(files, directory);
156        } else {
157                // throw up a dialog to tell user the problem
158            JOptionPane.showMessageDialog(this,
159                "When selecting multiple granules, they must be consecutive and from the same satellite.");
160        }
161        return false;
162    }
163
164    /**
165     * Test whether a set of files are consecutive Suomi NPP granules,
166     * any sensor. NOTE: This method works when the file list contains
167     * multiple products ONLY because once we've validate one product,
168     * the time check will be a negative number when comparing the FIRST
169     * granule of product 2 with the LAST granule of product 1. A better
170     * implementation would be to pass in the filename map like the 
171     * one generated in SuomiNPPDataSource constructor.
172     * 
173     * @param files
174     * @return 0 if consecutive tests pass for all files
175     *        -1 if tests fail
176     *         1 if tests pass but file order is backward 
177     *           (decreasing time order)
178     */
179    
180    private int testConsecutiveGranules(File[] files) {
181        int testResult = -1;
182        if (files == null) return testResult;
183        
184        // TJJ Jan 2016 - different checks for NASA data, 6 minutes per granule
185        File f = files[0];
186
187        if (f.getName().matches(JPSSUtilities.SUOMI_NPP_REGEX_NASA)) {
188                        // compare start time of current granule with end time of previous
189                // difference should be very small - under a second
190                long prvTime = -1;
191                testResult = 0;
192                for (int i = 0; i < files.length; i++) {
193                    if ((files[i] != null) && !files[i].isDirectory()) {
194                        if (files[i].exists()) {
195                                String fileName = files[i].getName();
196                                int dateIndex = fileName.lastIndexOf("_d2") + 2;
197                                int timeIndex = fileName.lastIndexOf("_t") + 2;
198                                String dateStr = fileName.substring(dateIndex, dateIndex + 8);
199                                String timeStr = fileName.substring(timeIndex, timeIndex + 4);
200                            // pull start and end time out of file name
201                            Date dS = null;
202                            try {
203                                                        dS = sdfNASA.parse(dateStr + timeStr);
204                            } catch (ParseException pe) {
205                                                        logger.error("Not recognized as valid Suomi NPP file name: " + fileName);
206                                                        testResult = -1;
207                                                        break;
208                            }
209                                                long curTime = dS.getTime();
210                                                // only check current with previous
211                                                if (prvTime > 0) {
212                                                        // make sure time diff does not exceed allowed threshold
213                                                        // consecutive granules should be less than 1 minute apart
214                                                        if ((curTime - prvTime) > CONSECUTIVE_GRANULE_MAX_GAP_MS_NASA) {
215                                                                testResult = -1;
216                                                                break;
217                                                        }
218                            // TJJ Inq #2265, #2370. Granules need to be increasing time order 
219                            // to properly georeference. If they are reverse order but pass
220                                                        // all consecutive tests, we just reverse the list before returning
221                            if (curTime < prvTime) {
222                                testResult = 1;
223                                break;
224                            }
225                                                }
226                                                prvTime = curTime;
227                        }
228                    }
229                }
230
231            // consecutive granule check for NOAA data
232        } else {
233                        // compare start time of current granule with end time of previous
234                // difference should be very small - under a second
235                long prvTime = -1;
236            long prvStartTime = -1;
237            long prvEndTime = -1;
238                testResult = 0;
239            int lastSeparator = -1;
240            int firstUnderscore = -1;
241            String prodStr = "";
242            String prevPrd = "";
243            String dateIdx = "_d2";
244            String startTimeIdx = "_t";
245            String endTimeIdx = "_e";
246            String curPlatformStr = null;
247            String prvPlatformStr = null;
248            int firstSeparator = -1;
249            int timeFieldStart = 2;
250            if (f.getName().matches(JPSSUtilities.JPSS_REGEX_ENTERPRISE_EDR)) {
251                dateIdx = "_s";
252                startTimeIdx = "_s";
253                endTimeIdx = "_e";
254                timeFieldStart = 10;
255            }
256                for (int i = 0; i < files.length; i++) {
257                    if ((files[i] != null) && !files[i].isDirectory()) {
258                        if (files[i].exists()) {
259                        String fileName = files[i].getName();
260
261                        // get platform - 3 chars after first separator char
262                        firstSeparator = fileName.indexOf(JPSSUtilities.JPSS_FIELD_SEPARATOR);
263                        curPlatformStr = fileName.substring(firstSeparator + 1, firstSeparator + 4);
264                        logger.debug("platform: " + curPlatformStr);
265                        if ((prvPlatformStr != null) && (! curPlatformStr.equals(prvPlatformStr))) {
266                            logger.warn("Mixed platforms in filelist: " +
267                                curPlatformStr + ", and: " + prvPlatformStr);
268                            testResult = -1;
269                            break;
270                        }
271                        prvPlatformStr = curPlatformStr;
272
273                        lastSeparator = fileName.lastIndexOf(File.separatorChar);
274                        firstUnderscore = fileName.indexOf("_", lastSeparator + 1);
275                        prodStr = fileName.substring(lastSeparator + 1, firstUnderscore);
276                        // reset check if product changes
277                        if (! prodStr.equals(prevPrd)) prvTime = -1;
278                        int dateIndex = fileName.lastIndexOf(dateIdx) + 2;
279                        int timeIndexStart = fileName.lastIndexOf(startTimeIdx) + timeFieldStart;
280                        int timeIndexEnd = fileName.lastIndexOf(endTimeIdx) + timeFieldStart;
281                        String dateStr = fileName.substring(dateIndex, dateIndex + 8);
282                        String timeStrStart = fileName.substring(timeIndexStart, timeIndexStart + 7);
283                        String timeStrEnd = fileName.substring(timeIndexEnd, timeIndexEnd + 7);
284                                // sanity check on file name lengths
285                                int fnLen = fileName.length();
286                                if ((dateIndex > fnLen) || (timeIndexStart > fnLen) || (timeIndexEnd > fnLen)) {
287                                        logger.warn("unexpected file name length for: " + fileName);
288                                        testResult = -1;
289                                        break;
290                                }
291                            // pull start and end time out of file name
292                            Date dS = null;
293                            Date dE = null;
294
295                            try {
296                                                        dS = sdf.parse(dateStr + timeStrStart);
297                                                        // due to nature of Suomi NPP file name encoding, we need a special
298                                                        // check here - end time CAN roll over to next day, while day part 
299                                                        // does not change.  if this happens, we tweak the date string
300                                                        String endDateStr = dateStr;
301                                                        String startHour = timeStrStart.substring(0, 2);
302                                                        String endHour = timeStrEnd.substring(0, 2);
303                                                        if ((startHour.equals("23")) && (endHour.equals("00"))) {
304                                                                // temporarily convert date to integer, increment, convert back
305                                                                int tmpDate = Integer.parseInt(dateStr);
306                                                                tmpDate++;
307                                                                endDateStr = "" + tmpDate;
308                                                                logger.info("Granule time spanning days case handled ok...");
309                                                        }
310                                                        dE = sdf.parse(endDateStr + timeStrEnd);
311                                                } catch (ParseException e) {
312                                                        logger.error("Not recognized as valid Suomi NPP file name: " + fileName);
313                                                        testResult = -1;
314                                                        break;
315                                                }
316                                                long curTime = dS.getTime();
317                                                long endTime = dE.getTime();
318
319                                                // only check current with previous
320                                                if (prvTime > 0) {
321
322                                                        // Make sure time diff does not exceed allowed threshold for the sensor
323                                                        // Whatever the granule size, the time gap cannot exceed our defined "slop"
324                                                        logger.debug("curTime (ms): " + curTime);
325                                                        logger.debug("prvTime (ms): " + prvTime);
326                                                        logger.debug("curTime - prvEndTime (ms): " + Math.abs(curTime - prvEndTime));
327                                                        if (Math.abs(curTime - prvEndTime) > CONSECUTIVE_GRANULE_MAX_GAP_MS) {
328                                                                // Make sure there really is a gap, and not granule overlap
329                                                                if (prvEndTime < curTime) {
330                                                                        testResult = -1;
331                                                                        break;
332                                                                }
333                                                        }
334
335                            // TJJ Inq #2265, #2370. Granules need to be increasing time order 
336                            // to properly georeference. If they are reverse order but pass
337                            // all consecutive tests, we just reverse the list before returning
338                                                        if (curTime < prvStartTime) {
339                                                            testResult = 1;
340                                                            break;
341                                                        }
342
343                                                }
344                                                prvTime = curTime;
345                                                prvStartTime = curTime;
346                                                prvEndTime = endTime;
347                                                prevPrd = prodStr;
348                        }
349                    }
350                }
351        }
352                return testResult;
353        }
354
355        /**
356     * Convert the given array of File objects
357     * to an array of String file names. Only
358     * include the files that actually exist.
359     *
360     * @param files Selected files
361     * @return Selected files as Strings
362     */
363    
364    protected String[] getFileNames(File[] files) {
365        if (files == null) {
366            return (String[]) null;
367        }
368        Vector<String> v = new Vector<String>();
369        String fileNotExistsError = "";
370
371        // NOTE:  If multiple files are selected, then missing files
372        // are not in the files array.  If one file is selected and
373        // it is not there, then it is in the array and file.exists()
374        // is false
375        for (int i = 0; i < files.length; i++) {
376            if ((files[i] != null) && !files[i].isDirectory()) {
377                if ( !files[i].exists()) {
378                    fileNotExistsError += "File does not exist: " + files[i] + "\n";
379                } else {
380                    v.add(files[i].toString());
381                }
382            }
383        }
384
385        if (fileNotExistsError.length() > 0) {
386            userMessage(fileNotExistsError);
387            return null;
388        }
389
390        return v.isEmpty()
391               ? null
392               : StringUtil.listToStringArray(v);
393    }
394    
395    /**
396     * Get the bottom panel for the chooser
397     * @return the bottom panel
398     */
399    
400    protected JPanel getBottomPanel() {
401        // No bottom panel at present
402        return null;
403    }
404    
405    /**
406     * Get the center panel for the chooser
407     * @return the center panel
408     */
409    
410    protected JPanel getCenterPanel() {
411        JPanel centerPanel = super.getCenterPanel();
412
413        JPanel jp = new JPanel(new BorderLayout()) {
414                public void paint(java.awt.Graphics g) {
415                        FileFilter ff = fileChooser.getFileFilter();
416                        if (! (ff instanceof SuomiNPPFilter)) {
417                                fileChooser.setAcceptAllFileFilterUsed(false);
418                                fileChooser.setFileFilter(new SuomiNPPFilter());
419                        }
420                        super.paint(g);
421                }
422        };
423        jp.add(centerPanel);
424
425        return jp; 
426    }
427    
428}