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