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.cyclone; 030 031import java.io.ByteArrayInputStream; 032import java.io.ByteArrayOutputStream; 033import java.io.File; 034import java.io.FileNotFoundException; 035import java.io.IOException; 036import java.net.URL; 037import java.text.SimpleDateFormat; 038import java.util.ArrayList; 039import java.util.Calendar; 040import java.util.Date; 041import java.util.GregorianCalendar; 042import java.util.Hashtable; 043import java.util.List; 044import java.util.TimeZone; 045import java.util.zip.GZIPInputStream; 046 047import org.apache.commons.net.ftp.FTP; 048import org.apache.commons.net.ftp.FTPClient; 049 050import ucar.unidata.data.BadDataException; 051import ucar.unidata.data.DataSourceDescriptor; 052import ucar.unidata.util.DateUtil; 053import ucar.unidata.util.IOUtil; 054import ucar.unidata.util.StringUtil; 055import visad.DateTime; 056import visad.Real; 057import visad.RealType; 058import visad.VisADException; 059import visad.georef.EarthLocation; 060import visad.georef.EarthLocationLite; 061 062/** 063 */ 064public class AtcfStormDataSource extends StormDataSource { 065 066 /** _more_ */ 067 private int BASEIDX = 0; 068 069 /** _more_ */ 070 private int IDX_BASIN = BASEIDX++; 071 072 /** _more_ */ 073 private int IDX_CY = BASEIDX++; 074 075 /** _more_ */ 076 private int IDX_YYYYMMDDHH = BASEIDX++; 077 078 /** _more_ */ 079 private int IDX_TECHNUM = BASEIDX++; 080 081 /** _more_ */ 082 private int IDX_TECH = BASEIDX++; 083 084 /** _more_ */ 085 private int IDX_TAU = BASEIDX++; 086 087 /** _more_ */ 088 private int IDX_LAT = BASEIDX++; 089 090 /** _more_ */ 091 private int IDX_LON = BASEIDX++; 092 093 /** _more_ */ 094 private int IDX_VMAX = BASEIDX++; 095 096 /** _more_ */ 097 private int IDX_MSLP = BASEIDX++; 098 099 /** _more_ */ 100 private int IDX_TY = BASEIDX++; 101 102 /** _more_ */ 103 private int IDX_RAD = BASEIDX++; 104 105 /** _more_ */ 106 private int IDX_WINDCODE = BASEIDX++; 107 108 /** _more_ */ 109 private int IDX_RAD1 = BASEIDX++; 110 111 /** _more_ */ 112 private int IDX_RAD2 = BASEIDX++; 113 114 /** _more_ */ 115 private int IDX_RAD3 = BASEIDX++; 116 117 /** _more_ */ 118 private int IDX_RAD4 = BASEIDX++; 119 120 /** _more_ */ 121 private int IDX_RADP = BASEIDX++; 122 123 /** _more_ */ 124 private int IDX_RRP = BASEIDX++; 125 126 /** _more_ */ 127 private int IDX_MRD = BASEIDX++; 128 129 /** _more_ */ 130 private int IDX_GUSTS = BASEIDX++; 131 132 /** _more_ */ 133 private int IDX_EYE = BASEIDX++; 134 135 /** _more_ */ 136 private int IDX_SUBREGION = BASEIDX++; 137 138 /** _more_ */ 139 private int IDX_MAXSEAS = BASEIDX++; 140 141 /** _more_ */ 142 private int IDX_INITIALS = BASEIDX++; 143 144 /** _more_ */ 145 private int IDX_DIR = BASEIDX++; 146 147 /** _more_ */ 148 private int IDX_SPEED = BASEIDX++; 149 150 /** _more_ */ 151 private int IDX_STORMNAME = BASEIDX++; 152 153 /** _more_ */ 154 private int IDX_DEPTH = BASEIDX++; 155 156 /** _more_ */ 157 private int IDX_SEAS = BASEIDX++; 158 159 /** _more_ */ 160 private int IDX_SEASCODE = BASEIDX++; 161 162 /** _more_ */ 163 private int IDX_SEAS1 = BASEIDX++; 164 165 /** _more_ */ 166 private int IDX_SEAS2 = BASEIDX++; 167 168 /** _more_ */ 169 private int IDX_SEAS3 = BASEIDX++; 170 171 /** _more_ */ 172 private int IDX_SEAS4 = BASEIDX++; 173 174 /** _more_ */ 175 private static final String PREFIX_ANALYSIS = "a"; 176 177 /** _more_ */ 178 private static final String PREFIX_BEST = "b"; 179 180 /** _more_ */ 181 private static final String WAY_BEST = "BEST"; 182 183 /** _more_ */ 184 private static final String WAY_CARQ = "CARQ"; 185 186 /** _more_ */ 187 private static final String WAY_WRNG = "WRNG"; 188 189 /** _more_ */ 190 private static String DEFAULT_PATH = "ftp://anonymous:password@ftp.nhc.noaa.gov/atcf"; 191 192 /** _more_ */ 193 private String path; 194 195 /** _more_ */ 196 private List<StormInfo> stormInfos; 197 198 /** _more_ */ 199 private StormTrackCollection localTracks; 200 201 /** 202 * _more_ 203 * 204 * @throws Exception 205 * _more_ 206 */ 207 public AtcfStormDataSource() throws Exception { 208 } 209 210 /** 211 * _more_ 212 * 213 * @return _more_ 214 */ 215 public String getFullDescription() { 216 return "ATCF Data Source<br>Path:" + path; 217 } 218 219 /** 220 * _more_ 221 * 222 * @param descriptor 223 * _more_ 224 * @param url 225 * _more_ 226 * @param properties 227 * _more_ 228 */ 229 public AtcfStormDataSource(DataSourceDescriptor descriptor, String url, 230 Hashtable properties) { 231 super(descriptor, "ATCF Storm Data", "ATCF Storm Data", properties); 232 if ((url == null) || (url.trim().length() == 0) 233 || url.trim().equalsIgnoreCase("default")) { 234 url = DEFAULT_PATH; 235 } 236 path = url; 237 } 238 239 /** 240 * _more_ 241 * 242 * @return _more_ 243 */ 244 public String getId() { 245 return "atcf"; 246 } 247 248 /** 249 * _more_ 250 * 251 * @param suffix 252 * _more_ 253 * 254 * @return _more_ 255 */ 256 private String getFullPath(String suffix) { 257 return path + "/" + suffix; 258 } 259 260 /** 261 * _more_ 262 */ 263 protected void initializeStormData() { 264 try { 265 incrOutstandingGetDataCalls(); 266 stormInfos = new ArrayList<StormInfo>(); 267 if (path.toLowerCase().endsWith(".atcf") 268 || path.toLowerCase().endsWith(".gz") 269 || path.toLowerCase().endsWith(".dat")) { 270 String name = IOUtil.stripExtension(IOUtil.getFileTail(path)); 271 StormInfo si = new StormInfo(name, new DateTime(new Date())); 272 stormInfos.add(si); 273 localTracks = new StormTrackCollection(); 274 readTracks(si, localTracks, path, null, true); 275 List<StormTrack> trackList = localTracks.getTracks(); 276 277 if (trackList.size() > 0) { 278 si.setStartTime(trackList.get(0).getStartTime()); 279 } 280 return; 281 } 282 283 byte[] techs = readFile(getFullPath("nhc_techlist.dat"), true); 284 if (techs != null) { 285 /* 286 * NUM TECH ERRS RETIRED COLOR DEFAULTS INT-DEFS RADII-DEFS 287 * LONG-NAME 00 CARQ 0 0 0 0 0 1 Combined ARQ Position 00 WRNG 0 288 * 0 0 0 0 1 Warning 289 */ 290 int cnt = 0; 291 for (String line : StringUtil.split(new String(techs), "\n", 292 true, true)) { 293 if (cnt++ == 0) { 294 continue; 295 } 296 if (line.length() > 67) { 297 String id = line.substring(3, 10).trim(); 298 String name = line.substring(67).trim(); 299 // System.out.println (id + ":" +name); 300 getWay(id, name); 301 } 302 } 303 } 304 305 // byte[] bytes = readFile(getFullPath("archive/storm.table"), 306 byte[] bytes = readFile(getFullPath("index/storm_list.txt"), false); 307 String stormTable = new String(bytes); 308 List lines = StringUtil.split(stormTable, "\n", true, true); 309 310 SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMddHH"); 311 fmt.setTimeZone(TimeZone.getTimeZone("UTC")); 312 for (int i = 0; i < lines.size(); i++) { 313 String line = (String) lines.get(i); 314 List toks = StringUtil.split(line, ",", true); 315 String name = (String) toks.get(0); 316 String basin = (String) toks.get(1); 317 String number = (String) toks.get(7); 318 String year = (String) toks.get(8); 319 int y = Integer.parseInt(year); 320 String id = basin + "_" + number + "_" + year; 321 if (name.equals("UNNAMED")) { 322 name = id; 323 } 324 String dttm = (String) toks.get(11); 325 Date date = fmt.parse(dttm); 326 StormInfo si = new StormInfo(id, name, basin, number, 327 new DateTime(date)); 328 stormInfos.add(si); 329 330 } 331 } catch (Exception exc) { 332 logException("Error initializing ATCF data", exc); 333 } finally { 334 decrOutstandingGetDataCalls(); 335 } 336 } 337 338 /** 339 * _more_ 340 * 341 * @return _more_ 342 */ 343 public List<StormInfo> getStormInfos() { 344 return stormInfos; 345 } 346 347 /** 348 * _more_ 349 * 350 * @param s 351 * _more_ 352 * 353 * @return _more_ 354 */ 355 private double getDouble(String s) { 356 if (s == null) { 357 return Double.NaN; 358 } 359 if (s.length() == 0) { 360 return Double.NaN; 361 } 362 return Double.valueOf(s).doubleValue(); 363 } 364 365 /** 366 * _more_ 367 * 368 * @throws VisADException 369 * _more_ 370 */ 371 protected void initParams() throws VisADException { 372 super.initParams(); 373 if (obsParams == null) { 374 obsParams = new StormParam[] { PARAM_STORMCATEGORY, 375 PARAM_MINPRESSURE, PARAM_MAXWINDSPEED_KTS }; 376 377 } 378 } 379 380 /** 381 * _more_ 382 * 383 * @param stormInfo 384 * _more_ 385 * @param tracks 386 * _more_ 387 * @param trackFile 388 * _more_ 389 * @param waysToUse 390 * _more_ 391 * @param throwError 392 * _more_ 393 * 394 * 395 * @return _more_ 396 * @throws Exception 397 * _more_ 398 */ 399 private boolean readTracks(StormInfo stormInfo, 400 StormTrackCollection tracks, String trackFile, 401 Hashtable<String, Boolean> waysToUse, boolean throwError) 402 throws Exception { 403 404 long t1 = System.currentTimeMillis(); 405 byte[] bytes = readFile(trackFile, true); 406 long t2 = System.currentTimeMillis(); 407 // System.err.println("read time:" + (t2 - t1)); 408 boolean isZip = trackFile.endsWith(".gz"); 409 if ((bytes == null) && isZip) { 410 String withoutGZ = trackFile.substring(0, trackFile.length() - 3); 411 bytes = readFile(withoutGZ, true); 412 isZip = false; 413 } 414 415 if (bytes == null) { 416 if (throwError) { 417 throw new BadDataException("Unable to read track file:" 418 + trackFile); 419 } 420 return false; 421 } 422 423 if (isZip) { 424 GZIPInputStream zin = new GZIPInputStream(new ByteArrayInputStream( 425 bytes)); 426 bytes = IOUtil.readBytes(zin); 427 zin.close(); 428 } 429 GregorianCalendar convertCal = new GregorianCalendar( 430 DateUtil.TIMEZONE_GMT); 431 convertCal.clear(); 432 433 String trackData = new String(bytes); 434 List lines = StringUtil.split(trackData, "\n", true, true); 435 SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMddHH"); 436 fmt.setTimeZone(TimeZone.getTimeZone("UTC")); 437 Hashtable trackMap = new Hashtable(); 438 Real altReal = new Real(RealType.Altitude, 0); 439 // System.err.println("obs:" + lines.size()); 440 /* 441 * Hashtable okWays = new Hashtable(); okWays.put(WAY_CARQ, ""); 442 * okWays.put(WAY_WRNG, ""); okWays.put(WAY_BEST, ""); okWays.put("ETA", 443 * ""); okWays.put("NGX", ""); okWays.put("BAMS", ""); 444 */ 445 Hashtable seenDate = new Hashtable(); 446 initParams(); 447 int xcnt = 0; 448 for (int i = 0; i < lines.size(); i++) { 449 String line = (String) lines.get(i); 450 if (i == 0) { 451 // System.err.println(line); 452 } 453 List toks = StringUtil.split(line, ",", true); 454 /* 455 * System.err.println(toks.size() + " " + BASEIDX); 456 * if(toks.size()<BASEIDX-1) { System.err.println("bad line:" + 457 * line); continue; } else { System.err.println("good line:" + 458 * line); } 459 */ 460 461 // BASIN,CY,YYYYMMDDHH,TECHNUM,TECH,TAU,LatN/S,LonE/W,VMAX,MSLP,TY,RAD,WINDCODE,RAD1,RAD2,RAD3,RAD4,RADP,RRP,MRD,GUSTS,EYE,SUBREGION,MAXSEAS,INITIALS,DIR,SPEED,STORMNAME,DEPTH,SEAS,SEASCODE,SEAS1,SEAS2,SEAS3,SEAS4 462 // AL, 01, 2007050612, , BEST, 0, 355N, 740W, 35, 1012, EX, 34, NEQ, 463 // 0, 0, 0, 120, 464 // AL, 01, 2007050812, 01, CARQ, -24, 316N, 723W, 55, 0, DB, 34, 465 // AAA, 0, 0, 0, 0, 466 467 String dateString = (String) toks.get(IDX_YYYYMMDDHH); 468 String wayString = (String) toks.get(IDX_TECH); 469 // if (okWays.get(wayString) == null) { 470 // continue; 471 // } 472 boolean isBest = wayString.equals(WAY_BEST); 473 boolean isWarning = wayString.equals(WAY_WRNG); 474 boolean isCarq = wayString.equals(WAY_CARQ); 475 476 int category = ((IDX_TY < toks.size()) ? getCategory((String) toks 477 .get(IDX_TY)) : CATEGORY_XX); 478 if (category != CATEGORY_XX) { 479 // System.err.println("cat:" + category); 480 } 481 482 String fhour = (String) toks.get(IDX_TAU); 483 int forecastHour = Integer.parseInt(fhour); 484 // A hack - we've seen some atfc files that have a 5 character 485 // forecast hour 486 // right padded with "00", eg., 01200 487 if ((fhour.length() == 5) && (forecastHour > 100)) { 488 forecastHour = forecastHour / 100; 489 } 490 491 if (isWarning || isCarq) { 492 forecastHour = -forecastHour; 493 } 494 495 // Check for unique dates for this way 496 String dttmkey = wayString + "_" + dateString + "_" + forecastHour; 497 if (seenDate.get(dttmkey) != null) { 498 continue; 499 } 500 seenDate.put(dttmkey, dttmkey); 501 502 Date dttm = fmt.parse(dateString); 503 convertCal.setTime(dttm); 504 String key; 505 Way way = getWay(wayString, null); 506 if (!isBest && (waysToUse != null) && (waysToUse.size() > 0) 507 && (waysToUse.get(wayString) == null)) { 508 continue; 509 } 510 511 if (isBest) { 512 key = wayString; 513 } else { 514 key = wayString + "_" + dateString; 515 convertCal.add(Calendar.HOUR_OF_DAY, forecastHour); 516 } 517 dttm = convertCal.getTime(); 518 StormTrack track = (StormTrack) trackMap.get(key); 519 if (track == null) { 520 way = (isBest ? Way.OBSERVATION : way); 521 track = new StormTrack(stormInfo, addWay(way), new DateTime( 522 dttm), obsParams); 523 trackMap.put(key, track); 524 tracks.addTrack(track); 525 } 526 String latString = (String) toks.get(IDX_LAT); 527 String lonString = (String) toks.get(IDX_LON); 528 String t = latString + " " + lonString; 529 530 boolean south = latString.endsWith("S"); 531 boolean west = lonString.endsWith("W"); 532 double latitude = Double.parseDouble(latString.substring(0, 533 latString.length() - 1)) / 10.0; 534 double longitude = Double.parseDouble(lonString.substring(0, 535 lonString.length() - 1)) / 10.0; 536 if (south) { 537 latitude = -latitude; 538 } 539 if (west) { 540 longitude = -longitude; 541 } 542 543 EarthLocation elt = new EarthLocationLite(new Real( 544 RealType.Latitude, latitude), new Real(RealType.Longitude, 545 longitude), altReal); 546 547 List<Real> attributes = new ArrayList<Real>(); 548 549 double windspeed = ((IDX_VMAX < toks.size()) ? getDouble((String) toks 550 .get(IDX_VMAX)) 551 : Double.NaN); 552 double pressure = ((IDX_MSLP < toks.size()) ? getDouble((String) toks 553 .get(IDX_MSLP)) 554 : Double.NaN); 555 attributes.add(PARAM_STORMCATEGORY.getReal((double) category)); 556 attributes.add(PARAM_MINPRESSURE.getReal(pressure)); 557 attributes.add(PARAM_MAXWINDSPEED_KTS.getReal(windspeed)); 558 559 StormTrackPoint stp = new StormTrackPoint(elt, new DateTime(dttm), 560 forecastHour, attributes); 561 562 track.addPoint(stp); 563 } 564 return true; 565 } 566 567 /** 568 * _more_ 569 * 570 * @return _more_ 571 */ 572 public String getWayName() { 573 return "Tech"; 574 } 575 576 /** 577 * _more_ 578 * 579 * @param stormInfo 580 * _more_ 581 * @param waysToUse 582 * _more_ 583 * @param observationWay 584 * _more_ 585 * 586 * @return _more_ 587 * 588 * @throws Exception 589 * _more_ 590 */ 591 public StormTrackCollection getTrackCollectionInner(StormInfo stormInfo, 592 Hashtable<String, Boolean> waysToUse, Way observationWay) 593 throws Exception { 594 if (localTracks != null) { 595 return localTracks; 596 } 597 598 long t1 = System.currentTimeMillis(); 599 StormTrackCollection tracks = new StormTrackCollection(); 600 601 String trackFile; 602 boolean justObs = (waysToUse != null) && (waysToUse.size() == 1) 603 && (waysToUse.get(Way.OBSERVATION.toString()) != null); 604 int nowYear = new GregorianCalendar(DateUtil.TIMEZONE_GMT) 605 .get(Calendar.YEAR); 606 int stormYear = getYear(stormInfo.getStartTime()); 607 // If its the current year then its in the aid_public dir 608 String aSubDir = ((stormYear == nowYear) ? "aid_public" 609 : ("archive/" + stormYear)); 610 String bSubDir = ((stormYear == nowYear) ? "btk" 611 : ("archive/" + stormYear)); 612 if (!justObs) { 613 trackFile = getFullPath(aSubDir + "/" + PREFIX_ANALYSIS 614 + stormInfo.getBasin().toLowerCase() 615 + stormInfo.getNumber() + stormYear + ".dat.gz"); 616 // What we think might be in the archive might actually be the last 617 // year 618 // and they haven't moved it into the archive 619 try { 620 readTracks(stormInfo, tracks, trackFile, waysToUse, true); 621 } catch (BadDataException bde) { 622 if (!aSubDir.equals("aid_public")) { 623 try { 624 trackFile = getFullPath("aid_public/" + PREFIX_ANALYSIS 625 + stormInfo.getBasin().toLowerCase() 626 + stormInfo.getNumber() + stormYear + ".dat.gz"); 627 readTracks(stormInfo, tracks, trackFile, waysToUse, 628 true); 629 } catch (BadDataException bde2) { 630 System.err.println("Failed reading 'A' file for storm:" 631 + stormInfo + " file:" + trackFile); 632 } 633 } 634 // System.err.println("Failed reading 'A' file for storm:" + 635 // stormInfo+" file:" + trackFile); 636 } 637 } 638 // Now read the b"est file 639 trackFile = getFullPath(bSubDir + "/" + PREFIX_BEST 640 + stormInfo.getBasin().toLowerCase() + stormInfo.getNumber() 641 + stormYear + ".dat.gz"); 642 try { 643 readTracks(stormInfo, tracks, trackFile, null, true); 644 } catch (BadDataException bde) { 645 if (!bSubDir.equals("btk")) { 646 try { 647 trackFile = getFullPath("btk/" + PREFIX_BEST 648 + stormInfo.getBasin().toLowerCase() 649 + stormInfo.getNumber() + stormYear + ".dat.gz"); 650 readTracks(stormInfo, tracks, trackFile, null, true); 651 } catch (BadDataException bde2) { 652 System.err.println("Failed reading 'B' file for storm:" 653 + stormInfo + " file:" + trackFile); 654 } 655 656 } 657 // System.err.println("Failed reading 'B' file for storm:" + 658 // stormInfo+" file:" + trackFile); 659 } 660 long t2 = System.currentTimeMillis(); 661 // System.err.println("time: " + (t2 - t1)); 662 663 return tracks; 664 } 665 666 /** 667 * Set the Directory property. 668 * 669 * @param value 670 * The new value for Directory 671 */ 672 public void setPath(String value) { 673 path = value; 674 } 675 676 /** 677 * Get the Directory property. 678 * 679 * @return The Directory 680 */ 681 public String getPath() { 682 return path; 683 } 684 685 /** 686 * _more_ 687 * 688 * @param file 689 * _more_ 690 * @param ignoreErrors 691 * _more_ 692 * 693 * @return _more_ 694 * 695 * @throws Exception 696 * _more_ 697 */ 698 private byte[] readFile(String file, boolean ignoreErrors) throws Exception { 699 if (new File(file).exists()) { 700 return IOUtil.readBytes(IOUtil.getInputStream(file, getClass())); 701 } 702 if (!file.startsWith("ftp:")) { 703 if (ignoreErrors) { 704 return null; 705 } 706 throw new FileNotFoundException("Could not read file: " + file); 707 } 708 709 URL url = new URL(file); 710 FTPClient ftp = new FTPClient(); 711 try { 712 ftp.connect(url.getHost()); 713 ftp.login("anonymous", "password"); 714 ftp.setFileType(FTP.IMAGE_FILE_TYPE); 715 ftp.enterLocalPassiveMode(); 716 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 717 if (ftp.retrieveFile(url.getPath(), bos)) { 718 return bos.toByteArray(); 719 } else { 720 throw new IOException("Unable to retrieve file:" + url); 721 } 722 } catch (org.apache.commons.net.ftp.FTPConnectionClosedException fcce) { 723 System.err.println("ftp error:" + fcce); 724 System.err.println(ftp.getReplyString()); 725 if (!ignoreErrors) { 726 throw fcce; 727 } 728 return null; 729 } catch (Exception exc) { 730 if (!ignoreErrors) { 731 throw exc; 732 } 733 return null; 734 } finally { 735 try { 736 ftp.logout(); 737 } catch (Exception exc) { 738 } 739 try { 740 ftp.disconnect(); 741 } catch (Exception exc) { 742 } 743 744 } 745 } 746 747}