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