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 */ 028 029package edu.wisc.ssec.mcidasv.data.hydra; 030 031import java.io.File; 032import java.io.IOException; 033import java.util.ArrayList; 034import java.util.HashMap; 035import java.util.HashSet; 036import java.util.Iterator; 037import java.util.List; 038import java.util.Map; 039import java.util.Set; 040import java.util.StringTokenizer; 041 042import org.slf4j.Logger; 043import org.slf4j.LoggerFactory; 044import ucar.nc2.Attribute; 045import ucar.nc2.NetcdfFile; 046 047/** 048 * Utility class to support Joint Polar Satellite System (JPSS) functionality. 049 * Documentation referenced is from Suomi NPP Common Data Format Control Book. 050 * See: 051 * http://npp.gsfc.nasa.gov/documents.html 052 * 053 * @author tommyj 054 * 055 */ 056 057public abstract class JPSSUtilities { 058 059 private static final Logger logger = 060 LoggerFactory.getLogger(JPSSUtilities.class); 061 062 public static final String JPSS_FIELD_SEPARATOR = "_"; 063 public static final int NASA_CREATION_DATE_INDEX = 28; 064 public static final int NOAA_CREATION_DATE_INDEX = 35; 065 066 // Flags to check for SIPS data V2.0.0 and higher 067 // Augments single fill value from prior versions 068 public static final String SIPS_BOWTIE_DELETED_FLAG = "Bowtie_Deleted"; 069 public static final String SIPS_FLAG_MEANINGS_ATTRIBUTE = "flag_meanings"; 070 public static final String SIPS_FLAG_VALUES_ATTRIBUTE = "flag_values"; 071 072 // This regular expression matches a Suomi NPP Data Product as defined by the 073 // NOAA spec in CDFCB-X Volume 1, Page 21 074 public static final String SUOMI_NPP_REGEX_NOAA = 075 // Product Id, Multiple (ex: VSSTO-GATMO-VSLTO) 076 "(\\w\\w\\w\\w\\w-)*" + 077 // Product Id, Single (ex: VSSTO) 078 "\\w\\w\\w\\w\\w" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 079 // Spacecraft Id (ex: npp) 080 "\\w\\w\\w" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 081 // Data Start Date (ex: dYYYYMMDD) 082 "d20[0-3]\\d[0-1]\\d[0-3]\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 083 // Data Start Time (ex: tHHMMSSS) 084 "t[0-2]\\d[0-5]\\d[0-6]\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 085 // Data Stop Time (ex: eHHMMSSS) 086 "e[0-2]\\d[0-5]\\d[0-6]\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 087 // Orbit Number (ex: b00015) 088 "b\\d\\d\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 089 // Creation Date (ex: cYYYYMMDDHHMMSSSSSSSS) 090 "c20[0-3]\\d[0-1]\\d[0-3]\\d[0-2]\\d[0-5]\\d[0-6]\\d\\d\\d\\d\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 091 // Origin (ex: navo) 092 "\\w\\w\\w\\w" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 093 // Domain (ex: ops) 094 "\\w\\w\\w" + 095 // HDF5 suffix 096 ".h5"; 097 098 // This regular expression matches a VIIRS Enterprise EDR 099 public static final String JPSS_REGEX_ENTERPRISE_EDR = 100 // Product Id, Single (ex: JRR-CloudPhase) 101 "(\\w|-)*" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 102 // Version (e.g v2r3) 103 "v\\dr\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 104 // Origin (ex: npp) 105 "\\w\\w\\w" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 106 // Data Start Date/Time (ex: sYYYYMMDDHHMMSSS) 107 "s20[1-3]\\d[0-1]\\d[0-3]\\d\\d\\d\\d\\d\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 108 // Data End Date/Time (ex: eYYYYMMDDHHMMSSS) 109 "e20[1-3]\\d[0-1]\\d[0-3]\\d\\d\\d\\d\\d\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 110 // Creation Date/Time (ex: cYYYYMMDDHHMMSSS) 111 "c20[1-3]\\d[0-1]\\d[0-3]\\d\\d\\d\\d\\d\\d\\d\\d" + 112 // NetCDF 4 suffix 113 ".nc"; 114 115 // This regular expression matches a Suomi NPP Data Product as defined by the 116 // NASA spec in TBD (XXX TJJ - find out where is this documented?) 117 public static final String SUOMI_NPP_REGEX_NASA = 118 // Product Id, Single (ex: VL1BM) 119 // NOTE: These are the only supported NASA data at this time 120 "(VL1BD|VL1BI|VL1BM)" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 121 // Platform - always Suomi NPP 122 "snpp" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 123 // Data Start Date (ex: dYYYYMMDD) 124 "d20[0-3]\\d[0-1]\\d[0-3]\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 125 // Data Start Time (ex: tHHMM) 126 "t[0-2]\\d[0-5]\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 127 // Creation Date (ex: cYYYYMMDDHHMMSSSSSSSS) 128 "c20[0-3]\\d[0-1]\\d[0-3]\\d\\d\\d\\d\\d\\d\\d" + 129 // NetCDF 4 suffix 130 ".nc"; 131 132 // This regular expression matches a Suomi NPP geolocation file as defined by the 133 // NASA spec in TBD 134 public static final String SUOMI_GEO_REGEX_NASA = 135 // Product Id, Single (ex: VGEOM) 136 // NOTE: This MUST match the list of product ids in static array in this file! 137 "(VGEOD|VGEOI|VGEOM)" + 138 JPSSUtilities.JPSS_FIELD_SEPARATOR + 139 // Platform - always Suomi NPP 140 "snpp" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 141 // Data Start Date (ex: dYYYYMMDD) 142 "d20[0-3]\\d[0-1]\\d[0-3]\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 143 // Data Start Time (ex: tHHMM) 144 "t[0-2]\\d[0-5]\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 145 // Creation Date (ex: cYYYYMMDDHHMMSSSSSSSS) 146 "c20[0-3]\\d[0-1]\\d[0-3]\\d\\d\\d\\d\\d\\d\\d" + 147 // NetCDF 4 suffix 148 ".nc"; 149 150 public static String[] validNASAVariableNames = { 151 "M01", 152 "M02", 153 "M03", 154 "M04", 155 "M05", 156 "M06", 157 "M07", 158 "M08", 159 "M09", 160 "M10", 161 "M11", 162 "M12", 163 "M13", 164 "M14", 165 "M15", 166 "M16", 167 "I01", 168 "I02", 169 "I03", 170 "I04", 171 "I05", 172 "DNB_observations" 173 }; 174 175 public static float[] ATMSChannelCenterFrequencies = { 176 23.8f, 177 31.4f, 178 50.3f, 179 51.76f, 180 52.8f, 181 53.596f, 182 54.40f, 183 54.94f, 184 55.50f, 185 57.29032f, 186 57.29033f, 187 57.29034f, 188 57.29035f, 189 57.29036f, 190 57.29037f, 191 88.20f, 192 165.5f, 193 183.3101f, 194 183.3102f, 195 183.3103f, 196 183.3104f, 197 183.3105f 198 }; 199 200 // the list of valid geolocation product ids 201 public static String[] geoProductIDs = { 202 "GATMO", 203 "GCRSO", 204 "GAERO", 205 "GCLDO", 206 "GDNBO", 207 "GNCCO", 208 "GIGTO", 209 "GIMGO", 210 "GITCO", 211 "GMGTO", 212 "GMODO", 213 "GMTCO", 214 "GNHFO", 215 "GOTCO", 216 "GOSCO", 217 "GONPO", 218 "GONCO", 219 "GCRIO", 220 "GATRO", 221 "IVMIM", 222 "VGEOD", 223 "VGEOI", 224 "VGEOM", 225 "VMUGE" 226 }; 227 228 // This regular expression matches a Suomi NPP geolocation granule, see 229 // NOAA spec in CDFCB-X Volume 1, Page 21 230 public static final String SUOMI_GEO_REGEX_NOAA = 231 // Geo Id, Single (ex: GMODO) 232 // NOTE: This MUST match the list of product ids in static array above! 233 "(GATMO|GCRSO|GAERO|GCLDO|GDNBO|GNCCO|GIGTO|GIMGO|GITCO|" + 234 "GMGTO|GMODO|GMTCO|GNHFO|GOTCO|GOSCO|GONPO|GONCO|GCRIO|GATRO|IVMIM|VMUGE)" + 235 JPSSUtilities.JPSS_FIELD_SEPARATOR + 236 // Spacecraft Id (ex: npp) 237 "\\w\\w\\w" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 238 // Data Start Date (ex: dYYYYMMDD) 239 "d20[0-3]\\d[0-1]\\d[0-3]\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 240 // Data Start Time (ex: tHHMMSSS) 241 "t[0-2]\\d[0-5]\\d[0-6]\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 242 // Data Stop Time (ex: eHHMMSSS) 243 "e[0-2]\\d[0-5]\\d[0-6]\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 244 // Orbit Number (ex: b00015) 245 "b\\d\\d\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 246 // Creation Date (ex: cYYYYMMDDHHMMSSSSSSSS) 247 "c20[0-3]\\d[0-1]\\d[0-3]\\d[0-2]\\d[0-5]\\d[0-6]\\d\\d\\d\\d\\d\\d\\d" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 248 // Origin (ex: navo) 249 "\\w\\w\\w\\w" + JPSSUtilities.JPSS_FIELD_SEPARATOR + 250 // Domain (ex: ops) 251 "\\w\\w\\w" + 252 // HDF5 suffix 253 ".h5"; 254 255 /** 256 * Determine if the input variable name is a valid NASA product 257 * 258 * @param varName Variable name to validate. 259 * 260 * @return {@code true} if {@code varName} passes checks. 261 */ 262 263 public static boolean isValidNASA(String varName) { 264 boolean isValid = false; 265 if (varName == null) return isValid; 266 for (String s : validNASAVariableNames) { 267 if (s.equals(varName)) { 268 isValid = true; 269 break; 270 } 271 } 272 return isValid; 273 } 274 275 /** 276 * An unusual method used to filter out files of type "GCRSO-SCRIF-SCRIS", 277 * where CrIS Science and Full Spectrum data are combined by CLASS into 278 * a single granule. These are deemed an odd aggregation that does not 279 * make sense combined into a single product, and are disallowed. 280 * 281 * @param fileList List of files to validate. 282 * 283 * @return {@code false} if {@code fileList} passes checks. Note reverse 284 * of typical boolean logic! True is retuned if data is INVALID. 285 */ 286 287 public static boolean isInvalidCris(List fileList) { 288 for (Object o : fileList) { 289 String filename = (String) o; 290 if (filename.contains("SCRIF-SCRIS")) { 291 return true; 292 } 293 } 294 return false; 295 } 296 297 /** 298 * Determine if the set if filenames constitutes contiguous SNPP granules 299 * of the same geographic coverage. 300 * 301 * @param fileList List of files to validate. 302 * 303 * @return {@code true} if {@code fileList} passes checks. 304 */ 305 306 public static boolean isValidSet(List fileList) { 307 308 // map with filename from start date through orbit will be used for comparisons 309 Map<String, List<String>> metadataMap = new HashMap<String, List<String>>(); 310 311 // Pass 1, populate the list of products selected, and empty maps 312 for (Object o : fileList) { 313 String filename = (String) o; 314 // start at last path separator to clip off absolute paths 315 int lastSeparator = filename.lastIndexOf(File.separatorChar); 316 // NOAA style products 317 if (filename.endsWith(".h5")) { 318 // products go to first underscore, see regex above for more detail 319 int firstUnderscore = filename.indexOf("_", lastSeparator + 1); 320 String prodStr = filename.substring(lastSeparator + 1, firstUnderscore); 321 if (! metadataMap.containsKey(prodStr)) { 322 List<String> l = new ArrayList<String>(); 323 metadataMap.put(prodStr, l); 324 } 325 } 326 // NASA style products 327 if (filename.endsWith(".nc")) { 328 // products end at first underscore, see regex above for more detail 329 int firstUnderscore = filename.indexOf("_", lastSeparator + 1); 330 int secondUnderscore = filename.indexOf("_", firstUnderscore + 1); 331 String prodStr = filename.substring(firstUnderscore + 1, secondUnderscore); 332 if (! metadataMap.containsKey(prodStr)) { 333 List<String> l = new ArrayList<String>(); 334 metadataMap.put(prodStr, l); 335 } 336 } 337 } 338 339 // Pass 2, build up the lists of meta data strings and full filenames 340 for (Object o : fileList) { 341 String filename = (String) o; 342 // start at last path separator to clip off absolute paths 343 int lastSeparator = filename.lastIndexOf(File.separatorChar); 344 345 // NOAA style products 346 if (filename.endsWith(".h5")) { 347 // products go to first underscore, see regex above for more detail 348 int firstUnderscore = filename.indexOf("_", lastSeparator + 1); 349 // this is the key for the maps 350 String prodStr = filename.substring(lastSeparator + 1, firstUnderscore); 351 // this is the value for the meta data map - start time through orbit 352 String metaStr = filename.substring(firstUnderscore + 1, firstUnderscore + 39); 353 // get the appropriate list, add the new value 354 List<String> l = (List<String>) metadataMap.get(prodStr); 355 l.add(metaStr); 356 metadataMap.put(prodStr, l); 357 } 358 359 // NASA style products 360 if (filename.endsWith(".nc")) { 361 // products end at first underscore, see regex above for more detail 362 int firstUnderscore = filename.indexOf("_", lastSeparator + 1); 363 int secondUnderscore = filename.indexOf("_", firstUnderscore + 1); 364 // this is the key for the maps 365 String prodStr = filename.substring(firstUnderscore + 1, secondUnderscore); 366 // this is the value for the meta data map - date and time 367 int dateIndex = filename.indexOf("_d"); 368 int creationIndex = filename.indexOf("_c"); 369 String metaStr = filename.substring(dateIndex + 1, creationIndex); 370 // get the appropriate list, add the new value 371 List<String> l = (List<String>) metadataMap.get(prodStr); 372 l.add(metaStr); 373 metadataMap.put(prodStr, l); 374 } 375 } 376 377 // loop over metadata map, every list much match the one for ALL other products 378 Set<String> s = metadataMap.keySet(); 379 Iterator iterator = s.iterator(); 380 List prvList = null; 381 while (iterator.hasNext()) { 382 String key = (String) iterator.next(); 383 List l = (List) metadataMap.get(key); 384 for (int i = 0; i < l.size(); i++) { 385 if (prvList != null) { 386 if (! l.equals(prvList)) return false; 387 } 388 } 389 prvList = l; 390 } 391 392 return true; 393 } 394 395 /** 396 * Replace last substring within input string which matches the input with the 397 * provided replacement string. 398 * 399 * @param string 400 * @param substring 401 * @param replacestr 402 * @return 403 */ 404 405 public static String replaceLast(String string, String substring, String replacestr) { 406 // Sanity check on input 407 if (string == null) return null; 408 if ((substring == null) || (replacestr == null)) return string; 409 410 int index = string.lastIndexOf(substring); 411 412 // substring not present 413 if (index == -1) 414 return string; 415 // it's there, swap it 416 return string.substring(0, index) + replacestr 417 + string.substring(index + substring.length()); 418 } 419 420 /** 421 * Determine if a set if filenames which constitutes contiguous SNPP 422 * granules of various products all share the same geolocation data type. 423 * 424 * @param fileList List of files to validate. 425 * @param directory Used when {@literal "GEO"} is not embedded within one 426 * of {@code fileList}. 427 * 428 * @return {@code true} if {@code fileList} passes checks. 429 */ 430 431 public static boolean hasCommonGeo(List fileList, File directory) { 432 Set<String> s = new HashSet<String>(); 433 boolean isCombinedProduct = false; 434 435 // loop through all filenames provided 436 for (Object o : fileList) { 437 isCombinedProduct = false; 438 String filename = (String) o; 439 440 // check the case where GEO is embedded in the data granules 441 int lastSeparator = filename.lastIndexOf(File.separatorChar); 442 int firstUnderscore = filename.indexOf("_", lastSeparator + 1); 443 String prodStr = filename.substring(lastSeparator + 1, firstUnderscore); 444 StringTokenizer st = new StringTokenizer(prodStr, "-"); 445 while (st.hasMoreTokens()) { 446 String singleProd = st.nextToken(); 447 for (int i = 0; i < JPSSUtilities.geoProductIDs.length; i++) { 448 if (singleProd.equals(JPSSUtilities.geoProductIDs[i])) { 449 s.add(singleProd); 450 isCombinedProduct = true; 451 break; 452 } 453 } 454 } 455 // GEO not embedded in file, need to see which GEO file is 456 // referenced in the global attribute 457 if (! isCombinedProduct) { 458 try { 459 String fileFullPath = directory.getAbsolutePath() + File.separator + filename; 460 NetcdfFile ncfile = NetcdfFile.open(fileFullPath); 461 Attribute a = ncfile.findGlobalAttribute("N_GEO_Ref"); 462 if (a != null) { 463 String geoFromAttr = a.getStringValue().substring(0, 5); 464 s.add(geoFromAttr); 465 } 466 ncfile.close(); 467 } catch (IOException ioe) { 468 logger.error("problem reading from attribute", ioe); 469 } 470 } 471 } 472 473 // if the products chosen utilize multiple GEO types, fail the selection 474 if (s.size() > 1) return false; 475 return true; 476 } 477 478}