001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.control;
030
031import static java.util.Objects.requireNonNull;
032
033import java.awt.Color;
034import java.awt.Component;
035import java.awt.Container;
036import java.awt.Dimension;
037import java.awt.Graphics;
038import java.awt.GridBagConstraints;
039import java.awt.Insets;
040import java.awt.Rectangle;
041import java.awt.event.ActionEvent;
042import java.awt.event.ActionListener;
043import java.awt.event.MouseEvent;
044import java.awt.geom.Rectangle2D;
045import java.rmi.RemoteException;
046import java.text.DecimalFormat;
047import java.util.ArrayList;
048import java.util.Collections;
049import java.util.Hashtable;
050import java.util.LinkedHashMap;
051import java.util.List;
052import java.util.Map;
053
054import javax.swing.AbstractCellEditor;
055import javax.swing.BorderFactory;
056import javax.swing.ButtonGroup;
057import javax.swing.JButton;
058import javax.swing.JCheckBox;
059import javax.swing.JColorChooser;
060import javax.swing.JComboBox;
061import javax.swing.JComponent;
062import javax.swing.JDialog;
063import javax.swing.JLabel;
064import javax.swing.JList;
065import javax.swing.JPanel;
066import javax.swing.JRadioButton;
067import javax.swing.JScrollPane;
068import javax.swing.JTabbedPane;
069import javax.swing.JTable;
070import javax.swing.JTextField;
071import javax.swing.ListCellRenderer;
072import javax.swing.border.Border;
073import javax.swing.event.MouseInputListener;
074import javax.swing.plaf.basic.BasicTableUI;
075import javax.swing.table.AbstractTableModel;
076import javax.swing.table.TableCellEditor;
077import javax.swing.table.TableCellRenderer;
078
079import org.slf4j.Logger;
080import org.slf4j.LoggerFactory;
081
082import ucar.unidata.idv.ControlContext;
083import ucar.unidata.idv.IdvConstants;
084import ucar.unidata.idv.MapViewManager;
085import ucar.unidata.idv.control.FlaggedDisplayable;
086import ucar.unidata.idv.control.McVHistogramWrapper;
087import ucar.visad.display.XYDisplay;
088
089import visad.DataReference;
090import visad.DataReferenceImpl;
091import visad.DisplayRealType;
092import visad.FlatField;
093import visad.RealTuple;
094import visad.Unit;
095import visad.VisADException;
096import visad.georef.MapProjection;
097
098import ucar.unidata.data.DataChoice;
099import ucar.unidata.data.DataSelection;
100import ucar.unidata.idv.DisplayControl;
101import ucar.unidata.idv.DisplayConventions;
102import ucar.unidata.idv.ViewManager;
103import ucar.unidata.idv.control.ControlWidget;
104import ucar.unidata.idv.control.WrapperWidget;
105import ucar.unidata.idv.ui.ParamDefaultsEditor;
106import ucar.unidata.util.ColorTable;
107import ucar.unidata.util.GuiUtils;
108import ucar.unidata.util.LogUtil;
109import ucar.unidata.util.Range;
110import ucar.visad.display.DisplayMaster;
111import ucar.visad.display.DisplayableData;
112
113import edu.wisc.ssec.mcidasv.Constants;
114import edu.wisc.ssec.mcidasv.McIDASV;
115import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralData;
116import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralDataSource;
117import edu.wisc.ssec.mcidasv.data.hydra.SpectrumAdapter;
118import edu.wisc.ssec.mcidasv.display.hydra.MultiSpectralDisplay;
119import edu.wisc.ssec.mcidasv.probes.ProbeEvent;
120import edu.wisc.ssec.mcidasv.probes.ProbeListener;
121import edu.wisc.ssec.mcidasv.probes.ReadoutProbe;
122
123public class MultiSpectralControl extends HydraControl {
124
125    private static final Logger logger =
126        LoggerFactory.getLogger(MultiSpectralControl.class);
127
128    private String PARAM = "BrightnessTemp";
129
130    // So MultiSpectralDisplay can consistently update the wavelength label
131    // Note hacky leading spaces - needed because GUI builder does not
132    // accept a horizontal strut component.
133    public static String WAVENUMLABEL = "   Wavelength: ";
134    
135    private JLabel wavelengthLabel = new JLabel();
136
137    private static final int DEFAULT_FLAGS = 
138        FLAG_COLORTABLE | FLAG_ZPOSITION;
139
140    private MultiSpectralDisplay display;
141
142    private DisplayMaster displayMaster;
143
144    private final JTextField wavenumbox =  
145        new JTextField(Float.toString(0f), 12);
146
147    final JTextField minBox = new JTextField(6);
148    final JTextField maxBox = new JTextField(6);
149
150    private final List<Hashtable<String, Object>> spectraProperties = new ArrayList<>();
151    private final List<Spectrum> spectra = new ArrayList<>();
152
153    private McVHistogramWrapper histoWrapper;
154
155    private float rangeMin;
156    private float rangeMax;
157    
158    private float origRangeMin;
159    private float origRangeMax;
160
161    // REALLY not thrilled with this...
162    private int probesSeen = 0;
163
164    // boring UI stuff
165    private final JTable probeTable = new JTable(new ProbeTableModel(this, spectra));
166    private final JScrollPane scrollPane = new JScrollPane(probeTable);
167    private final JButton addProbe = new JButton("Add Probe");
168    private final JButton removeProbe = new JButton("Remove Probe");
169    private JCheckBox use360Box;
170
171    private boolean blackBackground = true;
172    private JRadioButton bgBlack;
173    private JRadioButton bgWhite;
174    private ButtonGroup bgColorGroup;
175
176    public MultiSpectralControl() {
177        super();
178        setHelpUrl("idv.controls.hydra.multispectraldisplaycontrol");
179    }
180
181    @Override public boolean init(final DataChoice choice)
182        throws VisADException, RemoteException 
183    {
184        ((McIDASV)getIdv()).getMcvDataManager().setHydraControl(choice, this);
185        Hashtable props = choice.getProperties();
186        PARAM = (String) props.get(MultiSpectralDataSource.paramKey);
187
188        List<DataChoice> choices = Collections.singletonList(choice);
189        histoWrapper = new McVHistogramWrapper("Histogram", choices, this);
190
191        Float fieldSelectorChannel =
192            (Float)getDataSelection().getProperty(Constants.PROP_CHAN);
193
194        display = new MultiSpectralDisplay(this);
195
196        if (fieldSelectorChannel != null) {
197            display.setWaveNumber(fieldSelectorChannel);
198        }
199
200        displayMaster = getViewManager().getMaster();
201
202        // map the data choice to display.
203        ((McIDASV) getIdv()).getMcvDataManager().setHydraDisplay(choice, display);
204
205        // initialize the Displayable with data before adding to DisplayControl
206        DisplayableData imageDisplay = display.getImageDisplay();
207        FlatField image = display.getImageData();
208
209        float[] rngvals = (image.getFloats(false))[0];
210        float[] minmax = minmax(rngvals);
211        rangeMin = minmax[0];
212        rangeMax = minmax[1];
213        
214        origRangeMin = rangeMin;
215        origRangeMax = rangeMax;
216
217        imageDisplay.setData(display.getImageData());
218        addDisplayable(imageDisplay, DEFAULT_FLAGS);
219
220        // put the multispectral display into the layer controls
221        addViewManager(display.getViewManager());
222
223        // tell the idv what options to give the user
224        setAttributeFlags(DEFAULT_FLAGS);
225
226        setProjectionInView(true);
227
228        // handle the user trying to add a new probe
229        addProbe.addActionListener(e -> {
230            addSpectrum(Color.YELLOW);
231            probeTable.revalidate();
232        });
233
234        // handle the user trying to remove an existing probe
235        removeProbe.addActionListener(e -> {
236            int index = probeTable.getSelectedRow();
237            if (index == -1) {
238                return;
239            }
240
241            removeSpectrum(index);
242        });
243        removeProbe.setEnabled(false);
244
245        // set up the table. in particular, enable/disable the remove button
246        // depending on whether or not there is a selected probe to remove.
247        probeTable.setDefaultRenderer(Color.class, new ColorRenderer(true));
248        probeTable.setDefaultEditor(Color.class, new ColorEditor());
249        probeTable.setPreferredScrollableViewportSize(new Dimension(500, 200));
250        probeTable.setUI(new HackyDragDropRowUI());
251        probeTable.getSelectionModel().addListSelectionListener(e -> {
252            if (!probeTable.getSelectionModel().isSelectionEmpty()) {
253                removeProbe.setEnabled(true);
254            } else {
255                removeProbe.setEnabled(false);
256            }
257        });
258
259        final boolean use360 = getIdv().getStore().get(Constants.PROP_HYDRA_360, false);
260        use360Box = new JCheckBox("0-360 Longitude Format", use360);
261        use360Box.addActionListener(e -> {
262            getIdv().getStore().put(Constants.PROP_HYDRA_360, use360Box.isSelected());
263            ProbeTableModel model = (ProbeTableModel)probeTable.getModel();
264            model.updateWith(spectra);
265            model.fireTableDataChanged();
266        });
267
268        bgBlack = new JRadioButton("Black");
269        bgBlack.addActionListener(e -> {
270            logger.trace("selected black background");
271            XYDisplay master = display.getMaster();
272            master.setBackground(Color.black);
273            master.setForeground(Color.white);
274            setBlackBackground(true);
275        });
276
277        bgWhite = new JRadioButton("White");
278        bgWhite.addActionListener(e -> {
279            logger.trace("selected white background");
280            XYDisplay master = display.getMaster();
281            master.setBackground(Color.white);
282            master.setForeground(Color.black);
283            setBlackBackground(false);
284        });
285
286        bgColorGroup = new ButtonGroup();
287        bgColorGroup.add(bgBlack);
288        bgColorGroup.add(bgWhite);
289
290        bgBlack.setSelected(getBlackBackground());
291        bgWhite.setSelected(!getBlackBackground());
292
293        setShowInDisplayList(false);
294
295        return true;
296    }
297    
298    /**
299     * Updates the Wavelength label when user manipulates drag line UI
300     * 
301     * @param s full label text, prefix and numeric value
302     * 
303     */
304    public void setWavelengthLabel(String s) {
305        if (s != null) {
306            wavelengthLabel.setText(s);
307        }
308    }
309
310    @Override public void initAfterUnPersistence(ControlContext vc,
311                                                 Hashtable properties,
312                                                 List preSelectedDataChoices)
313    {
314        super.initAfterUnPersistence(vc, properties, preSelectedDataChoices);
315
316        XYDisplay master = display.getMaster();
317        if (getBlackBackground()) {
318            master.setBackground(Color.black);
319            master.setForeground(Color.white);
320        } else {
321            master.setBackground(Color.white);
322            master.setForeground(Color.black);
323        }
324    }
325
326    @Override public void initDone() {
327        try {
328            display.showChannelSelector();
329
330            // TODO: this is ugly.
331            Float fieldSelectorChannel =
332                (Float)getDataSelection().getProperty(Constants.PROP_CHAN);
333            if (fieldSelectorChannel == null) {
334                fieldSelectorChannel = 0f;
335            }
336            handleChannelChange(fieldSelectorChannel, false);
337
338            displayMaster.setDisplayInactive();
339
340            // this if-else block is detecting whether or not a bundle is
341            // being loaded; if true, then we'll have a list of spectra props.
342            // otherwise just throw two default spectrums/probes on the screen.
343            if (!spectraProperties.isEmpty()) {
344                for (Hashtable<String, Object> table : spectraProperties) {
345                    Color c = (Color)table.get("color");
346                    Spectrum s = addSpectrum(c);
347                    s.setProperties(table);
348                }
349                spectraProperties.clear();
350            } else {
351                addSpectra(Color.MAGENTA, Color.CYAN);
352            }
353            displayMaster.setDisplayActive();
354        } catch (Exception e) {
355            logException("MultiSpectralControl.initDone", e);
356        }
357    }
358
359    /**
360     * Overridden by McIDAS-V so that {@literal "hide"} probes when their display
361     * is turned off. Otherwise users can wind up with probes on the screen which
362     * aren't associated with any displayed data.
363     * 
364     * @param on {@code true} if we're visible, {@code false} otherwise.
365     * 
366     * @see DisplayControl#setDisplayVisibility(boolean)
367     */
368    
369    @Override public void setDisplayVisibility(boolean on) {
370        super.setDisplayVisibility(on);
371        for (Spectrum s : spectra) {
372            if (s.isVisible()) {
373                s.getProbe().quietlySetVisible(on);
374            }
375        }
376    }
377    
378    /**
379     * Overridden so that the probes in the main display window can handle 
380     * changes to z-axis.
381     * 
382     * @throws VisADException Problem creating VisAD object.
383     * @throws RemoteException RemoteException Java RMI error.
384     */
385    @Override public void applyZPosition() 
386        throws VisADException, RemoteException 
387    {
388        deactivateDisplays();
389        double zpos = getVerticalValue(getZPosition());
390        DisplayRealType drt = getNavigatedDisplay().getDisplayAltitudeType();
391        for (int i = 0, n = displayables.size(); i < n; i++) {
392            FlaggedDisplayable fd = (FlaggedDisplayable) displayables.get(i);
393            if (!fd.ok(FLAG_ZPOSITION)) {
394                continue;
395            }
396            fd.displayable.setConstantPosition(zpos, drt);
397        }
398        for (Spectrum s : spectra) {
399            s.getProbe().getPointSelector().setZ(zpos);
400        }
401        activateDisplays();
402    }
403    
404    /**
405     * Overridden so that the probes can re-apply their current locations to
406     * their {@link ReadoutProbe.PointSelector PointSelectors}.
407     */
408    @Override public void projectionChanged() {
409        super.projectionChanged();
410        MapProjection projection = 
411            ((MapViewManager)getViewManager()).getMainProjection();
412        for (Spectrum s : spectra) {
413            ReadoutProbe rp = s.getProbe();
414            rp.projectionChanged(projection);
415        }
416    }
417    
418    // this will get called before init() by the IDV's bundle magic.
419    public void setSpectraProperties(final List<Hashtable<String, Object>> props) {
420        spectraProperties.clear();
421        spectraProperties.addAll(props);
422    }
423
424    public List<Hashtable<String, Object>> getSpectraProperties() {
425        List<Hashtable<String, Object>> props = new ArrayList<>(spectra.size());
426        for (Spectrum s : spectra) {
427            props.add(s.getProperties());
428        }
429        return props;
430    }
431
432    protected void updateList(final List<Spectrum> updatedSpectra) {
433        spectra.clear();
434
435        List<String> dataRefIds = new ArrayList<>(updatedSpectra.size());
436        for (Spectrum spectrum : updatedSpectra) {
437            dataRefIds.add(spectrum.getSpectrumRefName());
438            spectra.add(spectrum);
439        }
440        display.reorderDataRefsById(dataRefIds);
441    }
442
443    /**
444     * Uses a variable-length array of {@link Color Colors} to create new
445     * readout probes using the specified colors.
446     * 
447     * @param colors Variable length array of {@code Colors}.
448     *               Shouldn't be {@code null}.
449     */
450    // TODO(jon): check for null.
451    protected void addSpectra(final Color... colors) {
452        Spectrum currentSpectrum = null;
453        try {
454            for (int i = colors.length-1; i >= 0; i--) {
455                probesSeen++;
456                Color color = colors[i];
457                String id = "Probe "+probesSeen;
458                currentSpectrum = new Spectrum(this, color, id);
459                spectra.add(currentSpectrum);
460            }
461            ((ProbeTableModel)probeTable.getModel()).updateWith(spectra);
462        } catch (Exception e) {
463            LogUtil.logException("MultiSpectralControl.addSpectra: error while adding spectra", e);
464        }
465    }
466
467    /**
468     * Creates a new {@link ReadoutProbe} with the specified {@link Color}.
469     * 
470     * @param color {@code Color} of the new {@code ReadoutProbe}. 
471     * {@code null} values are not allowed.
472     * 
473     * @return {@link Spectrum} wrapper for the newly created 
474     * {@code ReadoutProbe}.
475     * 
476     * @throws NullPointerException if {@code color} is {@code null}.
477     */
478    public Spectrum addSpectrum(final Color color) {
479        Spectrum spectrum = null;
480        try {
481            probesSeen++;
482            String id = "Probe "+probesSeen;
483            spectrum = new Spectrum(this, color, id);
484            spectra.add(spectrum);
485        } catch (Exception e) {
486            LogUtil.logException("MultiSpectralControl.addSpectrum: error creating new spectrum", e);
487        }
488        ((ProbeTableModel)probeTable.getModel()).updateWith(spectra);
489        return spectrum;
490    }
491
492    /**
493     * Attempts to remove the {@link Spectrum} at the given {@code index}.
494     * 
495     * @param index Index of the probe to be removed (within {@link #spectra}).
496     */
497    public void removeSpectrum(final int index) {
498        List<Spectrum> newSpectra = new ArrayList<>(spectra);
499        int mappedIndex = newSpectra.size() - (index + 1);
500        Spectrum removed = newSpectra.get(mappedIndex);
501        newSpectra.remove(mappedIndex);
502        try {
503            removed.removeValueDisplay();
504        } catch (Exception e) {
505            LogUtil.logException("MultiSpectralControl.removeSpectrum: error removing spectrum", e);
506        }
507
508        updateList(newSpectra);
509
510        // need to signal that the table should update?
511        ProbeTableModel model = (ProbeTableModel)probeTable.getModel();
512        model.updateWith(newSpectra);
513        probeTable.revalidate();
514    }
515
516    /**
517     * Iterates through the list of {@link Spectrum Spectrums} that manage each
518     * {@link ReadoutProbe} associated with this display control and calls
519     * {@link Spectrum#removeValueDisplay()} in an effort to remove this 
520     * control's probes.
521     * 
522     * @see #spectra
523     */
524    public void removeSpectra() {
525        try {
526            for (Spectrum s : spectra) {
527                s.removeValueDisplay();
528            }
529        } catch (Exception e) {
530            LogUtil.logException("MultiSpectralControl.removeSpectra: error removing spectrum", e);
531        }
532    }
533
534    /**
535     * Makes each {@link ReadoutProbe} in this display control attempt to 
536     * redisplay its readout value.
537     * 
538     * <p>Sometimes the probes don't initialize correctly and this method is 
539     * a stop-gap solution.
540     */
541    public void pokeSpectra() {
542        for (Spectrum s : spectra) {
543            s.pokeValueDisplay();
544        }
545        try {
546            //-display.refreshDisplay();
547        } catch (Exception e) {
548            LogUtil.logException("MultiSpectralControl.pokeSpectra: error refreshing display", e);
549        }
550    }
551
552    @Override public DataSelection getDataSelection() {
553        DataSelection selection = super.getDataSelection();
554        if (display != null) {
555            selection.putProperty(Constants.PROP_CHAN, display.getWaveNumber());
556            try {
557                selection.putProperty(SpectrumAdapter.channelIndex_name, display.getChannelIndex());
558            } catch (Exception e) {
559                LogUtil.logException("MultiSpectralControl.getDataSelection", e);
560            }
561        }
562        return selection;
563    }
564
565    @Override public void setDataSelection(final DataSelection newSelection) {
566        super.setDataSelection(newSelection);
567    }
568
569    @Override public MapProjection getDataProjection() {
570        MapProjection mp = null;
571        Rectangle2D rect =
572            MultiSpectralData.getLonLatBoundingBox(display.getImageData());
573
574        try {
575            mp = new LambertAEA(rect);
576        } catch (Exception e) {
577            logException("MultiSpectralControl.getDataProjection", e);
578        }
579
580        return mp;
581    }
582
583    public static float[] minmax(float[] values) {
584        float min =  Float.MAX_VALUE;
585        float max = -Float.MAX_VALUE;
586        for (int k = 0; k < values.length; k++) {
587            float val = values[k];
588            if ((val == val) && (val < Float.POSITIVE_INFINITY) && (val > Float.NEGATIVE_INFINITY)) {
589                if (val < min) {
590                    min = val;
591                }
592                if (val > max) {
593                    max = val;
594                }
595            }
596        }
597        return new float[] { min, max };
598    }
599
600    /**
601     * Convenience method for extracting the parameter name.
602     *
603     * @return Results from {@link DataChoice#getName()}, or {@link #PARAM} if
604     * the {@code DataChoice} is (somehow) {@code null}.
605     */
606    private String getParameterName() {
607        String parameterName = PARAM;
608        DataChoice choice = getDataChoice();
609        if (choice != null) {
610            parameterName = choice.getName();
611        }
612        return parameterName;
613    }
614
615    /**
616     * Get the initial {@link Range} for the data and color table.
617     *
618     * <p>Note: if there is a parameter default range associated with the
619     * current parameter name, that will be returned. If there is <b>not</b> a
620     * parameter default range match, a {@code Range} consisting of
621     * {@link #rangeMin} and {@link #rangeMax} will be returned.
622     * </p>
623     *
624     * @return Initial {@code Range} for data and color table.
625     *
626     * @throws VisADException if VisAD had problems.
627     * @throws RemoteException if there was a Java RMI problem.
628     */
629    @Override protected Range getInitialRange() throws VisADException,
630        RemoteException
631    {
632        String parameterName = getParameterName();
633        Unit dispUnit = getDisplayUnit();
634        DisplayConventions conventions = getDisplayConventions();
635        Range paramRange = conventions.getParamRange(parameterName, dispUnit);
636        if (paramRange == null) {
637            paramRange = new Range(rangeMin, rangeMax);
638        }
639        return paramRange;
640    }
641    
642    /**
643     * Get the initial {@link ColorTable} associated with this control's
644     * parameter name.
645     *
646     * <p>Note: if there is a parameter default color table associated with
647     * the parameter name, that color table will be returned. If there are
648     * <b>no</b> parameter defaults associated with the parameter name,
649     * then the {@code ColorTable} associated with {@literal "BrightnessTemp"}
650     * is returned (this is a {@literal "legacy"} behavior).
651     * </p>
652     *
653     * @return {@code ColorTable} to use.
654     */
655    @Override protected ColorTable getInitialColorTable() {
656        String parameterName = getParameterName();
657        DisplayConventions conventions = getDisplayConventions();
658        ParamDefaultsEditor defaults = conventions.getParamDefaultsEditor();
659        ColorTable ct = defaults.getParamColorTable(parameterName, false);
660        if (ct == null) {
661            ct = conventions.getParamColorTable(PARAM);
662        }
663        return ct;
664    }
665
666    @Override public Container doMakeContents() {
667        try {
668            JTabbedPane pane = new JTabbedPane();
669            pane.add("Display", GuiUtils.inset(getDisplayTab(), 5));
670            pane.add("Settings", 
671                     GuiUtils.inset(GuiUtils.top(doMakeWidgetComponent()), 5));
672            pane.add("Histogram", GuiUtils.inset(GuiUtils.top(getHistogramTabComponent()), 5));
673            GuiUtils.handleHeavyWeightComponentsInTabs(pane);
674            return pane;
675        } catch (Exception e) {
676            logException("MultiSpectralControl.doMakeContents", e);
677        }
678        return null;
679    }
680
681    @Override public void doRemove() throws VisADException, RemoteException {
682        // forcibly clear the value displays when the user has elected to kill
683        // the display. the readouts will persist otherwise.
684        removeSpectra();
685        super.doRemove();
686    }
687
688    /**
689     *  Runs through the list of ViewManager-s and tells each to destroy.
690     *  Creates a new viewManagers list.
691     */
692    @Override protected void clearViewManagers() {
693        if (viewManagers == null) {
694            return;
695        }
696
697        List<ViewManager> tmp = new ArrayList<>(viewManagers);
698        viewManagers = null;
699        for (ViewManager vm : tmp) {
700            if (vm != null) {
701                vm.destroy();
702            }
703        }
704    }
705
706    @SuppressWarnings("unchecked")
707    @Override protected JComponent doMakeWidgetComponent() {
708        List<Component> widgetComponents;
709        try {
710            List<ControlWidget> controlWidgets = new ArrayList<>(15);
711            getControlWidgets(controlWidgets);
712            controlWidgets.add(new WrapperWidget(this, GuiUtils.rLabel("Background Color:"), GuiUtils.hbox(bgBlack, bgWhite)));
713            controlWidgets.add(new WrapperWidget(this, GuiUtils.rLabel("Readout Probes:"), scrollPane));
714            controlWidgets.add(new WrapperWidget(this, GuiUtils.rLabel(" "), GuiUtils.hbox(addProbe, removeProbe, GuiUtils.right(use360Box))));
715            widgetComponents = ControlWidget.fillList(controlWidgets);
716        } catch (Exception e) {
717            LogUtil.logException("Problem building the MultiSpectralControl settings", e);
718            widgetComponents = new ArrayList<>(5);
719            widgetComponents.add(new JLabel("Error building component..."));
720        }
721
722        GuiUtils.tmpInsets = new Insets(4, 8, 4, 8);
723        GuiUtils.tmpFill = GridBagConstraints.HORIZONTAL;
724        return GuiUtils.doLayout(widgetComponents, 2, GuiUtils.WT_NY, GuiUtils.WT_N);
725    }
726
727    protected MultiSpectralDisplay getMultiSpectralDisplay() {
728        return display;
729    }
730
731    public boolean updateImage(final float newChan) {
732        if (!display.setWaveNumber(newChan)) {
733            return false;
734        }
735        
736        DisplayableData imageDisplay = display.getImageDisplay();
737        
738        try {
739            FlatField image = display.getImageData();
740            displayMaster.setDisplayInactive(); //try to consolidate display transforms
741            imageDisplay.setData(image);
742            pokeSpectra();
743            displayMaster.setDisplayActive();
744            updateHistogramTab();
745            
746            // Inquiry 2784 Request 3
747            // NOTE: updateHistogramTab updates the rangeMin/rangeMax fields,
748            // so it *must* be called before the following setRange call.
749            // this ensures the color table/map updates as expected.
750            setRange(new Range(rangeMin, rangeMax));
751            // end Inquiry 2784 Request 3 stuff
752        } catch (Exception e) {
753            LogUtil.logException("MultiSpectralControl.updateImage", e);
754            return false;
755        }
756
757        return true;
758    }
759
760    // be sure to update the displayed image even if a channel change 
761    // originates from the msd itself.
762    @Override public void handleChannelChange(final float newChan) {
763        handleChannelChange(newChan, true);
764    }
765
766    public void handleChannelChange(final float newChan, boolean update) {
767        if (update) {
768            if (updateImage(newChan)) {
769                wavenumbox.setText(Float.toString(newChan));
770            }
771        } else {
772            wavenumbox.setText(Float.toString(newChan));
773        }
774    }
775
776    private JComponent getDisplayTab() {
777        List<JComponent> compList = new ArrayList<>(5);
778
779        if (display.getBandSelectComboBox() == null) {
780          final JLabel nameLabel = GuiUtils.rLabel("Wavenumber: ");
781
782          wavenumbox.addActionListener(e -> {
783              String tmp = wavenumbox.getText().trim();
784              updateImage(Float.valueOf(tmp));
785          });
786          compList.add(nameLabel);
787          compList.add(wavenumbox);
788        } else {
789          final JComboBox bandBox = display.getBandSelectComboBox();
790          bandBox.addActionListener(e -> {
791             String bandName = (String) bandBox.getSelectedItem();
792             Float channel = (Float)display.getMultiSpectralData().getBandNameMap().get(bandName);
793             updateImage(channel.floatValue());
794          });
795          JLabel nameLabel = new JLabel("Band: ");
796          compList.add(nameLabel);
797          compList.add(bandBox);
798          compList.add(wavelengthLabel);
799        }
800
801        JPanel waveNo = GuiUtils.center(GuiUtils.doLayout(compList, 3, GuiUtils.WT_N, GuiUtils.WT_N));
802        return GuiUtils.centerBottom(display.getDisplayComponent(), waveNo);
803    }
804
805    private JComponent getHistogramTabComponent() {
806        updateHistogramTab();
807        JComponent histoComp = histoWrapper.doMakeContents();
808        JLabel rangeLabel = GuiUtils.rLabel("Range   ");
809        JLabel minLabel = GuiUtils.rLabel("Min");
810        JLabel maxLabel = GuiUtils.rLabel("   Max");
811        List<JComponent> rangeComps = new ArrayList<>();
812        rangeComps.add(rangeLabel);
813        rangeComps.add(minLabel);
814        rangeComps.add(minBox);
815        rangeComps.add(maxLabel);
816        rangeComps.add(maxBox);
817        minBox.addActionListener(ae -> {
818            rangeMin = Float.valueOf(minBox.getText().trim());
819            rangeMax = Float.valueOf(maxBox.getText().trim());
820            histoWrapper.modifyRange((int) rangeMin, (int) rangeMax);
821        });
822        maxBox.addActionListener(ae -> {
823            rangeMin = Float.valueOf(minBox.getText().trim());
824            rangeMax = Float.valueOf(maxBox.getText().trim());
825            histoWrapper.modifyRange((int) rangeMin, (int) rangeMax);
826        });
827        JPanel rangePanel =
828            GuiUtils.center(GuiUtils.doLayout(rangeComps, 5, GuiUtils.WT_N, GuiUtils.WT_N));
829        JButton resetButton = new JButton("Reset");
830        resetButton.addActionListener(ae -> resetColorTable());
831
832        JPanel resetPanel = 
833            GuiUtils.center(GuiUtils.inset(GuiUtils.wrap(resetButton), 4));
834
835        return GuiUtils.topCenterBottom(histoComp, rangePanel, resetPanel);
836    }
837
838    private void updateHistogramTab() {
839        try {
840            FlatField ff = display.getImageData();
841            histoWrapper.loadData(ff);
842            org.jfree.data.Range range = histoWrapper.getRange();
843            rangeMin = (float)range.getLowerBound();
844            rangeMax = (float)range.getUpperBound();
845            minBox.setText(Integer.toString((int)rangeMin));
846            maxBox.setText(Integer.toString((int)rangeMax));
847        } catch (IllegalArgumentException e) {
848            histoWrapper.clearHistogram();
849            histoWrapper.resetPlot();
850            rangeMin = Float.NaN;
851            rangeMax = Float.NaN;
852            minBox.setText("NaN");
853            maxBox.setText("NaN");
854        } catch (RemoteException | VisADException e) {
855            logException("MultiSpectralControl.getHistogramTabComponent", e);
856        }
857    }
858
859    public void resetColorTable() {
860        histoWrapper.doReset();
861        histoWrapper.modifyRange((int) origRangeMin, (int) origRangeMax);
862        // TJJ Jan 2019 - make sure and reset the min/max input boxes too
863        minBox.setText(Integer.toString((int) origRangeMin));
864        maxBox.setText(Integer.toString((int) origRangeMax));
865    }
866
867    protected void contrastStretch(final double low, final double high) {
868        try {
869            org.jfree.data.Range range = histoWrapper.getRange();
870            rangeMin = (float)range.getLowerBound();
871            rangeMax = (float)range.getUpperBound();
872            minBox.setText(Integer.toString((int)rangeMin));
873            maxBox.setText(Integer.toString((int)rangeMax));
874            setRange(getInitialColorTable().getName(), new Range(low, high));
875        } catch (Exception e) {
876            logException("MultiSpectralControl.contrastStretch", e);
877        }
878    }
879
880    // sole use is for persistence!
881    public boolean getBlackBackground() {
882        return blackBackground;
883    }
884
885    // sole use is for persistence!
886    public void setBlackBackground(boolean value) {
887        blackBackground = value;
888    }
889
890    private static class Spectrum implements ProbeListener {
891
892        private static final Logger logger = LoggerFactory.getLogger(Spectrum.class);
893
894        private final MultiSpectralControl control;
895
896        /** 
897         * Display that is displaying the spectrum associated with 
898         * {@code probe}'s location. 
899         */
900        private final MultiSpectralDisplay display;
901
902        /** VisAD's reference to this spectrum. */
903        private final DataReference spectrumRef;
904
905        /** 
906         * Probe that appears in the {@literal "image display"} associated with
907         * the current display control. 
908         */
909        private ReadoutProbe probe;
910
911        /** Whether or not {@code probe} is visible. */
912        private boolean isVisible = true;
913
914        /** 
915         * Human-friendly ID for this spectrum and probe. Used in 
916         * {@link MultiSpectralControl#probeTable}. 
917         */
918        private final String myId;
919
920        /**
921         * Initializes a new Spectrum that is {@literal "bound"} to
922         * {@code control} and whose color is {@code color}.
923         * 
924         * @param control Display control that contains this spectrum and the
925         * associated {@link ReadoutProbe}. Cannot be null.
926         * @param color Color of {@code probe}. Cannot be {@code null}.
927         * @param myId Human-friendly ID used a reference for this
928         * spectrum/probe. Cannot be {@code null}.
929         * 
930         * @throws NullPointerException if {@code control}, {@code color}, or 
931         * {@code myId} is {@code null}.
932         * @throws VisADException if VisAD-land had some problems.
933         * @throws RemoteException if VisAD's RMI stuff had problems.
934         */
935        public Spectrum(final MultiSpectralControl control, final Color color, final String myId) throws VisADException, RemoteException {
936            this.control = control;
937            this.display = control.getMultiSpectralDisplay();
938            this.myId = myId;
939            spectrumRef = new DataReferenceImpl(hashCode() + "_spectrumRef");
940            display.addRef(spectrumRef, color);
941            String pattern = control.getStore().get(IdvConstants.PREF_LATLON_FORMAT, "##0.0");
942            probe = new ReadoutProbe(control, display.getImageData(), color, pattern, control.getDisplayVisibility());
943            this.updatePosition(probe.getEarthPosition());
944            probe.addProbeListener(this);
945    
946            control.addAttributedDisplayable(probe.getValueDisplay(), FLAG_ZPOSITION);
947        }
948
949        public void probePositionChanged(final ProbeEvent<RealTuple> e) {
950            RealTuple position = e.getNewValue();
951            updatePosition(position);
952        }
953
954        public void probeFormatPatternChanged(final ProbeEvent<String> e) {
955
956        }
957
958        public void updatePosition(RealTuple position) {
959            try {
960                FlatField spectrum = display.getMultiSpectralData().getSpectrum(position);
961                spectrumRef.setData(spectrum);
962            } catch (Exception ex) {
963                logger.error("Error updating postion.", ex);
964            }
965        }
966
967        public String getValue() {
968            return probe.getValue();
969        }
970
971        public double getLatitude() {
972            return probe.getLatitude();
973        }
974
975        public double getLongitude() {
976            return probe.getLongitude();
977        }
978
979        public Color getColor() {
980            return probe.getColor();
981        }
982
983        public String getId() {
984            return myId;
985        }
986
987        public DataReference getSpectrumRef() {
988            return spectrumRef;
989        }
990
991        public String getSpectrumRefName() {
992            return hashCode() + "_spectrumRef";
993        }
994
995        public void setColor(final Color color) {
996            if (color == null) {
997                throw new NullPointerException("Can't use a null color");
998            }
999
1000            try {
1001                display.updateRef(spectrumRef, color);
1002                probe.quietlySetColor(color);
1003            } catch (Exception ex) {
1004                logger.error("Error setting color", ex);
1005            }
1006        }
1007
1008        /**
1009         * Shows and hides this spectrum/probe. Note that an {@literal "hidden"}
1010         * spectrum merely uses an alpha value of zero for the spectrum's 
1011         * color--nothing is actually removed!
1012         * 
1013         * <p>Also note that if our {@link MultiSpectralControl} has its visibility 
1014         * toggled {@literal "off"}, the probe itself will not be shown. 
1015         * <b>It will otherwise behave as if it is visible!</b>
1016         * 
1017         * @param visible {@code true} for {@literal "visible"}, {@code false} otherwise.
1018         */
1019        public void setVisible(final boolean visible) {
1020            isVisible = visible;
1021            Color c = probe.getColor();
1022            int alpha = visible ? 255 : 0;
1023            c = new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha);
1024            try {
1025                display.updateRef(spectrumRef, c);
1026                // only bother actually *showing* the probe if its display is 
1027                // actually visible.
1028                if (control.getDisplayVisibility()) {
1029                    probe.quietlySetVisible(visible);
1030                }
1031            } catch (Exception e) {
1032                LogUtil.logException("There was a problem setting the visibility of probe \""+spectrumRef+"\" to "+visible, e);
1033            }
1034        }
1035
1036        public boolean isVisible() {
1037            return isVisible;
1038        }
1039
1040        protected ReadoutProbe getProbe() {
1041            return probe;
1042        }
1043
1044        public void probeColorChanged(final ProbeEvent<Color> e) {
1045            logger.trace("color change event={}", e);
1046        }
1047
1048        public void probeVisibilityChanged(final ProbeEvent<Boolean> e) {
1049            logger.trace("probe event={}", e);
1050            Boolean newVal = e.getNewValue();
1051            if (newVal != null) {
1052                isVisible = newVal;
1053            }
1054        }
1055
1056        public Hashtable<String, Object> getProperties() {
1057            Hashtable<String, Object> table = new Hashtable<>();
1058            table.put("color", probe.getColor());
1059            table.put("visibility", isVisible);
1060            table.put("lat", probe.getLatitude());
1061            table.put("lon", probe.getLongitude());
1062            return table;
1063        }
1064
1065        public void setProperties(final Hashtable<String, Object> table) {
1066            if (table == null) {
1067                throw new NullPointerException("properties table cannot be null");
1068            }
1069            Color color = (Color)table.get("color");
1070            Double lat = (Double)table.get("lat");
1071            Double lon = (Double)table.get("lon");
1072            Boolean visibility = (Boolean)table.get("visibility");
1073            probe.setLatLon(lat, lon);
1074            probe.setColor(color);
1075            setVisible(visibility);
1076        }
1077
1078        public void pokeValueDisplay() {
1079            probe.setField(display.getImageData());
1080            try {
1081                //FlatField spectrum = display.getMultiSpectralData().getSpectrum(probe.getEarthPosition());
1082                //spectrumRef.setData(spectrum);
1083            } catch (Exception e) { }
1084        }
1085
1086        public void removeValueDisplay() throws VisADException, RemoteException {
1087            probe.handleProbeRemoval();
1088            display.removeRef(spectrumRef);
1089        }
1090    }
1091
1092    // TODO(jon): MultiSpectralControl should become the table model.
1093    private static class ProbeTableModel extends AbstractTableModel implements ProbeListener {
1094//        private static final String[] COLUMNS = { 
1095//            "Visibility", "Probe ID", "Value", "Spectrum", "Latitude", "Longitude", "Color" 
1096//        };
1097
1098        private static final String[] COLUMNS = { 
1099            "Visibility", "Probe ID", "Value", "Latitude", "Longitude", "Color" 
1100        };
1101
1102        private final Map<ReadoutProbe, Integer> probeToIndex = new LinkedHashMap<>();
1103        private final Map<Integer, Spectrum> indexToSpectrum = new LinkedHashMap<>();
1104        private final MultiSpectralControl control;
1105
1106        public ProbeTableModel(final MultiSpectralControl control, final List<Spectrum> probes) {
1107            this.control = requireNonNull(control);
1108            updateWith(requireNonNull(probes));
1109        }
1110
1111        public void probeColorChanged(final ProbeEvent<Color> e) {
1112            ReadoutProbe probe = e.getProbe();
1113            if (!probeToIndex.containsKey(probe)) {
1114                return;
1115            }
1116            int index = probeToIndex.get(probe);
1117            fireTableCellUpdated(index, 5);
1118        }
1119
1120        public void probeVisibilityChanged(final ProbeEvent<Boolean> e) {
1121            ReadoutProbe probe = e.getProbe();
1122            if (!probeToIndex.containsKey(probe)) {
1123                return;
1124            }
1125            int index = probeToIndex.get(probe);
1126            fireTableCellUpdated(index, 0);
1127        }
1128
1129        public void probePositionChanged(final ProbeEvent<RealTuple> e) {
1130            ReadoutProbe probe = e.getProbe();
1131            if (!probeToIndex.containsKey(probe)) {
1132                return;
1133            }
1134            int index = probeToIndex.get(probe);
1135            fireTableRowsUpdated(index, index);
1136        }
1137
1138        public void probeFormatPatternChanged(final ProbeEvent<String> e) {
1139            ReadoutProbe probe = e.getProbe();
1140            if (!probeToIndex.containsKey(probe)) {
1141                return;
1142            }
1143            int index = probeToIndex.get(probe);
1144            fireTableRowsUpdated(index, index);
1145        }
1146
1147        public void updateWith(final List<Spectrum> updatedSpectra) {
1148            requireNonNull(updatedSpectra);
1149
1150            probeToIndex.clear();
1151            indexToSpectrum.clear();
1152
1153            for (int i = 0, j = updatedSpectra.size()-1; i < updatedSpectra.size(); i++, j--) {
1154                Spectrum spectrum = updatedSpectra.get(j);
1155                ReadoutProbe probe = spectrum.getProbe();
1156                if (!probe.hasListener(this)) {
1157                    probe.addProbeListener(this);
1158                }
1159                probeToIndex.put(spectrum.getProbe(), i);
1160                indexToSpectrum.put(i, spectrum);
1161            }
1162        }
1163
1164        public int getColumnCount() {
1165            return COLUMNS.length;
1166        }
1167
1168        public int getRowCount() {
1169            if (probeToIndex.size() != indexToSpectrum.size()) {
1170                throw new AssertionError("");
1171            }
1172            return probeToIndex.size();
1173        }
1174
1175//        public Object getValueAt(final int row, final int column) {
1176//            Spectrum spectrum = indexToSpectrum.get(row);
1177//            switch (column) {
1178//                case 0: return spectrum.isVisible();
1179//                case 1: return spectrum.getId();
1180//                case 2: return spectrum.getValue();
1181//                case 3: return "notyet";
1182//                case 4: return formatPosition(spectrum.getLatitude());
1183//                case 5: return formatPosition(spectrum.getLongitude());
1184//                case 6: return spectrum.getColor();
1185//                default: throw new AssertionError("uh oh");
1186//            }
1187//        }
1188        public Object getValueAt(final int row, final int column) {
1189            DecimalFormat format = new DecimalFormat(control.getIdv().getStore().get(Constants.PREF_LATLON_FORMAT, "##0.0"));
1190            boolean use360 = control.use360Box.isSelected();
1191            Spectrum spectrum = indexToSpectrum.get(row);
1192            switch (column) {
1193                case 0: return spectrum.isVisible();
1194                case 1: return spectrum.getId();
1195                case 2: return spectrum.getValue();
1196                case 3: return format.format(spectrum.getLatitude());
1197                case 4: return format.format(use360 ? clamp360(spectrum.getLongitude()) : clamp180(spectrum.getLongitude()));
1198                case 5: return spectrum.getColor();
1199                default: throw new AssertionError("uh oh");
1200            }
1201        }
1202
1203        public boolean isCellEditable(final int row, final int column) {
1204            switch (column) {
1205                case 0: return true;
1206                case 5: return true;
1207                default: return false;
1208            }
1209        }
1210
1211        public void setValueAt(final Object value, final int row, final int column) {
1212            Spectrum spectrum = indexToSpectrum.get(row);
1213            boolean didUpdate = true;
1214            switch (column) {
1215                case 0: spectrum.setVisible((Boolean)value); break;
1216                case 5: spectrum.setColor((Color)value); break;
1217                default: didUpdate = false; break;
1218            }
1219
1220            if (didUpdate) {
1221                fireTableCellUpdated(row, column);
1222            }
1223        }
1224
1225        public void moveRow(final int origin, final int destination) {
1226            // get the dragged spectrum (and probe)
1227            Spectrum dragged = indexToSpectrum.get(origin);
1228            ReadoutProbe draggedProbe = dragged.getProbe();
1229
1230            // get the current spectrum (and probe)
1231            Spectrum current = indexToSpectrum.get(destination);
1232            ReadoutProbe currentProbe = current.getProbe();
1233
1234            // update references in indexToSpetrum
1235            indexToSpectrum.put(destination, dragged);
1236            indexToSpectrum.put(origin, current);
1237
1238            // update references in probeToIndex
1239            probeToIndex.put(draggedProbe, destination);
1240            probeToIndex.put(currentProbe, origin);
1241
1242            // build a list of the spectra, ordered by index
1243            List<Spectrum> updated = new ArrayList<>(indexToSpectrum.size());
1244            for (int i = indexToSpectrum.size()-1; i >= 0; i--) {
1245                updated.add(indexToSpectrum.get(i));
1246            }
1247
1248            // send it to control.
1249            control.updateList(updated);
1250        }
1251
1252        public String getColumnName(final int column) {
1253            return COLUMNS[column];
1254        }
1255
1256        public Class<?> getColumnClass(final int column) {
1257            return getValueAt(0, column).getClass();
1258        }
1259
1260        public static double clamp180(double value) {
1261            return ((((value + 180.0) % 360.0) + 360.0) % 360.0) - 180.0;
1262        }
1263
1264        public static double clamp360(double value) {
1265            boolean positive = value > 0.0;
1266            value = ((value % 360.0) + 360.0) % 360.0;
1267            if ((value == 0.0) && positive) {
1268                value = 360.0;
1269            }
1270            return value;
1271        }
1272    }
1273
1274    public class ColorEditor extends AbstractCellEditor implements TableCellEditor, ActionListener {
1275        private Color currentColor = Color.CYAN;
1276        private final JButton button = new JButton();
1277        private final JColorChooser colorChooser = new JColorChooser();
1278        private JDialog dialog;
1279        protected static final String EDIT = "edit";
1280
1281//        private final JComboBox combobox = new JComboBox(GuiUtils.COLORS); 
1282
1283        public ColorEditor() {
1284            button.setActionCommand(EDIT);
1285            button.addActionListener(this);
1286            button.setBorderPainted(false);
1287
1288//            combobox.setActionCommand(EDIT);
1289//            combobox.addActionListener(this);
1290//            combobox.setBorder(new EmptyBorder(0, 0, 0, 0));
1291//            combobox.setOpaque(true);
1292//            ColorRenderer whut = new ColorRenderer(true);
1293//            combobox.setRenderer(whut);
1294//            
1295//            dialog = JColorChooser.createDialog(combobox, "pick a color", true, colorChooser, this, null);
1296            dialog = JColorChooser.createDialog(button, "pick a color", true, colorChooser, this, null);
1297        }
1298        public void actionPerformed(ActionEvent e) {
1299            if (EDIT.equals(e.getActionCommand())) {
1300                //The user has clicked the cell, so
1301                //bring up the dialog.
1302//                button.setBackground(currentColor);
1303                colorChooser.setColor(currentColor);
1304                dialog.setVisible(true);
1305
1306                //Make the renderer reappear.
1307                fireEditingStopped();
1308
1309            } else { //User pressed dialog's "OK" button.
1310                currentColor = colorChooser.getColor();
1311            }
1312        }
1313
1314        //Implement the one CellEditor method that AbstractCellEditor doesn't.
1315        public Object getCellEditorValue() {
1316            return currentColor;
1317        }
1318
1319        //Implement the one method defined by TableCellEditor.
1320        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
1321            currentColor = (Color)value;
1322            return button;
1323//            return combobox;
1324        }
1325    }
1326
1327    public class ColorRenderer extends JLabel implements TableCellRenderer, ListCellRenderer {
1328        Border unselectedBorder = null;
1329        Border selectedBorder = null;
1330        boolean isBordered = true;
1331
1332        public ColorRenderer(boolean isBordered) {
1333            this.isBordered = isBordered;
1334            setHorizontalAlignment(CENTER);
1335            setVerticalAlignment(CENTER);
1336            setOpaque(true);
1337        }
1338
1339        public Component getTableCellRendererComponent(JTable table, Object color, boolean isSelected, boolean hasFocus, int row, int column) {
1340            Color newColor = (Color)color;
1341            setBackground(newColor);
1342            if (isBordered) {
1343                if (isSelected) {
1344                    if (selectedBorder == null) {
1345                        selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, table.getSelectionBackground());
1346                    }
1347                    setBorder(selectedBorder);
1348                } else {
1349                    if (unselectedBorder == null) {
1350                        unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, table.getBackground());
1351                    }
1352                    setBorder(unselectedBorder);
1353                }
1354            }
1355
1356            setToolTipText(String.format("RGB: red=%d, green=%d, blue=%d", newColor.getRed(), newColor.getGreen(), newColor.getBlue()));
1357            return this;
1358        }
1359
1360        public Component getListCellRendererComponent(JList list, Object color, int index, boolean isSelected, boolean cellHasFocus) {
1361            Color newColor = (Color)color;
1362            setBackground(newColor);
1363            if (isBordered) {
1364                if (isSelected) {
1365                    if (selectedBorder == null) {
1366                        selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, list.getSelectionBackground());
1367                    }
1368                    setBorder(selectedBorder);
1369                } else {
1370                    if (unselectedBorder == null) {
1371                        unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, list.getBackground());
1372                    }
1373                    setBorder(unselectedBorder);
1374                }
1375            }
1376            setToolTipText(String.format("RGB: red=%d, green=%d, blue=%d", newColor.getRed(), newColor.getGreen(), newColor.getBlue()));
1377            return this;
1378        }
1379    }
1380
1381    public class HackyDragDropRowUI extends BasicTableUI {
1382
1383        private boolean inDrag = false;
1384        private int start;
1385        private int offset;
1386
1387        protected MouseInputListener createMouseInputListener() {
1388            return new HackyMouseInputHandler();
1389        }
1390
1391        public void paint(Graphics g, JComponent c) {
1392            super.paint(g, c);
1393
1394            if (!inDrag) {
1395                return;
1396            }
1397
1398            int width = table.getWidth();
1399            int height = table.getRowHeight();
1400            g.setColor(table.getParent().getBackground());
1401            Rectangle rect = table.getCellRect(table.getSelectedRow(), 0, false);
1402            g.copyArea(rect.x, rect.y, width, height, rect.x, offset);
1403
1404            if (offset < 0) {
1405                g.fillRect(rect.x, rect.y + (height + offset), width, (offset * -1));
1406            } else {
1407                g.fillRect(rect.x, rect.y, width, offset);
1408            }
1409        }
1410
1411        class HackyMouseInputHandler extends MouseInputHandler {
1412
1413            public void mouseDragged(MouseEvent e) {
1414                int row = table.getSelectedRow();
1415                if (row < 0) {
1416                    return;
1417                }
1418
1419                inDrag = true;
1420
1421                int height = table.getRowHeight();
1422                int middleOfSelectedRow = (height * row) + (height / 2);
1423
1424                int toRow = -1;
1425                int yLoc = (int)e.getPoint().getY();
1426
1427                // goin' up?
1428                if (yLoc < (middleOfSelectedRow - height)) {
1429                    toRow = row - 1;
1430                } else if (yLoc > (middleOfSelectedRow + height)) {
1431                    toRow = row + 1;
1432                }
1433
1434                ProbeTableModel model = (ProbeTableModel)table.getModel();
1435                if ((toRow >= 0) && (toRow < table.getRowCount())) {
1436                    model.moveRow(row, toRow);
1437                    table.setRowSelectionInterval(toRow, toRow);
1438                    start = yLoc;
1439                }
1440
1441                offset = (start - yLoc) * -1;
1442                table.repaint();
1443            }
1444
1445            public void mousePressed(MouseEvent e) {
1446                super.mousePressed(e);
1447                start = (int)e.getPoint().getY();
1448            }
1449
1450            public void mouseReleased(MouseEvent e){
1451                super.mouseReleased(e);
1452                inDrag = false;
1453                table.repaint();
1454            }
1455        }
1456    }
1457}