001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2025
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 https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.display.hydra;
030
031import java.awt.Color;
032import java.awt.Component;
033import java.awt.event.ActionEvent;
034import java.awt.event.ActionListener;
035import java.io.File;
036import java.io.FileWriter;
037import java.io.PrintWriter;
038import java.rmi.RemoteException;
039import java.util.ArrayList;
040import java.util.Enumeration;
041import java.util.HashMap;
042import java.util.Hashtable;
043import java.util.List;
044import java.util.Map;
045
046import javax.swing.JComboBox;
047import javax.swing.JOptionPane;
048
049import org.python.antlr.ast.Str;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052
053import visad.CellImpl;
054import visad.ConstantMap;
055import visad.DataReference;
056import visad.DataReferenceImpl;
057import visad.Display;
058import visad.DisplayEvent;
059import visad.DisplayListener;
060import visad.FlatField;
061import visad.FunctionType;
062import visad.Gridded1DSet;
063import visad.Gridded2DSet;
064import visad.LocalDisplay;
065import visad.Real;
066import visad.RealTuple;
067import visad.RealTupleType;
068import visad.RealType;
069import visad.ScalarMap;
070import visad.VisADException;
071import visad.bom.RubberBandBoxRendererJ3D;
072
073import ucar.unidata.data.DirectDataChoice;
074import ucar.unidata.idv.ViewManager;
075import ucar.unidata.util.LogUtil;
076import ucar.visad.display.DisplayableData;
077import ucar.visad.display.XYDisplay;
078
079import edu.wisc.ssec.mcidasv.control.HydraCombo;
080import edu.wisc.ssec.mcidasv.control.HydraControl;
081import edu.wisc.ssec.mcidasv.control.LinearCombo;
082import edu.wisc.ssec.mcidasv.control.MultiSpectralControl;
083import edu.wisc.ssec.mcidasv.data.HydraDataSource;
084import edu.wisc.ssec.mcidasv.data.hydra.GrabLineRendererJ3D;
085import edu.wisc.ssec.mcidasv.data.hydra.HydraRGBDisplayable;
086import edu.wisc.ssec.mcidasv.data.hydra.MultiDimensionSubset;
087import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralData;
088import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralDataSource;
089import edu.wisc.ssec.mcidasv.data.hydra.SuomiNPPDataSource;
090
091public class MultiSpectralDisplay implements DisplayListener {
092
093    private static final Logger logger = LoggerFactory.getLogger(MultiSpectralDisplay.class);
094    
095    private static final String DISP_NAME = "Spectrum";
096    private static int cnt = 1;
097
098    private DirectDataChoice dataChoice;
099
100    private ViewManager viewManager;
101
102    private float[] initialRangeX;
103    private float[] initialRangeY = { 180f, 320f };
104
105    private RealType domainType;
106    private RealType rangeType;
107    private RealType uniqueRangeType;
108
109    private ScalarMap xmap;
110    private ScalarMap ymap;
111
112    private LocalDisplay display;
113
114    private FlatField image;
115
116    private FlatField spectrum = null;
117
118    private boolean imageExpired = true;
119
120    private MultiSpectralData data;
121
122    private float waveNumber;
123
124    private List<DataReference> displayedThings = new ArrayList<>();
125    private HashMap<String, DataReference> idToRef = new HashMap<>();
126    private HashMap<DataReference, ConstantMap[]> colorMaps = new HashMap<>();
127
128    private HydraControl displayControl;
129
130    private DisplayableData imageDisplay = null;
131
132    private XYDisplay master;
133
134    private Gridded1DSet domainSet;
135
136    private JComboBox bandSelectComboBox = null;
137
138    public MultiSpectralDisplay(final HydraControl control) 
139        throws VisADException, RemoteException 
140    {
141        displayControl = control;
142        dataChoice = (DirectDataChoice)displayControl.getDataChoice();
143
144        init();
145        // writeToCSV();
146    }
147
148    public MultiSpectralDisplay(final DirectDataChoice dataChoice) 
149        throws VisADException, RemoteException 
150    {
151        this.dataChoice = dataChoice;
152        init();
153    }
154
155    // TODO: generalize this so that you can grab the image data for any
156    // channel
157    public FlatField getImageData() {
158        try {
159            if ((imageExpired) || (image == null)) {
160                imageExpired = false;
161
162                MultiDimensionSubset select = null;
163                Hashtable table = dataChoice.getProperties();
164                Enumeration keys = table.keys();
165                while (keys.hasMoreElements()) {
166                    Object key = keys.nextElement();
167                    if (key instanceof MultiDimensionSubset) {
168                        select = (MultiDimensionSubset) table.get(key);
169                    }
170                }
171                Map<String, double[]> subset = select.getSubset();
172                image = data.getImage(waveNumber, subset);
173                image = changeRangeType(image, uniqueRangeType);
174            }
175        } catch (Exception e) {
176            LogUtil.logException("MultiSpectralDisplay.getImageData", e);
177        }
178
179        return image;
180    }
181
182    public FlatField getImageDataFrom(final float channel) {
183        FlatField imageData = null;
184        try {
185            MultiDimensionSubset select = null;
186            Hashtable table = dataChoice.getProperties();
187            Enumeration keys = table.keys();
188            while (keys.hasMoreElements()) {
189                Object key = keys.nextElement();
190                if (key instanceof MultiDimensionSubset) {
191                    select = (MultiDimensionSubset) table.get(key);
192                }
193            }
194            Map<String, double[]> subset = select.getSubset();
195            imageData = data.getImage(channel, subset);
196            uniqueRangeType = RealType.getRealType(rangeType.getName()+"_"+cnt++);
197            imageData = changeRangeType(imageData, uniqueRangeType);
198        } catch (Exception e) {
199            LogUtil.logException("MultiSpectralDisplay.getImageDataFrom", e);
200        }
201        return imageData;
202    }
203
204    private FlatField changeRangeType(FlatField image, RealType newRangeType) throws VisADException, RemoteException {
205        FunctionType ftype = (FunctionType)image.getType();
206        FlatField new_image = new FlatField(
207            new FunctionType(ftype.getDomain(), newRangeType), image.getDomainSet());
208        new_image.setSamples(image.getFloats(false), false);
209        return new_image;
210    }
211   
212    public XYDisplay getMaster() {
213        return master;
214    }
215
216    public LocalDisplay getDisplay() {
217        return display;
218    }
219
220    public Component getDisplayComponent() {
221      return master.getDisplayComponent();
222    }
223
224    public RealType getDomainType() {
225        return domainType;
226    }
227
228    public RealType getRangeType() {
229        return rangeType;
230    }
231
232    public ViewManager getViewManager() {
233        return viewManager;
234    }
235
236    public MultiSpectralData getMultiSpectralData() {
237        return data;
238    }
239
240    public Gridded1DSet getDomainSet() {
241        return domainSet;
242    }
243
244    private void init() throws VisADException, RemoteException {
245
246        HydraDataSource source = 
247              (HydraDataSource) dataChoice.getDataSource();
248
249        // TODO revisit this, may want to move method up to base class HydraDataSource
250        if (source instanceof SuomiNPPDataSource) {
251            data = ((SuomiNPPDataSource) source).getMultiSpectralData(dataChoice);
252        }
253        
254        if (source instanceof MultiSpectralDataSource) {
255            data = ((MultiSpectralDataSource) source).getMultiSpectralData(dataChoice);
256        }
257
258        waveNumber = data.init_wavenumber;
259
260        try {
261            spectrum = data.getSpectrum(new int[] { 1, 1 });
262        } catch (Exception e) {
263            LogUtil.logException("MultiSpectralDisplay.init", e);
264        }
265
266        domainSet = (Gridded1DSet)spectrum.getDomainSet();
267        initialRangeX = getXRange(domainSet);
268        initialRangeY = data.getDataRange();
269
270        domainType = getDomainType(spectrum);
271        rangeType = getRangeType(spectrum);
272
273        master = new XYDisplay(DISP_NAME, domainType, rangeType);
274
275        setDisplayMasterAttributes(master);
276
277        // set up the x- and y-axis
278        xmap = new ScalarMap(domainType, Display.XAxis);
279        ymap = new ScalarMap(rangeType, Display.YAxis);
280
281        xmap.setRange(initialRangeX[0], initialRangeX[1]);
282        ymap.setRange(initialRangeY[0], initialRangeY[1]);
283
284        display = master.getDisplay();
285        display.addMap(xmap);
286        display.addMap(ymap);
287        display.addDisplayListener(this);
288
289        new RubberBandBox(this, xmap, ymap);
290
291        if (displayControl == null) { //- add in a ref for the default spectrum, ie no DisplayControl
292            DataReferenceImpl spectrumRef = new DataReferenceImpl(hashCode() + "_spectrumRef");
293            spectrumRef.setData(spectrum);
294            addRef(spectrumRef, Color.WHITE);
295        }
296
297        if (data.hasBandNames()) {
298            bandSelectComboBox = new JComboBox(data.getBandNames().toArray());
299            bandSelectComboBox.setSelectedItem(data.init_bandName);
300            bandSelectComboBox.addActionListener(new ActionListener() {
301                public void actionPerformed(ActionEvent e) {
302                    String bandName = (String)bandSelectComboBox.getSelectedItem();
303                    if (bandName == null)
304                        return;
305
306                    Map<String, Float> bandMap = data.getBandNameMap();
307                    if (bandMap == null)
308                        return;
309
310                    if (!bandMap.containsKey(bandName))
311                        return;
312
313                    setWaveNumber(bandMap.get(bandName));
314                }
315            });
316        }
317    }
318
319    /**
320     * McIDAS Inquiry #2535-3141
321     * Get multispectral data as a string for CSV files
322     * @return data as a comma separated string
323     */
324    public String getDataAsString() {
325        try {
326            domainSet = (Gridded1DSet) spectrum.getDomainSet();
327
328            int[] idx = new int[domainSet.getLength()];
329
330            for (int i = 0; i < domainSet.getLength(); i++) {idx[i] = i;}
331
332            float[][] out = domainSet.indexToValue(idx);
333            double[][] out2 = spectrum.unpackValues();
334
335            StringBuilder build = new StringBuilder();
336
337            build.append(domainType.toString());
338            build.append(",");
339            build.append(rangeType.toString());
340            build.append("\n");
341
342            for (int i = 0; i < out[0].length; i++) {
343                build.append(out[0][i]);
344                build.append(",");
345                build.append(out2[0][i]);
346                build.append("\n");
347            }
348
349            return build.toString();
350
351        } catch (Exception e) {
352            logger.warn("Writing to CSV failed", e);
353            return "NO STRING CREATED";
354        }
355    }
356
357    public JComboBox getBandSelectComboBox() {
358      return bandSelectComboBox;
359    }
360
361    // TODO: HACK!!
362    public void setDisplayControl(final HydraControl control) {
363        displayControl = control;
364    }
365
366    public void displayChanged(final DisplayEvent e) throws VisADException, RemoteException {
367        // TODO: write a method like isChannelUpdate(EVENT_ID)? or maybe just 
368        // deal with a super long if-statement and put an "OR MOUSE_RELEASED" 
369        // up here?
370        if (e.getId() == DisplayEvent.MOUSE_RELEASED_CENTER) {
371            // McIDAS Inquiry #1057-3141
372            float val = getSelectorValue(channelSelector);
373            setWaveNumber(val);
374            if (displayControl != null)
375                displayControl.handleChannelChange(val);
376        }
377        else if (e.getId() == DisplayEvent.MOUSE_PRESSED_LEFT) {
378            if (e.getInputEvent().isControlDown()) {
379                xmap.setRange(initialRangeX[0], initialRangeX[1]);
380                ymap.setRange(initialRangeY[0], initialRangeY[1]);
381            }
382        }
383        else if (e.getId() == DisplayEvent.MOUSE_RELEASED) {
384            float val = getSelectorValue(channelSelector);
385            if (val != waveNumber) {
386                // TODO: setWaveNumber needs to be rethought, as it calls
387                // setSelectorValue which is redundant in the cases of dragging
388                // or clicking
389                setWaveNumber(val);
390                if (displayControl != null)
391                    displayControl.handleChannelChange(val);
392            }
393        }
394    }
395
396    public DisplayableData getImageDisplay() {
397        if (imageDisplay == null) {
398            try {
399                uniqueRangeType = RealType.getRealType(rangeType.getName()+"_"+cnt++);
400                imageDisplay = new HydraRGBDisplayable("image", uniqueRangeType, null, true, displayControl);
401            } catch (Exception e) {
402                LogUtil.logException("MultiSpectralDisplay.getImageDisplay", e);
403            }
404        }
405        return imageDisplay;
406    }
407
408    public float getWaveNumber() {
409        return waveNumber;
410    }
411
412    public int getChannelIndex() throws Exception {
413      return data.getChannelIndexFromWavenumber(waveNumber);
414    }
415
416    public void refreshDisplay() throws VisADException, RemoteException {
417        if (display == null)
418            return;
419
420        synchronized (displayedThings) {
421            for (DataReference ref : displayedThings) {
422                display.removeReference(ref);
423                display.addReference(ref, colorMaps.get(ref));
424            }
425        }
426    }
427
428    public boolean hasNullData() {
429        try {
430            synchronized (displayedThings) {
431                for (DataReference ref : displayedThings) {
432                    if (ref.getData() == null)
433                        return true;
434                }
435            }
436        } catch (Exception e) { }
437        return false;
438    }
439
440    /** ID of the selector that controls the displayed channel. */
441    private final String channelSelector = hashCode() + "_chanSelect";
442
443    /** The map of selector IDs to selectors. */
444    private final Map<String, DragLine> selectors = 
445        new HashMap<String, DragLine>();
446
447    public void showChannelSelector() {
448        try {
449            createSelector(channelSelector, Color.GREEN);
450        } catch (Exception e) {
451            LogUtil.logException("MultiSpectralDisplay.showChannelSelector", e);
452        }
453    }
454
455    public void hideChannelSelector() {
456        try {
457            DragLine selector = removeSelector(channelSelector);
458            selector = null;
459        } catch (Exception e) {
460            LogUtil.logException("MultiSpectralDisplay.hideChannelSelector", e);
461        }
462    }
463
464    public DragLine createSelector(final String id, final Color color) throws Exception {
465        if (id == null)
466            throw new NullPointerException("selector id cannot be null");
467        if (color == null)
468            throw new NullPointerException("selector color cannot be null");
469        return createSelector(id, makeColorMap(color));
470    }
471
472    public DragLine createSelector(final String id, final ConstantMap[] color) throws Exception {
473        if (id == null)
474            throw new NullPointerException("selector id cannot be null");
475        if (color == null)
476            throw new NullPointerException("selector color cannot be null");
477
478        if (selectors.containsKey(id))
479            return selectors.get(id);
480
481        DragLine selector = new DragLine(this, id, color, initialRangeY);
482        selector.setHydraControl(displayControl);
483        selector.setSelectedValue(waveNumber);
484        selectors.put(id, selector);
485        return selector;
486    }
487
488    public DragLine getSelector(final String id) {
489        return selectors.get(id);
490    }
491
492    public float getSelectorValue(final String id) {
493        DragLine selector = selectors.get(id);
494        if (selector == null)
495            return Float.NaN;
496        return selector.getSelectedValue();
497    }
498
499    public void setSelectorValue(final String id, final float value) 
500        throws VisADException, RemoteException 
501    {
502        DragLine selector = selectors.get(id);
503        if (selector != null)
504            selector.setSelectedValue(value);
505    }
506
507    // BAD BAD BAD BAD
508    public void updateControlSelector(final String id, final float value) {
509        if (displayControl == null)
510            return;
511        if (displayControl instanceof LinearCombo) {
512            ((LinearCombo)displayControl).updateSelector(id, value);
513        } else if (displayControl instanceof HydraCombo) {
514            ((HydraCombo)displayControl).updateComboPanel(id, value);
515        }
516    }
517
518    public DragLine removeSelector(final String id) {
519        DragLine selector = selectors.remove(id);
520        if (selector == null)
521            return null;
522        selector.annihilate();
523        return selector;
524    }
525
526    public List<DragLine> getSelectors() {
527        return new ArrayList<DragLine>(selectors.values());
528    }
529
530    /**
531     * @return Whether or not the channel selector is being displayed.
532     */
533    public boolean displayingChannel() {
534        return (getSelector(channelSelector) != null);
535    }
536
537    public void removeRef(final DataReference thing) throws VisADException, 
538        RemoteException 
539    {
540        if (display == null)
541            return;
542
543        synchronized (displayedThings) {
544            displayedThings.remove(thing);
545            colorMaps.remove(thing);
546            idToRef.remove(thing.getName());
547            display.removeReference(thing);
548        }
549    }
550
551    public void addRef(final DataReference thing, final Color color) 
552        throws VisADException, RemoteException 
553    {
554        if (display == null)
555            return;
556
557        synchronized (displayedThings) {
558            ConstantMap[] colorMap = makeColorMap(color);
559
560            displayedThings.add(thing);
561            idToRef.put(thing.getName(), thing);
562            ConstantMap[] constMaps;
563            if (data.hasBandNames()) {
564                constMaps = new ConstantMap[colorMap.length+2];
565                System.arraycopy(colorMap, 0, constMaps, 0, colorMap.length);
566                constMaps[colorMap.length] = new ConstantMap(1f, Display.PointMode);
567                constMaps[colorMap.length+1] = new ConstantMap(5f, Display.PointSize);
568            } else {
569                constMaps = colorMap;
570            }
571            colorMaps.put(thing, constMaps);
572
573            display.addReference(thing, constMaps);
574        }
575    }
576
577    public void updateRef(final DataReference thing, final Color color)
578        throws VisADException, RemoteException 
579    {
580        ConstantMap[] colorMap = makeColorMap(color);
581        ConstantMap[] constMaps;
582        if (data.hasBandNames()) {
583            constMaps = new ConstantMap[colorMap.length+2];
584            System.arraycopy(colorMap, 0, constMaps, 0, colorMap.length);
585            constMaps[colorMap.length] = new ConstantMap(1f, Display.PointMode);
586            constMaps[colorMap.length+1] = new ConstantMap(5f, Display.PointSize);
587        } else {
588            constMaps = colorMap;
589        }
590        colorMaps.put(thing, constMaps);
591        idToRef.put(thing.getName(), thing);
592        refreshDisplay();
593    }
594
595    public void reorderDataRefsById(final List<String> dataRefIds) {
596        if (dataRefIds == null)
597            throw new NullPointerException("");
598
599        synchronized (displayedThings) {
600            try {
601                displayedThings.clear();
602                for (String refId : dataRefIds) {
603                    DataReference ref = idToRef.get(refId);
604                    ConstantMap[] color = colorMaps.get(ref);
605                    display.removeReference(ref);
606                    display.addReference(ref, color);
607                }
608            } catch (Exception e) { }
609        }
610    }
611
612    // TODO: needs work
613    public boolean setWaveNumber(final float val) {
614        if (data == null)
615            return false;
616
617        if (waveNumber == val)
618            return true;
619
620        try {
621            if (spectrum == null) { 
622              spectrum = data.getSpectrum(new int[] { 1, 1 });
623            }
624
625            Gridded1DSet domain = (Gridded1DSet)spectrum.getDomainSet();
626            int[] idx = domain.valueToIndex(new float[][] { { val } });
627            float[][] tmp = domain.indexToValue(idx);
628            float channel = tmp[0][0];
629
630            setSelectorValue(channelSelector, channel);
631
632            imageExpired = true;
633        } catch (Exception e) {
634            LogUtil.logException("MultiSpectralDisplay.setDisplayedWaveNum", e);
635            return false;
636        }
637
638        waveNumber = val;
639
640        if (data.hasBandNames()) {
641            String name = data.getBandNameFromWaveNumber(waveNumber);
642            bandSelectComboBox.setSelectedItem(name);
643        }
644
645        return true;
646    }
647
648    /**
649     * @return The ConstantMap representation of {@code color}.
650     */
651    public static ConstantMap[] makeColorMap(final Color color)
652        throws VisADException, RemoteException 
653    {
654        float r = color.getRed() / 255f;
655        float g = color.getGreen() / 255f;
656        float b = color.getBlue() / 255f;
657        float a = color.getAlpha() / 255f;
658        return new ConstantMap[] { new ConstantMap(r, Display.Red),
659                                   new ConstantMap(g, Display.Green),
660                                   new ConstantMap(b, Display.Blue),
661                                   new ConstantMap(a, Display.Alpha) };
662    }
663
664    /**
665     * Provides {@code master} some sensible default attributes.
666     */
667    private static void setDisplayMasterAttributes(final XYDisplay master) 
668        throws VisADException, RemoteException 
669    {
670        master.showAxisScales(true);
671        master.setAspect(2.5, 0.75);
672
673        double[] proj = master.getProjectionMatrix();
674        proj[0] = 0.35;
675        proj[5] = 0.35;
676        proj[10] = 0.35;
677
678        master.setProjectionMatrix(proj);
679    }
680
681    /**
682     * @return The minimum and maximum values found on the x-axis.
683     */
684    private static float[] getXRange(final Gridded1DSet domain) {
685        return new float[] { domain.getLow()[0], domain.getHi()[0] };
686    }
687
688    public static RealType getRangeType(final FlatField spectrum) {
689        return (((FunctionType)spectrum.getType()).getFlatRange().getRealComponents())[0];
690    }
691
692    private static RealType getDomainType(final FlatField spectrum) {
693        return (((FunctionType)spectrum.getType()).getDomain().getRealComponents())[0];
694    }
695
696    private static class RubberBandBox extends CellImpl {
697
698        private static final String RBB = "_rubberband";
699        
700        private DataReference rubberBand;
701
702        private boolean init = false;
703
704        private ScalarMap xmap;
705
706        private ScalarMap ymap;
707
708        public RubberBandBox(final MultiSpectralDisplay msd,
709            final ScalarMap x, final ScalarMap y) throws VisADException,
710            RemoteException 
711        {
712            RealType domainType = msd.getDomainType();
713            RealType rangeType = msd.getRangeType();
714
715            LocalDisplay display = msd.getDisplay();
716
717            rubberBand = new DataReferenceImpl(hashCode() + RBB);
718            rubberBand.setData(new RealTuple(new RealTupleType(domainType,
719                rangeType), new double[] { Double.NaN, Double.NaN }));
720
721            display.addReferences(new RubberBandBoxRendererJ3D(domainType,
722                rangeType, 1, 1), new DataReference[] { rubberBand }, null);
723
724            xmap = x;
725            ymap = y;
726
727            this.addReference(rubberBand);
728        }
729
730        public void doAction() throws VisADException, RemoteException {
731            if (!init) {
732                init = true;
733                return;
734            }
735
736            Gridded2DSet set = (Gridded2DSet)rubberBand.getData();
737
738            float[] low = set.getLow();
739            float[] high = set.getHi();
740
741            xmap.setRange(low[0], high[0]);
742            ymap.setRange(low[1], high[1]);
743        }
744    }
745
746    public static class DragLine extends CellImpl {
747        private final String selectorId = hashCode() + "_selector";
748        private final String lineId = hashCode() + "_line";
749        private final String controlId;
750
751        private ConstantMap[] mappings = new ConstantMap[5];
752
753        private DataReference line;
754
755        private DataReference selector;
756
757        private MultiSpectralDisplay multiSpectralDisplay;
758        
759        private HydraControl hydraControl;
760
761        private RealType domainType;
762        private RealType rangeType;
763
764        private RealTupleType tupleType;
765
766        private LocalDisplay display;
767
768        private float[] YRANGE;
769
770        private float lastSelectedValue;
771
772        public DragLine(final MultiSpectralDisplay msd, final String controlId, final Color color) throws Exception {
773            this(msd, controlId, makeColorMap(color));
774        }
775
776        public DragLine(final MultiSpectralDisplay msd, final String controlId, final Color color, float[] YRANGE) throws Exception {
777            this(msd, controlId, makeColorMap(color), YRANGE);
778        }
779
780        public DragLine(final MultiSpectralDisplay msd, final String controlId,
781            final ConstantMap[] color) throws Exception
782        {
783            this(msd, controlId, color, new float[] {180f, 320f});
784        }
785
786        public DragLine(final MultiSpectralDisplay msd, final String controlId, 
787            final ConstantMap[] color, float[] YRANGE) throws Exception 
788        {
789            if (msd == null)
790                throw new NullPointerException("must provide a non-null MultiSpectralDisplay");
791            if (controlId == null)
792                throw new NullPointerException("must provide a non-null control ID");
793            if (color == null)
794                throw new NullPointerException("must provide a non-null color");
795
796            this.controlId = controlId;
797            this.multiSpectralDisplay = msd;
798            this.YRANGE = YRANGE;
799            lastSelectedValue = multiSpectralDisplay.getWaveNumber();
800
801            for (int i = 0; i < color.length; i++) {
802                mappings[i] = (ConstantMap)color[i].clone();
803            }
804            mappings[4] = new ConstantMap(-0.5, Display.YAxis);
805
806            Gridded1DSet domain = multiSpectralDisplay.getDomainSet();
807
808            domainType = multiSpectralDisplay.getDomainType();
809            rangeType = multiSpectralDisplay.getRangeType();
810            tupleType = new RealTupleType(domainType, rangeType);
811
812            selector = new DataReferenceImpl(selectorId);
813            line = new DataReferenceImpl(lineId);
814
815            display = multiSpectralDisplay.getDisplay();
816
817            display.addReferences(new GrabLineRendererJ3D(domain), new DataReference[] { selector }, new ConstantMap[][] { mappings });
818            display.addReference(line, cloneMappedColor(color));
819
820            addReference(selector);
821        }
822
823        private static ConstantMap[] cloneMappedColor(final ConstantMap[] color) throws Exception {
824            assert color != null && color.length >= 3 : color;
825            return new ConstantMap[] { 
826                (ConstantMap)color[0].clone(),
827                (ConstantMap)color[1].clone(),
828                (ConstantMap)color[2].clone(),
829            };
830        }
831
832        public void annihilate() {
833            try {
834                display.removeReference(selector);
835                display.removeReference(line);
836            } catch (Exception e) {
837                LogUtil.logException("DragLine.annihilate", e);
838            }
839        }
840
841        public String getControlId() {
842            return controlId;
843        }
844
845        /**
846         * Handles drag and drop updates.
847         */
848        public void doAction() throws VisADException, RemoteException {
849            setSelectedValue(getSelectedValue());
850        }
851
852        public float getSelectedValue() {
853            float val = (float)display.getDisplayRenderer().getDirectAxisValue(domainType);
854            if (Float.isNaN(val))
855                val = lastSelectedValue;
856            return val;
857        }
858
859        public void setSelectedValue(final float val) throws VisADException,
860            RemoteException 
861        {
862            // don't do work for stupid values
863            if ((Float.isNaN(val)) 
864                || (selector.getThing() != null && val == lastSelectedValue))
865                return;
866
867            line.setData(new Gridded2DSet(tupleType,
868                new float[][] { { val, val }, { YRANGE[0], YRANGE[1] } }, 2));
869
870            selector.setData(new Real(domainType, val));
871            lastSelectedValue = val;
872            
873            if (hydraControl instanceof MultiSpectralControl) {
874                ((MultiSpectralControl) hydraControl).setWavelengthLabel
875                (
876                                MultiSpectralControl.WAVENUMLABEL + val
877                );
878            }
879            multiSpectralDisplay.updateControlSelector(controlId, val);
880        }
881
882                /**
883                 * Set the display control so we can call back and update
884                 * wavelength readout in real time.
885                 * 
886                 * @param hydraControl the display control to set
887                 */
888        
889                public void setHydraControl(HydraControl hydraControl) {
890                        this.hydraControl = hydraControl;
891                }
892    }
893}