001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028package edu.wisc.ssec.mcidasv.probes;
029
030import static java.util.Objects.requireNonNull;
031import static visad.RealTupleType.SpatialCartesian2DTuple;
032import static visad.RealTupleType.SpatialEarth2DTuple;
033
034import java.awt.Color;
035import java.beans.PropertyChangeEvent;
036import java.beans.PropertyChangeListener;
037import java.rmi.RemoteException;
038import java.text.DecimalFormat;
039import java.util.List;
040import java.util.concurrent.CopyOnWriteArrayList;
041
042import ucar.unidata.collab.SharableImpl;
043import ucar.unidata.idv.control.DisplayControlImpl;
044import ucar.unidata.util.LogUtil;
045import ucar.unidata.view.geoloc.NavigatedDisplay;
046import ucar.visad.display.DisplayMaster;
047import ucar.visad.display.PointProbe;
048import ucar.visad.display.SelectorDisplayable;
049import ucar.visad.display.SelectorPoint;
050import ucar.visad.display.TextDisplayable;
051
052import visad.ActionImpl;
053import visad.ConstantMap;
054import visad.Data;
055import visad.Display;
056import visad.DisplayEvent;
057import visad.DisplayListener;
058import visad.FlatField;
059import visad.MathType;
060import visad.Real;
061import visad.RealTuple;
062import visad.RealTupleType;
063import visad.Text;
064import visad.TextType;
065import visad.Tuple;
066import visad.TupleType;
067import visad.VisADException;
068import visad.VisADGeometryArray;
069import visad.georef.EarthLocationTuple;
070import visad.georef.MapProjection;
071
072import edu.wisc.ssec.mcidasv.control.LambertAEA;
073import edu.wisc.ssec.mcidasv.util.MakeToString;
074import org.slf4j.Logger;
075import org.slf4j.LoggerFactory;
076
077/**
078 * {@code ReadoutProbe} is a probe that combines a {@literal "pickable"} probe
079 * widget with an adjacent text {@literal "readout"} of the data value at the 
080 * probe's current location.
081 * 
082 * <p>Primarily used with 
083 * {@link edu.wisc.ssec.mcidasv.control.MultiSpectralControl}.</p>
084 */
085public class ReadoutProbe 
086    extends SharableImpl 
087    implements PropertyChangeListener, DisplayListener 
088{
089
090    public static final String SHARE_PROFILE =
091        "ReadoutProbeDeux.SHARE_PROFILE";
092
093    public static final String SHARE_POSITION =
094        "ReadoutProbeDeux.SHARE_POSITION";
095
096    private static final Color DEFAULT_COLOR = Color.MAGENTA;
097
098    private static final TupleType TUPTYPE = makeTupleType();
099    
100    private static final Logger logger =
101        LoggerFactory.getLogger(ReadoutProbe.class);
102
103    private final List<ProbeListener> listeners = new CopyOnWriteArrayList<>();
104
105    /** Displays the value of the data at the current position. */
106    private final TextDisplayable valueDisplay =
107        createValueDisplay(DEFAULT_COLOR);
108
109    private final PointSelector pointSelector =
110        new PointSelector(getInitialProbePosition());
111
112    private final DisplayMaster master;
113
114    private Color currentColor;
115
116    private String currentValue = "NaN";
117
118    private double currentLatitude = Double.NaN;
119    private double currentLongitude = Double.NaN;
120
121    private float pointSize = 1.0f;
122
123    private FlatField field;
124
125    private static final DecimalFormat numFmt = new DecimalFormat();
126    
127    /** Used to keep track of the last zoom {@literal "level"}. */
128    private float lastScale = Float.MIN_VALUE;
129
130    /**
131     * Create a {@literal "HYDRA"} probe that allows for displaying things 
132     * like value at current position, current color, and location.
133     *
134     * <p>Note: <b>none</b> of the parameters permit {@code null} values.</p>
135     *
136     * @param control {@literal "Layer"} that will be probed.
137     * @param flatField Data to probe.
138     * @param color {@code Color} of the probe.
139     * @param pattern Format string to use with probe's location values.
140     * @param visible Whether or not the probe is visible.
141     *
142     * @throws NullPointerException if any of the given parameters are 
143     *                              {@code null}.
144     * @throws VisADException if VisAD had problems.
145     * @throws RemoteException if VisAD had problems.
146     */
147    public ReadoutProbe(final DisplayControlImpl control,
148                        final FlatField flatField,
149                        final Color color,
150                        final String pattern,
151                        final boolean visible)
152        throws VisADException, RemoteException
153    {
154        requireNonNull(control, "DisplayControlImpl can't be null");
155        requireNonNull(flatField, "Field can't be null");
156        requireNonNull(color, "Color can't be null");
157        requireNonNull(pattern, "Pattern can't be null");
158        
159        master = control.getNavigatedDisplay();
160        field = flatField;
161        
162        initSharable();
163        
164        pointSelector.setColor(color);
165        valueDisplay.setVisible(visible);
166        valueDisplay.setColor(color);
167        currentColor = color;
168        pointSelector.setVisible(visible);
169        pointSelector.setPointSize(pointSize);
170        pointSelector.setAutoSize(true);
171        pointSelector.setPointSize(getDisplayScale());
172        pointSelector.setZ(control.getZPosition());
173        
174        numFmt.applyPattern(pattern);
175        
176        master.addDisplayable(valueDisplay);
177        master.addDisplayable(pointSelector);
178        setField(flatField);
179        
180        // done mostly to avoid using "this" while we're still within the 
181        // constructor
182        addListeners();
183    }
184    
185    /**
186     * Add this probe instance to the relevant listeners.
187     */
188    private void addListeners() {
189        pointSelector.addPropertyChangeListener(this);
190        master.getDisplay().addDisplayListener(this);
191    }
192
193    /**
194     * Called whenever the probe fires off a {@link PropertyChangeEvent}. 
195     * 
196     * <p>Only handles position changes right now, all other events are 
197     * discarded.</p>
198     *
199     * @param e Object that describes the property change.
200     * 
201     * @throws NullPointerException if passed a {@code null} 
202     * {@code PropertyChangeEvent}.
203     */
204    @Override public void propertyChange(final PropertyChangeEvent e) {
205        requireNonNull(e, "Cannot handle a null property change event");
206        if (e.getPropertyName().equals(SelectorDisplayable.PROPERTY_POSITION)) {
207            RealTuple prev = getEarthPosition();
208            RealTuple current = getEarthPosition();
209            fireProbePositionChanged(prev, current);
210            handleProbeUpdate();
211        }
212    }
213    
214    /**
215     * Called for events happening in the {@link visad.DisplayImpl} 
216     * associated with {@link DisplayMaster}. 
217     * 
218     * <p>The only event that is actually handled is 
219     * {@link DisplayEvent#FRAME_DONE}, which allows us to snap the text 
220     * value displayable to the actual {@literal "pickable"} probe.</p>
221     * 
222     * @param e Event to handle.
223     */
224    @Override public void displayChanged(DisplayEvent e) {
225        // "snap" the text to the probe when zooming. the test for display
226        // scale values is to ensure we don't attempt to update if the zoom
227        // level didn't change.
228        if (e.getId() == DisplayEvent.FRAME_DONE) {
229            float currentScale = getDisplayScale();
230            if (Float.compare(lastScale, currentScale) != 0) {
231                handleProbeUpdate();
232                lastScale = currentScale;
233            }
234        }
235    }
236
237    /**
238     * Sets the {@link FlatField} associated with this probe to the given
239     * {@code field}.
240     *
241     * @param flatField New {@code FlatField} for this probe.
242     *
243     * @throws NullPointerException if passed a {@code null} {@code field}.
244     */
245    public void setField(final FlatField flatField) {
246        requireNonNull(flatField);
247        this.field = flatField;
248        handleProbeUpdate();
249    }
250
251    /**
252     * Adds a {@link ProbeListener} to the listener list so that it can be
253     * notified when the probe is changed.
254     * 
255     * @param listener {@code ProbeListener} to register. {@code null} 
256     * listeners are not allowed.
257     * 
258     * @throws NullPointerException if {@code listener} is null.
259     */
260    public void addProbeListener(final ProbeListener listener) {
261        requireNonNull(listener, "Can't add a null listener");
262        listeners.add(listener);
263    }
264
265    /**
266     * Removes a {@link ProbeListener} from the notification list.
267     * 
268     * @param listener {@code ProbeListener} to remove. {@code null} values
269     * are permitted, but since they are not allowed to be added...
270     */
271    public void removeProbeListener(final ProbeListener listener) {
272        listeners.remove(listener);
273    }
274
275    /**
276     * Determine whether or not a given {@link ProbeListener} is listening to
277     * the current probe.
278     *
279     * @param listener {@code ProbeListener} to check. {@code null} values are
280     * permitted.
281     *
282     * @return {@code true} if {@code listener} has been added to the list of
283     * {@code ProbeListener} objects, {@code false} otherwise.
284     */
285    public boolean hasListener(final ProbeListener listener) {
286        return listeners.contains(listener);
287    }
288
289    /**
290     * Notifies the registered {@link ProbeListener ProbeListeners} that this
291     * probe's position has changed.
292     * 
293     * @param previous Previous position. Cannot be {@code null}.
294     * @param current Current position. Cannot be {@code null}.
295     */
296    protected void fireProbePositionChanged(final RealTuple previous,
297                                            final RealTuple current)
298    {
299        requireNonNull(previous);
300        requireNonNull(current);
301
302        ProbeEvent<RealTuple> event =
303            new ProbeEvent<>(this, previous, current);
304        for (ProbeListener listener : listeners) {
305            listener.probePositionChanged(event);
306        }
307    }
308
309    /**
310     * Notifies the registered {@link ProbeListener ProbeListeners} that this
311     * probe's color has changed.
312     * 
313     * @param previous Previous color. Cannot be {@code null}.
314     * @param current Current color. Cannot be {@code null}.
315     */
316    protected void fireProbeColorChanged(final Color previous,
317                                         final Color current)
318    {
319        requireNonNull(previous);
320        requireNonNull(current);
321
322        ProbeEvent<Color> event =
323            new ProbeEvent<>(this, previous, current);
324        for (ProbeListener listener : listeners) {
325            listener.probeColorChanged(event);
326        }
327    }
328
329    /**
330     * Notifies registered {@link ProbeListener ProbeListeners} that this
331     * probe's visibility has changed. Only takes a {@literal "previous"}
332     * value, which is negated to form the {@literal "current"} value.
333     * 
334     * @param previous Visibility <b>before</b> change.
335     */
336    protected void fireProbeVisibilityChanged(final boolean previous) {
337        ProbeEvent<Boolean> event =
338            new ProbeEvent<>(this, previous, !previous);
339        for (ProbeListener listener : listeners) {
340            listener.probeVisibilityChanged(event);
341        }
342    }
343
344    /**
345     * Notifies the registered {@link ProbeListener ProbeListeners} that this
346     * probe's location format pattern has changed.
347     *
348     * @param previous Previous location format pattern.
349     * @param current Current location format pattern.
350     */
351     protected void fireProbeFormatPatternChanged(final String previous,
352                                                  final String current)
353     {
354         ProbeEvent<String> event =
355             new ProbeEvent<>(this, previous, current);
356         for (ProbeListener listener : listeners) {
357             listener.probeFormatPatternChanged(event);
358         }
359     }
360
361    /**
362     * Change the color of this {@code ReadoutProbe} instance.
363     *
364     * @param color New color. Cannot be {@code null}.
365     */
366    public void setColor(final Color color) {
367        requireNonNull(color, "Cannot set a probe to a null color");
368        setColor(color, false);
369    }
370
371    public PointSelector getPointSelector() {
372        return pointSelector;
373    }
374    
375    public TextDisplayable getValueDisplay() {
376        return valueDisplay;
377    }
378    
379    /**
380     * Change the color of this {@code ReadoutProbe} instance and control 
381     * whether or not listeners should be notified.
382     *
383     * <p>Note that if {@code color} is the same as {@code currentColor},
384     * nothing will happen (the method exits early).</p>
385     *
386     * @param color New color for this probe. Cannot be {@code null}.
387     * @param quietly Whether or not to notify the list of
388     * {@link ProbeListener ProbeListeners} of a color change.
389     */
390    private void setColor(final Color color, final boolean quietly) {
391        assert color != null;
392
393        if (currentColor.equals(color)) {
394            return;
395        }
396
397        try {
398            pointSelector.setColor(color);
399            valueDisplay.setColor(color);
400            Color prev = currentColor;
401            currentColor = color;
402
403            if (!quietly) {
404                fireProbeColorChanged(prev, currentColor);
405            }
406        } catch (Exception e) {
407            LogUtil.logException("Couldn't set the color of the probe", e);
408        }
409    }
410    
411    /**
412     * Get the current color of this {@code ReadoutProbe} instance.
413     * 
414     * @return {@code Color} of this {@code ReadoutProbe}.
415     */
416    public Color getColor() {
417        return currentColor;
418    }
419    
420    /**
421     * Get the current {@literal "readout value"} of this 
422     * {@code ReadoutProbe} instance. 
423     * 
424     * @return The value of the data at the probe's current location.
425     */
426    public String getValue() {
427        return currentValue;
428    }
429    
430    /**
431     * Get the current latitude of this {@code ReadoutProbe} instance.
432     * 
433     * @return Current latitude of the probe.
434     */
435    public double getLatitude() {
436        return currentLatitude;
437    }
438    
439    /**
440     * Get the current longitude of this {@code ReadoutProbe} instance.
441     *
442     * @return Current longitude of the probe.
443     */
444    public double getLongitude() {
445        return currentLongitude;
446    }
447
448    public void setLatLon(final double latitude, final double longitude) {
449        try {
450            EarthLocationTuple elt =
451                new EarthLocationTuple(latitude, longitude, 0.0);
452            double[] tmp =
453                ((NavigatedDisplay)master).getSpatialCoordinates(elt, null);
454            pointSelector.setPosition(tmp[0], tmp[1]);
455        } catch (Exception e) {
456            LogUtil.logException("Failed to set the pointSelector's position", e);
457        }
458    }
459
460    public void quietlySetVisible(final boolean visibility) {
461        try {
462            pointSelector.setVisible(visibility);
463            valueDisplay.setVisible(visibility);
464        } catch (Exception e) {
465            LogUtil.logException("Couldn't set the probe's internal visibility", e);
466        }
467    }
468
469    public void quietlySetColor(final Color newColor) {
470        setColor(newColor, true);
471    }
472
473    /**
474     * Update the location format pattern for the current probe.
475     *
476     * @param pattern New location format pattern. Cannot be {@code null}.
477     */
478    public void setFormatPattern(final String pattern) {
479        setFormatPattern(pattern, false);
480    }
481
482    /**
483     * Update the location format pattern for the current probe, but
484     * <b>do not</b> fire off any events.
485     *
486     * @param pattern New location format pattern. Cannot be {@code null}.
487     */
488    public void quietlySetFormatPattern(final String pattern) {
489        setFormatPattern(pattern, true);
490    }
491
492    /**
493     * Update the location format pattern for the current probe and optionally
494     * fire off an update event.
495     *
496     * @param pattern New location format pattern. Cannot be {@code null}.
497     * @param quietly Whether or not to fire a format pattern change update.
498     */
499    private void setFormatPattern(final String pattern,
500                                  final boolean quietly)
501    {
502        String previous = numFmt.toPattern();
503        numFmt.applyPattern(pattern);
504        if (!quietly) {
505            fireProbeFormatPatternChanged(previous, pattern);
506        }
507    }
508
509    /**
510     * Returns the number format string current being used.
511     *
512     * @return Location format pattern string.
513     */
514    public String getFormatPattern() {
515        return numFmt.toPattern();
516    }
517    
518    public void handleProbeUpdate() {
519        RealTuple pos = getEarthPosition();
520        if (pos == null) {
521            return;
522        }
523
524        Tuple positionValue = valueAtPosition(pos, field);
525        if (positionValue == null) {
526            return;
527        }
528
529        try {
530            valueDisplay.setData(positionValue);
531        } catch (Exception e) {
532            LogUtil.logException("Failed to set readout value", e);
533        }
534    }
535    
536    /**
537     * Called when this probe has been removed.
538     */
539    public void handleProbeRemoval() {
540        listeners.clear();
541        try {
542            master.getDisplay().removeDisplayListener(this);
543            master.removeDisplayable(valueDisplay);
544            master.removeDisplayable(pointSelector);
545        } catch (Exception e) {
546            LogUtil.logException("Problem removing visible portions of readout probe", e);
547        }
548        currentColor = null;
549        field = null;
550    }
551
552    /**
553     * Get the scaling factor for probes and such. The scaling is
554     * the parameter that gets passed to TextControl.setSize() and
555     * ShapeControl.setScale().
556     * 
557     * @return ratio of the current matrix scale factor to the
558     * saved matrix scale factor.
559     */
560    public final float getDisplayScale() {
561        float scale = 1.0f;
562        try {
563            scale = master.getDisplayScale();
564        } catch (Exception e) {
565            LogUtil.logException("Error getting display scale.", e);
566        }
567        return scale;
568    }
569
570    public void setXYPosition(final RealTuple position) {
571        if (position == null) {
572            throw new NullPointerException("cannot use a null position");
573        }
574
575        try {
576            pointSelector.setPosition(position);
577        } catch (Exception e) {
578            LogUtil.logException("Had problems setting probe's xy position", e);
579        }
580    }
581
582    public RealTuple getXYPosition() {
583        RealTuple position = null;
584        try {
585            position = pointSelector.getPosition();
586        } catch (Exception e) {
587            LogUtil.logException("Could not determine the probe's xy location", e);
588        }
589        return position;
590    }
591    
592    /**
593     * Get the current {@literal "earth location"} of the probe.
594     * 
595     * <p>Note: this method will attempt to change the {@link #currentLatitude} 
596     * and {@link #currentLongitude} fields.</p>
597     * 
598     * @return Location of {@link #pointSelector}, or {@code null} if the 
599     *         location could not be determined.
600     */
601    public EarthLocationTuple getEarthPosition() {
602        EarthLocationTuple earthTuple = null;
603        try {
604            double[] values = pointSelector.getPosition().getValues();
605            earthTuple = (EarthLocationTuple)((NavigatedDisplay)master).getEarthLocation(values[0], values[1], 1.0, true);
606            currentLatitude = earthTuple.getLatitude().getValue();
607            currentLongitude = earthTuple.getLongitude().getValue();
608        } catch (Exception e) {
609            LogUtil.logException("Could not determine the probe's earth location", e);
610        }
611        return earthTuple;
612    }
613    
614    /**
615     * Respond to the projection having been changed.
616     * 
617     * @param newProjection New projection. Can be {@code null}.
618     */
619    public void projectionChanged(MapProjection newProjection) {
620        setLatLon(currentLatitude, currentLongitude);
621        handleProbeUpdate();
622    }
623    
624    private Tuple valueAtPosition(final RealTuple position,
625                                  final FlatField imageData)
626    {
627        assert position != null : "Cannot provide a null position";
628        assert imageData != null : "Cannot provide a null image";
629        double[] values = position.getValues();
630        
631        // offset slightly so that the value readout isn't directly on top of
632        // the actual pointSelector
633        double offset = 0.5 * getDisplayScale();
634        
635        if (values[1] < -180) {
636            values[1] += 360f;
637        }
638        
639        if (values[0] > 180) {
640            values[0] -= 360f;
641        }
642        
643        Tuple positionTuple = null;
644        try {
645            // TODO(jon): do the positionFormat stuff in here. maybe this'll 
646            // have to be an instance method?
647            
648            // "corrected" is where the text should be positioned
649            RealTuple corrected = makeEarth2dTuple(values[0] + offset,
650                                                   values[1] + offset);
651            
652            // "probeLoc" is where pointSelector is positioned
653            RealTuple probeLoc = makeEarth2dTuple(values[0], values[1]);
654            
655            Real realVal = (Real)imageData.evaluate(probeLoc, Data.NEAREST_NEIGHBOR, Data.NO_ERRORS);
656            float val = (float)realVal.getValue();
657            if (Float.isNaN(val)) {
658                currentValue = "NaN";
659            } else {
660                currentValue = numFmt.format(realVal.getValue());
661            }
662            positionTuple = new Tuple(TUPTYPE, new Data[] { corrected, new Text(TextType.Generic, currentValue) });
663        } catch (Exception e) {
664            LogUtil.logException("Encountered trouble when determining value at pointSelector position", e);
665        }
666        return positionTuple;
667    }
668    
669    /**
670     * Returns a {@link RealTupleType#SpatialEarth2DTuple SpatialEarth2DTuple}
671     * for the given latitude and longitude.
672     * 
673     * <p>Be aware that for whatever reason VisAD wants the longitude first,
674     * then the latitude.</p>
675     * 
676     * @param lat Latitude of the position.
677     * @param lon Longitude of the position.
678     * 
679     * @return {@code SpatialEarth2DTuple} containing {@code lat} and 
680     *         {@code lon}.
681     *
682     * @throws VisADException Problem creating VisAD object.
683     * @throws RemoteException Java RMI error.
684     */
685    private static RealTuple makeEarth2dTuple(double lat, double lon)
686        throws VisADException, RemoteException
687    {
688        return new RealTuple(SpatialEarth2DTuple, new double[] { lon, lat });
689    }
690    
691    private static RealTuple getInitialProbePosition() {
692        RealTuple position = null;
693        try {
694            position = new RealTuple(SpatialCartesian2DTuple,
695                                     new double[] { 0.0, 0.0 });
696        } catch (Exception e) {
697            LogUtil.logException("Problem with finding an initial probe position", e);
698        }
699        return position;
700    }
701
702    private static TextDisplayable createValueDisplay(final Color color) {
703        assert color != null;
704
705        DecimalFormat fmt = new DecimalFormat();
706        fmt.setMaximumIntegerDigits(3);
707        fmt.setMaximumFractionDigits(1);
708
709        TextDisplayable td = null;
710        try {
711            td = new TextDisplayable(TextType.Generic);
712            td.setLineWidth(2f);
713            td.setColor(color);
714            td.setNumberFormat(fmt);
715        } catch (Exception e) {
716            LogUtil.logException("Problem creating readout value container", e);
717        }
718        return td;
719    }
720
721    private static TupleType makeTupleType() {
722        TupleType t = null;
723        try {
724            t = new TupleType(new MathType[] { SpatialEarth2DTuple, TextType.Generic });
725        } catch (Exception e) {
726            LogUtil.logException("Problem creating readout tuple type", e);
727        }
728        return t;
729    }
730
731    /**
732     * Returns a brief summary of a ReadoutProbe. Please note that this format
733     * is subject to change.
734     * 
735     * @return String that looks like {@code [ReadProbe@HASHCODE: color=..., 
736     * latitude=..., longitude=..., value=...]}
737     */
738    public String toString() {
739        return MakeToString.fromInstance(this)
740                           .add("color", currentColor)
741                           .add("latitude", currentLatitude)
742                           .add("longitude", currentLongitude)
743                           .add("value", currentValue).toString();
744    }
745    
746    /**
747     * This class is a reimplementation of {@link PointProbe} that whose 
748     * mouse movement is limited to the x- and y- axes.
749     * 
750     * <p>To change the position of the instance along the z-axis, try something
751     * like the following:
752     * {@code new PointSelector().setZ(zPosition)}.
753     * </p>
754     */
755    public static class PointSelector extends SelectorDisplayable {
756        
757        /** pointSelector */
758        private SelectorPoint point;
759        
760        /** flag for whether we're in the process of setting the position */
761        private volatile boolean settingPosition = false;
762        
763        /**
764         * Construct a point pointSelector.
765         *
766         * @throws VisADException Problem creating VisAD object.
767         * @throws RemoteException Java RMI error.
768         */
769        public PointSelector() throws VisADException, RemoteException {
770            this(0, 0);
771        }
772        
773        /**
774         * Construct a point pointSelector at the location specified.
775         *
776         * @param x X position.
777         * @param y Y position.
778         *
779         * @throws VisADException Problem creating VisAD object.
780         * @throws RemoteException Java RMI error.
781         */
782        public PointSelector(double x, double y) 
783            throws VisADException, RemoteException 
784        {
785            this(new RealTuple(SpatialCartesian2DTuple,
786                               new double[] { x, y }));
787        }
788    
789        /**
790         * Construct a pointSelector at the position specified.
791         *
792         * @param position Position of the pointSelector.
793         *
794         * @throws VisADException Problem creating VisAD object.
795         * @throws RemoteException Java RMI error.
796         */
797        public PointSelector(RealTuple position)
798            throws VisADException, RemoteException 
799        {
800            point = new SelectorPoint("Probe point", position);
801            
802            addDisplayable(point);
803            setPosition(position);
804            point.addAction(new ActionImpl("point listener") {
805                @Override public void doAction() {
806                    if (settingPosition) {
807                        return;
808                    }
809                    notifyListenersOfMove();
810                }
811            });
812        }
813        
814        /**
815         * Get the selector point
816         *
817         * @return the selector point
818         */
819        public SelectorPoint getSelectorPoint() {
820            return point;
821        }
822    
823        /**
824         * Set if any of the axis movements are fixed
825         *
826         * @param x x fixed
827         * @param y y fixed
828         * @param z z fixed
829         */
830        public void setFixed(boolean x, boolean y, boolean z) {
831            point.setFixed(x, y, z);
832        }
833    
834        public void setZ(double newz) {
835            try {
836                point.addConstantMap(new ConstantMap(newz, Display.ZAxis));
837            } catch (VisADException | RemoteException e) {
838                logger.error("problem setting z", e);
839            }
840        }
841        
842        /**
843         * Get the point scale
844         *
845         * @return the point scale
846         */
847        public float getPointScale() {
848            if (point != null) {
849                return point.getScale();
850            }
851            return 1.0f;
852        }
853        
854        /**
855         * Set the type of marker used for the pointSelector.
856         *
857         * @param marker  marker as a VisADGeometryArray
858         *
859         * @throws RemoteException Java RMI error
860         * @throws VisADException Problem creating VisAD object.
861         */
862        public void setMarker(VisADGeometryArray marker)
863            throws VisADException, RemoteException
864        {
865            point.setMarker(marker);
866        }
867        
868        /**
869         * Set the type of marker used for the pointSelector.
870         *
871         * @param marker {@link ucar.visad.ShapeUtility ShapeUtility} marker.
872         *
873         * @throws VisADException Problem creating VisAD object.
874         * @throws RemoteException Java RMI error.
875         */
876        public void setMarker(String marker)
877            throws VisADException, RemoteException
878        {
879            point.setMarker(marker);
880        }
881        
882        /**
883         * Set whether the marker should automatically resize as the
884         * display is zoomed.
885         *
886         * @param yesorno  true to automatically resize the marker.
887         *
888         * @throws VisADException Problem creating VisAD object.
889         * @throws RemoteException Java RMI error.
890         */
891        public void setAutoSize(boolean yesorno)
892            throws VisADException, RemoteException
893        {
894            point.setAutoSize(yesorno);
895        }
896        
897        /**
898         * Get the position of the pointSelector.
899         * 
900         * @return Current position.
901         */
902        public RealTuple getPosition() {
903            return point.getPoint();
904        }
905        
906        /**
907         * Set the pointSelector's x/y position
908         *
909         * @param x X position.
910         * @param y X position.
911         *
912         * @throws VisADException Problem creating VisAD object.
913         * @throws RemoteException Java RMI error.
914         */
915        public void setPosition(double x, double y)
916            throws VisADException, RemoteException 
917        {
918            setPosition(
919                new RealTuple(SpatialCartesian2DTuple,
920                              new double[] { x, y }));
921        }
922        
923        /**
924         * Set the pointSelector's position.
925         *
926         * @param position Position of the pointSelector
927         *
928         * @throws VisADException Problem creating VisAD object.
929         * @throws RemoteException Java RMI error.
930         */
931        public void setPosition(RealTuple position)
932            throws VisADException, RemoteException 
933        {
934            settingPosition = true;
935            try {
936                point.setPoint(position);
937            } finally {
938                settingPosition = false;
939            }
940        }
941    }
942}