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