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}