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}