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