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