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 */
028package edu.wisc.ssec.mcidasv.data;
029
030import static javax.swing.BorderFactory.createTitledBorder;
031import static javax.swing.GroupLayout.Alignment.BASELINE;
032import static javax.swing.GroupLayout.Alignment.LEADING;
033import static javax.swing.GroupLayout.Alignment.TRAILING;
034import static javax.swing.LayoutStyle.ComponentPlacement.RELATED;
035
036import java.awt.Container;
037import java.awt.Dimension;
038import java.awt.EventQueue;
039import java.awt.Font;
040import java.awt.event.WindowEvent;
041import java.awt.event.WindowAdapter;
042import java.io.ByteArrayInputStream;
043import java.io.ByteArrayOutputStream;
044import java.io.File;
045import java.io.IOException;
046import java.nio.file.Paths;
047import java.util.ArrayList;
048import java.util.List;
049import java.util.stream.Collectors;
050
051import javax.swing.BoxLayout;
052import javax.swing.GroupLayout;
053import javax.swing.JButton;
054import javax.swing.JComboBox;
055import javax.swing.JDialog;
056import javax.swing.JEditorPane;
057import javax.swing.JFrame;
058import javax.swing.JLabel;
059import javax.swing.JOptionPane;
060import javax.swing.JPanel;
061import javax.swing.JRadioButton;
062import javax.swing.JScrollPane;
063import javax.swing.JSeparator;
064import javax.swing.JTextField;
065import javax.swing.JTextPane;
066import javax.swing.JToolBar;
067import javax.swing.ScrollPaneConstants;
068import javax.swing.UIManager;
069import javax.swing.WindowConstants;
070import javax.swing.border.EmptyBorder;
071import javax.swing.event.HyperlinkEvent;
072import javax.swing.text.PlainDocument;
073
074import org.slf4j.Logger;
075import org.slf4j.LoggerFactory;
076
077import net.miginfocom.swing.MigLayout;
078import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
079import edu.wisc.ssec.mcidasv.util.McVGuiUtils.IconPanel;
080import edu.wisc.ssec.mcidasv.util.McVGuiUtils.Prefer;
081import edu.wisc.ssec.mcidasv.util.McVGuiUtils.Width;
082import edu.wisc.ssec.mcidasv.util.WebBrowser;
083import edu.wisc.ssec.mcidasv.Constants;
084import ucar.ma2.Array;
085import ucar.nc2.Variable;
086import ucar.nc2.dataset.NetcdfDataset;
087import ucar.nc2.ncml.NcMLReader;
088import ucar.nc2.dt.grid.GridDataset;
089import ucar.unidata.util.FileManager;
090import ucar.unidata.util.GuiUtils;
091import ucar.unidata.util.IOUtil;
092import ucar.unidata.util.LayoutUtil;
093import ucar.unidata.data.DataSourceDescriptor;
094import ucar.unidata.data.grid.GeoGridDataSource;
095import ucar.unidata.idv.IntegratedDataViewer;
096import visad.ConstantMap;
097import visad.Display;
098import visad.FlatField;
099import visad.python.JPythonMethods;
100import visad.ss.BasicSSCell;
101import visad.ss.FancySSCell;
102
103/**
104 * GUI widget that allows users to attempt to specify an {@literal "NCML"}
105 * if a given NetCDF is not CF-compliant.
106 */
107public class BadNetCDFWidget implements Constants {
108    
109    private static final Logger logger =
110        LoggerFactory.getLogger(BadNetCDFWidget.class);
111    
112    private IntegratedDataViewer idv;
113    
114    private NetcdfDataset ncFile;
115    private List<Variable> varList;
116    private List<String> varNames;
117    
118    // For NcML Editor
119    private JEditorPane NcMLeditor;
120    
121    // For variable display
122    BasicSSCell display;
123    ConstantMap[] cmaps;
124    
125    // For nav specification
126    private JRadioButton radioLatLonVars = new JRadioButton("Variables", true);
127    private JRadioButton radioLatLonBounds = new JRadioButton("Bounds", false);
128    
129    private JComboBox refComboBox = new JComboBox();
130    private JComboBox latComboBox = new JComboBox();
131    private JComboBox lonComboBox = new JComboBox();
132
133    private JPanel panelLatLonVars = new JPanel();
134    private JPanel panelLatLonBounds = new JPanel();
135
136    private JTextField textLatUL = new JTextField();
137    private JTextField textLonUL = new JTextField();
138    private JTextField textLatLR = new JTextField();
139    private JTextField textLonLR = new JTextField();
140
141    /**
142     * Handles problems from {@code openDataset}.
143     *
144     * @param ncFile NetCDF that caused a problem. Cannot be {@code null}.
145     * @param idv Reference to the IDV. Cannot be {@code null}.
146     */
147    public BadNetCDFWidget(NetcdfDataset ncFile, IntegratedDataViewer idv) {
148        this.idv = idv;
149        this.ncFile = ncFile;
150        varList = ncFile.getVariables();
151        varNames = new ArrayList<>(varList.size());
152        varNames.addAll(varList.stream().map(Variable::getFullName).collect(Collectors.toList()));
153    }
154
155    /**
156     * Passes through any exception from openDataset - this function
157     * doesn't provide an IDV and should only be used for testing.
158     *
159     * (Some functionality using the rest of the IDV won't work.)
160     *
161     * @param filepath Path to a NetCDF file.
162     *
163     * @throws IOException if there was a problem.
164     */
165    public BadNetCDFWidget(String filepath) throws IOException {
166        this(NetcdfDataset.openDataset(filepath), null);
167    }
168
169    /**
170     * Displays our "main menu" of choices to fix the given file. Everything
171     * else needed can get called from here.
172     */
173    public void showChoices() {
174        EventQueue.invokeLater(() -> {
175            try {
176                BadNetCDFDialog dialog = new BadNetCDFDialog();
177                dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
178                dialog.setVisible(true);
179                dialog.toFront();
180            } catch (Exception e) {
181                logger.error("Could not show choices", e);
182            }
183        });
184    }
185
186    /**
187     * Creates an editor for NcML and displays it in a window. This includes
188     * buttons for saving just the NcML and the full NetCDF file with the
189     * changes made.
190     */
191    private void showNcMLEditor() {
192        NcMLeditor = new JEditorPane();
193        
194        // We use this to store the actual ncml - 10000 is just the number
195        // toolsUI used
196        ByteArrayOutputStream bos = new ByteArrayOutputStream(10000);
197        try {
198            ncFile.writeNcml(bos, null);
199            NcMLeditor.setText(bos.toString());
200            NcMLeditor.setCaretPosition(0);
201        } catch (IOException ioe) {
202            logger.error("Could not write ncml", ioe);
203            // DataSourceImpl - doesn't work if we're not a data source
204            // setInError(true, false, "");
205            return;
206        }
207        
208        NcMLeditor.setEditable(true);
209
210        // Set the font style.
211        NcMLeditor.setFont(new Font("Courier", Font.PLAIN, 12));
212        
213        // Set the tab size
214        NcMLeditor.getDocument().putProperty(PlainDocument.tabSizeAttribute, 2);
215
216        // Button to save NcML as text, 
217        // popup allows them to specify where.
218        JButton saveNcMLBtn = new JButton("Save NcML as text");
219        saveNcMLBtn.addActionListener(e -> {
220            // Begin with getting the filename we want to write to.
221            String ncLocation = ncFile.getLocation();
222
223            if (ncLocation == null) {
224                ncLocation = "test";
225            }
226            int pos = ncLocation.lastIndexOf(".");
227            if (pos > 0) {
228                ncLocation = ncLocation.substring(0, pos);
229            }
230            String filename = FileManager.getWriteFile(ncLocation + ".ncml");
231            if (filename == null) {
232                return;
233            }
234
235            // Once we have that, we can actually write to the file!
236            try {
237                IOUtil.writeFile(new File(filename), NcMLeditor.getText());
238            } catch (Exception exc) {
239                logger.error("Could not write to '"+filename+'\'', exc);
240            }
241        });
242
243        // Button to merge the NcML with NetCDF 
244        // a'la ToolsUI and write it back out as NetCDF3.
245        JButton saveNetCDFBtn = new JButton("Merge and save NetCDF");
246        saveNetCDFBtn.addActionListener(e -> {
247            // Begin with getting the filename we want to write to.
248            String ncLocation = ncFile.getLocation();
249
250            if (ncLocation == null) {
251                ncLocation = "test";
252            }
253            int pos = ncLocation.lastIndexOf(".");
254            if (pos > 0) {
255                ncLocation = ncLocation.substring(0, pos);
256            }
257            String filename = FileManager.getWriteFile(ncLocation + ".nc");
258            if (filename == null) {
259                return;
260            }
261
262            // Once we have that, we can actually write to the file!
263            try {
264                ByteArrayInputStream bis =
265                    new ByteArrayInputStream(NcMLeditor.getText().getBytes());
266                NcMLReader.writeNcMLToFile(bis, filename);
267            } catch (Exception exc) {
268                logger.error("Could not write to '"+filename+'\'', exc);
269            }
270        });
271        
272        // Button to load this data into McV from NcML
273        JButton sendToMcVBtn = new JButton("Attempt to load with this NcML");
274        sendToMcVBtn.addActionListener(ae -> {
275            // TODO: save the current NcML into the NetcdfDataSource
276            createIDVdisplay();
277        });
278
279        JToolBar toolbar = new JToolBar("NcML Editor Controls");
280
281        toolbar.add(saveNcMLBtn);
282        toolbar.add(saveNetCDFBtn);
283        toolbar.add(sendToMcVBtn);
284
285        JScrollPane scrollPane =
286            new JScrollPane(NcMLeditor,
287                ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
288                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
289
290        // TODO: PREFERRED SIZE?
291        scrollPane.setPreferredSize(new Dimension(600, 600));
292        
293        JPanel panel = LayoutUtil.topCenter(toolbar, scrollPane);
294        JFrame editorWindow =
295            GuiUtils.makeWindow("NcML Editor", LayoutUtil.inset(panel, 10), 0, 0);
296        editorWindow.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
297        editorWindow.setVisible(true);
298        editorWindow.toFront();
299    }
300
301    /**
302     * Takes our ncFile and puts it back into IDV (McV).
303     */
304    private void createIDVdisplay() {
305
306        // Make a NetcdfDataset from our NcML
307        ByteArrayInputStream bis =
308            new ByteArrayInputStream(NcMLeditor.getText().getBytes());
309
310        try {
311            ncFile = NcMLReader.readNcML(bis, null);
312        } catch (IOException e1) {
313            logger.error("Could not read ncml", e1);
314            return;
315        }
316
317        // Now try to turn that NetcdfDataset into a legitimate DataSource!
318        GridDataset gd;
319
320        try {
321            gd = new GridDataset(ncFile);
322        } catch (IOException e) {
323            logger.error("could not create grid dataset from netcdf file", e);
324            return;
325        }
326        
327        ncFile.getLocation();
328        DataSourceDescriptor dsd = new DataSourceDescriptor();
329        dsd.setLabel("NcML DS Label");
330        GeoGridDataSource ggds =
331            new GeoGridDataSource(dsd, gd, "NcML Data Source", ncFile.getLocation());
332        ggds.initAfterCreation();
333        idv.getDataManager().addDataSource(ggds);
334    }
335
336    /**
337     * Shows a window that gives a choice of variables.
338     */
339    private void showVarPicker() {
340        final JComboBox<String> varDD = new JComboBox<>();
341        GuiUtils.setListData(varDD, varNames);
342
343        varDD.addActionListener(ae -> {
344            JComboBox<String> cb = (JComboBox<String>) ae.getSource();
345            Variable plotVar = varList.get(cb.getSelectedIndex());
346            String varName = (String) cb.getSelectedItem();
347
348            float [] varVals;
349            try {
350                // TODO: Is there a better way to convert this?
351                // Is there another function like reshape?
352                Array varArray = plotVar.read();
353                varVals  = (float[])varArray.get1DJavaArray(float.class);
354            } catch (IOException IOexe) {
355                logger.error("error while reading from variable '" + plotVar + '\'', IOexe);
356                return;
357            }
358
359            int size = plotVar.getDimensions().size();
360            if( size != 2) {
361                JOptionPane.showMessageDialog(null,
362                    ("<html>Variables must have 2 dimensions to be displayed here.<br><br>\"" + varName + "\" has " + size + ".</html>"),
363                    "Invalid Dimensions",
364                    JOptionPane.ERROR_MESSAGE);
365                return;
366            }
367
368            int xdim = plotVar.getDimensions().get(0).getLength();
369            int ydim = plotVar.getDimensions().get(1).getLength();
370
371            float[][] var2D = reshape(varVals, ydim, xdim);
372
373            //JPythonMethods.plot(varVals);
374            //JPythonMethods.plot(var2D);
375
376            try {
377                FlatField varField = JPythonMethods.field(var2D);
378
379                ConstantMap[] cmaps1 = {
380                    new ConstantMap(1.0, Display.Red),
381                    new ConstantMap(1.0, Display.Green),
382                    new ConstantMap(1.0, Display.Blue)
383                };
384
385                // Clear out the display or we get some weird stuff going on.
386                display.clearCell();
387                display.clearMaps();
388                display.clearDisplay();
389
390                display.addData(varField, cmaps1);
391            } catch (Exception exe) {
392                logger.error("problem encountered", exe);
393            }
394        });
395        
396        //BasicSSCell display = new FancySSCell("Variable!");
397        //display.setDimension(BasicSSCell.JAVA3D_3D);
398
399        try {
400            // Heavily borrowed from VISAD's JPythonMethods
401            display = new FancySSCell("Variable Viewer");
402            display.setDimension(BasicSSCell.JAVA3D_3D);
403            display.setPreferredSize(new Dimension(256, 256));
404            JFrame frame = new JFrame("Variable Viewer");
405            JPanel pane = new JPanel();
406            pane.setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS));
407            frame.setContentPane(pane);
408            pane.add(varDD);
409            pane.add(display);
410            JButton controls = new JButton("Controls");
411            JPanel buttons = new JPanel();
412            buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
413            buttons.add(controls);
414            pane.add(buttons);
415            final FancySSCell fdisp = (FancySSCell) display;
416            fdisp.setAutoShowControls(false);
417            
418            controls.addActionListener(e -> fdisp.showWidgetFrame());
419            
420            frame.pack();
421            frame.setVisible(true);
422            frame.toFront();
423        } catch (Exception exe) {
424            logger.error("problem displaying", exe);
425        }
426    }
427
428    /**
429     * Reshape a 1D float array into a 2D float array.
430     *
431     * @param arr Array to reshape.
432     * @param m First dimension.
433     * @param n Second dimension.
434     *
435     * @return {@code arr} reshaped into a new 2D array.
436     */
437    private static float[][] reshape(float[] arr, int m, int n) {
438        float[][] newArr = new float[m][n];
439        int index = 0;
440        for (int i = 0; i < n; i++) {
441            for (int j = 0; j< m; j++) {
442                newArr[j][i] = arr[index++];
443            }
444        }
445        return newArr;
446    }
447
448    /**
449     * Shows a window that gives the opportunity to either define coordinate
450     * variables or specify corner points.
451     *
452     * Borrowed heavily from FlatFileChooser's makeNavigationPanel for style.
453     */
454    private void showNavChooser() {
455        JPanel midPanel = new JPanel();
456        midPanel.setBorder(createTitledBorder("Navigation"));
457
458        GuiUtils.setListData(refComboBox, varNames);
459        GuiUtils.setListData(latComboBox, varNames);
460        GuiUtils.setListData(lonComboBox, varNames);
461        
462        McVGuiUtils.setComponentWidth(latComboBox, Width.QUADRUPLE);
463        McVGuiUtils.setComponentWidth(lonComboBox, Width.QUADRUPLE);
464        McVGuiUtils.setComponentWidth(refComboBox, Width.QUADRUPLE);
465
466        panelLatLonVars =
467            McVGuiUtils.topBottom(
468                McVGuiUtils.makeLabeledComponent("Latitude:",latComboBox),
469                McVGuiUtils.makeLabeledComponent("Longitude:",lonComboBox),
470                Prefer.NEITHER);
471
472        GuiUtils.buttonGroup(radioLatLonVars, radioLatLonBounds);
473
474        // Images to make the bounds more clear
475        IconPanel urPanel =
476            new IconPanel("/edu/wisc/ssec/mcidasv/images/upper_right.gif");
477        IconPanel llPanel =
478            new IconPanel("/edu/wisc/ssec/mcidasv/images/lower_left.gif");
479        
480        McVGuiUtils.setComponentWidth(textLatUL);
481        McVGuiUtils.setComponentWidth(textLonUL);
482        McVGuiUtils.setComponentWidth(textLatLR);
483        McVGuiUtils.setComponentWidth(textLonLR);
484        panelLatLonBounds = McVGuiUtils.topBottom(
485            McVGuiUtils.makeLabeledComponent("UL Lat/Lon:", LayoutUtil.leftRight(LayoutUtil.hbox(textLatUL, textLonUL), urPanel)),
486            McVGuiUtils.makeLabeledComponent("LR Lat/Lon:", LayoutUtil.leftRight(llPanel, LayoutUtil.hbox(textLatLR, textLonLR))),
487            Prefer.NEITHER);
488        
489        panelLatLonBounds =
490            McVGuiUtils.topBottom(
491                panelLatLonBounds,
492                McVGuiUtils.makeLabeledComponent("Reference:", refComboBox),
493                Prefer.NEITHER);
494
495        McVGuiUtils.setComponentWidth(radioLatLonVars);
496        McVGuiUtils.setComponentWidth(radioLatLonBounds);
497        
498        // Add a bit of a buffer to both
499        panelLatLonVars = LayoutUtil.inset(panelLatLonVars, 5);
500        panelLatLonBounds = LayoutUtil.inset(panelLatLonBounds, 5);
501        
502        GroupLayout layout = new GroupLayout(midPanel);
503        midPanel.setLayout(layout);
504        layout.setHorizontalGroup(
505            layout.createParallelGroup(LEADING)
506            .addGroup(layout.createSequentialGroup()
507                .addContainerGap()
508                .addGroup(layout.createParallelGroup(LEADING)
509                    .addGroup(layout.createSequentialGroup()
510                        .addComponent(radioLatLonVars)
511                        .addGap(GAP_RELATED)
512                        .addComponent(panelLatLonVars))
513                    .addGroup(layout.createSequentialGroup()
514                        .addComponent(radioLatLonBounds)
515                        .addGap(GAP_RELATED)
516                        .addComponent(panelLatLonBounds)))
517                .addContainerGap())
518        );
519        layout.setVerticalGroup(
520            layout.createParallelGroup(LEADING)
521            .addGroup(TRAILING, layout.createSequentialGroup()
522                .addContainerGap()
523                .addGroup(layout.createParallelGroup(BASELINE)
524                    .addComponent(radioLatLonVars)
525                    .addComponent(panelLatLonVars))
526                .addPreferredGap(RELATED)
527                .addGroup(layout.createParallelGroup(BASELINE)
528                    .addComponent(radioLatLonBounds)
529                    .addComponent(panelLatLonBounds))
530                .addPreferredGap(RELATED)
531                .addContainerGap())
532        );
533 
534
535        
536        radioLatLonVars.addActionListener(e -> checkSetLatLon());
537        
538        radioLatLonBounds.addActionListener(e -> checkSetLatLon());
539        
540        JButton goBtn = new JButton("Go!");
541        goBtn.addActionListener(ae -> {
542            boolean isVar = radioLatLonVars.isSelected();
543            if (isVar) {
544                navVarAction();
545            } else {
546                navCornersAction();
547            }
548        });
549
550        JPanel wholePanel =
551            McVGuiUtils.topBottom(midPanel, goBtn, Prefer.NEITHER);
552
553        JFrame myWindow =
554            GuiUtils.makeWindow(
555                "Pick Your Navigation!",
556                LayoutUtil.inset(wholePanel, 10),
557                0, 0);
558
559        checkSetLatLon();
560        myWindow.setVisible(true);
561        myWindow.toFront();
562    }
563    
564    
565    /**
566     * Enable or disable widgets for navigation.
567     */
568    private void checkSetLatLon() {
569        boolean isVar = radioLatLonVars.isSelected();
570        GuiUtils.enableTree(panelLatLonVars, isVar);
571        GuiUtils.enableTree(panelLatLonBounds, !isVar);
572    }
573
574    /**
575     * One of the two workhorses of our nav chooser, it alters the chosen 
576     * (existing) variables so they can be used as lat/lon pairs.
577     */
578    private void navVarAction() {
579    }
580
581    /**
582     * One of the two workhorses of our nav chooser, it creates new 
583     * variables for lat/lon based on the specified cornerpoints and
584     * reference variable (for dimensions).
585     */
586    private void navCornersAction() {
587    }
588    
589    public class BadNetCDFDialog extends JDialog {
590
591        private static final long serialVersionUID = 1L;
592
593        /**
594         * Create the dialog.
595         */
596        
597        public BadNetCDFDialog() {
598            setTitle("Non-Compliant NetCDF Tool");
599            setLocation(100,100);
600            Container contentPane = getContentPane();
601            
602            JLabel headerLabel =
603                new JLabel("McIDAS-V is unable to read your file.");
604
605            // TJJ Aug 2019 - TROPOMI L1B files sneak through to here, kinda hacky but
606            // might as well mention as of this date we handle L2 but not L1B
607
608            String filename = Paths.get(ncFile.getLocation()).getFileName().toString();
609            if (TropomiIOSP.TROPOMI_MATCHER.matcher(filename).matches()) {
610                if (filename.contains("_L1B_") || filename.contains("_L2__NP")) {
611                    headerLabel.setText("<html>" + headerLabel.getText() + "<br>" +
612                       "Only TROPOMI Level 2 Products are supported at this time.</html>");
613                }
614            }
615
616            headerLabel.setFont(UIManager.getFont("OptionPane.font"));
617            headerLabel.setBorder(new EmptyBorder(0, 0, 4, 0));
618            
619            JTextPane messageTextPane = new JTextPane();
620            Font textPaneFont = UIManager.getFont("TextPane.font");
621            String fontCss =
622                String.format("style=\"font-family: '%s'; font-size: %d;\"", textPaneFont.getFamily(), textPaneFont.getSize());
623            messageTextPane.setBackground(UIManager.getColor("Label.background"));
624            messageTextPane.setContentType("text/html");
625            messageTextPane.setDragEnabled(false);
626            messageTextPane.setText("<html>\n<body "+
627                fontCss + 
628                ">To verify if your file is CF-compliant, you can run your file through an online compliance checker " + 
629                "(<a href=\"https://compliance.ioos.us/index.html\">example CF-compliance utility</a>). " + 
630                "<br/><br/> \n\nIf the checker indicates that your file is not compliant you can attempt to fix it using " + 
631                "the NcML Editor provided in this window.<br/><br/>\n\nIn a future release of McIDAS-V, this interface will " +  
632                "present you with choices for the variables necessary for McIDAS-V to display your data.<br/></font></body></html>");
633            messageTextPane.setEditable(false);
634            messageTextPane.addHyperlinkListener(e -> {
635                HyperlinkEvent.EventType type = e.getEventType();
636                if (HyperlinkEvent.EventType.ACTIVATED.equals(type)) {
637                    String url = (e.getURL() == null)
638                               ? e.getDescription()
639                               : e.getURL().toString();
640                    WebBrowser.browse(url);
641                }
642            });
643
644            JSeparator separator = new JSeparator();
645            // seems pretty dumb to have to do this sizing business in order
646            // to get the separator to appear, right?
647            // check out the following:
648            // http://docs.oracle.com/javase/tutorial/uiswing/components/separator.html
649            //
650            // "Separators have almost no API and are extremely easy to use as 
651            // long as you keep one thing in mind: In most implementations, 
652            // a vertical separator has a preferred height of 0, and a 
653            // horizontal separator has a preferred width of 0. This means a 
654            // separator is not visible unless you either set its preferred 
655            // size or put it in under the control of a layout manager such as 
656            // BorderLayout or BoxLayout that stretches it to fill its 
657            // available display area."
658            // WHO ON EARTH DECIDED THAT WAS SENSIBLE DEFAULT BEHAVIOR FOR A
659            // SEPARATOR WIDGET!?
660            separator.setMinimumSize(new Dimension(1, 12));
661            separator.setPreferredSize(new Dimension(1, 12));
662            
663            JLabel editorLabel =
664                new JLabel("Open the file in the NcML editor:");
665            
666            JButton editorButton = new JButton("NcML Editor");
667            editorButton.addActionListener(e -> showNcMLEditor());
668            
669            JLabel viewLabel =
670                new JLabel("I just want to view one of the variables:");
671            
672            JButton viewButton = new JButton("View Variable");
673            viewButton.addActionListener(e -> showVarPicker());
674            
675            JButton closeButton = new JButton("Close");
676            closeButton.addActionListener(e -> dispose());
677            
678            JLabel noncompliantLabel =
679                new JLabel("I have navigation variables, they just aren't CF-compliant: (FEATURE INCOMPLETE)");
680            
681            JButton noncompliantButton = new JButton("Choose Nav");
682            noncompliantButton.addActionListener(e -> showNavChooser());
683            this.addWindowListener(new WindowAdapter() {
684                @Override public void windowClosing(WindowEvent e) {
685                    BadNetCDFDialog.this.dispose();
686                }
687            });
688
689            contentPane.setLayout(new MigLayout(
690                "", 
691                "[grow][]", 
692                "[][grow][][][][][][]"));
693            contentPane.add(headerLabel,        "spanx 2, alignx left, aligny top, wrap");
694            contentPane.add(messageTextPane,    "spanx 2, grow, wrap");
695            contentPane.add(separator,          "spanx 2, growx, aligny top, wrap");
696            contentPane.add(editorLabel,        "alignx left, aligny baseline");
697            contentPane.add(editorButton,       "growx, aligny baseline, wrap");
698            contentPane.add(viewLabel,          "alignx left, aligny baseline");
699            contentPane.add(viewButton,         "growx, aligny baseline, wrap");
700            contentPane.add(noncompliantLabel,  "alignx left, aligny baseline");
701            contentPane.add(noncompliantButton, "growx, aligny baseline, wrap");
702            contentPane.add(closeButton,        "alignx left, aligny baseline");
703            pack();
704        }
705    }
706
707    /**
708     * Tester function to pick a file and send it through the paces.
709     *
710     * @param args Incoming arguments.
711     */
712    public static void main(String... args) {
713        String testfile = FileManager.getReadFile();
714        BadNetCDFWidget bfReader;
715        try {
716            bfReader = new BadNetCDFWidget(testfile);
717            bfReader.showChoices();
718            //bfReader.showNavChooser();
719        } catch (Exception exe) {
720            logger.error("Could not read '"+testfile+'\'', exe);
721        }
722    }
723}