001/*
002 * $Id: ReadoutProbe.java,v 1.16 2011/03/24 16:06:34 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 */
030package edu.wisc.ssec.mcidasv.probes;
031
032import static edu.wisc.ssec.mcidasv.util.Contract.*;
033
034import java.awt.Color;
035import java.beans.PropertyChangeEvent;
036import java.beans.PropertyChangeListener;
037import java.rmi.RemoteException;
038import java.text.DecimalFormat;
039import java.util.concurrent.CopyOnWriteArrayList;
040
041import ucar.unidata.collab.SharableImpl;
042import ucar.unidata.util.LogUtil;
043import ucar.unidata.view.geoloc.NavigatedDisplay;
044import ucar.visad.ShapeUtility;
045import ucar.visad.display.DisplayMaster;
046import ucar.visad.display.LineProbe;
047import ucar.visad.display.SelectorDisplayable;
048import ucar.visad.display.TextDisplayable;
049
050import visad.Data;
051import visad.FlatField;
052import visad.MathType;
053import visad.Real;
054import visad.RealTuple;
055import visad.RealTupleType;
056import visad.Text;
057import visad.TextType;
058import visad.Tuple;
059import visad.TupleType;
060import visad.VisADException;
061import visad.georef.EarthLocationTuple;
062
063public 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}