All Downloads are FREE. Search and download functionalities are using the official Maven repository.

us.ihmc.scs2.sessionVisualizer.jfx.plotter.Plotter2D Maven / Gradle / Ivy

package us.ihmc.scs2.sessionVisualizer.jfx.plotter;

import static us.ihmc.scs2.sessionVisualizer.jfx.yoGraphic.YoGraphicFXItem.YO_GRAPHICFX_ITEM_KEY;

import javafx.animation.AnimationTimer;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableMap;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Region;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
import javafx.util.Duration;
import us.ihmc.euclid.tuple2D.Point2D;
import us.ihmc.euclid.tuple2D.Vector2D;
import us.ihmc.scs2.sessionVisualizer.jfx.tools.JavaFXMissingTools;
import us.ihmc.scs2.sessionVisualizer.jfx.tools.JavaFXToEuclidConversions;
import us.ihmc.scs2.sessionVisualizer.jfx.yoComposite.Tuple2DProperty;
import us.ihmc.scs2.sessionVisualizer.jfx.yoGraphic.YoGraphicFXItem;
import us.ihmc.scs2.sessionVisualizer.jfx.yoGraphic.YoGroupFX;
import us.ihmc.scs2.sessionVisualizer.jfx.yoGraphic.YoLineFX2D;
import us.ihmc.scs2.sessionVisualizer.jfx.yoGraphic.YoPolygonFX2D;

public class Plotter2D extends Region
{
   private final Group root = new Group();

   private final PlotterGrid2D grid2D = new PlotterGrid2D(root.localToSceneTransformProperty());

   private final Translate rootTranslation = new Translate();
   private final Translate trackingTranslation = new Translate();
   private final ObjectProperty mouseButtonForTranslation = new SimpleObjectProperty<>(this, "mouseButtonForTranslation", MouseButton.PRIMARY);
   private final Scale rootScale = new Scale(1.0, -1.0);
   private final DoubleProperty scaleModifier = new SimpleDoubleProperty(this, "scaleModifier", -0.0025);
   private final DoubleProperty minScale = new SimpleDoubleProperty(this, "minScale", 0.01);

   private final Point2D center = new Point2D();

   private final Property coordinateToTrack = new SimpleObjectProperty<>(this, "coordinateToTrackProperty", null);
   private final AnimationTimer trackingAnimation = new AnimationTimer()
   {
      private boolean initialized = false;

      @Override
      public void start()
      {
         initialized = false;
         super.start();
      }

      @Override
      public void handle(long now)
      {
         Tuple2DProperty coordinateProperty = coordinateToTrack.getValue();
         if (coordinateProperty == null)
         {
            stop();
            return;
         }

         Point2D coordinateInWorld = coordinateProperty.toPoint2DInWorld();

         if (coordinateInWorld.containsNaN())
            return;

         if (!initialized)
         {
            rootTranslation.setX(0.5 * contentWidth());
            rootTranslation.setY(0.5 * contentHeight());
            trackingTranslation.setX(-coordinateInWorld.getX());
            trackingTranslation.setY(-coordinateInWorld.getY());
            center.set(coordinateInWorld);
            initialized = true;
         }
         else
         {
            trackingTranslation.setX(-coordinateInWorld.getX());
            trackingTranslation.setY(-coordinateInWorld.getY());
            center.set(computeCenterLocal());
         }
      }
   };

   private final ObjectProperty activeTooltip = new SimpleObjectProperty<>(this, "activeTooltipProperty", null);

   public Plotter2D()
   {
      getChildren().add(grid2D.getRoot());
      getChildren().add(root);
      root.getTransforms().addAll(rootTranslation, rootScale, trackingTranslation);

      addEventHandler(MouseEvent.ANY, createTranslationEventHandler());
      addEventHandler(ScrollEvent.ANY, createScaleEventHandler());
      requestParentLayout();
      setManaged(false);

      coordinateToTrack.addListener((o, oldValue, newValue) ->
      {
         trackingAnimation.stop();

         if (oldValue != null)
         {
            trackingTranslation.setX(0.0);
            trackingTranslation.setY(0.0);
         }

         if (newValue != null)
         {
            trackingAnimation.start();
         }
      });

      root.getChildren().addListener(new GroupTooltipHandler());

      activeTooltip.addListener((o, oldValue, newValue) ->
      {
         if (oldValue != null)
            oldValue.hide();
      });
   }

