001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2016
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 http://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        private static final long CONSECUTIVE_GRANULE_MAX_GAP_MS = 60000;
057        private static final long CONSECUTIVE_GRANULE_MAX_GAP_MS_NASA = 360000;
058        
059        // date formatters for converting Suomi NPP day/time from file name for consecutive granule check
060    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmSSS");
061    private static final SimpleDateFormat sdfNASA = new SimpleDateFormat("yyyyMMddHHmm");
062
063    /**
064     * Create the chooser with the given manager and xml
065     *
066     * @param mgr The manager
067     * @param root The xml
068     *
069     */
070    
071    public SuomiNPPChooser(IdvChooserManager mgr, org.w3c.dom.Element root) {
072        super(mgr, root);
073    }
074   
075    /**
076     * Make the file chooser
077     *
078     * @param path   the initial path
079     *
080     * @return  the file chooser
081     */
082    
083    protected JFileChooser doMakeFileChooser(String path) {
084        if (fileChooser == null) {
085                logger.debug("Creating Suomi NPP File Chooser...");
086                fileChooser = new SuomiNPPFileChooser(path, this);
087        } else {
088                logger.debug("2nd call to doMakeFileChooser, why?");
089        }
090        return fileChooser;
091    }
092
093    /**
094     * Handle the selection of the set of files
095     *
096     * @param files The files the user chose
097     * @param directory The directory they chose them from
098     * @return True if the file was successful
099     * @throws Exception
100     */
101    
102    protected boolean selectFilesInner(File[] files, File directory)
103            throws Exception {
104        if ((files == null) || (files.length == 0)) {
105            userMessage("Please select a file");
106            return false;
107        }
108
109        // make a list of just the file names
110        ArrayList<String> fileNames = new ArrayList<String>();
111        for (int i = 0; i < files.length; i++) {
112                fileNames.add(files[i].getName());
113        }
114        
115        // ensure these files make sense as a set to create a single SNPP data source
116        if (! JPSSUtilities.isValidSet(fileNames)) {
117                JOptionPane.showMessageDialog(null, 
118                        "Unable to group selected data as a single data source.");
119                return false;
120        }
121        
122        // ensure these files make sense as a set to create a single SNPP data source
123        if (! JPSSUtilities.hasCommonGeo(fileNames, directory)) {
124                JOptionPane.showMessageDialog(null, 
125                        "Unable to group selected data as a single data source.");
126                return false;
127        }
128        
129        // At present, Suomi NPP chooser only allows selecting sets of consecutive granules
130        int granulesAreConsecutive = -1;
131        // Consecutive granule check - can only aggregate a contiguous set
132        if (files.length > 1) {
133           granulesAreConsecutive = testConsecutiveGranules(files);
134        }
135        
136        // Need to reverse file list, so granules are increasing time order
137        if (granulesAreConsecutive == 1) {
138           Collections.reverse(Arrays.asList(files));
139        }
140        
141        if ((granulesAreConsecutive >= 0) || (files.length == 1)) {
142                return super.selectFilesInner(files, directory);
143        } else {
144                // throw up a dialog to tell user the problem
145            JOptionPane.showMessageDialog(this,
146                "When selecting multiple granules, they must be consecutive.");
147        }
148        return false;
149    }
150
151    /**
152     * Test whether a set of files are consecutive Suomi NPP granules,
153     * any sensor. NOTE: This method works when the file list contains
154     * multiple products ONLY because once we've validate one product,
155     * the time check will be a negative number when comparing the FIRST
156     * granule of product 2 with the LAST granule of product 1. A better
157     * implementation would be to pass in the filename map like the 
158     * one generated in SuomiNPPDataSource constructor.
159     * 
160     * @param files
161     * @return 0 if consecutive tests pass for all files
162     *        -1 if tests fail
163     *         1 if tests pass but file order is backward 
164     *           (decreasing time order)
165     */
166    
167    private int testConsecutiveGranules(File[] files) {
168        int testResult = -1;
169        if (files == null) return testResult;
170        
171        // TJJ Jan 2016 - different checks for NASA data, 6 minutes per granule
172        File f = files[0];
173        if (f.getName().matches(JPSSUtilities.SUOMI_NPP_REGEX_NASA)) {
174                        // compare start time of current granule with end time of previous
175                // difference should be very small - under a second
176                long prvTime = -1;
177                testResult = 0;
178                for (int i = 0; i < files.length; i++) {
179                    if ((files[i] != null) && !files[i].isDirectory()) {
180                        if (files[i].exists()) {
181                                String fileName = files[i].getName();
182                                int dateIndex = fileName.lastIndexOf("_d2") + 2;
183                                int timeIndex = fileName.lastIndexOf("_t") + 2;
184                                String dateStr = fileName.substring(dateIndex, dateIndex + 8);
185                                String timeStr = fileName.substring(timeIndex, timeIndex + 4);
186                            // pull start and end time out of file name
187                            Date dS = null;
188                            try {
189                                                        dS = sdfNASA.parse(dateStr + timeStr);
190                            } catch (ParseException pe) {
191                                                        logger.error("Not recognized as valid Suomi NPP file name: " + fileName);
192                                                        testResult = -1;
193                                                        break;
194                            }
195                                                long curTime = dS.getTime();
196                                                // only check current with previous
197                                                if (prvTime > 0) {
198                                                        // make sure time diff does not exceed allowed threshold
199                                                        // consecutive granules should be less than 1 minute apart
200                                                        if ((curTime - prvTime) > CONSECUTIVE_GRANULE_MAX_GAP_MS_NASA) {
201                                                                testResult = -1;
202                                                                break;
203                                                        }
204                            // TJJ Inq #2265, #2370. Granules need to be increasing time order 
205                            // to properly georeference. If they are reverse order but pass
206                                                        // all consecutive tests, we just reverse the list before returning
207                            if (curTime < prvTime) {
208                                testResult = 1;
209                                break;
210                            }
211                                                }
212                                                prvTime = curTime;
213                        }
214                    }
215                }
216            // consecutive granule check for NOAA data
217        } else {
218                        // compare start time of current granule with end time of previous
219                // difference should be very small - under a second
220                long prvTime = -1;
221            long prvStartTime = -1;
222                testResult = 0;
223            int lastSeparator = -1;
224            int firstUnderscore = -1;
225            String prodStr = "";
226            String prevPrd = "";
227                for (int i = 0; i < files.length; i++) {
228                    if ((files[i] != null) && !files[i].isDirectory()) {
229                        if (files[i].exists()) {
230                                String fileName = files[i].getName(); 
231                        lastSeparator = fileName.lastIndexOf(File.separatorChar);
232                        firstUnderscore = fileName.indexOf("_", lastSeparator + 1);
233                        prodStr = fileName.substring(lastSeparator + 1, firstUnderscore);
234                        // reset check if product changes
235                        if (! prodStr.equals(prevPrd)) prvTime = -1;
236                                int dateIndex = fileName.lastIndexOf("_d2") + 2;
237                                int timeIndexStart = fileName.lastIndexOf("_t") + 2;
238                                int timeIndexEnd = fileName.lastIndexOf("_e") + 2;
239                                String dateStr = fileName.substring(dateIndex, dateIndex + 8);
240                                String timeStrStart = fileName.substring(timeIndexStart, timeIndexStart + 7);
241                                String timeStrEnd = fileName.substring(timeIndexEnd, timeIndexEnd + 7);
242                                // sanity check on file name lengths
243                                int fnLen = fileName.length();
244                                if ((dateIndex > fnLen) || (timeIndexStart > fnLen) || (timeIndexEnd > fnLen)) {
245                                        logger.warn("unexpected file name length for: " + fileName);
246                                        testResult = -1;
247                                        break;
248                                }
249                            // pull start and end time out of file name
250                            Date dS = null;
251                            Date dE = null;
252                            try {
253                                                        dS = sdf.parse(dateStr + timeStrStart);
254                                                        // due to nature of Suomi NPP file name encoding, we need a special
255                                                        // check here - end time CAN roll over to next day, while day part 
256                                                        // does not change.  if this happens, we tweak the date string
257                                                        String endDateStr = dateStr;
258                                                        String startHour = timeStrStart.substring(0, 2);
259                                                        String endHour = timeStrEnd.substring(0, 2);
260                                                        if ((startHour.equals("23")) && (endHour.equals("00"))) {
261                                                                // temporarily convert date to integer, increment, convert back
262                                                                int tmpDate = Integer.parseInt(dateStr);
263                                                                tmpDate++;
264                                                                endDateStr = "" + tmpDate;
265                                                                logger.info("Granule time spanning days case handled ok...");
266                                                        }
267                                                        dE = sdf.parse(endDateStr + timeStrEnd);
268                                                } catch (ParseException e) {
269                                                        logger.error("Not recognized as valid Suomi NPP file name: " + fileName);
270                                                        testResult = -1;
271                                                        break;
272                                                }
273                                                long curTime = dS.getTime();
274                                                long endTime = dE.getTime();
275                                                // only check current with previous
276                                                if (prvTime > 0) {
277                                                        // make sure time diff does not exceed allowed threshold
278                                                        // consecutive granules should be less than 1 minute apart
279                                                        if ((curTime - prvTime) > CONSECUTIVE_GRANULE_MAX_GAP_MS) {
280                                                                testResult = -1;
281                                                                break;
282                                                        }
283                            // TJJ Inq #2265, #2370. Granules need to be increasing time order 
284                            // to properly georeference. If they are reverse order but pass
285                            // all consecutive tests, we just reverse the list before returning
286                                                        if (curTime < prvStartTime) {
287                                                            testResult = 1;
288                                                            break;
289                                                        }
290                                                }
291                                                prvTime = endTime;
292                                                prvStartTime = curTime;
293                        }
294                        prevPrd = prodStr;
295                    }
296                }
297        }
298                return testResult;
299        }
300
301        /**
302     * Convert the given array of File objects
303     * to an array of String file names. Only
304     * include the files that actually exist.
305     *
306     * @param files Selected files
307     * @return Selected files as Strings
308     */
309    
310    protected String[] getFileNames(File[] files) {
311        if (files == null) {
312            return (String[]) null;
313        }
314        Vector<String> v = new Vector<String>();
315        String fileNotExistsError = "";
316
317        // NOTE:  If multiple files are selected, then missing files
318        // are not in the files array.  If one file is selected and
319        // it is not there, then it is in the array and file.exists()
320        // is false
321        for (int i = 0; i < files.length; i++) {
322            if ((files[i] != null) && !files[i].isDirectory()) {
323                if ( !files[i].exists()) {
324                    fileNotExistsError += "File does not exist: " + files[i] + "\n";
325                } else {
326                    v.add(files[i].toString());
327                }
328            }
329        }
330
331        if (fileNotExistsError.length() > 0) {
332            userMessage(fileNotExistsError);
333            return null;
334        }
335
336        return v.isEmpty()
337               ? null
338               : StringUtil.listToStringArray(v);
339    }
340    
341    /**
342     * Get the bottom panel for the chooser
343     * @return the bottom panel
344     */
345    
346    protected JPanel getBottomPanel() {
347        // No bottom panel at present
348        return null;
349    }
350    
351    /**
352     * Get the center panel for the chooser
353     * @return the center panel
354     */
355    
356    protected JPanel getCenterPanel() {
357        JPanel centerPanel = super.getCenterPanel();
358
359        JPanel jp = new JPanel(new BorderLayout()) {
360                public void paint(java.awt.Graphics g) {
361                        FileFilter ff = fileChooser.getFileFilter();
362                        if (! (ff instanceof SuomiNPPFilter)) {
363                                fileChooser.setAcceptAllFileFilterUsed(false);
364                                fileChooser.setFileFilter(new SuomiNPPFilter());
365                        }
366                        super.paint(g);
367                }
368        };
369        jp.add(centerPanel);
370
371        return jp; 
372    }
373    
374}