001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
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 https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.control;
030
031
032import java.awt.Color;
033import java.awt.event.ActionEvent;
034import java.awt.event.ActionListener;
035import java.beans.PropertyChangeEvent;
036import java.rmi.RemoteException;
037import java.text.DecimalFormat;
038import java.util.List;
039
040import javax.swing.JMenu;
041import javax.swing.JMenuItem;
042
043import visad.CoordinateSystem;
044import visad.Data;
045import visad.DataReference;
046import visad.DataReferenceImpl;
047import visad.DisplayEvent;
048import visad.DisplayListener;
049import visad.FlatField;
050import visad.FunctionType;
051import visad.MathType;
052import visad.Real;
053import visad.RealTuple;
054import visad.RealTupleType;
055import visad.RealType;
056import visad.Text;
057import visad.TextType;
058import visad.Tuple;
059import visad.TupleType;
060import visad.VisADException;
061import visad.georef.EarthLocationTuple;
062import visad.georef.LatLonPoint;
063
064import ucar.unidata.collab.Sharable;
065import ucar.unidata.data.grid.GridUtil;
066import ucar.unidata.idv.ViewDescriptor;
067import ucar.unidata.idv.control.GridDisplayControl;
068import ucar.unidata.util.GuiUtils;
069import ucar.unidata.util.LogUtil;
070import ucar.unidata.util.Misc;
071import ucar.unidata.util.TwoFacedObject;
072import ucar.unidata.view.geoloc.NavigatedDisplay;
073import ucar.visad.ShapeUtility;
074import ucar.visad.display.DisplayMaster;
075import ucar.visad.display.PointProbe;
076import ucar.visad.display.SelectorDisplayable;
077import ucar.visad.display.TextDisplayable;
078
079
080
081/**
082 * An abstract base class that manages a vertical probe
083 * To create a probe call doMakeProbe
084 * To be notified of changes override:
085 * void probePositionChanged (double x, double y);
086 *
087 * @author IDV development team
088 * @version $Revision$Date: 2011/03/24 16:06:32 $
089 */
090public class Grid2DReadoutProbe extends GridDisplayControl {
091
092    /** profile sharing property */
093    public static final String SHARE_PROFILE =
094        "LineProbeControl.SHARE_PROFILE";
095
096    /** the line probe */
097    //-protected LineProbe probe;
098    protected PointProbe probe;
099
100    /** the initial position */
101    private RealTuple initPosition;
102
103    /** The shape for the probe point */
104    private String marker;
105
106    /** The point size */
107    private float pointSize = 1.0f;
108
109    /** Keep around for the label macros */
110    protected String positionText;
111
112    private static final TupleType TUPTYPE = makeTupleType();
113
114    private DataReference positionRef = null;
115
116    private Color currentColor = Color.MAGENTA;
117
118    private RealTuple currentPosition = null;
119
120    private Tuple locationValue = null;
121
122    private TextDisplayable valueDisplay = null;
123
124    private FlatField image = null;
125
126    private RealTupleType earthTupleType = null;
127 
128    private boolean isLonLat = true;
129
130    private DisplayMaster master;
131
132    private DecimalFormat numFmt;
133
134    /**
135     * Default Constructor.
136     */
137    public Grid2DReadoutProbe(FlatField grid2d, DisplayMaster master) 
138           throws VisADException, RemoteException {
139        super();
140        earthTupleType = check2DEarthTuple(grid2d);
141        if (earthTupleType != null) {
142          isLonLat = earthTupleType.equals(RealTupleType.SpatialEarth2DTuple);
143        }
144        setAttributeFlags(FLAG_COLOR);
145        initSharable();
146
147        currentPosition = new RealTuple(RealTupleType.Generic2D);
148
149        positionRef = new DataReferenceImpl(hashCode() + "_positionRef");
150
151        valueDisplay = createValueDisplayer(currentColor);
152        this.image = grid2d;
153        this.master = master;
154
155        master.addDisplayable(valueDisplay);
156        setSharing(true);
157
158        master.getDisplay().addDisplayListener( new DisplayListener() {
159            public void displayChanged(DisplayEvent de) {
160              if (de.getId() == DisplayEvent.MOUSE_RELEASED) {
161                try {
162                  RealTuple position = getPosition();
163                  doShare(SHARE_POSITION, position);
164                } catch (Exception e) {
165                    logException("doMoveProfile", e);
166                }
167              }
168            }
169        });
170        numFmt = new DecimalFormat();
171        numFmt.setMaximumFractionDigits(2);
172    }
173
174    /**
175     * Default doMakeProbe method.
176     *
177     * @throws RemoteException  Java RMI error
178     * @throws VisADException   VisAD Error
179     */
180    public void doMakeProbe() throws VisADException, RemoteException {
181        doMakeProbe(getColor());
182    }
183
184    /**
185     * Make the probe with the specific {@code Color}.
186     *
187     * @param c  color for probe.
188     *
189     * @throws RemoteException  Java RMI error
190     * @throws VisADException   VisAD Error
191     */
192    public void doMakeProbe(Color c) throws VisADException, RemoteException {
193        //doMakeProbe(c, getDefaultViewDescriptor());
194    }
195
196
197    /**
198     * Make the probe with the specific {@code ViewDescriptor}.
199     *
200     * @param view  view descriptor
201     *
202     * @throws RemoteException  Java RMI error
203     * @throws VisADException   VisAD Error
204     */
205    public void doMakeProbe(ViewDescriptor view)
206            throws VisADException, RemoteException {
207        //doMakeProbe(getColor(), view);
208    }
209
210    /**
211     * Make the probe with the specific {@link Color} and associate it with
212     * the given {@link DisplayMaster}.
213     *
214     * @param probeColor Color of the probe.
215     * @param master {@code DisplayMaster} of the display we will be probing.
216     *
217     * @throws RemoteException  Java RMI error
218     * @throws VisADException   VisAD Error
219     */
220    //-public void doMakeProbe(Color probeColor, ViewDescriptor view)
221    public void doMakeProbe(Color probeColor, DisplayMaster master)
222            throws VisADException, RemoteException {
223        probe = null;
224        /*
225        if (getDisplayAltitudeType().equals(Display.Radius)) {
226            //      System.err.println("Probe 1");
227            probe = new LineProbe(
228                new RealTuple(
229                    RealTupleType.SpatialEarth2DTuple, new double[] { 0,
230                    0 }));
231        */
232        if (initPosition != null) {
233            //      System.err.println("Probe 2");
234            //-probe = new LineProbe(initPosition);
235            probe = new PointProbe(initPosition);
236        } else {
237            //      System.err.println("Probe 3");
238            //-probe = new LineProbe(getInitialLinePosition());
239            probe = new PointProbe(getInitialLinePosition());
240            //            probe = new LineProbe(getGridCenterPosition());
241        }
242        initPosition = probe.getPosition();
243
244        // it is a little colored cube 8 pixels across
245        probe.setColor(probeColor);
246        probe.setVisible(true);
247        probe.addPropertyChangeListener(this);
248        probe.setPointSize(1f);
249        if (marker != null) {
250            /*probe.setMarker(
251                SelectorPoint.reduce(ShapeUtility.makeShape(marker))); */
252        }
253        probe.setAutoSize(true);
254        master.addDisplayable(probe);
255    }
256
257
258    /**
259     * Handle changes
260     *
261     * @param evt The event
262     */
263    public void propertyChange(PropertyChangeEvent evt) {
264        if (evt.getPropertyName().equals(
265                SelectorDisplayable.PROPERTY_POSITION)) {
266            doMoveProbe();
267        } else {
268            super.propertyChange(evt);
269        }
270    }
271
272    /**
273     * Reset the position of the probe to the center.
274     */
275    public void resetProbePosition() {
276        try {
277            setProbePosition(0.0, 0.0);
278        } catch (Exception exc) {
279            logException("Resetting probe position", exc);
280        }
281    }
282
283
284    /**
285     * Get edit menu items
286     *
287     * @param items      list of menu items
288     * @param forMenuBar  true if for the menu bar
289     */
290    protected void getEditMenuItems(List items, boolean forMenuBar) {
291        if (probe != null) {
292            JMenuItem mi = new JMenuItem("Reset Probe Position");
293            mi.addActionListener(new ActionListener() {
294                public void actionPerformed(ActionEvent ae) {
295                    resetProbePosition();
296                }
297            });
298            items.add(mi);
299        }
300        super.getEditMenuItems(items, forMenuBar);
301    }
302
303    /**
304     * Set the probe position.  Probes are set in XY space.
305     *
306     * @param xy  X and Y position of the probe.
307     *
308     * @throws VisADException  problem setting probe position
309     * @throws RemoteException  problem setting probe position on remote display
310     */
311    public void setProbePosition(RealTuple xy)
312            throws VisADException, RemoteException {
313        probe.setPosition(xy);
314    }
315
316    /**
317     * Set the probe position from display x and y positions.
318     *
319     * @param x    X position of the probe.
320     * @param y    Y position of the probe.
321     *
322     * @throws VisADException  problem setting probe position
323     * @throws RemoteException  problem setting probe position on remote display
324     */
325    public void setProbePosition(double x, double y)
326            throws VisADException, RemoteException {
327        setProbePosition(new RealTuple(new Real[] {
328            new Real(RealType.XAxis, x),
329            new Real(RealType.YAxis, y) }));
330    }
331
332    /**
333     * Set the initial position of the probe.  This is used by the
334     * XML persistense.
335     *
336     * @param p  position
337     */
338    public void setPosition(RealTuple p) {
339        initPosition = p;
340    }
341
342    /**
343     * Get the position of the probe.  This is used by the
344     * XML persistense.
345     *
346     * @return current probe position or null if probe has not been created.
347     *
348     * @throws RemoteException  Java RMI error
349     * @throws VisADException   VisAD Error
350     */
351    public RealTuple getPosition() throws VisADException, RemoteException {
352        return ((probe != null)
353                ? probe.getPosition()
354                : null);
355    }
356
357    /**
358     * Get the initial position of the probe set during unpersistence.
359     *
360     * @return  initial position or {@code null} if not set during
361     *          initialization.
362     */
363    public RealTuple getInitialPosition() {
364        return initPosition;
365    }
366
367    /**
368     * Method called when sharing is enabled.
369     *
370     * @param from  Sharable that send the data.
371     * @param dataId  identifier for data to be shared
372     * @param data   data to be shared.
373     */
374    public void receiveShareData(Sharable from, Object dataId,
375                                 Object[] data) {
376        if (dataId.equals(SHARE_POSITION)) {
377            if (probe == null) {
378                return;
379            }
380            try {
381                probe.setPosition((RealTuple) data[0]);
382                probePositionChanged(getPosition());
383            } catch (Exception e) {
384                logException("receiveShareData:" + dataId, e);
385            }
386            return;
387        }
388        super.receiveShareData(from, dataId, data);
389    }
390
391
392    /**
393     * Method called when probe is moved.
394     */
395    protected void doMoveProbe() {
396        try {
397            RealTuple position = getPosition();
398            probePositionChanged(position);
399            //-doShare(SHARE_POSITION, position);
400        } catch (Exception e) {
401            logException("doMoveProfile", e);
402        }
403    }
404
405    /**
406     * This gets called when either the user moves the probe point or
407     * when we get a sharable event to move the probe point. Subclasses
408     * need to implement this.
409     *
410     * @param newPos New position for the probe.
411     */
412    protected void probePositionChanged(final RealTuple newPos) {
413        if (!currentPosition.equals(newPos)) {
414            updatePosition(newPos);
415            updateLocationValue();
416            currentPosition = newPos;
417        }
418    }
419
420    protected void updatePosition(final RealTuple position) {
421        double[] vals = position.getValues();
422        try {
423            EarthLocationTuple elt = (EarthLocationTuple)boxToEarth(
424                new double[] { vals[0], vals[1], 1.0 });
425
426            positionRef.setData(elt.getLatLonPoint());
427        } catch (Exception e) {
428            LogUtil.logException("HydraImageProbe.updatePosition", e);
429        }
430    }
431
432    private void updateLocationValue() {
433        Tuple tup = null;
434        RealTuple earthTuple;
435        
436
437        try {
438            RealTuple location = (RealTuple)positionRef.getData();
439          
440            if (location == null) {
441                return;
442            }
443
444            if (image == null) {
445                return;
446            }
447
448            double[] vals = location.getValues();
449            if (vals[1] < -180) {
450                vals[1] += 360f;
451            }
452
453            if (vals[1] > 180) {
454                vals[1] -= 360f;
455            }
456
457            if (earthTupleType != null) {
458              RealTuple lonLat =
459                 new RealTuple(RealTupleType.SpatialEarth2DTuple,
460                     new double[] { vals[1], vals[0] });
461              RealTuple latLon = new RealTuple(RealTupleType.LatitudeLongitudeTuple,
462                     new double[] { vals[0], vals[1] });
463              RealTuple rtup = lonLat;
464              if (!(isLonLat)) {
465                rtup = latLon;
466              }
467           
468              Real val = null;
469              Data dat = image.evaluate(rtup, Data.NEAREST_NEIGHBOR, Data.NO_ERRORS);
470
471              if ( ((FunctionType)image.getType()).getRange() instanceof RealTupleType ) { 
472                RealTuple tmp = (RealTuple)dat;
473                val = (tmp.getRealComponents())[0];
474              } else {
475                val = (Real)dat;
476              }
477              float fval = (float)val.getValue();
478
479              tup = new Tuple(TUPTYPE,
480                        new Data[] { lonLat, new Text(TextType.Generic, numFmt.format(fval)) });
481            }
482
483            valueDisplay.setData(tup);
484        } catch (Exception e) {
485            LogUtil.logException("HydraImageProbe.updateLocationValue", e);
486        }
487
488        if (tup != null) {
489            locationValue = tup;
490        }
491    }
492
493    public NavigatedDisplay  getNavigatedDisplay() {
494        return (NavigatedDisplay)master;
495    }
496
497    public static RealTupleType check2DEarthTuple(FlatField field) {
498        CoordinateSystem cs;
499        FunctionType ftype = (FunctionType) field.getType();
500        RealTupleType domain = ftype.getDomain();
501        if ( (domain.equals(RealTupleType.SpatialEarth2DTuple)) ||
502            (domain.equals(RealTupleType.LatitudeLongitudeTuple)) ) {
503            return domain;
504        } else if ((cs = domain.getCoordinateSystem()) != null) {
505            RealTupleType ref = cs.getReference();
506            if (ref.equals(RealTupleType.SpatialEarth2DTuple) ||
507                ref.equals(RealTupleType.LatitudeLongitudeTuple)) {
508                return ref;
509            }
510        }
511        return null;
512    }
513
514    private static TextDisplayable createValueDisplayer(final Color color)
515        throws VisADException, RemoteException
516    {
517        DecimalFormat fmt = new DecimalFormat();
518        fmt.setMaximumIntegerDigits(3);
519        fmt.setMaximumFractionDigits(1);
520        TextDisplayable td = new TextDisplayable(TextType.Generic);
521        td.setLineWidth(2f);
522        td.setColor(color);
523        td.setTextSize(1.75f);
524        return td;
525    }
526
527    private static TupleType makeTupleType() {
528        TupleType t = null;
529        try {
530            t = new TupleType(new MathType[] {RealTupleType.SpatialEarth2DTuple,
531                                              TextType.Generic});
532        } catch (Exception e) {
533            LogUtil.logException("HydraImageProbe.makeTupleType", e);
534        }
535        return t;
536    }
537
538    /**
539     * Respond to a change in the display's projection.  In this case
540     * we fire the probePositionChanged() method with the probe's
541     * position.
542     */
543    public void projectionChanged() {
544        super.projectionChanged();
545        try {
546            probePositionChanged(getPosition());
547        } catch (Exception exc) {
548            logException("projectionChanged", exc);
549        }
550    }
551
552    /**
553     * Make a menu for controlling the probe size, shape and position.
554     *
555     * @param probeMenu The menu to add to
556     *
557     * @return The menu
558     */
559    public JMenu doMakeProbeMenu(JMenu probeMenu) {
560        JMenu posMenu = new JMenu("Position");
561        probeMenu.add(posMenu);
562        posMenu.add(GuiUtils.makeMenuItem("Reset Probe Position", this,
563                                          "resetProbePosition"));
564
565        JMenu sizeMenu = new JMenu("Size");
566        probeMenu.add(sizeMenu);
567
568        sizeMenu.add(GuiUtils.makeMenuItem("Increase", this,
569                                           "increaseProbeSize"));
570        sizeMenu.add(GuiUtils.makeMenuItem("Decrease", this,
571                                           "decreaseProbeSize"));
572
573        JMenu shapeMenu = new JMenu("Probe Shape");
574        probeMenu.add(shapeMenu);
575        for (int i = 0; i < ShapeUtility.SHAPES.length; i++) {
576            TwoFacedObject tof = ShapeUtility.SHAPES[i];
577            String         lbl = tof.toString();
578            if (Misc.equals(tof.getId(), marker)) {
579                lbl = ">" + lbl;
580            }
581            JMenuItem mi = GuiUtils.makeMenuItem(lbl, this, "setMarker",
582                               tof.getId());
583            shapeMenu.add(mi);
584        }
585        GuiUtils.limitMenuSize(shapeMenu, "Shape Group ", 10);
586        return probeMenu;
587    }
588
589    /**
590     * Increase the probe size
591     */
592    public void increaseProbeSize() {
593        if (probe == null) {
594            return;
595        }
596        pointSize = probe.getPointScale();
597        setPointSize(pointSize + pointSize * 0.5f);
598    }
599
600
601    /**
602     * Decrease the probe size
603     */
604    public void decreaseProbeSize() {
605        if (probe == null) {
606            return;
607        }
608        pointSize = probe.getPointScale();
609        pointSize = pointSize - pointSize * 0.5f;
610        if (pointSize < 0.1f) {
611            pointSize = 0.1f;
612        }
613        setPointSize(pointSize);
614    }
615
616
617    /**
618     *  Set the PointSize property.
619     *
620     *  @param value The new value for PointSize
621     */
622    public void setPointSize(float value) {
623        pointSize = value;
624        if (probe != null) {
625            try {
626                probe.setAutoSize(false);
627                probe.setPointSize(pointSize);
628                probe.setAutoSize(true);
629            } catch (Exception exc) {
630                logException("Increasing probe size", exc);
631            }
632        }
633    }
634
635    /**
636     *  Get the PointSize property.
637     *
638     *  @return The PointSize
639     */
640    public float getPointSize() {
641        return pointSize;
642    }
643
644
645    /**
646     * Get initial XY position from grid data.
647     *
648     * @return initial XY position of grid center point in VisAD space
649     *
650     * @throws RemoteException Java RMI problem
651     * @throws VisADException VisAD problem
652     */
653    public RealTuple getGridCenterPosition()
654            throws VisADException, RemoteException {
655        RealTuple pos = new RealTuple(RealTupleType.SpatialCartesian2DTuple,
656                                      new double[] { 0,
657                0 });
658        if (getGridDataInstance() != null) {
659            LatLonPoint rt = GridUtil.getCenterLatLonPoint(
660                                 getGridDataInstance().getGrid());
661            RealTuple xyz = earthToBoxTuple(new EarthLocationTuple(rt,
662                                new Real(RealType.Altitude, 0)));
663            if (xyz != null) {
664                pos = new RealTuple(new Real[] { (Real) xyz.getComponent(0),
665                        (Real) xyz.getComponent(1) });
666            }
667        }
668        return pos;
669    }
670
671
672    /**
673     * Get initial XY position from the screen
674     *
675     * @return initial XY position  in VisAD space
676     *
677     * @throws RemoteException Java RMI problem
678     * @throws VisADException VisAD problem
679     */
680    public RealTuple getInitialLinePosition()
681            throws VisADException, RemoteException {
682        //-double[] center = getScreenCenter();
683        double[] center = new double[] {0,0};
684        return new RealTuple(RealTupleType.SpatialCartesian2DTuple,
685                             new double[] { center[0],
686                                            center[1] });
687    }
688
689    /**
690     * Set the Marker property.
691     *
692     * @param value The new value for Marker
693     */
694    public void setMarker(String value) {
695        marker = value;
696        if ((probe != null) && (marker != null)) {
697            try {
698                probe.setAutoSize(false);
699                probe.setAutoSize(true);
700            } catch (Exception exc) {
701                logException("Setting marker", exc);
702            }
703        }
704    }
705
706    /**
707     * Get the Marker property.
708     *
709     * @return The Marker
710     */
711    public String getMarker() {
712        return marker;
713    }
714
715    /**
716     * Add any macro name/label pairs
717     *
718     * @param names List of macro names
719     * @param labels List of macro labels
720     */
721    protected void getMacroNames(List names, List labels) {
722        super.getMacroNames(names, labels);
723        names.addAll(Misc.newList(MACRO_POSITION));
724        labels.addAll(Misc.newList("Probe Position"));
725    }
726
727    /**
728     * Add any macro name/value pairs.
729     *
730     *
731     * @param template template
732     * @param patterns The macro names
733     * @param values The macro values
734     */
735    protected void addLabelMacros(String template, List patterns,
736                                  List values) {
737        super.addLabelMacros(template, patterns, values);
738        patterns.add(MACRO_POSITION);
739        values.add(positionText);
740    }
741
742    /**
743     * This method is called  to update the legend labels when
744     * some state has changed in this control that is reflected in the labels.
745     */
746    protected void updateLegendLabel() {
747        super.updateLegendLabel();
748        // if the display label has the position, we'll update the list also
749        String template = getDisplayListTemplate();
750        if (template.contains(MACRO_POSITION)) {
751            updateDisplayList();
752        }
753    }
754
755    /**
756     * Append any label information to the list of labels.
757     *
758     * @param labels   in/out list of labels
759     * @param legendType The type of legend, BOTTOM_LEGEND or SIDE_LEGEND
760     */
761    public void getLegendLabels(List labels, int legendType) {
762        super.getLegendLabels(labels, legendType);
763        labels.add(positionText);
764    }
765}
766