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