   private class GroupTooltipHandler implements ListChangeListener
   {
      private static final String TOOLTIP_LISTENER_PROPERTY_KEY = GroupTooltipHandler.class.getName() + " - TooltipListener";
      private final ObservableMap installedTooltips = FXCollections.observableHashMap();

      @Override
      public void onChanged(Change change)
      {
         while (change.next())
         {
            for (Node node : change.getAddedSubList())
            {
               installTooltip(node);
            }

            for (Node node : change.getRemoved())
            {
               Tooltip tooltip = installedTooltips.remove(node);

               if (tooltip == null)
               {
                  if (node instanceof Group)
                     ((Group) node).getChildren().removeListener(this);
               }
               else
               {
                  tooltip.textProperty().unbind();
                  Tooltip.uninstall(node, tooltip);
               }
            }
         }
      }

      public void installTooltip(Node node)
      {
         YoGraphicFXItem yoGraphicFX = (YoGraphicFXItem) node.getProperties().get(YO_GRAPHICFX_ITEM_KEY);
         if (yoGraphicFX == null || yoGraphicFX instanceof YoGroupFX)
         {
            if (node instanceof Group group)
            {
               installTooltipsToDescendants(group);
            }
         }
         else
         {
            Tooltip tooltip = new Tooltip(node.getId());
            tooltip.setShowDelay(Duration.millis(200));
            tooltip.setShowDuration(Duration.seconds(20));
            tooltip.textProperty().bind(node.idProperty());

            ChangeListener listener = (o, oldValue, newValue) ->
            {
               if (newValue)
                  activeTooltip.set(tooltip);
            };
            tooltip.getProperties().put(TOOLTIP_LISTENER_PROPERTY_KEY, listener);
            tooltip.showingProperty().addListener(listener);
            ChangeListener tooltipShowingListener = (o, oldValue, newValue) ->
            {
               if (newValue)
                  activeTooltip.set(tooltip);
            };
            tooltip.showingProperty().addListener(tooltipShowingListener);
            if (!(yoGraphicFX instanceof YoPolygonFX2D) && !(yoGraphicFX instanceof YoLineFX2D)) // The JavaFX bounding box for Polygon/Path/Line is artificially over-grown
               node.setPickOnBounds(true);
            installedTooltips.put(node, tooltip);
            Tooltip.install(node, tooltip);
         }
      }

      public void installTooltipsToDescendants(Group group)
      {
         for (Node child : group.getChildren())
         {
            installTooltip(child);
         }

         group.getChildren().addListener(this);
      }

      public void uninstallTooltip(Node node)
      {
         Tooltip tooltip = installedTooltips.remove(node);

         if (tooltip == null)
         {
            if (node instanceof Group group)
               uninstallTooltipsFromDescendants(group);
         }
         else
         {
            tooltip.textProperty().unbind();
            @SuppressWarnings("unchecked")
            ChangeListener listener = (ChangeListener) tooltip.getProperties().remove(TOOLTIP_LISTENER_PROPERTY_KEY);
            tooltip.showingProperty().removeListener(listener);
            Tooltip.uninstall(node, tooltip);
         }
      }

      public void uninstallTooltipsFromDescendants(Group group)
      {
         group.getChildren().removeListener(this);

         for (Node child : group.getChildren())
         {
            uninstallTooltip(child);
         }
      }
   }

   public Property coordinateToTrackProperty()
   {
      return coordinateToTrack;
   }

   public void setScale(double scale)
   {
      rootScale.setX(scale);
      rootScale.setY(-scale);
   }

   public void setFieldOfView(double centerX, double centerY, double rangeX, double rangeY)
   {
      setFieldOfView(centerX, centerY, rangeX, rangeY, true);
   }

   public void setFieldOfView(double centerX, double centerY, double rangeX, double rangeY, boolean axisEquals)
   {
      double contentWidth = contentWidth();
      double contentHeight = contentHeight();

      double scaleX = contentWidth / rangeX;
      double scaleY = contentHeight / rangeY;

      if (axisEquals)
      {
         double scale = Math.min(scaleX, scaleY);
         scaleX = scale;
         scaleY = scale;
      }

      rootScale.setX(scaleX);
      rootScale.setY(-scaleY);

      rootTranslation.setX(0.5 * contentWidth - centerX * rootScale.getX());
      rootTranslation.setY(0.5 * contentHeight - centerY * rootScale.getY());
      activeTooltip.set(null);
   }

