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.chooser; 030 031import java.beans.PropertyChangeEvent; 032import java.beans.PropertyChangeListener; 033import java.io.File; 034import java.io.IOException; 035import java.nio.file.Files; 036import java.nio.file.Paths; 037import java.util.Objects; 038 039import javax.swing.*; 040import javax.swing.event.AncestorEvent; 041import javax.swing.event.AncestorListener; 042import javax.swing.filechooser.FileFilter; 043import javax.swing.filechooser.FileNameExtensionFilter; 044 045import edu.wisc.ssec.mcidasv.Constants; 046import edu.wisc.ssec.mcidasv.McIDASV; 047import edu.wisc.ssec.mcidasv.util.McVGuiUtils; 048import edu.wisc.ssec.mcidasv.util.pathwatcher.DirectoryWatchService; 049import edu.wisc.ssec.mcidasv.util.pathwatcher.OnFileChangeListener; 050import org.bushe.swing.event.annotation.AnnotationProcessor; 051import org.bushe.swing.event.annotation.EventTopicSubscriber; 052import org.slf4j.Logger; 053import org.slf4j.LoggerFactory; 054 055import ucar.unidata.idv.chooser.IdvChooser; 056 057import static edu.wisc.ssec.mcidasv.McIDASV.getStaticMcv; 058import static ucar.unidata.idv.chooser.IdvChooser.PREF_DEFAULTDIR; 059 060/** 061 * An extension of JFileChooser to handle Two-Line Element (TLE) 062 * files, for plotting satellite orbit tracks. 063 * 064 * @author Gail Dengel and Tommy Jasmin 065 * 066 */ 067public class TLEFileChooser extends JFileChooser implements AncestorListener, PropertyChangeListener { 068 069 private static final String ID = "tlefilechooser"; 070 071 /** 072 * auto-generated default value 073 */ 074 private static final long serialVersionUID = 1L; 075 private static final Logger logger = LoggerFactory.getLogger(TLEFileChooser.class); 076 077 /* the enclosing orbit track chooser */ 078 private PolarOrbitTrackChooser potc = null; 079 080 /** This is mostly used to preemptively null-out the listener. */ 081 protected OnFileChangeListener watchListener; 082 083 /** 084 * Value is controlled via {@link #ancestorAdded(AncestorEvent)} and 085 * {@link #ancestorRemoved(AncestorEvent)} 086 */ 087 private boolean trulyVisible; 088 089 /** 090 * Create the file chooser 091 * 092 * @param chooser {@code PolarOrbitTrackChooser} to which this {@code TLEFileChooser} belongs. 093 * @param directory Initial directory. 094 * @param filename Initial filename within {@code directory}. 095 */ 096 public TLEFileChooser(PolarOrbitTrackChooser chooser, String directory, String filename) { 097 super(directory); 098 AnnotationProcessor.process(this); 099 potc = chooser; 100 101 logger.debug("TLEFileChooser constructor..."); 102 setControlButtonsAreShown(false); 103 setMultiSelectionEnabled(false); 104 FileFilter filter = new FileNameExtensionFilter("TLE files", "txt"); 105 addChoosableFileFilter(filter); 106 setAcceptAllFileFilterUsed(false); 107 setFileFilter(filter); 108 addPropertyChangeListener(this); 109 addAncestorListener(this); 110 111 File tmpFile = new File(directory + File.separatorChar + filename); 112// logger.trace("tmpFile='{}' exists='{}'", tmpFile, tmpFile.exists()); 113 setSelectedFile(null); 114 setSelectedFile(tmpFile); 115// final JList list = McVGuiUtils.getDescendantOfType(JList.class, this, "Enabled", true); 116// list.requestFocus(); 117 } 118 119 @Override public void setSelectedFile(File file) { 120 // i REALLY don't know how to explain this one...but don't remove the 121 // following if-else stuff. at least on OSX, it has *something* to do with 122 // whether or not the UI actually shows the file selection. 123 // what is somewhat weird is that commenting out the current if-else 124 // and doing something like: 125 // if (file != null) { 126 // boolean weird = file.exists(); 127 // } 128 // does *NOT* work--but maybe HotSpot is optimizing away the unused code, right? 129 // wrong! the following also does not work: 130 // if (file != null && file.exists()) { 131 // logger.trace("exists!"); 132 // } 133 // i will note that calls to this method appear to be happening on threads 134 // other than the EDT...but using SwingUtilities.invokeLater and 135 // SwingUtilities.invokeAndWait have not worked so far (and I've tried 136 // the obvious places in the code, including POTC.doMakeContents()). 137 if (file != null) { 138 logger.trace("setting file='{}' exists={}", file, file.exists()); 139 } else { 140 logger.trace("setting file='{}' exists=NULL", file); 141 } 142 super.setSelectedFile(file); 143 } 144 145 /** 146 * Approve the selection 147 */ 148 @Override public void approveSelection() { 149 super.approveSelection(); 150 potc.doLoad(); 151 } 152 153 public void setPotc(PolarOrbitTrackChooser potc) { 154 this.potc = potc; 155 } 156 157 public PolarOrbitTrackChooser getPotc() { 158 return potc; 159 } 160 161 @Override public void propertyChange(PropertyChangeEvent pce) { 162 String propName = pce.getPropertyName(); 163 if (propName.equals(SELECTED_FILE_CHANGED_PROPERTY)) { 164 // tell the chooser we have a file to load 165 handleFileChanged(); 166 } else if (JFileChooser.DIRECTORY_CHANGED_PROPERTY.equals(propName)) { 167 String newPath = pce.getNewValue().toString(); 168 handleChangeWatchService(newPath); 169 } 170 } 171 172 protected void handleFileChanged() { 173 if (potc != null) { 174 File f = getSelectedFile(); 175 if ((f != null) && accept(f) && potc.localMode()) { 176 if (!f.isDirectory()) { 177 // update last visited directory here 178 String potcId = PREF_DEFAULTDIR + potc.getId(); 179 String potcFileId = PREF_DEFAULTDIR + potc.getId() + ".file"; 180 String dir = getSelectedFile().getParent(); 181 String file = getSelectedFile().getName(); 182 potc.getIdv().getStateManager().writePreference( 183 potcId, dir 184 ); 185 potc.getIdv().getStateManager().writePreference( 186 potcFileId, file 187 ); 188 potc.enableLoadFromFile(true); 189 } 190 } else { 191 potc.enableLoadFromFile(false); 192 } 193 } else { 194 logger.warn("null potc, must be set by caller before use."); 195 } 196 } 197 198 /** 199 * Change the path that the file chooser is presenting to the user. 200 * 201 * <p>This value will be written to the user's preferences so that the user 202 * can pick up where they left off after restarting McIDAS-V.</p> 203 * 204 * @param newPath Path to set. 205 */ 206 public void setPath(String newPath) { 207 String id = PREF_DEFAULTDIR + ID; 208 potc.getIdv().getStateManager().writePreference(id, newPath); 209 } 210 211 /** 212 * See the javadoc for {@link #getPath(String)}. 213 * 214 * <p>The difference between the two is that this method passes the value 215 * of {@code System.getProperty("user.home")} to {@link #getPath(String)} 216 * as the default value.</p> 217 * 218 * @return Path to use for the chooser. 219 */ 220 public String getPath() { 221 return getPath(System.getProperty("user.home")); 222 } 223 224 /** 225 * Get the path the {@link JFileChooser} should be using. 226 * 227 * <p>If the path in the user's preferences is {@code null} 228 * (or does not exist), {@code defaultValue} will be returned.</p> 229 * 230 * <p>If there is a nonexistent path in the preferences file, 231 * {@link FileChooser#findValidParent(String)} will be used.</p> 232 * 233 * @param defaultValue Default path to use if there is a {@literal "bad"} 234 * path in the user's preferences. 235 * Cannot be {@code null}. 236 * 237 * @return Path to use for the chooser. 238 * 239 * @throws NullPointerException if {@code defaultValue} is {@code null}. 240 */ 241 public String getPath(final String defaultValue) { 242 Objects.requireNonNull(defaultValue, "Default value may not be null"); 243 String prop = PREF_DEFAULTDIR + ID; 244 String tempPath = (String)potc.getIdv().getPreference(prop); 245 if ((tempPath == null)) { 246 tempPath = defaultValue; 247 } else if (!Files.exists(Paths.get(tempPath))) { 248 tempPath = FileChooser.findValidParent(tempPath); 249 } 250 return tempPath; 251 } 252 253 /** 254 * Respond to path changes in the {@code JFileChooser}. 255 * 256 * <p>This method will disable monitoring of the previous path and then 257 * enable monitoring of {@code newPath}.</p> 258 * 259 * @param newPath New path to begin watching. 260 */ 261 public void handleChangeWatchService(final String newPath) { 262 DirectoryWatchService watchService = 263 ((McIDASV)potc.getIdv()).getWatchService(); 264 265 if ((watchService != null) && (watchListener != null)) { 266 logger.trace("now watching '{}'", newPath); 267 268 setPath(newPath); 269 270 handleStopWatchService(Constants.EVENT_FILECHOOSER_STOP, 271 "changed directory"); 272 273 handleStartWatchService(Constants.EVENT_FILECHOOSER_START, 274 "new directory"); 275 } 276 } 277 278 /** 279 * Begin monitoring the directory returned by {@link #getPath()} for 280 * changes. 281 * 282 * @param topic Artifact from {@code EventBus} annotation. Not used. 283 * @param reason Optional {@literal "Reason"} for starting. 284 * Helpful for logging. 285 */ 286 @EventTopicSubscriber(topic=Constants.EVENT_FILECHOOSER_START) 287 public void handleStartWatchService(final String topic, 288 final Object reason) 289 { 290 McIDASV mcv = (McIDASV)potc.getIdv(); 291 boolean offscreen = mcv.getArgsManager().getIsOffScreen(); 292 boolean initDone = mcv.getHaveInitialized(); 293 String watchPath = getPath(); 294 if (isTrulyVisible() && !offscreen && initDone) { 295 try { 296 watchListener = createWatcher(); 297 mcv.watchDirectory(watchPath, "*", watchListener); 298 logger.trace("watching '{}' pattern: '{}' (reason: '{}')", 299 watchPath, "*", reason); 300 } catch (IOException e) { 301 logger.error("error creating watch service", e); 302 } 303 } 304 } 305 306 /** 307 * Disable directory monitoring (if it was enabled in the first place). 308 * 309 * @param topic Artifact from {@code EventBus} annotation. Not used. 310 * @param reason Optional {@literal "Reason"} for starting. 311 * Helpful for logging. 312 */ 313 @EventTopicSubscriber(topic= Constants.EVENT_FILECHOOSER_STOP) 314 public void handleStopWatchService(final String topic, 315 final Object reason) 316 { 317 logger.trace("stopping service (reason: '{}')", reason); 318 319 DirectoryWatchService service = getStaticMcv().getWatchService(); 320 service.unregister(watchListener); 321 322 service = null; 323 watchListener = null; 324 } 325 326 /** 327 * Creates a directory monitoring 328 * {@link edu.wisc.ssec.mcidasv.util.pathwatcher.Service service}. 329 * 330 * @return Directory monitor that will respond to changes. 331 */ 332 private OnFileChangeListener createWatcher() { 333 watchListener = new OnFileChangeListener() { 334 335 /** {@inheritDoc} */ 336 @Override public void onFileCreate(String filePath) { 337 DirectoryWatchService service = 338 getStaticMcv().getWatchService(); 339 if (service.isRunning()) { 340 SwingUtilities.invokeLater(() -> rescanCurrentDirectory()); 341 } 342 } 343 344 /** {@inheritDoc} */ 345 @Override public void onFileModify(String filePath) { 346 DirectoryWatchService service = 347 getStaticMcv().getWatchService(); 348 if (service.isRunning()) { 349 SwingUtilities.invokeLater(() -> rescanCurrentDirectory()); 350 } 351 } 352 353 /** {@inheritDoc} */ 354 @Override public void onFileDelete(String filePath) { 355 refreshIfNeeded(filePath); 356 } 357 358 /** {@inheritDoc} */ 359 @Override public void onWatchInvalidation(String filePath) { 360 refreshIfNeeded(filePath); 361 } 362 }; 363 return watchListener; 364 } 365 366 /** 367 * Used to handle the {@link OnFileChangeListener#onFileDelete(String)} and 368 * {@link OnFileChangeListener#onWatchInvalidation(String)} events. 369 * 370 * @param filePath Path of interest. Cannot be {@code null}. 371 */ 372 private void refreshIfNeeded(String filePath) { 373 DirectoryWatchService service = getStaticMcv().getWatchService(); 374 if (service.isRunning()) { 375 setPath(FileChooser.findValidParent(filePath)); 376 SwingUtilities.invokeLater(() -> { 377 setCurrentDirectory(new File(getPath())); 378 rescanCurrentDirectory(); 379 }); 380 } 381 } 382 383 /** 384 * {@inheritDoc} 385 */ 386 @Override public void ancestorAdded(AncestorEvent ancestorEvent) { 387 // keep the calls to setTrulyVisible as the first step. that way 388 // isTrulyVisible should work as expected. 389 setTrulyVisible(true); 390 391 handleStartWatchService(Constants.EVENT_FILECHOOSER_START, 392 "chooser is visible"); 393 SwingUtilities.invokeLater(this::rescanCurrentDirectory); 394 } 395 396 /** 397 * {@inheritDoc} 398 */ 399 @Override public void ancestorRemoved(AncestorEvent ancestorEvent) { 400 // keep the calls to setTrulyVisible as the first step. that way 401 // isTrulyVisible should work as expected. 402 setTrulyVisible(false); 403 404 handleStopWatchService(Constants.EVENT_FILECHOOSER_STOP, 405 "chooser is not visible"); 406 407 } 408 409 /** 410 * Not implemented. 411 * 412 * @param ancestorEvent Ignored. 413 */ 414 @Override public void ancestorMoved(AncestorEvent ancestorEvent) {} 415 416 /** 417 * Determine if this file chooser is actually visible to the user. 418 * 419 * @return Whether or not this component has been made visible. 420 */ 421 public boolean isTrulyVisible() { 422 return trulyVisible; 423 } 424 425 /** 426 * Set whether or not this file chooser is actually visible to the user. 427 * 428 * @param value {@code true} means visible. 429 */ 430 private void setTrulyVisible(boolean value) { 431 trulyVisible = value; 432 } 433 434}