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 }