/** * Copyright (c) 2017 European Organisation for Nuclear Research (CERN), All Rights Reserved. */ package de.gsi.chart.plugins; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.scene.Cursor; import javafx.scene.paint.Color; import javafx.scene.shape.Line; import javafx.scene.shape.Polygon; import de.gsi.chart.axes.Axis; import de.gsi.dataset.event.EventListener; import de.gsi.dataset.event.EventSource; import de.gsi.dataset.event.UpdateEvent; /** * Plugin indicating a specific X or Y value as a line drawn on the plot area, with an optional {@link #textProperty() * text label} describing the value. * * @author mhrabia */ public abstract class AbstractSingleValueIndicator extends AbstractValueIndicator implements EventSource, ValueIndicator { /** * The default distance between the data point coordinates and mouse cursor that triggers shifting the line. */ protected static final int DEFAULT_PICKING_DISTANCE = 30; protected static final double MIDDLE_POSITION = 0.5; protected static final String STYLE_CLASS_LABEL = "value-indicator-label"; protected static final String STYLE_CLASS_LINE = "value-indicator-line"; protected static final String STYLE_CLASS_MARKER = "value-indicator-marker"; protected static double triangleHalfWidth = 5.0; private final transient AtomicBoolean autoNotification = new AtomicBoolean(true); private final transient List<EventListener> updateListeners = Collections.synchronizedList(new LinkedList<>()); private boolean autoRemove = false; /** * Line indicating the value. */ protected final Line line = new Line(); protected final Line pickLine = new Line(); /** * small triangle marker as handler to shift the line marker */ protected final Polygon triangle = new Polygon(); private final DoubleProperty pickingDistance = new SimpleDoubleProperty(this, "pickingDistance", DEFAULT_PICKING_DISTANCE) { @Override protected void invalidated() { if (get() <= 0) { throw new IllegalArgumentException("The " + getName() + " must be a positive value"); } } }; private final DoubleProperty value = new SimpleDoubleProperty(this, "value") { @Override protected void invalidated() { layoutChildren(); } }; private final DoubleProperty labelPosition = new SimpleDoubleProperty(this, "labelPosition", 0.5) { @Override protected void invalidated() { if (get() < 0 || get() > 1) { throw new IllegalArgumentException("labelPosition must be in rage [0,1]"); } layoutChildren(); } }; /** * Creates a new instance of AbstractSingleValueIndicator. * * @param axis reference axis * @param value a X value to be indicated * @param text the text to be shown by the label. Value of {@link #textProperty()}. */ protected AbstractSingleValueIndicator(Axis axis, final double value, final String text) { super(axis, text); setValue(value); initLine(); initTriangle(); editableIndicatorProperty().addListener((ch, o, n) -> updateMouseListener(n)); updateMouseListener(isEditable()); // Need to add them so that at initialization of the stage the CCS is // applied and we can calculate label's // width and height getChartChildren().addAll(line, triangle, label); this.value.addListener( (ch, o, n) -> invokeListener(new UpdateEvent(this, "value changed to " + n + " for axis " + axis))); } @Override public AtomicBoolean autoNotification() { return autoNotification; } /** * Returns the value of the {@link #labelPositionProperty()}. * * @return the relative position of the {@link #textProperty() text label} */ public final double getLabelPosition() { return labelPositionProperty().get(); } /** * Returns the value of the {@link #pickingDistanceProperty()}. * * @return the current picking distance */ public final double getPickingDistance() { return pickingDistanceProperty().get(); } /** * Returns the indicated value. * * @return indicated value */ @Override public final double getValue() { return valueProperty().get(); } private void initLine() { // mouse transparent if not editable line.setMouseTransparent(true); pickLine.setPickOnBounds(true); pickLine.setStroke(Color.TRANSPARENT); pickLine.setStrokeWidth(getPickingDistance()); pickLine.mouseTransparentProperty().bind(editableIndicatorProperty().not()); pickLine.setOnMousePressed(mouseEvent -> { /* * Record a delta distance for the drag and drop operation. Because layoutLine() sets the start/end points * we have to use these here. It is enough to use the start point. For X indicators, start x and end x are * identical and for Y indicators start y and end y are identical. */ dragDelta.x = pickLine.getStartX() - mouseEvent.getX(); dragDelta.y = pickLine.getStartY() - mouseEvent.getY(); pickLine.setCursor(Cursor.MOVE); mouseEvent.consume(); }); } private void initTriangle() { triangle.visibleProperty().bind(editableIndicatorProperty()); triangle.mouseTransparentProperty().bind(editableIndicatorProperty().not()); triangle.setPickOnBounds(true); final double a = AbstractSingleValueIndicator.triangleHalfWidth; triangle.getPoints().setAll(-a, -a, -a, +a, +a, +a, +a, -a); triangle.setOnMousePressed(mouseEvent -> { /* * Record a delta distance for the drag and drop operation. Because the whole node is translated in * layoutMarker we use the layout position here. */ dragDelta.x = triangle.getLayoutX() - mouseEvent.getX(); dragDelta.y = triangle.getLayoutY() - mouseEvent.getY(); triangle.setCursor(Cursor.MOVE); mouseEvent.consume(); }); } /** * @return {@code true} indicator should be removed if there is no listener attached to it */ public boolean isAutoRemove() { return autoRemove; } /** * Relative position, between 0.0 (left, bottom) and 1.0 (right, top) of the description {@link #textProperty() * label} in the plot area. * <p> * <b>Default value: 0.5</b> * </p> * * @return labelPosition property */ public final DoubleProperty labelPositionProperty() { return labelPosition; } /** * Sets the line coordinates. * * @param startX start x coordinate * @param startY start y coordinate * @param endX stop x coordinate * @param endY stop y coordinate */ protected void layoutLine(final double startX, final double startY, final double endX, final double endY) { line.setStartX(startX); line.setStartY(startY); line.setEndX(endX); line.setEndY(endY); pickLine.setStartX(startX); pickLine.setStartY(startY); pickLine.setEndX(endX); pickLine.setEndY(endY); addChildNodeIfNotPresent(line); addChildNodeIfNotPresent(pickLine); // pickLine.toBack(); } /** * Sets the marker coordinates. * * @param startX start x coordinate * @param startY start y coordinate * @param endX stop x coordinate * @param endY stop y coordinate */ protected void layoutMarker(final double startX, final double startY, final double endX, final double endY) { if (!triangle.isVisible()) { return; } triangle.setTranslateX(startX); triangle.setTranslateY(startY); addChildNodeIfNotPresent(triangle); } /** * Distance of the mouse cursor from the line (in pixel) that should trigger the moving of the line. By default * initialized to {@value #DEFAULT_PICKING_DISTANCE}. * * @return the picking distance property */ public final DoubleProperty pickingDistanceProperty() { return pickingDistance; } /** * @param autoRemove {@code true} indicator should be removed if there is no listener attached to it */ public void setAutoRemove(boolean autoRemove) { this.autoRemove = autoRemove; } /** * Sets the new value of the {@link #labelPositionProperty()}. * * @param value the label position, between 0.0 and 1.0 (both inclusive) */ public final void setLabelPosition(final double value) { labelPositionProperty().set(value); } /** * Sets the value of {@link #pickingDistanceProperty()}. * * @param distance the new picking distance */ public final void setPickingDistance(final double distance) { pickingDistanceProperty().set(distance); } /** * Sets the value that should be indicated. * * @param newValue value to be indicated */ @Override public final void setValue(final double newValue) { valueProperty().set(newValue); } @Override public List<EventListener> updateEventListener() { return updateListeners; } private void updateMouseListener(final boolean state) { if (state) { pickLine.setOnMouseReleased(mouseEvent -> pickLine.setCursor(Cursor.HAND)); pickLine.setOnMouseEntered(mouseEvent -> pickLine.setCursor(Cursor.HAND)); triangle.setOnMouseReleased(mouseEvent -> triangle.setCursor(Cursor.HAND)); triangle.setOnMouseEntered(mouseEvent -> triangle.setCursor(Cursor.HAND)); label.setOnMouseReleased(mouseEvent -> label.setCursor(Cursor.HAND)); label.setOnMouseEntered(mouseEvent -> label.setCursor(Cursor.HAND)); } else { pickLine.setOnMouseReleased(null); pickLine.setOnMouseEntered(null); triangle.setOnMouseReleased(null); triangle.setOnMouseEntered(null); label.setOnMouseReleased(null); label.setOnMouseEntered(null); } } /** * Value indicated by this plugin. * * @return value property */ @Override public final DoubleProperty valueProperty() { return value; } }