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