   public EventHandler createTranslationEventHandler()
   {
      return new EventHandler<>()
      {
         Point2D oldMouseLocation;

         @Override
         public void handle(MouseEvent event)
         {
            if ((event.getButton() != mouseButtonForTranslation.get()) || event.isStillSincePress())
               return;

            if (event.getEventType() == MouseEvent.MOUSE_PRESSED)
            {
               oldMouseLocation = getMouseSceneLocation(event);
               return;
            }

            if (event.getEventType() != MouseEvent.MOUSE_DRAGGED)
               return;

            Point2D newMouseLocation = getMouseSceneLocation(event);

            if (oldMouseLocation != null)
            {
               Vector2D drag = new Vector2D();
               drag.sub(newMouseLocation, oldMouseLocation);
               JavaFXMissingTools.addEquals(rootTranslation, drag);
               center.set(computeCenterLocal());
               updateGrid();
            }
            oldMouseLocation = newMouseLocation;
            activeTooltip.set(null);
         }
      };
   }

   private Point2D getMouseSceneLocation(MouseEvent event)
   { // The presence of a Tooltip in the plotter appears to cause event.getX() and event.getY() to return Double.NaN
      if (Double.isNaN(event.getX()) || Double.isNaN(event.getY()))
      {
         javafx.geometry.Point2D local = root.screenToLocal(event.getScreenX(), event.getScreenY());
         javafx.geometry.Point2D scene = root.localToScene(local);
         return new Point2D(scene.getX(), scene.getY());
      }
      else
      {
         return new Point2D(event.getX(), event.getY());
      }
   }

   public EventHandler createScaleEventHandler()
   {
      return new EventHandler<>()
      {
         @Override
         public void handle(ScrollEvent event)
         {
            double verticalDrag = event.getDeltaY();
            double oldScale = rootScale.getX();
            double newScale = oldScale * (1.0 - scaleModifier.get() * verticalDrag);
            newScale = Math.max(minScale.get(), newScale);
            double trackedLocationX = event.getX();
            double trackedLocationY = event.getY();

            javafx.geometry.Point2D preScaleLocation;
            javafx.geometry.Point2D postScaleLocation;

            if (Double.isNaN(trackedLocationX) || Double.isNaN(trackedLocationY))
            {
               // Apparently this can happen with Tooltips in the plotter.
               trackedLocationX = event.getScreenX();
               trackedLocationY = event.getScreenY();
               preScaleLocation = root.screenToLocal(trackedLocationX, trackedLocationY);
               rootScale.setX(newScale);
               rootScale.setY(-newScale);
               postScaleLocation = root.screenToLocal(trackedLocationX, trackedLocationY);
            }
            else
            {
               preScaleLocation = root.sceneToLocal(trackedLocationX, trackedLocationY);
               rootScale.setX(newScale);
               rootScale.setY(-newScale);
               postScaleLocation = root.sceneToLocal(trackedLocationX, trackedLocationY);
            }

            rootTranslation.setX(rootTranslation.getX() + rootScale.getX() * (postScaleLocation.getX() - preScaleLocation.getX()));
            rootTranslation.setY(rootTranslation.getY() + rootScale.getY() * (postScaleLocation.getY() - preScaleLocation.getY()));
            activeTooltip.set(null);

            center.set(computeCenterLocal());
            updateGrid();
         }
      };
   }

   private Point2D computeCenterLocal()
   {
      return JavaFXToEuclidConversions.convertPoint2D(root.sceneToLocal(0.5 * contentWidth(), 0.5 * contentHeight()));
   }

   public void updateGrid()
   {
      grid2D.update(snappedTopInset(), snappedLeftInset(), contentWidth(), contentHeight());
   }

   private double contentWidth()
   {
      return getWidth() - (snappedLeftInset() + snappedRightInset());
   }

   private double contentHeight()
   {
      return getHeight() - (snappedTopInset() + snappedBottomInset());
   }

   @Override
   protected void layoutChildren()
   {
      recenter();
      updateGrid();
   }

   private void recenter()
   {
      Vector2D translation = new Vector2D();
      translation.sub(computeCenterLocal(), center);
      translation.scale(rootScale.getX(), rootScale.getY());
      JavaFXMissingTools.addEquals(rootTranslation, translation);
   }

   public ObjectProperty mouseButtonForTranslationProperty()
   {
      return mouseButtonForTranslation;
   }

   public Group getRoot()
   {
      return root;
   }

   public PlotterGrid2D getGrid2D()
   {
      return grid2D;
   }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy