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 */
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        // ensure these files make sense as a set to create a single SNPP data source
119        if (! JPSSUtilities.isValidSet(fileNames)) {
120                JOptionPane.showMessageDialog(null, 
121                        "Unable to group selected data as a single data source.");
122                return false;
123        }
124        
125        // ensure these files make sense as a set to create a single SNPP data source
126        if (! JPSSUtilities.hasCommonGeo(fileNames, directory)) {
127                JOptionPane.showMessageDialog(null, 
128                        "Unable to group selected data as a single data source.");
129                return false;
130        }
131        
132        // At present, Suomi NPP chooser only allows selecting sets of consecutive granules
133        int granulesAreConsecutive = -1;
134        // Consecutive granule check - can only aggregate a contiguous set
135        if (files.length > 1) {
136           granulesAreConsecutive = testConsecutiveGranules(files);
137        }
138        
139        // Need to reverse file list, so granules are increasing time order
140        if (granulesAreConsecutive == 1) {
141           Collections.reverse(Arrays.asList(files));
142        }
143        
144        if ((granulesAreConsecutive >= 0) || (files.length == 1)) {
145                return super.selectFilesInner(files, directory);
146        } else {
147                // throw up a dialog to tell user the problem
148            JOptionPane.showMessageDialog(this,
149                "When selecting multiple granules, they must be consecutive and from the same satellite.");
150        }
151        return false;
152    }
153
154    /**
155     * Test whether a set of files are consecutive Suomi NPP granules,
156     * any sensor. NOTE: This method works when the file list contains
157     * multiple products ONLY because once we've validate one product,
158     * the time check will be a negative number when comparing the FIRST
159     * granule of product 2 with the LAST granule of product 1. A better
160     * implementation would be to pass in the filename map like the 
161     * one generated in SuomiNPPDataSource constructor.
162     * 
163     * @param files
164     * @return 0 if consecutive tests pass for all files
165     *        -1 if tests fail
166     *         1 if tests pass but file order is backward 
167     *           (decreasing time order)
168     */
169    
170    private int testConsecutiveGranules(File[] files) {
171        int testResult = -1;
172        if (files == null) return testResult;
173        
174        // TJJ Jan 2016 - different checks for NASA data, 6 minutes per granule
175        File f = files[0];
176
177        if (f.getName().matches(JPSSUtilities.SUOMI_NPP_REGEX_NASA)) {
178                        // compare start time of current granule with end time of previous
179                // difference should be very small - under a second
180                long prvTime = -1;
181                testResult = 0;
182                for (int i = 0; i < files.length; i++) {
183                    if ((files[i] != null) && !files[i].isDirectory()) {
184                        if (files[i].exists()) {
185                                String fileName = files[i].getName();
186                                int dateIndex = fileName.lastIndexOf("_d2") + 2;
187                                int timeIndex = fileName.lastIndexOf("_t") + 2;
188                                String dateStr = fileName.substring(dateIndex, dateIndex + 8);
189                                String timeStr = fileName.substring(timeIndex, timeIndex + 4);
190                            // pull start and end time out of file name
191                            Date dS = null;
192                            try {
193                                                        dS = sdfNASA.parse(dateStr + timeStr);
194                            } catch (ParseException pe) {
195                                                        logger.error("Not recognized as valid Suomi NPP file name: " + fileName);
196                                                        testResult = -1;
197                                                        break;
198                            }
199                                                long curTime = dS.getTime();
200                                                // only check current with previous
201                                                if (prvTime > 0) {
202                                                        // make sure time diff does not exceed allowed threshold
203                                                        // consecutive granules should be less than 1 minute apart
204                                                        if ((curTime - prvTime) > CONSECUTIVE_GRANULE_MAX_GAP_MS_NASA) {
205                                                                testResult = -1;
206                                                                break;
207                                                        }
208                            // TJJ Inq #2265, #2370. Granules need to be increasing time order 
209                            // to properly georeference. If they are reverse order but pass
210                                                        // all consecutive tests, we just reverse the list before returning
211                            if (curTime < prvTime) {
212                                testResult = 1;
213                                break;
214                            }
215                                                }
216                                                prvTime = curTime;
217                        }
218                    }
219                }
220
221            // consecutive granule check for NOAA data
222        } else {
223                        // compare start time of current granule with end time of previous
224                // difference should be very small - under a second
225                long prvTime = -1;
226            long prvStartTime = -1;
227            long prvEndTime = -1;
228                testResult = 0;
229            int lastSeparator = -1;
230            int firstUnderscore = -1;
231            String prodStr = "";
232            String prevPrd = "";
233            String dateIdx = "_d2";
234            String startTimeIdx = "_t";
235            String endTimeIdx = "_e";
236            String curPlatformStr = null;
237            String prvPlatformStr = null;
238            int firstSeparator = -1;
239            int timeFieldStart = 2;
240            if (f.getName().matches(JPSSUtilities.JPSS_REGEX_ENTERPRISE_EDR)) {
241                dateIdx = "_s";
242                startTimeIdx = "_s";
243                endTimeIdx = "_e";
244                timeFieldStart = 10;
245            }
246                for (int i = 0; i < files.length; i++) {
247                    if ((files[i] != null) && !files[i].isDirectory()) {
248                        if (files[i].exists()) {
249                        String fileName = files[i].getName();
250
251                        // get platform - 3 chars after first separator char
252                        firstSeparator = fileName.indexOf(JPSSUtilities.JPSS_FIELD_SEPARATOR);
253                        curPlatformStr = fileName.substring(firstSeparator + 1, firstSeparator + 4);
254                        logger.debug("platform: " + curPlatformStr);
255                        if ((prvPlatformStr != null) && (! curPlatformStr.equals(prvPlatformStr))) {
256                            logger.warn("Mixed platforms in filelist: " +
257                                curPlatformStr + ", and: " + prvPlatformStr);
258                            testResult = -1;
259                            break;
260                        }
261                        prvPlatformStr = curPlatformStr;
262
263                        lastSeparator = fileName.lastIndexOf(File.separatorChar);
264                        firstUnderscore = fileName.indexOf("_", lastSeparator + 1);
265                        prodStr = fileName.substring(lastSeparator + 1, firstUnderscore);
266                        // reset check if product changes
267                        if (! prodStr.equals(prevPrd)) prvTime = -1;
268                        int dateIndex = fileName.lastIndexOf(dateIdx) + 2;
269                        int timeIndexStart = fileName.lastIndexOf(startTimeIdx) + timeFieldStart;
270                        int timeIndexEnd = fileName.lastIndexOf(endTimeIdx) + timeFieldStart;
271                        String dateStr = fileName.substring(dateIndex, dateIndex + 8);
272                        String timeStrStart = fileName.substring(timeIndexStart, timeIndexStart + 7);
273                        String timeStrEnd = fileName.substring(timeIndexEnd, timeIndexEnd + 7);
274                                // sanity check on file name lengths
275                                int fnLen = fileName.length();
276                                if ((dateIndex > fnLen) || (timeIndexStart > fnLen) || (timeIndexEnd > fnLen)) {
277                                        logger.warn("unexpected file name length for: " + fileName);
278                                        testResult = -1;
279                                        break;
280                                }
281                            // pull start and end time out of file name
282                            Date dS = null;
283                            Date dE = null;
284
285                            try {
286                                                        dS = sdf.parse(dateStr + timeStrStart);
287                                                        // due to nature of Suomi NPP file name encoding, we need a special
288                                                        // check here - end time CAN roll over to next day, while day part 
289                                                        // does not change.  if this happens, we tweak the date string
290                                                        String endDateStr = dateStr;
291                                                        String startHour = timeStrStart.substring(0, 2);
292                                                        String endHour = timeStrEnd.substring(0, 2);
293                                                        if ((startHour.equals("23")) && (endHour.equals("00"))) {
294                                                                // temporarily convert date to integer, increment, convert back
295                                                                int tmpDate = Integer.parseInt(dateStr);
296                                                                tmpDate++;
297                                                                endDateStr = "" + tmpDate;
298                                                                logger.info("Granule time spanning days case handled ok...");
299                                                        }
300                                                        dE = sdf.parse(endDateStr + timeStrEnd);
301                                                } catch (ParseException e) {
302                                                        logger.error("Not recognized as valid Suomi NPP file name: " + fileName);
303                                                        testResult = -1;
304                                                        break;
305                                                }
306                                                long curTime = dS.getTime();
307                                                long endTime = dE.getTime();
308
309                                                // only check current with previous
310                                                if (prvTime > 0) {
311
312                                                        // Make sure time diff does not exceed allowed threshold for the sensor
313                                                        // Whatever the granule size, the time gap cannot exceed our defined "slop"
314                                                        logger.debug("curTime (ms): " + curTime);
315                                                        logger.debug("prvTime (ms): " + prvTime);
316                                                        logger.debug("curTime - prvEndTime (ms): " + Math.abs(curTime - prvEndTime));
317                                                        if (Math.abs(curTime - prvEndTime) > CONSECUTIVE_GRANULE_MAX_GAP_MS) {
318                                                                // Make sure there really is a gap, and not granule overlap
319                                                                if (prvEndTime < curTime) {
320                                                                        testResult = -1;
321                                                                        break;
322                                                                }
323                                                        }
324
325                            // TJJ Inq #2265, #2370. Granules need to be increasing time order 
326                            // to properly georeference. If they are reverse order but pass
327                            // all consecutive tests, we just reverse the list before returning
328                                                        if (curTime < prvStartTime) {
329                                                            testResult = 1;
330                                                            break;
331                                                        }
332
333                                                }
334                                                prvTime = curTime;
335                                                prvStartTime = curTime;
336                                                prvEndTime = endTime;
337                                                prevPrd = prodStr;
338                        }
339                    }
340                }
341        }
342                return testResult;
343        }
344
345        /**
346     * Convert the given array of File objects
347     * to an array of String file names. Only
348     * include the files that actually exist.
349     *
350     * @param files Selected files
351     * @return Selected files as Strings
352     */
353    
354    protected String[] getFileNames(File[] files) {
355        if (files == null) {
356            return (String[]) null;
357        }
358        Vector<String> v = new Vector<String>();
359        String fileNotExistsError = "";
360
361        // NOTE:  If multiple files are selected, then missing files
362        // are not in the files array.  If one file is selected and
363        // it is not there, then it is in the array and file.exists()
364        // is false
365        for (int i = 0; i < files.length; i++) {
366            if ((files[i] != null) && !files[i].isDirectory()) {
367                if ( !files[i].exists()) {
368                    fileNotExistsError += "File does not exist: " + files[i] + "\n";
369                } else {
370                    v.add(files[i].toString());
371                }
372            }
373        }
374
375        if (fileNotExistsError.length() > 0) {
376            userMessage(fileNotExistsError);
377            return null;
378        }
379
380        return v.isEmpty()
381               ? null
382               : StringUtil.listToStringArray(v);
383    }
384    
385    /**
386     * Get the bottom panel for the chooser
387     * @return the bottom panel
388     */
389    
390    protected JPanel getBottomPanel() {
391        // No bottom panel at present
392        return null;
393    }
394    
395    /**
396     * Get the center panel for the chooser
397     * @return the center panel
398     */
399    
400    protected JPanel getCenterPanel() {
401        JPanel centerPanel = super.getCenterPanel();
402
403        JPanel jp = new JPanel(new BorderLayout()) {
404                public void paint(java.awt.Graphics g) {
405                        FileFilter ff = fileChooser.getFileFilter();
406                        if (! (ff instanceof SuomiNPPFilter)) {
407                                fileChooser.setAcceptAllFileFilterUsed(false);
408                                fileChooser.setFileFilter(new SuomiNPPFilter());
409                        }
410                        super.paint(g);
411                }
412        };
413        jp.add(centerPanel);
414
415        return jp; 
416    }
417    
418}