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.control;
030
031import static edu.wisc.ssec.mcidasv.probes.ReadoutProbe.makeEarth2dTuple;
032import static java.util.Objects.requireNonNull;
033
034import java.awt.BorderLayout;
035import java.awt.Color;
036import java.awt.Component;
037import java.awt.Container;
038import java.awt.Dimension;
039import java.awt.FlowLayout;
040import java.awt.Graphics;
041import java.awt.GridBagConstraints;
042import java.awt.GridLayout;
043import java.awt.Insets;
044import java.awt.Rectangle;
045import java.awt.Window;
046import java.awt.event.ActionEvent;
047import java.awt.event.ActionListener;
048import java.awt.event.MouseEvent;
049import java.awt.geom.Rectangle2D;
050import java.io.FileWriter;
051import java.io.IOException;
052import java.rmi.RemoteException;
053import java.text.DecimalFormat;
054import java.util.ArrayList;
055import java.util.Collections;
056import java.util.Hashtable;
057import java.util.LinkedHashMap;
058import java.util.List;
059import java.util.Map;
060import java.util.Objects;
061import java.util.concurrent.CancellationException;
062import java.util.concurrent.ExecutionException;
063
064import javax.swing.*;
065import javax.swing.border.Border;
066import javax.swing.event.MouseInputListener;
067import javax.swing.plaf.basic.BasicTableUI;
068import javax.swing.table.AbstractTableModel;
069import javax.swing.table.TableCellEditor;
070import javax.swing.table.TableCellRenderer;
071
072import org.slf4j.Logger;
073import org.slf4j.LoggerFactory;
074
075import ucar.unidata.idv.ControlContext;
076import ucar.unidata.idv.IdvConstants;
077import ucar.unidata.idv.MapViewManager;
078import ucar.unidata.idv.control.FlaggedDisplayable;
079import ucar.unidata.idv.control.McVHistogramWrapper;
080import ucar.unidata.idv.ui.IdvWindow;
081import ucar.visad.display.XYDisplay;
082
083import visad.Data;
084import visad.DataReference;
085import visad.DataReferenceImpl;
086import visad.DisplayRealType;
087import visad.FlatField;
088import visad.Real;
089import visad.RealTuple;
090import visad.Unit;
091import visad.VisADException;
092import visad.georef.MapProjection;
093
094import ucar.unidata.data.DataChoice;
095import ucar.unidata.data.DataSelection;
096import ucar.unidata.idv.DisplayControl;
097import ucar.unidata.idv.DisplayConventions;
098import ucar.unidata.idv.ViewManager;
099import ucar.unidata.idv.control.ControlWidget;
100import ucar.unidata.idv.control.WrapperWidget;
101import ucar.unidata.idv.ui.ParamDefaultsEditor;
102import ucar.unidata.util.ColorTable;
103import ucar.unidata.util.FileManager;
104import ucar.unidata.util.GuiUtils;
105import ucar.unidata.util.LogUtil;
106import ucar.unidata.util.Misc;
107import ucar.unidata.util.Range;
108import ucar.visad.display.DisplayMaster;
109import ucar.visad.display.DisplayableData;
110
111import edu.wisc.ssec.mcidasv.Constants;
112import edu.wisc.ssec.mcidasv.McIDASV;
113import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralData;
114import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralDataSource;
115import edu.wisc.ssec.mcidasv.data.hydra.SpectrumAdapter;
116import edu.wisc.ssec.mcidasv.display.hydra.MultiSpectralDisplay;
117import edu.wisc.ssec.mcidasv.probes.ProbeEvent;
118import edu.wisc.ssec.mcidasv.probes.ProbeListener;
119import edu.wisc.ssec.mcidasv.probes.ReadoutProbe;
120import edu.wisc.ssec.mcidasv.ui.UIManager;
121
122public class MultiSpectralControl extends HydraControl {
123
124    private static final Logger logger =
125        LoggerFactory.getLogger(MultiSpectralControl.class);
126
127    private String PARAM = "BrightnessTemp";
128
129    // So MultiSpectralDisplay can consistently update the wavelength label
130    // Note hacky leading spaces - needed because GUI builder does not
131    // accept a horizontal strut component.
132    public static String WAVENUMLABEL = "   Wavelength: ";
133    
134    private JLabel wavelengthLabel = new JLabel();
135
136    private static final int DEFAULT_FLAGS = 
137        FLAG_COLORTABLE | FLAG_ZPOSITION;
138
139    private MultiSpectralDisplay display;
140
141    private DisplayMaster displayMaster;
142
143    private final JTextField wavenumbox =  
144        new JTextField(Float.toString(0f), 12);
145
146    final JTextField minBox = new JTextField(6);
147    final JTextField maxBox = new JTextField(6);
148
149    private final List<Hashtable<String, Object>> spectraProperties = new ArrayList<>();
150    private final List<Spectrum> spectra = new ArrayList<>();
151
152    private McVHistogramWrapper histoWrapper;
153
154    private float rangeMin;
155    private float rangeMax;
156    
157    private float origRangeMin;
158    private float origRangeMax;
159
160    // REALLY not thrilled with this...
161    private int probesSeen = 0;
162
163    // boring UI stuff
164    private final JTable probeTable = new JTable(new ProbeTableModel(this, spectra));
165    private final JScrollPane scrollPane = new JScrollPane(probeTable);
166    private final JButton addProbe = new JButton("Add Probe");
167    private final JButton removeProbe = new JButton("Remove Probe");
168    private JCheckBox use360Box;
169
170    private boolean blackBackground = true;
171    private JRadioButton bgBlack;
172    private JRadioButton bgWhite;
173    private ButtonGroup bgColorGroup;
174
175    /** Used to trigger the CSV export process. */
176    private JButton saveAsCsv;
177
178    /** Dialog that allows users to see export progress as well as cancel current export. */
179    private CsvDialog exportCsvDialog;
180
181    public MultiSpectralControl() {
182        super();
183        setHelpUrl("idv.controls.hydra.multispectraldisplaycontrol");
184    }
185
186    @Override public boolean init(final DataChoice choice)
187        throws VisADException, RemoteException 
188    {
189        ((McIDASV)getIdv()).getMcvDataManager().setHydraControl(choice, this);
190        Hashtable props = choice.getProperties();
191        PARAM = (String) props.get(MultiSpectralDataSource.paramKey);
192
193        List<DataChoice> choices = Collections.singletonList(choice);
194        histoWrapper = new McVHistogramWrapper("Histogram", choices, this);
195
196        Float fieldSelectorChannel =
197            (Float)getDataSelection().getProperty(Constants.PROP_CHAN);
198
199        display = new MultiSpectralDisplay(this);
200
201        if (fieldSelectorChannel != null) {
202            display.setWaveNumber(fieldSelectorChannel);
203        }
204
205        displayMaster = getViewManager().getMaster();
206
207        // map the data choice to display.
208        ((McIDASV) getIdv()).getMcvDataManager().setHydraDisplay(choice, display);
209
210        // initialize the Displayable with data before adding to DisplayControl
211        DisplayableData imageDisplay = display.getImageDisplay();
212        FlatField image = display.getImageData();
213
214        float[] rngvals = (image.getFloats(false))[0];
215        float[] minmax = minmax(rngvals);
216        rangeMin = minmax[0];
217        rangeMax = minmax[1];
218        
219        origRangeMin = rangeMin;
220        origRangeMax = rangeMax;
221
222        imageDisplay.setData(display.getImageData());
223        addDisplayable(imageDisplay, DEFAULT_FLAGS);
224
225        // put the multispectral display into the layer controls
226        addViewManager(display.getViewManager());
227
228        // tell the idv what options to give the user
229        setAttributeFlags(DEFAULT_FLAGS);
230
231        setProjectionInView(true);
232
233        // handle the user trying to add a new probe
234        addProbe.addActionListener(e -> {
235            addSpectrum(Color.YELLOW);
236            probeTable.revalidate();
237        });
238
239        // handle the user trying to remove an existing probe
240        removeProbe.addActionListener(e -> {
241            int index = probeTable.getSelectedRow();
242            if (index == -1) {
243                return;
244            }
245
246            removeSpectrum(index);
247        });
248        removeProbe.setEnabled(false);
249
250        // set up the table. in particular, enable/disable the remove button
251        // depending on whether or not there is a selected probe to remove.
252        probeTable.setDefaultRenderer(Color.class, new ColorRenderer(true));
253        probeTable.setDefaultEditor(Color.class, new ColorEditor());
254        probeTable.setPreferredScrollableViewportSize(new Dimension(500, 200));
255        probeTable.setUI(new HackyDragDropRowUI());
256        probeTable.getSelectionModel().addListSelectionListener(e -> {
257            if (!probeTable.getSelectionModel().isSelectionEmpty()) {
258                removeProbe.setEnabled(true);
259            } else {
260                removeProbe.setEnabled(false);
261            }
262        });
263
264        final boolean use360 = getIdv().getStore().get(Constants.PROP_HYDRA_360, false);
265        use360Box = new JCheckBox("0-360 Longitude Format", use360);
266        use360Box.addActionListener(e -> {
267            getIdv().getStore().put(Constants.PROP_HYDRA_360, use360Box.isSelected());
268            ProbeTableModel model = (ProbeTableModel)probeTable.getModel();
269            model.updateWith(spectra);
270            model.fireTableDataChanged();
271        });
272
273        bgBlack = new JRadioButton("Black");
274        bgBlack.addActionListener(e -> {
275            logger.trace("selected black background");
276            XYDisplay master = display.getMaster();
277            master.setBackground(Color.black);
278            master.setForeground(Color.white);
279            setBlackBackground(true);
280        });
281
282        bgWhite = new JRadioButton("White");
283        bgWhite.addActionListener(e -> {
284            logger.trace("selected white background");
285            XYDisplay master = display.getMaster();
286            master.setBackground(Color.white);
287            master.setForeground(Color.black);
288            setBlackBackground(false);
289        });
290
291        bgColorGroup = new ButtonGroup();
292        bgColorGroup.add(bgBlack);
293        bgColorGroup.add(bgWhite);
294
295        bgBlack.setSelected(getBlackBackground());
296        bgWhite.setSelected(!getBlackBackground());
297
298        setShowInDisplayList(true);
299
300        return true;
301    }
302    
303    /**
304     * Updates the Wavelength label when user manipulates drag line UI
305     * 
306     * @param s full label text, prefix and numeric value
307     * 
308     */
309    public void setWavelengthLabel(String s) {
310        if (s != null) {
311            wavelengthLabel.setText(s);
312        }
313    }
314
315    @Override public void initAfterUnPersistence(ControlContext vc,
316                                                 Hashtable properties,
317                                                 List preSelectedDataChoices)
318    {
319        super.initAfterUnPersistence(vc, properties, preSelectedDataChoices);
320
321        XYDisplay master = display.getMaster();
322        if (getBlackBackground()) {
323            master.setBackground(Color.black);
324            master.setForeground(Color.white);
325        } else {
326            master.setBackground(Color.white);
327            master.setForeground(Color.black);
328        }
329    }
330
331    @Override public void initDone() {
332        try {
333            display.showChannelSelector();
334
335            // TODO: this is ugly.
336            Float fieldSelectorChannel =
337                (Float)getDataSelection().getProperty(Constants.PROP_CHAN);
338            if (fieldSelectorChannel == null) {
339                fieldSelectorChannel = 0f;
340            }
341            handleChannelChange(fieldSelectorChannel, false);
342
343            displayMaster.setDisplayInactive();
344
345            // this if-else block is detecting whether or not a bundle is
346            // being loaded; if true, then we'll have a list of spectra props.
347            // otherwise just throw two default spectrums/probes on the screen.
348            if (!spectraProperties.isEmpty()) {
349                for (Hashtable<String, Object> table : spectraProperties) {
350                    Color c = (Color)table.get("color");
351                    Spectrum s = addSpectrum(c);
352                    s.setProperties(table);
353                }
354                spectraProperties.clear();
355            } else {
356                addSpectra(Color.MAGENTA, Color.CYAN);
357            }
358            displayMaster.setDisplayActive();
359        } catch (Exception e) {
360            logException("MultiSpectralControl.initDone", e);
361        }
362    }
363
364    /**
365     * Overridden by McIDAS-V so that {@literal "hide"} probes when their display
366     * is turned off. Otherwise users can wind up with probes on the screen which
367     * aren't associated with any displayed data.
368     * 
369     * @param on {@code true} if we're visible, {@code false} otherwise.
370     * 
371     * @see DisplayControl#setDisplayVisibility(boolean)
372     */
373    
374    @Override public void setDisplayVisibility(boolean on) {
375        super.setDisplayVisibility(on);
376        for (Spectrum s : spectra) {
377            if (s.isVisible()) {
378                s.getProbe().quietlySetVisible(on);
379            }
380        }
381    }
382    
383    /**
384     * Overridden so that the probes in the main display window can handle 
385     * changes to z-axis.
386     * 
387     * @throws VisADException Problem creating VisAD object.
388     * @throws RemoteException RemoteException Java RMI error.
389     */
390    @Override public void applyZPosition() 
391        throws VisADException, RemoteException 
392    {
393        deactivateDisplays();
394        double zpos = getVerticalValue(getZPosition());
395        DisplayRealType drt = getNavigatedDisplay().getDisplayAltitudeType();
396        for (int i = 0, n = displayables.size(); i < n; i++) {
397            FlaggedDisplayable fd = (FlaggedDisplayable) displayables.get(i);
398            if (!fd.ok(FLAG_ZPOSITION)) {
399                continue;
400            }
401            fd.displayable.setConstantPosition(zpos, drt);
402        }
403        for (Spectrum s : spectra) {
404            s.getProbe().getPointSelector().setZ(zpos);
405        }
406        activateDisplays();
407    }
408    
409    /**
410     * Overridden so that the probes can re-apply their current locations to
411     * their {@link ReadoutProbe.PointSelector PointSelectors}.
412     */
413    @Override public void projectionChanged() {
414        super.projectionChanged();
415        MapProjection projection = 
416            ((MapViewManager)getViewManager()).getMainProjection();
417        for (Spectrum s : spectra) {
418            ReadoutProbe rp = s.getProbe();
419            rp.projectionChanged(projection);
420        }
421    }
422    
423    // this will get called before init() by the IDV's bundle magic.
424    public void setSpectraProperties(final List<Hashtable<String, Object>> props) {
425        spectraProperties.clear();
426        spectraProperties.addAll(props);
427    }
428
429    public List<Hashtable<String, Object>> getSpectraProperties() {
430        List<Hashtable<String, Object>> props = new ArrayList<>(spectra.size());
431        for (Spectrum s : spectra) {
432            props.add(s.getProperties());
433        }
434        return props;
435    }
436
437    protected void updateList(final List<Spectrum> updatedSpectra) {
438        spectra.clear();
439
440        List<String> dataRefIds = new ArrayList<>(updatedSpectra.size());
441        for (Spectrum spectrum : updatedSpectra) {
442            dataRefIds.add(spectrum.getSpectrumRefName());
443            spectra.add(spectrum);
444        }
445        display.reorderDataRefsById(dataRefIds);
446    }
447
448    /**
449     * Uses a variable-length array of {@link Color Colors} to create new
450     * readout probes using the specified colors.
451     * 
452     * @param colors Variable length array of {@code Colors}.
453     *               Shouldn't be {@code null}.
454     */
455    // TODO(jon): check for null.
456    protected void addSpectra(final Color... colors) {
457        Spectrum currentSpectrum = null;
458        try {
459            for (int i = colors.length-1; i >= 0; i--) {
460                probesSeen++;
461                Color color = colors[i];
462                String id = "Probe "+probesSeen;
463                currentSpectrum = new Spectrum(this, color, id);
464                spectra.add(currentSpectrum);
465            }
466            ((ProbeTableModel)probeTable.getModel()).updateWith(spectra);
467        } catch (Exception e) {
468            LogUtil.logException("MultiSpectralControl.addSpectra: error while adding spectra", e);
469        }
470    }
471
472    /**
473     * Creates a new {@link ReadoutProbe} with the specified {@link Color}.
474     * 
475     * @param color {@code Color} of the new {@code ReadoutProbe}. 
476     * {@code null} values are not allowed.
477     * 
478     * @return {@link Spectrum} wrapper for the newly created 
479     * {@code ReadoutProbe}.
480     * 
481     * @throws NullPointerException if {@code color} is {@code null}.
482     */
483    public Spectrum addSpectrum(final Color color) {
484        Spectrum spectrum = null;
485        try {
486            probesSeen++;
487            String id = "Probe "+probesSeen;
488            spectrum = new Spectrum(this, color, id);
489            spectra.add(spectrum);
490        } catch (Exception e) {
491            LogUtil.logException("MultiSpectralControl.addSpectrum: error creating new spectrum", e);
492        }
493        ((ProbeTableModel)probeTable.getModel()).updateWith(spectra);
494        return spectrum;
495    }
496
497    /**
498     * Attempts to remove the {@link Spectrum} at the given {@code index}.
499     * 
500     * @param index Index of the probe to be removed (within {@link #spectra}).
501     */
502    public void removeSpectrum(final int index) {
503        List<Spectrum> newSpectra = new ArrayList<>(spectra);
504        int mappedIndex = newSpectra.size() - (index + 1);
505        Spectrum removed = newSpectra.get(mappedIndex);
506        newSpectra.remove(mappedIndex);
507        try {
508            removed.removeValueDisplay();
509        } catch (Exception e) {
510            LogUtil.logException("MultiSpectralControl.removeSpectrum: error removing spectrum", e);
511        }
512
513        updateList(newSpectra);
514
515        // need to signal that the table should update?
516        ProbeTableModel model = (ProbeTableModel)probeTable.getModel();
517        model.updateWith(newSpectra);
518        probeTable.revalidate();
519    }
520
521    /**
522     * Iterates through the list of {@link Spectrum Spectrums} that manage each
523     * {@link ReadoutProbe} associated with this display control and calls
524     * {@link Spectrum#removeValueDisplay()} in an effort to remove this 
525     * control's probes.
526     * 
527     * @see #spectra
528     */
529    public void removeSpectra() {
530        try {
531            for (Spectrum s : spectra) {
532                s.removeValueDisplay();
533            }
534        } catch (Exception e) {
535            LogUtil.logException("MultiSpectralControl.removeSpectra: error removing spectrum", e);
536        }
537    }
538
539    /**
540     * Makes each {@link ReadoutProbe} in this display control attempt to 
541     * redisplay its readout value.
542     * 
543     * <p>Sometimes the probes don't initialize correctly and this method is 
544     * a stop-gap solution.
545     */
546    public void pokeSpectra() {
547        for (Spectrum s : spectra) {
548            s.pokeValueDisplay();
549        }
550        try {
551            //-display.refreshDisplay();
552        } catch (Exception e) {
553            LogUtil.logException("MultiSpectralControl.pokeSpectra: error refreshing display", e);
554        }
555    }
556
557    @Override public DataSelection getDataSelection() {
558        DataSelection selection = super.getDataSelection();
559        if (display != null) {
560            selection.putProperty(Constants.PROP_CHAN, display.getWaveNumber());
561            try {
562                selection.putProperty(SpectrumAdapter.channelIndex_name, display.getChannelIndex());
563            } catch (Exception e) {
564                LogUtil.logException("MultiSpectralControl.getDataSelection", e);
565            }
566        }
567        return selection;
568    }
569
570    @Override public void setDataSelection(final DataSelection newSelection) {
571        super.setDataSelection(newSelection);
572    }
573
574    @Override public MapProjection getDataProjection() {
575        MapProjection mp = null;
576        Rectangle2D rect =
577            MultiSpectralData.getLonLatBoundingBox(display.getImageData());
578
579        try {
580            mp = new LambertAEA(rect);
581        } catch (Exception e) {
582            logException("MultiSpectralControl.getDataProjection", e);
583        }
584
585        return mp;
586    }
587
588    public static float[] minmax(float[] values) {
589        float min =  Float.MAX_VALUE;
590        float max = -Float.MAX_VALUE;
591        for (int k = 0; k < values.length; k++) {
592            float val = values[k];
593            if ((val == val) && (val < Float.POSITIVE_INFINITY) && (val > Float.NEGATIVE_INFINITY)) {
594                if (val < min) {
595                    min = val;
596                }
597                if (val > max) {
598                    max = val;
599                }
600            }
601        }
602        return new float[] { min, max };
603    }
604
605    /**
606     * Convenience method for extracting the parameter name.
607     *
608     * @return Results from {@link DataChoice#getName()}, or {@link #PARAM} if
609     * the {@code DataChoice} is (somehow) {@code null}.
610     */
611    private String getParameterName() {
612        String parameterName = PARAM;
613        DataChoice choice = getDataChoice();
614        if (choice != null) {
615            parameterName = choice.getName();
616        }
617        return parameterName;
618    }
619
620    /**
621     * Get the initial {@link Range} for the data and color table.
622     *
623     * <p>Note: if there is a parameter default range associated with the
624     * current parameter name, that will be returned. If there is <b>not</b> a
625     * parameter default range match, a {@code Range} consisting of
626     * {@link #rangeMin} and {@link #rangeMax} will be returned.
627     * </p>
628     *
629     * @return Initial {@code Range} for data and color table.
630     *
631     * @throws VisADException if VisAD had problems.
632     * @throws RemoteException if there was a Java RMI problem.
633     */
634    @Override protected Range getInitialRange() throws VisADException,
635        RemoteException
636    {
637        String parameterName = getParameterName();
638        Unit dispUnit = getDisplayUnit();
639        DisplayConventions conventions = getDisplayConventions();
640        Range paramRange = conventions.getParamRange(parameterName, dispUnit);
641        if (paramRange == null) {
642            paramRange = new Range(rangeMin, rangeMax);
643        }
644        return paramRange;
645    }
646    
647    /**
648     * Get the initial {@link ColorTable} associated with this control's
649     * parameter name.
650     *
651     * <p>Note: if there is a parameter default color table associated with
652     * the parameter name, that color table will be returned. If there are
653     * <b>no</b> parameter defaults associated with the parameter name,
654     * then the {@code ColorTable} associated with {@literal "BrightnessTemp"}
655     * is returned (this is a {@literal "legacy"} behavior).
656     * </p>
657     *
658     * @return {@code ColorTable} to use.
659     */
660    @Override protected ColorTable getInitialColorTable() {
661        String parameterName = getParameterName();
662        DisplayConventions conventions = getDisplayConventions();
663        ParamDefaultsEditor defaults = conventions.getParamDefaultsEditor();
664        ColorTable ct = defaults.getParamColorTable(parameterName, false);
665        if (ct == null) {
666            ct = conventions.getParamColorTable(PARAM);
667        }
668        return ct;
669    }
670
671    @Override public Container doMakeContents() {
672        try {
673            JTabbedPane pane = new JTabbedPane();
674            pane.add("Display", GuiUtils.inset(getDisplayTab(), 5));
675            pane.add("Settings", 
676                     GuiUtils.inset(GuiUtils.top(doMakeWidgetComponent()), 5));
677            pane.add("Histogram", GuiUtils.inset(GuiUtils.top(getHistogramTabComponent()), 5));
678            GuiUtils.handleHeavyWeightComponentsInTabs(pane);
679            return pane;
680        } catch (Exception e) {
681            logException("MultiSpectralControl.doMakeContents", e);
682        }
683        return null;
684    }
685
686    @Override public void doRemove() throws VisADException, RemoteException {
687        // forcibly clear the value displays when the user has elected to kill
688        // the display. the readouts will persist otherwise.
689        removeSpectra();
690        super.doRemove();
691    }
692
693    /**
694     *  Runs through the list of ViewManager-s and tells each to destroy.
695     *  Creates a new viewManagers list.
696     */
697    @Override protected void clearViewManagers() {
698        if (viewManagers == null) {
699            return;
700        }
701
702        List<ViewManager> tmp = new ArrayList<>(viewManagers);
703        viewManagers = null;
704        for (ViewManager vm : tmp) {
705            if (vm != null) {
706                vm.destroy();
707            }
708        }
709    }
710
711    @SuppressWarnings("unchecked")
712    @Override protected JComponent doMakeWidgetComponent() {
713        List<Component> widgetComponents;
714        try {
715            List<ControlWidget> controlWidgets = new ArrayList<>(15);
716            getControlWidgets(controlWidgets);
717            controlWidgets.add(new WrapperWidget(this, GuiUtils.rLabel("Background Color:"), GuiUtils.hbox(bgBlack, bgWhite)));
718            controlWidgets.add(new WrapperWidget(this, GuiUtils.rLabel("Readout Probes:"), scrollPane));
719            controlWidgets.add(new WrapperWidget(this, GuiUtils.rLabel(" "), GuiUtils.hbox(addProbe, removeProbe, GuiUtils.right(use360Box))));
720            widgetComponents = ControlWidget.fillList(controlWidgets);
721        } catch (Exception e) {
722            LogUtil.logException("Problem building the MultiSpectralControl settings", e);
723            widgetComponents = new ArrayList<>(5);
724            widgetComponents.add(new JLabel("Error building component..."));
725        }
726
727        GuiUtils.tmpInsets = new Insets(4, 8, 4, 8);
728        GuiUtils.tmpFill = GridBagConstraints.HORIZONTAL;
729        return GuiUtils.doLayout(widgetComponents, 2, GuiUtils.WT_NY, GuiUtils.WT_N);
730    }
731
732    protected MultiSpectralDisplay getMultiSpectralDisplay() {
733        return display;
734    }
735
736    public boolean updateImage(final float newChan) {
737        if (!display.setWaveNumber(newChan)) {
738            return false;
739        }
740        
741        DisplayableData imageDisplay = display.getImageDisplay();
742        
743        try {
744            FlatField image = display.getImageData();
745            displayMaster.setDisplayInactive(); //try to consolidate display transforms
746            imageDisplay.setData(image);
747            pokeSpectra();
748            displayMaster.setDisplayActive();
749            updateHistogramTab();
750            
751            // Inquiry 2784 Request 3
752            // NOTE: updateHistogramTab updates the rangeMin/rangeMax fields,
753            // so it *must* be called before the following setRange call.
754            // this ensures the color table/map updates as expected.
755            setRange(new Range(rangeMin, rangeMax));
756            // end Inquiry 2784 Request 3 stuff
757        } catch (Exception e) {
758            LogUtil.logException("MultiSpectralControl.updateImage", e);
759            return false;
760        }
761
762        return true;
763    }
764
765    // be sure to update the displayed image even if a channel change 
766    // originates from the msd itself.
767    @Override public void handleChannelChange(final float newChan) {
768        handleChannelChange(newChan, true);
769    }
770
771    public void handleChannelChange(final float newChan, boolean update) {
772        if (update) {
773            if (updateImage(newChan)) {
774                wavenumbox.setText(Float.toString(newChan));
775            }
776        } else {
777            wavenumbox.setText(Float.toString(newChan));
778        }
779    }
780
781    private JComponent getDisplayTab() {
782        List<JComponent> compList = new ArrayList<>(5);
783
784        saveAsCsv = new JButton("Save...");
785        saveAsCsv.addActionListener(e-> writeToCSV());
786
787        if (display.getBandSelectComboBox() == null) {
788          final JButton nameLabel = new JButton("Wavenumber");
789          nameLabel.addActionListener(e -> {
790              if (nameLabel.getText().equals("Wavenumber")) {
791                  nameLabel.setText("Wavelength");
792                  String tmp = wavenumbox.getText().trim();
793                  wavenumbox.setText(String.valueOf(1/Float.valueOf(tmp)));
794              } else {
795                  nameLabel.setText("Wavenumber");
796                  String tmp = wavenumbox.getText().trim();
797                  wavenumbox.setText(String.valueOf(1/Float.valueOf(tmp)));
798              }
799          });
800
801          wavenumbox.addActionListener(e -> {
802              if (nameLabel.getText().equals("Wavenumber")) {
803                  String tmp = wavenumbox.getText().trim();
804                  updateImage(Float.valueOf(tmp));
805              } else {
806                  String tmp = wavenumbox.getText().trim();
807                  updateImage(1/Float.valueOf(tmp));
808              }
809          });
810
811          compList.add(nameLabel);
812          compList.add(wavenumbox);
813        } else {
814          final JComboBox bandBox = display.getBandSelectComboBox();
815          bandBox.addActionListener(e -> {
816             String bandName = (String) bandBox.getSelectedItem();
817             Float channel = (Float)display.getMultiSpectralData().getBandNameMap().get(bandName);
818             updateImage(channel.floatValue());
819          });
820          JLabel nameLabel = new JLabel("Band: ");
821
822          compList.add(nameLabel);
823          compList.add(bandBox);
824        }
825        compList.add(saveAsCsv);
826
827        JPanel waveNo = GuiUtils.center(GuiUtils.doLayout(compList, compList.size(), GuiUtils.WT_N, GuiUtils.WT_N));
828        return GuiUtils.centerBottom(display.getDisplayComponent(), waveNo);
829    }
830
831    /**
832     * Write multispectral data to a CSV file.
833     *
834     * <p>Now with file choosers!</p>
835     */
836    public void writeToCSV() {
837        // McIDAS Inquiry #2535-3141
838        String filename = FileManager.getWriteFile(Misc.newList(FileManager.FILTER_CSV), FileManager.SUFFIX_CSV);
839        if (filename == null) {
840            return;
841        }
842
843        CsvTask task = new CsvTask(filename);
844        task.addPropertyChangeListener(evt -> {
845//            logger.trace("property changed: {}", evt);
846            switch (evt.getPropertyName()) {
847                case "state":
848                    if (evt.getNewValue() == SwingWorker.StateValue.DONE) {
849                        saveAsCsv.setEnabled(true);
850                        if (exportCsvDialog != null) {
851                            SwingUtilities.invokeLater(() -> exportCsvDialog.taskOver());
852                        }
853                    } else {
854                        saveAsCsv.setEnabled(false);
855                        if (exportCsvDialog != null) {
856                            SwingUtilities.invokeLater(() -> exportCsvDialog.setVisible(true));
857                        }
858                    }
859                    break;
860                case "progress":
861                    int value = (Integer)evt.getNewValue();
862                    if (exportCsvDialog != null) {
863                        exportCsvDialog.setProgress(value);
864                        SwingUtilities.invokeLater(() -> exportCsvDialog.setProgress(value));
865                    }
866                    break;
867                default:
868//                    logger.trace("unknown evt type: {}", evt);
869                    if (exportCsvDialog != null) {
870                        SwingUtilities.invokeLater(() -> exportCsvDialog.taskOver());
871                    } else {
872                        task.cancel(true);
873                    }
874                    saveAsCsv.setEnabled(true);
875                    break;
876            }
877        });
878
879        createCsvDialog(task);
880        task.execute();
881    }
882
883    /**
884     * Create the dialog used to show our CSV export progress.
885     *
886     * <p>Be aware that the dialog will not be visible until {@link CsvTask#execute() execute} is called.</p>
887     *
888     * <p>The {@link CsvDialog} will automatically close upon completion or the user cancelling.</p>
889     *
890     * @param task CSV export task. Cannot be {@code null}.
891     */
892    private void createCsvDialog(CsvTask task) {
893        // attempting to center the modal CsvDialog over the data explorer window.
894        // if it somehow does not exist, try main display window.
895        UIManager mcvUI = (UIManager)McIDASV.getStaticMcv().getIdvUIManager();
896        IdvWindow window = mcvUI.getDashboardWindow();
897        if (window == null) {
898            // not sure how this would be possible, given that the multispectralcontrol
899            // is embedded within the Data Explorer window!
900            window = IdvWindow.getActiveWindow();
901        }
902        exportCsvDialog = new CsvDialog(
903                window.getWindow(),
904                getOuterContents(),
905                "Exporting values...",
906                task);
907    }
908
909
910    private JComponent getHistogramTabComponent() {
911        updateHistogramTab();
912        JComponent histoComp = histoWrapper.doMakeContents();
913        JLabel rangeLabel = GuiUtils.rLabel("Range   ");
914        JLabel minLabel = GuiUtils.rLabel("Min");
915        JLabel maxLabel = GuiUtils.rLabel("   Max");
916        List<JComponent> rangeComps = new ArrayList<>();
917        rangeComps.add(rangeLabel);
918        rangeComps.add(minLabel);
919        rangeComps.add(minBox);
920        rangeComps.add(maxLabel);
921        rangeComps.add(maxBox);
922        minBox.addActionListener(ae -> {
923            rangeMin = Float.valueOf(minBox.getText().trim());
924            rangeMax = Float.valueOf(maxBox.getText().trim());
925            histoWrapper.modifyRange((int) rangeMin, (int) rangeMax);
926        });
927        maxBox.addActionListener(ae -> {
928            rangeMin = Float.valueOf(minBox.getText().trim());
929            rangeMax = Float.valueOf(maxBox.getText().trim());
930            histoWrapper.modifyRange((int) rangeMin, (int) rangeMax);
931        });
932        JPanel rangePanel =
933            GuiUtils.center(GuiUtils.doLayout(rangeComps, 5, GuiUtils.WT_N, GuiUtils.WT_N));
934        JButton resetButton = new JButton("Reset");
935        resetButton.addActionListener(ae -> resetColorTable());
936
937        JPanel resetPanel = 
938            GuiUtils.center(GuiUtils.inset(GuiUtils.wrap(resetButton), 4));
939
940        return GuiUtils.topCenterBottom(histoComp, rangePanel, resetPanel);
941    }
942
943    private void updateHistogramTab() {
944        try {
945            FlatField ff = display.getImageData();
946            histoWrapper.loadData(ff);
947            org.jfree.data.Range range = histoWrapper.getRange();
948            rangeMin = (float)range.getLowerBound();
949            rangeMax = (float)range.getUpperBound();
950            minBox.setText(Integer.toString((int)rangeMin));
951            maxBox.setText(Integer.toString((int)rangeMax));
952        } catch (IllegalArgumentException e) {
953            histoWrapper.clearHistogram();
954            histoWrapper.resetPlot();
955            rangeMin = Float.NaN;
956            rangeMax = Float.NaN;
957            minBox.setText("NaN");
958            maxBox.setText("NaN");
959        } catch (RemoteException | VisADException e) {
960            logException("MultiSpectralControl.getHistogramTabComponent", e);
961        }
962    }
963
964    public void resetColorTable() {
965        histoWrapper.doReset();
966        histoWrapper.modifyRange((int) origRangeMin, (int) origRangeMax);
967        // TJJ Jan 2019 - make sure and reset the min/max input boxes too
968        minBox.setText(Integer.toString((int) origRangeMin));
969        maxBox.setText(Integer.toString((int) origRangeMax));
970    }
971
972    protected void contrastStretch(final double low, final double high) {
973        try {
974            org.jfree.data.Range range = histoWrapper.getRange();
975            rangeMin = (float)range.getLowerBound();
976            rangeMax = (float)range.getUpperBound();
977            minBox.setText(Integer.toString((int)rangeMin));
978            maxBox.setText(Integer.toString((int)rangeMax));
979            setRange(getInitialColorTable().getName(), new Range(low, high));
980        } catch (Exception e) {
981            logException("MultiSpectralControl.contrastStretch", e);
982        }
983    }
984
985    // sole use is for persistence!
986    public boolean getBlackBackground() {
987        return blackBackground;
988    }
989
990    // sole use is for persistence!
991    public void setBlackBackground(boolean value) {
992        blackBackground = value;
993    }
994
995    class CsvDialog extends JDialog {
996        private final JProgressBar progressBar;
997
998        CsvDialog(Window parentWindow, Component parentComponent,
999                  String title, CsvTask task) {
1000            super(parentWindow, title);
1001            setModalityType(ModalityType.APPLICATION_MODAL);
1002            setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
1003
1004            getContentPane().setLayout(new GridLayout(1,1));
1005
1006            JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
1007            mainPanel.setBorder(BorderFactory.createEmptyBorder(15, 10, 5, 10));
1008
1009            JPanel centerPanel = new JPanel(new BorderLayout());
1010            mainPanel.add(centerPanel, BorderLayout.CENTER);
1011
1012            JPanel southPanel = new JPanel(new BorderLayout());
1013            mainPanel.add(southPanel, BorderLayout.SOUTH);
1014
1015            this.progressBar = new JProgressBar(0, 100);
1016            this.progressBar.setStringPainted(true);
1017            southPanel.add(progressBar, BorderLayout.CENTER);
1018
1019            JPanel buttonsPanel = new JPanel(new FlowLayout());
1020            southPanel.add(buttonsPanel, BorderLayout.SOUTH);
1021
1022            JButton cancelButton = new JButton("Cancel");
1023            cancelButton.addActionListener(e -> task.cancel(true));
1024            buttonsPanel.add(cancelButton);
1025            getContentPane().add(mainPanel);
1026            pack();
1027            setLocationRelativeTo(parentComponent);
1028        }
1029
1030        void setProgress(int percent) {
1031            this.progressBar.setValue(percent);
1032        }
1033
1034        void taskOver() {
1035            setVisible(false);
1036            dispose();
1037        }
1038    }
1039
1040    class CsvTask extends SwingWorker<String, Object> {
1041        private final String path;
1042
1043        /**
1044         * Creates a CSV exporting task that's meant to store its values in the file specified by {@code path}.
1045         *
1046         * @param path File to write to. Cannot be {@code null}.
1047         */
1048        public CsvTask(String path) {
1049            this.path = Objects.requireNonNull(path, "path cannot be null");
1050        }
1051
1052        /**
1053         * Does the work of exporting and formatting CSV values from a {@link MultiSpectralControl} in a
1054         * background thread.
1055         *
1056         * @return String representing the contents to write to a CSV file. May be {@code null}.
1057         */
1058        @Override public String doInBackground() {
1059            String csvContents = null;
1060            try {
1061                csvContents = exportSpectra();
1062            } catch (Exception e) {
1063                logger.warn("Something went wrong extracting values", e);
1064            }
1065            return csvContents;
1066        }
1067
1068        /**
1069         * Called (on the event dispatch thread) when {@link #doInBackground()} has completed.
1070         */
1071        @Override protected void done() {
1072            try {
1073                String contents = get();
1074                if (contents == null) {
1075                    logger.error("Could not extract CSV values, contents are null.");
1076                } else {
1077                    writeToPath(contents);
1078                }
1079            } catch (CancellationException e) {
1080                firePropertyChange("state", StateValue.STARTED, SwingWorker.StateValue.DONE);
1081            } catch (InterruptedException e) {
1082                throw new RuntimeException(e);
1083            } catch (ExecutionException e) {
1084                String why = null;
1085                Throwable cause = e.getCause();
1086                why = Objects.requireNonNullElse(cause, e).getMessage();
1087                logger.warn("Error writing to path: " + path, why);
1088            }
1089        }
1090
1091        /**
1092         * Write the given string to {@link #path}.
1093         *
1094         * @param csvContents CSV file contents. Should not be {@code null}.
1095         */
1096        private void writeToPath(String csvContents) {
1097            try (FileWriter writer = new FileWriter(this.path)) {
1098                writer.write(csvContents);
1099                writer.flush();
1100            } catch (IOException ex) {
1101                logger.warn("Could not write to file", ex);
1102            }
1103        }
1104
1105        /**
1106         * Iterates through each channel/wavenumber and extracts the value of the existing ReadoutProbes
1107         * at their current positions.
1108         *
1109         * <p>Be warned, this can be slow when there are thousands of channels. Definitely do not
1110         * run this method on the event dispatch thread.</p>
1111         *
1112         * @param wavelengths Array containing all the channels/wavenumbers. Cannot be {@code null}.
1113         *
1114         * @return Two-dimensional array of doubles. First array represents the probes,
1115         * second dimension is the value of a probe at each channel/wavenumber.
1116         *
1117         * @throws VisADException if there was a problem
1118         * @throws RemoteException if there was somehow an RMI problem
1119         */
1120        private double[][] collectValues(double[] wavelengths) throws VisADException, RemoteException{
1121            double[][] values = new double[spectra.size()][wavelengths.length];
1122
1123            RealTuple[] probeLocations = new RealTuple[spectra.size()];
1124            for (int i = 0; i < spectra.size(); i++) {
1125                Spectrum s = spectra.get(i);
1126                RealTuple location = s.getProbe().getEarthPosition();
1127                double[] locVals = location.getValues();
1128                if (locVals[1] < -180) {
1129                    locVals[1] += 360f;
1130                }
1131
1132                if (locVals[0] > 180) {
1133                    locVals[0] -= 360f;
1134                }
1135                probeLocations[i] = makeEarth2dTuple(locVals[0], locVals[1]);
1136            }
1137            for (int i = 0; (i < wavelengths.length) && !isCancelled(); i++) {
1138                FlatField imageData = display.getImageDataFrom((float)wavelengths[i]);
1139                for (int j = 0; j < probeLocations.length; j++) {
1140                    RealTuple probeLoc = probeLocations[j];
1141                    Real realVal = (Real)imageData.evaluate(probeLoc, Data.NEAREST_NEIGHBOR, Data.NO_ERRORS);
1142                    values[j][i] = realVal.getValue();
1143                }
1144                // set percentage complete. iterating via display.getImageDataFrom(...)
1145                // is easily the slowest part of the CSV export process, so simply
1146                // figuring out the number of wavelengths processed likely suffices for
1147                // a simple progress bar.
1148                setProgress((int)(100 * ((float)i / (float)wavelengths.length)));
1149            }
1150            return values;
1151        }
1152
1153        /**
1154         * Coordinates the collecting of probe data as well as creating a string representing
1155         * the contents of the CSV file.
1156         *
1157         * @return String value that can be written to a CSV file.
1158         *
1159         * @throws Exception if there was some problem
1160         */
1161        private String exportSpectra() throws Exception {
1162            DecimalFormat format = new DecimalFormat(getStore().get(Constants.PREF_LATLON_FORMAT, "##0.0"));
1163            boolean use360 = false;
1164            if (use360Box != null) {
1165                use360 = use360Box.isSelected();
1166            }
1167
1168            double[] wavelengths = display.getDomainSet().getDoubles()[0];
1169            double[][] values = collectValues(wavelengths);
1170
1171            // each wavelen,value1,value2 generally less than 50 chars,
1172            // will need wavelengths.length of 'em.
1173            // +200 to account for the length of the column headers
1174            StringBuilder builder = new StringBuilder(wavelengths.length * 50 + 200);
1175            // Wavelen, Probe 1 LATLON BrightnessTemp, Probe 2 LATLON BrightnessTemp, Probe 3 LatLon BrightnessTemp
1176            builder.append("Wavelength,");
1177            for (int i = 0; i < spectra.size(); i++) {
1178                Spectrum s = spectra.get(i);
1179                double lon = use360
1180                        ? ProbeTableModel.clamp360(s.getLongitude())
1181                        : ProbeTableModel.clamp180(s.getLongitude());
1182                builder.append(s.getId())
1183                        .append(" (Latitude: ").append(format.format(s.getLatitude()))
1184                        .append("; Longitude: ").append(format.format(lon)).append(')');
1185                if (i < spectra.size() - 1) {
1186                    builder.append(',');
1187                }
1188            }
1189            builder.append('\n');
1190            for (int i = 0; i < wavelengths.length; i++) {
1191                builder.append(wavelengths[i]).append(',');
1192                for (int j = 0; j < spectra.size(); j++) {
1193                    builder.append(values[j][i]);
1194                    if (j < spectra.size() - 1) {
1195                        builder.append(',');
1196                    }
1197                }
1198                builder.append('\n');
1199            }
1200            return builder.toString();
1201        }
1202    }
1203
1204    private static class Spectrum implements ProbeListener {
1205
1206        private static final Logger logger = LoggerFactory.getLogger(Spectrum.class);
1207
1208        private final MultiSpectralControl control;
1209
1210        /** 
1211         * Display that is displaying the spectrum associated with 
1212         * {@code probe}'s location. 
1213         */
1214        private final MultiSpectralDisplay display;
1215
1216        /** VisAD's reference to this spectrum. */
1217        private final DataReference spectrumRef;
1218
1219        /** 
1220         * Probe that appears in the {@literal "image display"} associated with
1221         * the current display control. 
1222         */
1223        private ReadoutProbe probe;
1224
1225        /** Whether or not {@code probe} is visible. */
1226        private boolean isVisible = true;
1227
1228        /** 
1229         * Human-friendly ID for this spectrum and probe. Used in 
1230         * {@link MultiSpectralControl#probeTable}. 
1231         */
1232        private final String myId;
1233
1234        /**
1235         * Initializes a new Spectrum that is {@literal "bound"} to
1236         * {@code control} and whose color is {@code color}.
1237         * 
1238         * @param control Display control that contains this spectrum and the
1239         * associated {@link ReadoutProbe}. Cannot be null.
1240         * @param color Color of {@code probe}. Cannot be {@code null}.
1241         * @param myId Human-friendly ID used a reference for this
1242         * spectrum/probe. Cannot be {@code null}.
1243         * 
1244         * @throws NullPointerException if {@code control}, {@code color}, or 
1245         * {@code myId} is {@code null}.
1246         * @throws VisADException if VisAD-land had some problems.
1247         * @throws RemoteException if VisAD's RMI stuff had problems.
1248         */
1249        public Spectrum(final MultiSpectralControl control, final Color color, final String myId) throws VisADException, RemoteException {
1250            this.control = control;
1251            this.display = control.getMultiSpectralDisplay();
1252            this.myId = myId;
1253            spectrumRef = new DataReferenceImpl(hashCode() + "_spectrumRef");
1254            display.addRef(spectrumRef, color);
1255            String pattern = control.getStore().get(IdvConstants.PREF_LATLON_FORMAT, "##0.0");
1256            probe = new ReadoutProbe(control, display.getImageData(), color, pattern, control.getDisplayVisibility());
1257            this.updatePosition(probe.getEarthPosition());
1258            probe.addProbeListener(this);
1259    
1260            control.addAttributedDisplayable(probe.getValueDisplay(), FLAG_ZPOSITION);
1261        }
1262
1263        public void probePositionChanged(final ProbeEvent<RealTuple> e) {
1264            RealTuple position = e.getNewValue();
1265            updatePosition(position);
1266        }
1267
1268        public void probeFormatPatternChanged(final ProbeEvent<String> e) {
1269
1270        }
1271
1272        public void updatePosition(RealTuple position) {
1273            try {
1274                FlatField spectrum = display.getMultiSpectralData().getSpectrum(position);
1275                spectrumRef.setData(spectrum);
1276            } catch (Exception ex) {
1277                logger.error("Error updating postion.", ex);
1278            }
1279        }
1280
1281        public String getValue() {
1282            return probe.getValue();
1283        }
1284
1285        public double getLatitude() {
1286            return probe.getLatitude();
1287        }
1288
1289        public double getLongitude() {
1290            return probe.getLongitude();
1291        }
1292
1293        public Color getColor() {
1294            return probe.getColor();
1295        }
1296
1297        public String getId() {
1298            return myId;
1299        }
1300
1301        public DataReference getSpectrumRef() {
1302            return spectrumRef;
1303        }
1304
1305        public String getSpectrumRefName() {
1306            return hashCode() + "_spectrumRef";
1307        }
1308
1309        public void setColor(final Color color) {
1310            if (color == null) {
1311                throw new NullPointerException("Can't use a null color");
1312            }
1313
1314            try {
1315                display.updateRef(spectrumRef, color);
1316                probe.quietlySetColor(color);
1317            } catch (Exception ex) {
1318                logger.error("Error setting color", ex);
1319            }
1320        }
1321
1322        /**
1323         * Shows and hides this spectrum/probe. Note that an {@literal "hidden"}
1324         * spectrum merely uses an alpha value of zero for the spectrum's 
1325         * color--nothing is actually removed!
1326         * 
1327         * <p>Also note that if our {@link MultiSpectralControl} has its visibility 
1328         * toggled {@literal "off"}, the probe itself will not be shown. 
1329         * <b>It will otherwise behave as if it is visible!</b>
1330         * 
1331         * @param visible {@code true} for {@literal "visible"}, {@code false} otherwise.
1332         */
1333        public void setVisible(final boolean visible) {
1334            isVisible = visible;
1335            Color c = probe.getColor();
1336            int alpha = visible ? 255 : 0;
1337            c = new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha);
1338            try {
1339                display.updateRef(spectrumRef, c);
1340                // only bother actually *showing* the probe if its display is 
1341                // actually visible.
1342                if (control.getDisplayVisibility()) {
1343                    probe.quietlySetVisible(visible);
1344                }
1345            } catch (Exception e) {
1346                LogUtil.logException("There was a problem setting the visibility of probe \""+spectrumRef+"\" to "+visible, e);
1347            }
1348        }
1349
1350        public boolean isVisible() {
1351            return isVisible;
1352        }
1353
1354        protected ReadoutProbe getProbe() {
1355            return probe;
1356        }
1357
1358        public void probeColorChanged(final ProbeEvent<Color> e) {
1359            logger.trace("color change event={}", e);
1360        }
1361
1362        public void probeVisibilityChanged(final ProbeEvent<Boolean> e) {
1363            logger.trace("probe event={}", e);
1364            Boolean newVal = e.getNewValue();
1365            if (newVal != null) {
1366                isVisible = newVal;
1367            }
1368        }
1369
1370        public Hashtable<String, Object> getProperties() {
1371            Hashtable<String, Object> table = new Hashtable<>();
1372            table.put("color", probe.getColor());
1373            table.put("visibility", isVisible);
1374            table.put("lat", probe.getLatitude());
1375            table.put("lon", probe.getLongitude());
1376            return table;
1377        }
1378
1379        public void setProperties(final Hashtable<String, Object> table) {
1380            if (table == null) {
1381                throw new NullPointerException("properties table cannot be null");
1382            }
1383            Color color = (Color)table.get("color");
1384            Double lat = (Double)table.get("lat");
1385            Double lon = (Double)table.get("lon");
1386            Boolean visibility = (Boolean)table.get("visibility");
1387            probe.setLatLon(lat, lon);
1388            probe.setColor(color);
1389            setVisible(visibility);
1390        }
1391
1392        public void pokeValueDisplay() {
1393            probe.setField(display.getImageData());
1394            try {
1395                //FlatField spectrum = display.getMultiSpectralData().getSpectrum(probe.getEarthPosition());
1396                //spectrumRef.setData(spectrum);
1397            } catch (Exception e) { }
1398        }
1399
1400        public void removeValueDisplay() throws VisADException, RemoteException {
1401            probe.handleProbeRemoval();
1402            display.removeRef(spectrumRef);
1403        }
1404    }
1405
1406    // TODO(jon): MultiSpectralControl should become the table model.
1407    private static class ProbeTableModel extends AbstractTableModel implements ProbeListener {
1408//        private static final String[] COLUMNS = { 
1409//            "Visibility", "Probe ID", "Value", "Spectrum", "Latitude", "Longitude", "Color" 
1410//        };
1411
1412        private static final String[] COLUMNS = { 
1413            "Visibility", "Probe ID", "Value", "Latitude", "Longitude", "Color" 
1414        };
1415
1416        private final Map<ReadoutProbe, Integer> probeToIndex = new LinkedHashMap<>();
1417        private final Map<Integer, Spectrum> indexToSpectrum = new LinkedHashMap<>();
1418        private final MultiSpectralControl control;
1419
1420        public ProbeTableModel(final MultiSpectralControl control, final List<Spectrum> probes) {
1421            this.control = requireNonNull(control);
1422            updateWith(requireNonNull(probes));
1423        }
1424
1425        public void probeColorChanged(final ProbeEvent<Color> e) {
1426            ReadoutProbe probe = e.getProbe();
1427            if (!probeToIndex.containsKey(probe)) {
1428                return;
1429            }
1430            int index = probeToIndex.get(probe);
1431            fireTableCellUpdated(index, 5);
1432        }
1433
1434        public void probeVisibilityChanged(final ProbeEvent<Boolean> e) {
1435            ReadoutProbe probe = e.getProbe();
1436            if (!probeToIndex.containsKey(probe)) {
1437                return;
1438            }
1439            int index = probeToIndex.get(probe);
1440            fireTableCellUpdated(index, 0);
1441        }
1442
1443        public void probePositionChanged(final ProbeEvent<RealTuple> e) {
1444            ReadoutProbe probe = e.getProbe();
1445            if (!probeToIndex.containsKey(probe)) {
1446                return;
1447            }
1448            int index = probeToIndex.get(probe);
1449            fireTableRowsUpdated(index, index);
1450        }
1451
1452        public void probeFormatPatternChanged(final ProbeEvent<String> e) {
1453            ReadoutProbe probe = e.getProbe();
1454            if (!probeToIndex.containsKey(probe)) {
1455                return;
1456            }
1457            int index = probeToIndex.get(probe);
1458            fireTableRowsUpdated(index, index);
1459        }
1460
1461        public void updateWith(final List<Spectrum> updatedSpectra) {
1462            requireNonNull(updatedSpectra);
1463
1464            probeToIndex.clear();
1465            indexToSpectrum.clear();
1466
1467            for (int i = 0, j = updatedSpectra.size()-1; i < updatedSpectra.size(); i++, j--) {
1468                Spectrum spectrum = updatedSpectra.get(j);
1469                ReadoutProbe probe = spectrum.getProbe();
1470                if (!probe.hasListener(this)) {
1471                    probe.addProbeListener(this);
1472                }
1473                probeToIndex.put(spectrum.getProbe(), i);
1474                indexToSpectrum.put(i, spectrum);
1475            }
1476        }
1477
1478        public int getColumnCount() {
1479            return COLUMNS.length;
1480        }
1481
1482        public int getRowCount() {
1483            if (probeToIndex.size() != indexToSpectrum.size()) {
1484                throw new AssertionError("");
1485            }
1486            return probeToIndex.size();
1487        }
1488
1489//        public Object getValueAt(final int row, final int column) {
1490//            Spectrum spectrum = indexToSpectrum.get(row);
1491//            switch (column) {
1492//                case 0: return spectrum.isVisible();
1493//                case 1: return spectrum.getId();
1494//                case 2: return spectrum.getValue();
1495//                case 3: return "notyet";
1496//                case 4: return formatPosition(spectrum.getLatitude());
1497//                case 5: return formatPosition(spectrum.getLongitude());
1498//                case 6: return spectrum.getColor();
1499//                default: throw new AssertionError("uh oh");
1500//            }
1501//        }
1502        public Object getValueAt(final int row, final int column) {
1503            DecimalFormat format = new DecimalFormat(control.getIdv().getStore().get(Constants.PREF_LATLON_FORMAT, "##0.0"));
1504            boolean use360 = control.use360Box.isSelected();
1505            Spectrum spectrum = indexToSpectrum.get(row);
1506            switch (column) {
1507                case 0: return spectrum.isVisible();
1508                case 1: return spectrum.getId();
1509                case 2: return spectrum.getValue();
1510                case 3: return format.format(spectrum.getLatitude());
1511                case 4: return format.format(use360 ? clamp360(spectrum.getLongitude()) : clamp180(spectrum.getLongitude()));
1512                case 5: return spectrum.getColor();
1513                default: throw new AssertionError("uh oh");
1514            }
1515        }
1516
1517        public boolean isCellEditable(final int row, final int column) {
1518            switch (column) {
1519                case 0: return true;
1520                case 5: return true;
1521                default: return false;
1522            }
1523        }
1524
1525        public void setValueAt(final Object value, final int row, final int column) {
1526            Spectrum spectrum = indexToSpectrum.get(row);
1527            boolean didUpdate = true;
1528            switch (column) {
1529                case 0: spectrum.setVisible((Boolean)value); break;
1530                case 5: spectrum.setColor((Color)value); break;
1531                default: didUpdate = false; break;
1532            }
1533
1534            if (didUpdate) {
1535                fireTableCellUpdated(row, column);
1536            }
1537        }
1538
1539        public void moveRow(final int origin, final int destination) {
1540            // get the dragged spectrum (and probe)
1541            Spectrum dragged = indexToSpectrum.get(origin);
1542            ReadoutProbe draggedProbe = dragged.getProbe();
1543
1544            // get the current spectrum (and probe)
1545            Spectrum current = indexToSpectrum.get(destination);
1546            ReadoutProbe currentProbe = current.getProbe();
1547
1548            // update references in indexToSpetrum
1549            indexToSpectrum.put(destination, dragged);
1550            indexToSpectrum.put(origin, current);
1551
1552            // update references in probeToIndex
1553            probeToIndex.put(draggedProbe, destination);
1554            probeToIndex.put(currentProbe, origin);
1555
1556            // build a list of the spectra, ordered by index
1557            List<Spectrum> updated = new ArrayList<>(indexToSpectrum.size());
1558            for (int i = indexToSpectrum.size()-1; i >= 0; i--) {
1559                updated.add(indexToSpectrum.get(i));
1560            }
1561
1562            // send it to control.
1563            control.updateList(updated);
1564        }
1565
1566        public String getColumnName(final int column) {
1567            return COLUMNS[column];
1568        }
1569
1570        public Class<?> getColumnClass(final int column) {
1571            return getValueAt(0, column).getClass();
1572        }
1573
1574        public static double clamp180(double value) {
1575            return ((((value + 180.0) % 360.0) + 360.0) % 360.0) - 180.0;
1576        }
1577
1578        public static double clamp360(double value) {
1579            boolean positive = value > 0.0;
1580            value = ((value % 360.0) + 360.0) % 360.0;
1581            if ((value == 0.0) && positive) {
1582                value = 360.0;
1583            }
1584            return value;
1585        }
1586    }
1587
1588    public class ColorEditor extends AbstractCellEditor implements TableCellEditor, ActionListener {
1589        private Color currentColor = Color.CYAN;
1590        private final JButton button = new JButton();
1591        private final JColorChooser colorChooser = new JColorChooser();
1592        private JDialog dialog;
1593        protected static final String EDIT = "edit";
1594
1595//        private final JComboBox combobox = new JComboBox(GuiUtils.COLORS); 
1596
1597        public ColorEditor() {
1598            button.setActionCommand(EDIT);
1599            button.addActionListener(this);
1600            button.setBorderPainted(false);
1601
1602//            combobox.setActionCommand(EDIT);
1603//            combobox.addActionListener(this);
1604//            combobox.setBorder(new EmptyBorder(0, 0, 0, 0));
1605//            combobox.setOpaque(true);
1606//            ColorRenderer whut = new ColorRenderer(true);
1607//            combobox.setRenderer(whut);
1608//            
1609//            dialog = JColorChooser.createDialog(combobox, "pick a color", true, colorChooser, this, null);
1610            dialog = JColorChooser.createDialog(button, "pick a color", true, colorChooser, this, null);
1611        }
1612        public void actionPerformed(ActionEvent e) {
1613            if (EDIT.equals(e.getActionCommand())) {
1614                //The user has clicked the cell, so
1615                //bring up the dialog.
1616//                button.setBackground(currentColor);
1617                colorChooser.setColor(currentColor);
1618                dialog.setVisible(true);
1619
1620                //Make the renderer reappear.
1621                fireEditingStopped();
1622
1623            } else { //User pressed dialog's "OK" button.
1624                currentColor = colorChooser.getColor();
1625            }
1626        }
1627
1628        //Implement the one CellEditor method that AbstractCellEditor doesn't.
1629        public Object getCellEditorValue() {
1630            return currentColor;
1631        }
1632
1633        //Implement the one method defined by TableCellEditor.
1634        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
1635            currentColor = (Color)value;
1636            return button;
1637//            return combobox;
1638        }
1639    }
1640
1641    public class ColorRenderer extends JLabel implements TableCellRenderer, ListCellRenderer {
1642        Border unselectedBorder = null;
1643        Border selectedBorder = null;
1644        boolean isBordered = true;
1645
1646        public ColorRenderer(boolean isBordered) {
1647            this.isBordered = isBordered;
1648            setHorizontalAlignment(CENTER);
1649            setVerticalAlignment(CENTER);
1650            setOpaque(true);
1651        }
1652
1653        public Component getTableCellRendererComponent(JTable table, Object color, boolean isSelected, boolean hasFocus, int row, int column) {
1654            Color newColor = (Color)color;
1655            setBackground(newColor);
1656            if (isBordered) {
1657                if (isSelected) {
1658                    if (selectedBorder == null) {
1659                        selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, table.getSelectionBackground());
1660                    }
1661                    setBorder(selectedBorder);
1662                } else {
1663                    if (unselectedBorder == null) {
1664                        unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, table.getBackground());
1665                    }
1666                    setBorder(unselectedBorder);
1667                }
1668            }
1669
1670            setToolTipText(String.format("RGB: red=%d, green=%d, blue=%d", newColor.getRed(), newColor.getGreen(), newColor.getBlue()));
1671            return this;
1672        }
1673
1674        public Component getListCellRendererComponent(JList list, Object color, int index, boolean isSelected, boolean cellHasFocus) {
1675            Color newColor = (Color)color;
1676            setBackground(newColor);
1677            if (isBordered) {
1678                if (isSelected) {
1679                    if (selectedBorder == null) {
1680                        selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, list.getSelectionBackground());
1681                    }
1682                    setBorder(selectedBorder);
1683                } else {
1684                    if (unselectedBorder == null) {
1685                        unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, list.getBackground());
1686                    }
1687                    setBorder(unselectedBorder);
1688                }
1689            }
1690            setToolTipText(String.format("RGB: red=%d, green=%d, blue=%d", newColor.getRed(), newColor.getGreen(), newColor.getBlue()));
1691            return this;
1692        }
1693    }
1694
1695    public class HackyDragDropRowUI extends BasicTableUI {
1696
1697        private boolean inDrag = false;
1698        private int start;
1699        private int offset;
1700
1701        protected MouseInputListener createMouseInputListener() {
1702            return new HackyMouseInputHandler();
1703        }
1704
1705        public void paint(Graphics g, JComponent c) {
1706            super.paint(g, c);
1707
1708            if (!inDrag) {
1709                return;
1710            }
1711
1712            int width = table.getWidth();
1713            int height = table.getRowHeight();
1714            g.setColor(table.getParent().getBackground());
1715            Rectangle rect = table.getCellRect(table.getSelectedRow(), 0, false);
1716            g.copyArea(rect.x, rect.y, width, height, rect.x, offset);
1717
1718            if (offset < 0) {
1719                g.fillRect(rect.x, rect.y + (height + offset), width, (offset * -1));
1720            } else {
1721                g.fillRect(rect.x, rect.y, width, offset);
1722            }
1723        }
1724
1725        class HackyMouseInputHandler extends MouseInputHandler {
1726
1727            public void mouseDragged(MouseEvent e) {
1728                int row = table.getSelectedRow();
1729                if (row < 0) {
1730                    return;
1731                }
1732
1733                inDrag = true;
1734
1735                int height = table.getRowHeight();
1736                int middleOfSelectedRow = (height * row) + (height / 2);
1737
1738                int toRow = -1;
1739                int yLoc = (int)e.getPoint().getY();
1740
1741                // goin' up?
1742                if (yLoc < (middleOfSelectedRow - height)) {
1743                    toRow = row - 1;
1744                } else if (yLoc > (middleOfSelectedRow + height)) {
1745                    toRow = row + 1;
1746                }
1747
1748                ProbeTableModel model = (ProbeTableModel)table.getModel();
1749                if ((toRow >= 0) && (toRow < table.getRowCount())) {
1750                    model.moveRow(row, toRow);
1751                    table.setRowSelectionInterval(toRow, toRow);
1752                    start = yLoc;
1753                }
1754
1755                offset = (start - yLoc) * -1;
1756                table.repaint();
1757            }
1758
1759            public void mousePressed(MouseEvent e) {
1760                super.mousePressed(e);
1761                start = (int)e.getPoint().getY();
1762            }
1763
1764            public void mouseReleased(MouseEvent e){
1765                super.mouseReleased(e);
1766                inDrag = false;
1767                table.repaint();
1768            }
1769        }
1770    }
1771}