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