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 }