001    /*
002     * $Id: MultiSpectralDisplay.java,v 1.39 2012/02/19 17:35:46 davep Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
007     * Space Science and Engineering Center (SSEC)
008     * University of Wisconsin - Madison
009     * 1225 W. Dayton Street, Madison, WI 53706, USA
010     * https://www.ssec.wisc.edu/mcidas
011     * 
012     * All Rights Reserved
013     * 
014     * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015     * some McIDAS-V source code is based on IDV and VisAD source code.  
016     * 
017     * McIDAS-V is free software; you can redistribute it and/or modify
018     * it under the terms of the GNU Lesser Public License as published by
019     * the Free Software Foundation; either version 3 of the License, or
020     * (at your option) any later version.
021     * 
022     * McIDAS-V is distributed in the hope that it will be useful,
023     * but WITHOUT ANY WARRANTY; without even the implied warranty of
024     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025     * GNU Lesser Public License for more details.
026     * 
027     * You should have received a copy of the GNU Lesser Public License
028     * along with this program.  If not, see http://www.gnu.org/licenses.
029     */
030    
031    package edu.wisc.ssec.mcidasv.display.hydra;
032    
033    import java.awt.Color;
034    import java.awt.Component;
035    import java.awt.event.ActionEvent;
036    import java.awt.event.ActionListener;
037    import java.rmi.RemoteException;
038    import java.util.ArrayList;
039    import java.util.Enumeration;
040    import java.util.HashMap;
041    import java.util.Hashtable;
042    import java.util.List;
043    import java.util.Map;
044    
045    import javax.swing.JComboBox;
046    
047    import org.slf4j.Logger;
048    import org.slf4j.LoggerFactory;
049    
050    import visad.CellImpl;
051    import visad.ConstantMap;
052    import visad.DataReference;
053    import visad.DataReferenceImpl;
054    import visad.Display;
055    import visad.DisplayEvent;
056    import visad.DisplayListener;
057    import visad.FlatField;
058    import visad.FunctionType;
059    import visad.Gridded1DSet;
060    import visad.Gridded2DSet;
061    import visad.LocalDisplay;
062    import visad.Real;
063    import visad.RealTuple;
064    import visad.RealTupleType;
065    import visad.RealType;
066    import visad.ScalarMap;
067    import visad.VisADException;
068    import visad.bom.RubberBandBoxRendererJ3D;
069    
070    import ucar.unidata.data.DirectDataChoice;
071    import ucar.unidata.idv.ViewManager;
072    import ucar.unidata.util.LogUtil;
073    import ucar.visad.display.DisplayableData;
074    import ucar.visad.display.XYDisplay;
075    
076    import edu.wisc.ssec.mcidasv.control.HydraCombo;
077    import edu.wisc.ssec.mcidasv.control.HydraControl;
078    import edu.wisc.ssec.mcidasv.control.LinearCombo;
079    import edu.wisc.ssec.mcidasv.data.HydraDataSource;
080    import edu.wisc.ssec.mcidasv.data.hydra.GrabLineRendererJ3D;
081    import edu.wisc.ssec.mcidasv.data.hydra.HydraRGBDisplayable;
082    import edu.wisc.ssec.mcidasv.data.hydra.MultiDimensionSubset;
083    import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralData;
084    import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralDataSource;
085    import edu.wisc.ssec.mcidasv.data.hydra.SuomiNPPDataSource;
086    
087    public class MultiSpectralDisplay implements DisplayListener {
088    
089        private static final Logger logger = LoggerFactory.getLogger(MultiSpectralDisplay.class);
090        
091        private static final String DISP_NAME = "Spectrum";
092        private static int cnt = 1;
093    
094        private DirectDataChoice dataChoice;
095    
096        private ViewManager viewManager;
097    
098        private float[] initialRangeX;
099        private float[] initialRangeY = { 180f, 320f };
100    
101        private RealType domainType;
102        private RealType rangeType;
103        private RealType uniqueRangeType;
104    
105        private ScalarMap xmap;
106        private ScalarMap ymap;
107    
108        private LocalDisplay display;
109    
110        private FlatField image;
111    
112        private FlatField spectrum = null;
113    
114        private boolean imageExpired = true;
115    
116        private MultiSpectralData data;
117    
118        private float waveNumber;
119    
120        private List<DataReference> displayedThings = new ArrayList<DataReference>();
121        private HashMap<String, DataReference> idToRef = new HashMap<String, DataReference>();
122        private HashMap<DataReference, ConstantMap[]> colorMaps = 
123            new HashMap<DataReference, ConstantMap[]>();
124    
125        private HydraControl displayControl;
126    
127        private DisplayableData imageDisplay = null;
128    
129        private XYDisplay master;
130    
131        private Gridded1DSet domainSet;
132    
133        private JComboBox bandSelectComboBox = null;
134    
135        public MultiSpectralDisplay(final HydraControl control) 
136            throws VisADException, RemoteException 
137        {
138            displayControl = control;
139            dataChoice = (DirectDataChoice)displayControl.getDataChoice();
140    
141            init();
142        }
143    
144        public MultiSpectralDisplay(final DirectDataChoice dataChoice) 
145            throws VisADException, RemoteException 
146        {
147            this.dataChoice = dataChoice;
148            init();
149        }
150    
151        // TODO: generalize this so that you can grab the image data for any
152        // channel
153        public FlatField getImageData() {
154            try {
155                if ((imageExpired) || (image == null)) {
156                    imageExpired = false;
157    
158                  MultiDimensionSubset select = null;
159                  Hashtable table = dataChoice.getProperties();
160                  Enumeration keys = table.keys();
161                  while (keys.hasMoreElements()) {
162                    Object key = keys.nextElement();
163                    if (key instanceof MultiDimensionSubset) {
164                      select = (MultiDimensionSubset) table.get(key);
165                    }
166                  }
167                  HashMap subset = select.getSubset();
168    //              logger.debug("waveNumber={} subset={}", waveNumber, subset);
169                  image = data.getImage(waveNumber, subset);
170                  image = changeRangeType(image, uniqueRangeType);
171                }
172            } catch (Exception e) {
173                LogUtil.logException("MultiSpectralDisplay.getImageData", e);
174            }
175    
176            return image;
177        }
178    
179        public FlatField getImageDataFrom(final float channel) {
180            FlatField imageData = null;
181            try {
182                MultiDimensionSubset select = null;
183                Hashtable table = dataChoice.getProperties();
184                Enumeration keys = table.keys();
185                while (keys.hasMoreElements()) {
186                  Object key = keys.nextElement();
187                  if (key instanceof MultiDimensionSubset) {
188                    select = (MultiDimensionSubset) table.get(key);
189                  }
190                }
191                HashMap subset = select.getSubset();
192                imageData = data.getImage(channel, subset);
193                uniqueRangeType = RealType.getRealType(rangeType.getName()+"_"+cnt++);
194                imageData = changeRangeType(imageData, uniqueRangeType);
195            } catch (Exception e) {
196                LogUtil.logException("MultiSpectralDisplay.getImageDataFrom", e);
197            }
198            return imageData;
199        }
200    
201        private FlatField changeRangeType(FlatField image, RealType newRangeType) throws VisADException, RemoteException {
202          FunctionType ftype = (FunctionType)image.getType();
203          FlatField new_image = new FlatField(
204             new FunctionType(ftype.getDomain(), newRangeType), image.getDomainSet());
205          new_image.setSamples(image.getFloats(false), false);
206          return new_image;
207        }
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                        HashMap<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.setSelectedValue(waveNumber);
438            selectors.put(id, selector);
439            return selector;
440        }
441    
442        public DragLine getSelector(final String id) {
443            return selectors.get(id);
444        }
445    
446        public float getSelectorValue(final String id) {
447            DragLine selector = selectors.get(id);
448            if (selector == null)
449                return Float.NaN;
450            return selector.getSelectedValue();
451        }
452    
453        public void setSelectorValue(final String id, final float value) 
454            throws VisADException, RemoteException 
455        {
456            DragLine selector = selectors.get(id);
457            if (selector != null)
458                selector.setSelectedValue(value);
459        }
460    
461        // BAD BAD BAD BAD
462        public void updateControlSelector(final String id, final float value) {
463            if (displayControl == null)
464                return;
465            if (displayControl instanceof LinearCombo) {
466                ((LinearCombo)displayControl).updateSelector(id, value);
467            } else if (displayControl instanceof HydraCombo) {
468                ((HydraCombo)displayControl).updateComboPanel(id, value);
469            }
470        }
471    
472        public DragLine removeSelector(final String id) {
473            DragLine selector = selectors.remove(id);
474            if (selector == null)
475                return null;
476            selector.annihilate();
477            return selector;
478        }
479    
480        public List<DragLine> getSelectors() {
481            return new ArrayList<DragLine>(selectors.values());
482        }
483    
484        /**
485         * @return Whether or not the channel selector is being displayed.
486         */
487        public boolean displayingChannel() {
488            return (getSelector(channelSelector) != null);
489        }
490    
491        public void removeRef(final DataReference thing) throws VisADException, 
492            RemoteException 
493        {
494            if (display == null)
495                return;
496    
497            synchronized (displayedThings) {
498                displayedThings.remove(thing);
499                colorMaps.remove(thing);
500                idToRef.remove(thing.getName());
501                display.removeReference(thing);
502            }
503        }
504    
505        public void addRef(final DataReference thing, final Color color) 
506            throws VisADException, RemoteException 
507        {
508            if (display == null)
509                return;
510    
511            synchronized (displayedThings) {
512                ConstantMap[] colorMap = makeColorMap(color);
513    
514                displayedThings.add(thing);
515                idToRef.put(thing.getName(), thing);
516                ConstantMap[] constMaps;
517                if (data.hasBandNames()) {
518                    constMaps = new ConstantMap[colorMap.length+2];
519                    System.arraycopy(colorMap, 0, constMaps, 0, colorMap.length);
520                    constMaps[colorMap.length] = new ConstantMap(1f, Display.PointMode);
521                    constMaps[colorMap.length+1] = new ConstantMap(5f, Display.PointSize);
522                } else {
523                    constMaps = colorMap;
524                }
525                colorMaps.put(thing, constMaps);
526    
527                display.addReference(thing, constMaps);
528            }
529        }
530    
531        public void updateRef(final DataReference thing, final Color color)
532            throws VisADException, RemoteException 
533        {
534            ConstantMap[] colorMap = makeColorMap(color);
535            ConstantMap[] constMaps;
536            if (data.hasBandNames()) {
537                constMaps = new ConstantMap[colorMap.length+2];
538                System.arraycopy(colorMap, 0, constMaps, 0, colorMap.length);
539                constMaps[colorMap.length] = new ConstantMap(1f, Display.PointMode);
540                constMaps[colorMap.length+1] = new ConstantMap(5f, Display.PointSize);
541            } else {
542                constMaps = colorMap;
543            }
544            colorMaps.put(thing, constMaps);
545            idToRef.put(thing.getName(), thing);
546            refreshDisplay();
547        }
548    
549        public void reorderDataRefsById(final List<String> dataRefIds) {
550            if (dataRefIds == null)
551                throw new NullPointerException("");
552    
553            synchronized (displayedThings) {
554                try {
555                    displayedThings.clear();
556                    for (String refId : dataRefIds) {
557                        DataReference ref = idToRef.get(refId);
558                        ConstantMap[] color = colorMaps.get(ref);
559                        display.removeReference(ref);
560                        display.addReference(ref, color);
561                    }
562                } catch (Exception e) { }
563            }
564        }
565    
566        // TODO: needs work
567        public boolean setWaveNumber(final float val) {
568            if (data == null)
569                return false;
570    
571            if (waveNumber == val)
572                return true;
573    
574            try {
575                if (spectrum == null) { 
576                  spectrum = data.getSpectrum(new int[] { 1, 1 });
577                }
578    
579                Gridded1DSet domain = (Gridded1DSet)spectrum.getDomainSet();
580                int[] idx = domain.valueToIndex(new float[][] { { val } });
581                float[][] tmp = domain.indexToValue(idx);
582                float channel = tmp[0][0];
583    
584                setSelectorValue(channelSelector, channel);
585    
586                imageExpired = true;
587            } catch (Exception e) {
588                LogUtil.logException("MultiSpectralDisplay.setDisplayedWaveNum", e);
589                return false;
590            }
591    
592            waveNumber = val;
593    
594            if (data.hasBandNames()) {
595                String name = data.getBandNameFromWaveNumber(waveNumber);
596                bandSelectComboBox.setSelectedItem(name);
597            }
598    
599            return true;
600        }
601    
602        /**
603         * @return The ConstantMap representation of <code>color</code>.
604         */
605        public static ConstantMap[] makeColorMap(final Color color)
606            throws VisADException, RemoteException 
607        {
608            float r = color.getRed() / 255f;
609            float g = color.getGreen() / 255f;
610            float b = color.getBlue() / 255f;
611            float a = color.getAlpha() / 255f;
612            return new ConstantMap[] { new ConstantMap(r, Display.Red),
613                                       new ConstantMap(g, Display.Green),
614                                       new ConstantMap(b, Display.Blue),
615                                       new ConstantMap(a, Display.Alpha) };
616        }
617    
618        /**
619         * Provides <code>master</code> some sensible default attributes.
620         */
621        private static void setDisplayMasterAttributes(final XYDisplay master) 
622            throws VisADException, RemoteException 
623        {
624            master.showAxisScales(true);
625            master.setAspect(2.5, 0.75);
626    
627            double[] proj = master.getProjectionMatrix();
628            proj[0] = 0.35;
629            proj[5] = 0.35;
630            proj[10] = 0.35;
631    
632            master.setProjectionMatrix(proj);
633        }
634    
635        /**
636         * @return The minimum and maximum values found on the x-axis.
637         */
638        private static float[] getXRange(final Gridded1DSet domain) {
639            return new float[] { domain.getLow()[0], domain.getHi()[0] };
640        }
641    
642        public static RealType getRangeType(final FlatField spectrum) {
643            return (((FunctionType)spectrum.getType()).getFlatRange().getRealComponents())[0];
644        }
645    
646        private static RealType getDomainType(final FlatField spectrum) {
647            return (((FunctionType)spectrum.getType()).getDomain().getRealComponents())[0];
648        }
649    
650        private static class RubberBandBox extends CellImpl {
651    
652            private static final String RBB = "_rubberband";
653            
654            private DataReference rubberBand;
655    
656            private boolean init = false;
657    
658            private ScalarMap xmap;
659    
660            private ScalarMap ymap;
661    
662            public RubberBandBox(final MultiSpectralDisplay msd,
663                final ScalarMap x, final ScalarMap y) throws VisADException,
664                RemoteException 
665            {
666                RealType domainType = msd.getDomainType();
667                RealType rangeType = msd.getRangeType();
668    
669                LocalDisplay display = msd.getDisplay();
670    
671                rubberBand = new DataReferenceImpl(hashCode() + RBB);
672                rubberBand.setData(new RealTuple(new RealTupleType(domainType,
673                    rangeType), new double[] { Double.NaN, Double.NaN }));
674    
675                display.addReferences(new RubberBandBoxRendererJ3D(domainType,
676                    rangeType, 1, 1), new DataReference[] { rubberBand }, null);
677    
678                xmap = x;
679                ymap = y;
680    
681                this.addReference(rubberBand);
682            }
683    
684            public void doAction() throws VisADException, RemoteException {
685                if (!init) {
686                    init = true;
687                    return;
688                }
689    
690                Gridded2DSet set = (Gridded2DSet)rubberBand.getData();
691    
692                float[] low = set.getLow();
693                float[] high = set.getHi();
694    
695                xmap.setRange(low[0], high[0]);
696                ymap.setRange(low[1], high[1]);
697            }
698        }
699    
700        public static class DragLine extends CellImpl {
701            private final String selectorId = hashCode() + "_selector";
702            private final String lineId = hashCode() + "_line";
703            private final String controlId;
704    
705            private ConstantMap[] mappings = new ConstantMap[5];
706    
707            private DataReference line;
708    
709            private DataReference selector;
710    
711            private MultiSpectralDisplay multiSpectralDisplay;
712    
713            private RealType domainType;
714            private RealType rangeType;
715    
716            private RealTupleType tupleType;
717    
718            private LocalDisplay display;
719    
720            private float[] YRANGE;
721    
722            private float lastSelectedValue;
723    
724            public DragLine(final MultiSpectralDisplay msd, final String controlId, final Color color) throws Exception {
725                this(msd, controlId, makeColorMap(color));
726            }
727    
728            public DragLine(final MultiSpectralDisplay msd, final String controlId, final Color color, float[] YRANGE) throws Exception {
729                this(msd, controlId, makeColorMap(color), YRANGE);
730            }
731    
732            public DragLine(final MultiSpectralDisplay msd, final String controlId,
733                final ConstantMap[] color) throws Exception
734            {
735                this(msd, controlId, color, new float[] {180f, 320f});
736            }
737    
738            public DragLine(final MultiSpectralDisplay msd, final String controlId, 
739                final ConstantMap[] color, float[] YRANGE) throws Exception 
740            {
741                if (msd == null)
742                    throw new NullPointerException("must provide a non-null MultiSpectralDisplay");
743                if (controlId == null)
744                    throw new NullPointerException("must provide a non-null control ID");
745                if (color == null)
746                    throw new NullPointerException("must provide a non-null color");
747    
748                this.controlId = controlId;
749                this.multiSpectralDisplay = msd;
750                this.YRANGE = YRANGE;
751                lastSelectedValue = multiSpectralDisplay.getWaveNumber();
752    
753                for (int i = 0; i < color.length; i++) {
754                    mappings[i] = (ConstantMap)color[i].clone();
755                }
756                mappings[4] = new ConstantMap(-0.5, Display.YAxis);
757    
758                Gridded1DSet domain = multiSpectralDisplay.getDomainSet();
759    
760                domainType = multiSpectralDisplay.getDomainType();
761                rangeType = multiSpectralDisplay.getRangeType();
762                tupleType = new RealTupleType(domainType, rangeType);
763    
764                selector = new DataReferenceImpl(selectorId);
765                line = new DataReferenceImpl(lineId);
766    
767                display = multiSpectralDisplay.getDisplay();
768                
769                display.addReferences(new GrabLineRendererJ3D(domain), new DataReference[] { selector }, new ConstantMap[][] { mappings });
770                display.addReference(line, cloneMappedColor(color));
771    
772                addReference(selector);
773            }
774    
775            private static ConstantMap[] cloneMappedColor(final ConstantMap[] color) throws Exception {
776                assert color != null && color.length >= 3 : color;
777                return new ConstantMap[] { 
778                    (ConstantMap)color[0].clone(),
779                    (ConstantMap)color[1].clone(),
780                    (ConstantMap)color[2].clone(),
781                };
782            }
783    
784            public void annihilate() {
785                try {
786                    display.removeReference(selector);
787                    display.removeReference(line);
788                } catch (Exception e) {
789                    LogUtil.logException("DragLine.annihilate", e);
790                }
791            }
792    
793            public String getControlId() {
794                return controlId;
795            }
796    
797            /**
798             * Handles drag and drop updates.
799             */
800            public void doAction() throws VisADException, RemoteException {
801                setSelectedValue(getSelectedValue());
802            }
803    
804            public float getSelectedValue() {
805                float val = (float)display.getDisplayRenderer().getDirectAxisValue(domainType);
806                if (Float.isNaN(val))
807                    val = lastSelectedValue;
808                return val;
809            }
810    
811            public void setSelectedValue(final float val) throws VisADException,
812                RemoteException 
813            {
814                // don't do work for stupid values
815                if ((Float.isNaN(val)) 
816                    || (selector.getThing() != null && val == lastSelectedValue))
817                    return;
818    
819                line.setData(new Gridded2DSet(tupleType,
820                    new float[][] { { val, val }, { YRANGE[0], YRANGE[1] } }, 2));
821    
822                selector.setData(new Real(domainType, val));
823                lastSelectedValue = val;
824                multiSpectralDisplay.updateControlSelector(controlId, val);
825            }
826        }
827    }