001    /*
002     * $Id: ReadoutProbe.java,v 1.17 2012/02/19 17:35:48 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    package edu.wisc.ssec.mcidasv.probes;
031    
032    import static edu.wisc.ssec.mcidasv.util.Contract.*;
033    
034    import java.awt.Color;
035    import java.beans.PropertyChangeEvent;
036    import java.beans.PropertyChangeListener;
037    import java.rmi.RemoteException;
038    import java.text.DecimalFormat;
039    import java.util.concurrent.CopyOnWriteArrayList;
040    
041    import ucar.unidata.collab.SharableImpl;
042    import ucar.unidata.util.LogUtil;
043    import ucar.unidata.view.geoloc.NavigatedDisplay;
044    import ucar.visad.ShapeUtility;
045    import ucar.visad.display.DisplayMaster;
046    import ucar.visad.display.LineProbe;
047    import ucar.visad.display.SelectorDisplayable;
048    import ucar.visad.display.TextDisplayable;
049    
050    import visad.Data;
051    import visad.FlatField;
052    import visad.MathType;
053    import visad.Real;
054    import visad.RealTuple;
055    import visad.RealTupleType;
056    import visad.Text;
057    import visad.TextType;
058    import visad.Tuple;
059    import visad.TupleType;
060    import visad.VisADException;
061    import visad.georef.EarthLocationTuple;
062    
063    public class ReadoutProbe extends SharableImpl implements PropertyChangeListener {
064    
065        public static final String SHARE_PROFILE = "ReadoutProbeDeux.SHARE_PROFILE";
066    
067        public static final String SHARE_POSITION = "ReadoutProbeDeux.SHARE_POSITION";
068    
069        private static final Color DEFAULT_COLOR = Color.MAGENTA;
070    
071        private static final TupleType TUPTYPE = makeTupleType();
072    
073        private final CopyOnWriteArrayList<ProbeListener> listeners = 
074            new CopyOnWriteArrayList<ProbeListener>();
075    
076        /** Displays the value of the data at the current position. */
077        private final TextDisplayable valueDisplay = createValueDisplay(DEFAULT_COLOR);
078    
079        private final LineProbe probe = new LineProbe(getInitialLinePosition());
080    
081        private final DisplayMaster master;
082    
083        private Color currentColor = DEFAULT_COLOR;
084    
085        private String currentValue = "NaN";
086    
087        private double currentLatitude = Double.NaN;
088        private double currentLongitude = Double.NaN;
089    
090        private float pointSize = 1.0f;
091    
092        private FlatField field;
093    
094        private static final DecimalFormat numFmt = new DecimalFormat();
095    
096        private RealTuple prevPos = null;
097    
098        public ReadoutProbe(final DisplayMaster master, final FlatField field, final Color color, final boolean visible) throws VisADException, RemoteException {
099            super();
100            notNull(master, "DisplayMaster can't be null");
101            notNull(field, "Field can't be null");
102            notNull(color, "Color can't be null");
103    
104            this.master = master;
105            this.field = field;
106    
107            initSharable();
108    
109            probe.setColor(color);
110            valueDisplay.setVisible(visible);
111            valueDisplay.setColor(color);
112            currentColor = color;
113            probe.setVisible(visible);
114            probe.setPointSize(pointSize);
115            probe.setAutoSize(true);
116            probe.addPropertyChangeListener(this);
117            probe.setPointSize(getDisplayScale());
118    
119            numFmt.setMaximumFractionDigits(2);
120    
121            master.addDisplayable(valueDisplay);
122            master.addDisplayable(probe);
123            setField(field);
124        }
125    
126        /**
127         * Called whenever the probe fires off a {@link PropertyChangeEvent}. Only
128         * handles position changes right now, all other events are discarded.
129         *
130         * @param e Object that describes the property change.
131         * 
132         * @throws NullPointerException if passed a {@code null} 
133         * {@code PropertyChangeEvent}.
134         */
135        public void propertyChange(final PropertyChangeEvent e) {
136            notNull(e, "Cannot handle a null property change event");
137            if (e.getPropertyName().equals(SelectorDisplayable.PROPERTY_POSITION)) {
138                RealTuple prev = getEarthPosition();
139                //handleProbeUpdate();
140                RealTuple current = getEarthPosition();
141                if (prevPos != null) {
142                  fireProbePositionChanged(prev, current);
143                  handleProbeUpdate();
144                }
145                prevPos = current;
146                //fireProbePositionChanged(prev, current);
147            }
148        }
149    
150        public void setField(final FlatField field) {
151            notNull(field);
152            this.field = field;
153            handleProbeUpdate();
154        }
155    
156        /**
157         * Adds a {@link ProbeListener} to the listener list so that it can be
158         * notified when the probe is changed.
159         * 
160         * @param listener {@code ProbeListener} to register. {@code null} 
161         * listeners are not allowed.
162         * 
163         * @throws NullPointerException if {@code listener} is null.
164         */
165        public void addProbeListener(final ProbeListener listener) {
166            notNull(listener, "Can't add a null listener");
167            listeners.add(listener);
168        }
169    
170        /**
171         * Removes a {@link ProbeListener} from the notification list.
172         * 
173         * @param listener {@code ProbeListener} to remove. {@code null} values
174         * are permitted, but since they are not allowed to be added...
175         */
176        public void removeProbeListener(final ProbeListener listener) {
177            listeners.remove(listener);
178        }
179    
180        public boolean hasListener(final ProbeListener listener) {
181            return listeners.contains(listener);
182        }
183    
184        /**
185         * Notifies the registered {@link ProbeListener}s that this probe's 
186         * position has changed.
187         * 
188         * @param previous Previous position.
189         * @param current Current position.
190         */
191        protected void fireProbePositionChanged(final RealTuple previous, final RealTuple current) {
192            notNull(previous);
193            notNull(current);
194    
195            ProbeEvent<RealTuple> event = new ProbeEvent<RealTuple>(this, previous, current);
196            for (ProbeListener listener : listeners)
197                listener.probePositionChanged(event);
198        }
199    
200        /**
201         * Notifies the registered {@link ProbeListener}s that this probe's color
202         * has changed.
203         * 
204         * @param previous Previous color.
205         * @param current Current color.
206         */
207        protected void fireProbeColorChanged(final Color previous, final Color current) {
208            notNull(previous);
209            notNull(current);
210    
211            ProbeEvent<Color> event = new ProbeEvent<Color>(this, previous, current);
212            for (ProbeListener listener : listeners)
213                listener.probeColorChanged(event);
214        }
215    
216        /**
217         * Notifies registered {@link ProbeListener}s that this probe's visibility
218         * has changed. Only takes a {@literal "previous"} value, which is negated
219         * to form the {@literal "current"} value.
220         * 
221         * @param previous Visibility <b>before</b> change.
222         */
223        protected void fireProbeVisibilityChanged(final boolean previous) {
224            ProbeEvent<Boolean> event = new ProbeEvent<Boolean>(this, previous, !previous);
225            for (ProbeListener listener : listeners)
226                listener.probeVisibilityChanged(event);
227        }
228    
229        public void setColor(final Color color) {
230            notNull(color, "Cannot set a probe to a null color");
231            setColor(color, false);
232        }
233    
234        private void setColor(final Color color, final boolean quietly) {
235            assert color != null;
236    
237            if (currentColor.equals(color))
238                return;
239    
240            try {
241                probe.setColor(color);
242                valueDisplay.setColor(color);
243                Color prev = currentColor;
244                currentColor = color;
245    
246                if (!quietly)
247                    fireProbeColorChanged(prev, currentColor);
248            } catch (Exception e) {
249                LogUtil.logException("Couldn't set the color of the probe", e);
250            }
251        }
252    
253        public Color getColor() {
254            return currentColor;
255        }
256    
257        public String getValue() {
258            return currentValue;
259        }
260    
261        public double getLatitude() {
262            return currentLatitude;
263        }
264    
265        public double getLongitude() {
266            return currentLongitude;
267        }
268    
269        public void setLatLon(final Double latitude, final Double longitude) {
270            notNull(latitude, "Null latitude values don't make sense!");
271            notNull(longitude, "Null longitude values don't make sense!");
272    
273            try {
274                EarthLocationTuple elt = new EarthLocationTuple(latitude, longitude, 0.0);
275                double[] tmp = ((NavigatedDisplay)master).getSpatialCoordinates(elt, null);
276                probe.setPosition(tmp[0], tmp[1]);
277            } catch (Exception e) {
278                LogUtil.logException("Failed to set the probe's position", e);
279            }
280        }
281    
282        public void quietlySetVisible(final boolean visibility) {
283            try {
284                probe.setVisible(visibility);
285                valueDisplay.setVisible(visibility);
286            } catch (Exception e) {
287                LogUtil.logException("Couldn't set the probe's internal visibility", e);
288            }
289        }
290    
291        public void quietlySetColor(final Color newColor) {
292            setColor(newColor, true);
293        }
294    
295        public void handleProbeUpdate() {
296            RealTuple pos = getEarthPosition();
297            if (pos == null)
298                return;
299    
300            Tuple positionValue = valueAtPosition(pos, field);
301            if (positionValue == null)
302                return;
303    
304            try {
305                valueDisplay.setData(positionValue);
306            } catch (Exception e) {
307                LogUtil.logException("Failed to set readout value", e);
308            }
309        }
310    
311        public void handleProbeRemoval() {
312            listeners.clear();
313            try {
314                master.removeDisplayable(valueDisplay);
315                master.removeDisplayable(probe);
316            } catch (Exception e) {
317                LogUtil.logException("Problem removing visible portions of readout probe", e);
318            }
319            currentColor = null;
320            field = null;
321        }
322    
323        /**
324         * Get the scaling factor for probes and such. The scaling is
325         * the parameter that gets passed to TextControl.setSize() and
326         * ShapeControl.setScale().
327         * 
328         * @return ratio of the current matrix scale factor to the
329         * saved matrix scale factor.
330         */
331        public float getDisplayScale() {
332            float scale = 1.0f;
333            try {
334                scale = master.getDisplayScale();
335            } catch (Exception e) {
336                System.err.println("Error getting display scale: "+e);
337            }
338            return scale;
339        }
340    
341        public void setXYPosition(final RealTuple position) {
342            if (position == null)
343                throw new NullPointerException("cannot use a null position");
344    
345            try {
346                probe.setPosition(position);
347            } catch (Exception e) {
348                LogUtil.logException("Had problems setting probe's xy position", e);
349            }
350        }
351    
352        public RealTuple getXYPosition() {
353            RealTuple position = null;
354            try {
355                position = probe.getPosition();
356            } catch (Exception e) {
357                LogUtil.logException("Could not determine the probe's xy location", e);
358            }
359            return position;
360        }
361    
362        public EarthLocationTuple getEarthPosition() {
363            EarthLocationTuple earthTuple = null;
364            try {
365                double[] values = probe.getPosition().getValues();
366                earthTuple = (EarthLocationTuple)((NavigatedDisplay)master).getEarthLocation(values[0], values[1], 1.0, true);
367                currentLatitude = earthTuple.getLatitude().getValue();
368                currentLongitude = earthTuple.getLongitude().getValue();
369            } catch (Exception e) {
370                LogUtil.logException("Could not determine the probe's earth location", e);
371            }
372            return earthTuple;
373        }
374    
375        private Tuple valueAtPosition(final RealTuple position, final FlatField imageData) {
376            assert position != null : "Cannot provide a null position";
377            assert imageData != null : "Cannot provide a null image";
378    
379            double[] values = position.getValues();
380            if (values[1] < -180)
381                values[1] += 360f;
382    
383            if (values[0] > 180)
384                values[0] -= 360f;
385    
386            Tuple positionTuple = null;
387            try {
388                // TODO(jon): do the positionFormat stuff in here. maybe this'll 
389                // have to be an instance method?
390                RealTuple corrected = new RealTuple(RealTupleType.SpatialEarth2DTuple, new double[] { values[1], values[0] });
391    
392                Real realVal = (Real)imageData.evaluate(corrected, Data.NEAREST_NEIGHBOR, Data.NO_ERRORS);
393                float val = (float)realVal.getValue();
394                if (Float.isNaN(val))
395                    currentValue = "NaN";
396                else
397                    currentValue = numFmt.format(realVal.getValue());
398    
399                positionTuple = new Tuple(TUPTYPE, new Data[] { corrected, new Text(TextType.Generic, currentValue) });
400            } catch (Exception e) {
401                LogUtil.logException("Encountered trouble when determining value at probe position", e);
402            }
403            return positionTuple;
404        }
405    
406        private static RealTuple getInitialLinePosition() {
407            RealTuple position = null;
408            try {
409                double[] center = new double[] { 0.0, 0.0 };
410                position = new RealTuple(RealTupleType.SpatialCartesian2DTuple, 
411                        new double[] { center[0], center[1] });
412            } catch (Exception e) {
413                LogUtil.logException("Problem with finding an initial probe position", e);
414            }
415            return position;
416        }
417    
418        private static TextDisplayable createValueDisplay(final Color color) {
419            assert color != null;
420    
421            DecimalFormat fmt = new DecimalFormat();
422            fmt.setMaximumIntegerDigits(3);
423            fmt.setMaximumFractionDigits(1);
424    
425            TextDisplayable td = null;
426            try {
427                td = new TextDisplayable(TextType.Generic);
428                td.setLineWidth(2f);
429                td.setColor(color);
430                td.setNumberFormat(fmt);
431            } catch (Exception e) {
432                LogUtil.logException("Problem creating readout value container", e);
433            }
434            return td;
435        }
436    
437        private static TupleType makeTupleType() {
438            TupleType t = null;
439            try {
440                t = new TupleType(new MathType[] { RealTupleType.SpatialEarth2DTuple, TextType.Generic });
441            } catch (Exception e) {
442                LogUtil.logException("Problem creating readout tuple type", e);
443            }
444            return t;
445        }
446    
447        /**
448         * Returns a brief summary of a ReadoutProbe. Please note that this format
449         * is subject to change.
450         * 
451         * @return String that looks like {@code [ReadProbe@HASHCODE: color=..., 
452         * latitude=..., longitude=..., value=...]}
453         */
454        public String toString() {
455            return String.format("[ReadoutProbe@%x: color=%s, latitude=%s, longitude=%s, value=%f]", 
456                hashCode(), getColor(), getLatitude(), getLongitude(), getValue());
457        }
458    }