001/*
002 * $Id: MultiSpectralControl.java,v 1.65 2011/03/24 16:06:32 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
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
031package edu.wisc.ssec.mcidasv.control;
032
033import java.awt.Color;
034import java.awt.Component;
035import java.awt.Container;
036import java.awt.Dimension;
037import java.awt.Graphics;
038import java.awt.GridBagConstraints;
039import java.awt.Insets;
040import java.awt.Rectangle;
041import java.awt.event.ActionEvent;
042import java.awt.event.ActionListener;
043import java.awt.event.MouseEvent;
044import java.awt.geom.Rectangle2D;
045import java.rmi.RemoteException;
046import java.text.DecimalFormat;
047import java.util.ArrayList;
048import java.util.Collections;
049import java.util.Hashtable;
050import java.util.LinkedHashMap;
051import java.util.List;
052import java.util.Map;
053
054import javax.swing.AbstractCellEditor;
055import javax.swing.BorderFactory;
056import javax.swing.JButton;
057import javax.swing.JColorChooser;
058import javax.swing.JComboBox;
059import javax.swing.JComponent;
060import javax.swing.JDialog;
061import javax.swing.JLabel;
062import javax.swing.JList;
063import javax.swing.JPanel;
064import javax.swing.JScrollPane;
065import javax.swing.JTabbedPane;
066import javax.swing.JTable;
067import javax.swing.JTextField;
068import javax.swing.ListCellRenderer;
069import javax.swing.border.Border;
070import javax.swing.event.ListSelectionEvent;
071import javax.swing.event.ListSelectionListener;
072import javax.swing.event.MouseInputListener;
073import javax.swing.plaf.basic.BasicTableUI;
074import javax.swing.table.AbstractTableModel;
075import javax.swing.table.TableCellEditor;
076import javax.swing.table.TableCellRenderer;
077
078import visad.DataReference;
079import visad.DataReferenceImpl;
080import visad.FlatField;
081import visad.RealTuple;
082import visad.VisADException;
083import visad.georef.MapProjection;
084
085import ucar.unidata.data.DataChoice;
086import ucar.unidata.data.DataSelection;
087import ucar.unidata.idv.DisplayControl;
088import ucar.unidata.idv.ViewManager;
089import ucar.unidata.idv.control.ControlWidget;
090import ucar.unidata.idv.control.WrapperWidget;
091import ucar.unidata.util.ColorTable;
092import ucar.unidata.util.GuiUtils;
093import ucar.unidata.util.LogUtil;
094import ucar.unidata.util.Range;
095import ucar.visad.display.DisplayMaster;
096import ucar.visad.display.DisplayableData;
097
098import edu.wisc.ssec.mcidasv.Constants;
099import edu.wisc.ssec.mcidasv.McIDASV;
100import edu.wisc.ssec.mcidasv.data.hydra.HydraRGBDisplayable;
101import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralData;
102import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralDataSource;
103import edu.wisc.ssec.mcidasv.data.hydra.SpectrumAdapter;
104import edu.wisc.ssec.mcidasv.display.hydra.MultiSpectralDisplay;
105import edu.wisc.ssec.mcidasv.probes.ProbeEvent;
106import edu.wisc.ssec.mcidasv.probes.ProbeListener;
107import edu.wisc.ssec.mcidasv.probes.ReadoutProbe;
108import edu.wisc.ssec.mcidasv.util.Contract;
109
110public 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}