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