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 // 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}