001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
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}