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