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

de.gsi.chart.Chart Maven / Gradle / Ivy

package de.gsi.chart;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.*;
import javafx.geometry.*;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Paint;
import javafx.stage.Window;
import javafx.util.Duration;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.gsi.chart.axes.Axis;
import de.gsi.chart.axes.spi.AbstractAxis;
import de.gsi.chart.axes.spi.DefaultNumericAxis;
import de.gsi.chart.legend.Legend;
import de.gsi.chart.legend.spi.DefaultLegend;
import de.gsi.chart.plugins.ChartPlugin;
import de.gsi.chart.renderer.Renderer;
import de.gsi.chart.renderer.spi.LabelledMarkerRenderer;
import de.gsi.chart.ui.ChartLayoutAnimator;
import de.gsi.chart.ui.HiddenSidesPane;
import de.gsi.chart.ui.ResizableCanvas;
import de.gsi.chart.ui.ToolBarFlowPane;
import de.gsi.chart.ui.css.CssPropertyFactory;
import de.gsi.chart.ui.geometry.Corner;
import de.gsi.chart.ui.geometry.Side;
import de.gsi.chart.utils.FXUtils;
import de.gsi.dataset.DataSet;
import de.gsi.dataset.event.EventListener;
import de.gsi.dataset.utils.AssertUtils;
import de.gsi.dataset.utils.NoDuplicatesList;
import de.gsi.dataset.utils.ProcessingProfiler;

/**
 * Chart designed primarily to display data traces using DataSet interfaces which are more flexible and efficient than
 * the observable lists used by XYChart. Brief history: original design inspired by Oracle, extended by CERN (i.e.
 * plugin concept/zoomer), modified to mitigate JavaFX performance issues and extended renderer
 * concept/canvas-concept/interfaces/+more plugins by GSI. Refactored and re-write in 2018 to make it compatible with
 * GPLv3 which -- in the spirit of 'Ship of Theseus' -- makes it de-facto a new development. Contributions, bug-fixes,
 * and modifications are welcome. Hope you find this library useful and enjoy!
 *
 * @author original conceptual design by Oracle (2010, 2014)
 * @author hbraeun, rstein, major refactoring, re-implementation and re-design
 */
public abstract class Chart extends HiddenSidesPane implements Observable {
    private static final Logger LOGGER = LoggerFactory.getLogger(Chart.class);
    private static final String CHART_CSS = Objects.requireNonNull(Chart.class.getResource("chart.css")).toExternalForm();
    private static final CssPropertyFactory CSS = new CssPropertyFactory<>(Control.getClassCssMetaData());
    private static final int DEFAULT_TRIGGER_DISTANCE = 50;
    protected static final boolean DEBUG = false; // for more verbose debugging

    protected BooleanBinding showingBinding;
    protected final BooleanProperty showing = new SimpleBooleanProperty(this, "showing", false);
    protected final ChangeListener showingListener = (ch2, o, n) -> showing.set(n);
    /**
     * When true any data changes will be animated.
     */
    private final BooleanProperty animated = new SimpleBooleanProperty(this, "animated", true);
    // TODO: Check whether 'this' or chart contents need to be added
    /**
     * Animator for animating stuff on the chart
     */
    protected final ChartLayoutAnimator animator = new ChartLayoutAnimator(this);

    /**
     * When true the chart will display a legend if the chart implementation supports a legend.
     */
    private final StyleableBooleanProperty legendVisible = CSS.createBooleanProperty(this, "legendVisible", true, () -> {
        updateLegend(getDatasets(), getRenderers());
        requestLayout();
    });

    // isCanvasChangeRequested is a recursion guard to update canvas only once
    protected boolean isCanvasChangeRequested;
    // layoutOngoing is a recursion guard to update canvas only once
    protected boolean layoutOngoing;
    protected final ObservableList axesList = FXCollections.observableList(new NoDuplicatesList<>());
    private final Map pluginGroups = new ConcurrentHashMap<>();
    private final ObservableList plugins = FXCollections.observableList(new LinkedList<>());
    private final ObservableList datasets = FXCollections.observableArrayList();
    protected final ObservableList allDataSets = FXCollections.observableArrayList();
    protected final List listeners = new ArrayList<>();
    protected final BooleanProperty autoNotification = new SimpleBooleanProperty(this, "autoNotification", true);
    private final ObservableList renderers = FXCollections.observableArrayList();
    {
        getRenderers().addListener(this::rendererChanged);
    }

    protected final ResizableCanvas canvas = new ResizableCanvas();
    // contains axes (left, bottom, top, right) panes & HiddenSidePane with the
    // Canvas at it's centre
    protected final GridPane axesAndCanvasPane = new GridPane();
    protected final Group pluginsArea = Chart.createChildGroup();

    protected boolean isAxesUpdate;
    // containing the plugin handler/modifier
    protected final ToolBarFlowPane toolBar = new ToolBarFlowPane(this);
    protected final BooleanProperty toolBarPinned = new SimpleBooleanProperty(this, "toolBarPinned", false);

    protected final HiddenSidesPane hiddenPane = new HiddenSidesPane();
    protected final Pane plotBackground = new Pane();
    protected final Pane plotForeGround = new Pane();
    protected final Pane canvasForeground = new Pane();

    protected final Map axesCorner = new ConcurrentHashMap<>(4);
    protected final Map axesPane = new ConcurrentHashMap<>(4);
    protected final Map measurementBar = new ConcurrentHashMap<>(4);
    protected final Map titleLegendCorner = new ConcurrentHashMap<>(4);
    protected final Map titleLegendPane = new ConcurrentHashMap<>(4);
    {
        for (final Corner corner : Corner.values()) {
            axesCorner.put(corner, new StackPane()); // NOPMD - default init
            titleLegendCorner.put(corner, new StackPane()); // NOPMD - default init
        }
        for (final Side side : Side.values()) {
            titleLegendPane.put(side, side.isVertical() ? new ChartHBox() : new ChartVBox()); // NOPMD - default init
            axesPane.put(side, side.isVertical() ? new ChartHBox() : new ChartVBox()); // NOPMD - default init
            if (side == Side.CENTER_HOR || side == Side.CENTER_VER) {
                axesPane.get(side).setMouseTransparent(true);
            }

            measurementBar.put(side, side.isVertical() ? new ChartHBox() : new ChartVBox()); // NOPMD - default
        }
    }

    private final EventListener axisChangeListener = obs -> FXUtils.runFX(() -> axesInvalidated(obs));
    protected final ListChangeListener axesChangeListenerLocal = this::axesChangedLocal;
    protected final ListChangeListener axesChangeListener = this::axesChanged;
    protected final ListChangeListener datasetChangeListener = this::datasetsChanged;
    protected final EventListener dataSetDataListener = obs -> FXUtils.runFX(this::dataSetInvalidated);
    protected final ListChangeListener pluginsChangedListener = this::pluginsChanged;
    protected final ChangeListener windowPropertyListener = (ch1, oldWindow, newWindow) -> {
        if (oldWindow != null) {
            oldWindow.showingProperty().removeListener(showingListener);
        }
        if (newWindow == null) {
            showing.set(false);
            return;
        }
        newWindow.showingProperty().addListener(showingListener);
    };
    private final ChangeListener scenePropertyListener = (ch, oldScene, newScene) -> {
        if (oldScene == newScene) {
            return;
        }
        if (oldScene != null) {
            // remove listener
            oldScene.windowProperty().removeListener(windowPropertyListener);
        }

        if (newScene == null) {
            showing.set(false);
            return;
        }

        // add listener
        newScene.windowProperty().addListener(windowPropertyListener);
    };
    {
        getDatasets().addListener(datasetChangeListener);
        getAxes().addListener(axesChangeListener);
        // update listener to propagate axes changes to chart changes
        getAxes().addListener(axesChangeListenerLocal);
    }

    protected final Label titleLabel = new Label();

    protected final StringProperty title = new StringPropertyBase() {
        @Override
        public Object getBean() {
            return Chart.this;
        }

        @Override
        public String getName() {
            return "title";
        }

        @Override
        protected void invalidated() {
            titleLabel.setText(get());
        }
    };

    /**
     * The side of the chart where the title is displayed default Side.TOP
     */
    private final StyleableObjectProperty titleSide = CSS.createObjectProperty(this, "titleSide", Side.TOP, false,
            StyleConverter.getEnumConverter(Side.class), (oldVal, newVal) -> {
                AssertUtils.notNull("Side must not be null", newVal);

                for (final Side s : Side.values()) {
                    getTitleLegendPane(s).getChildren().remove(titleLabel);
                }
                getTitleLegendPane(newVal).getChildren().add(titleLabel);
                return (newVal);
            }, this::requestLayout);

    /**
     * The side of the chart where the title is displayed default Side.TOP
     */
    private final StyleableObjectProperty measurementBarSide = CSS.createObjectProperty(this, "measurementBarSide", Side.RIGHT, false,
            StyleConverter.getEnumConverter(Side.class), (oldVal, newVal) -> {
                AssertUtils.notNull("Side must not be null", newVal);
                return newVal;
            }, this::requestLayout);

    /**
     * The side of the chart where the legend should be displayed default value Side.BOTTOM
     */
    private final StyleableObjectProperty legendSide = CSS.createObjectProperty(this, "legendSide", Side.BOTTOM, false,
            StyleConverter.getEnumConverter(Side.class), (oldVal, newVal) -> {
                AssertUtils.notNull("Side must not be null", newVal);

                final Legend legend = getLegend();
                if (legend == null) {
                    return newVal;
                }
                for (final Side s : Side.values()) {
                    getTitleLegendPane(s).getChildren().remove(legend.getNode());
                }
                getTitleLegendPane(newVal).getChildren().add(legend.getNode());
                legend.setVertical(newVal.isVertical());

                return newVal;
            }, this::requestLayout);

    /**
     * The node to display as the Legend. Subclasses can set a node here to be displayed on a side as the legend. If no
     * legend is wanted then this can be set to null
     */
    private final ObjectProperty legend = new SimpleObjectProperty<>(this, "legend", new DefaultLegend()) {
        private Legend oldLegend = get();
        {
            getTitleLegendPane(getLegendSide()).getChildren().add(oldLegend.getNode());
        }

        @Override
        protected void invalidated() {
            Legend newLegend = get();

            if (oldLegend != null) {
                for (final Side s : Side.values()) {
                    getTitleLegendPane(s).getChildren().remove(oldLegend.getNode());
                }
            }

            if (newLegend != null) {
                if (getLegendSide() != null && isLegendVisible()) {
                    getTitleLegendPane(getLegendSide()).getChildren().add(newLegend.getNode());
                }
                newLegend.getNode().setVisible(isLegendVisible());
            }
            super.set(newLegend);
            oldLegend = newLegend;
            updateLegend(getDatasets(), getRenderers());
        }
    };

    private final StyleableObjectProperty toolBarSide = CSS.createObjectProperty(this, "toolBarSide", Side.TOP, false,
            StyleConverter.getEnumConverter(Side.class), (oldVal, newVal) -> {
                AssertUtils.notNull("Side must not be null", newVal);
                // remove tool bar from potential other chart side pane locations
                Chart.this.setTop(null);
                Chart.this.setBottom(null);
                Chart.this.setLeft(null);
                Chart.this.setRight(null);
                switch (newVal) {
                case LEFT:
                    getToolBar().setOrientation(Orientation.VERTICAL);
                    Chart.this.setLeft(getToolBar());
                    break;
                case RIGHT:
                    getToolBar().setOrientation(Orientation.VERTICAL);
                    Chart.this.setRight(getToolBar());
                    break;
                case BOTTOM:
                    getToolBar().setOrientation(Orientation.HORIZONTAL);
                    Chart.this.setBottom(getToolBar());
                    break;
                case TOP:
                default:
                    getToolBar().setOrientation(Orientation.HORIZONTAL);
                    Chart.this.setTop(getToolBar());
                    break;
                }
                return (newVal);
            }, this::requestLayout);

    /**
     * Creates a new default Chart instance.
     *
     * @param axes axes to be added to the chart
     */
    public Chart(Axis... axes) {
        for (int dim = 0; dim < axes.length; dim++) {
            final Axis axis = axes[dim];
            if (!(axis instanceof AbstractAxis)) {
                continue;
            }
            final AbstractAxis abstractAxis = (AbstractAxis) axis;
            if (abstractAxis.getDimIndex() < 0) {
                abstractAxis.setDimIndex(dim);
            }
        }

        setTriggerDistance(Chart.DEFAULT_TRIGGER_DISTANCE);
        setMinSize(0, 0);
        setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE);
        setMaxSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE);
        setPadding(Insets.EMPTY);

        // populate SidesPane with default container
        final BorderPane localBorderPane = new BorderPane();
        axesAndCanvasPane.setPadding(Insets.EMPTY);
        localBorderPane.setCenter(new StackPane(plotBackground, axesAndCanvasPane, plotForeGround));
        plotBackground.toBack();
        plotForeGround.toFront();
        plotForeGround.setMouseTransparent(true);

        for (final Side side : Side.values()) {
            BorderPane.setAlignment(getMeasurementBar(side), Pos.CENTER);
        }
        localBorderPane.setTop(getMeasurementBar(Side.TOP));
        localBorderPane.setBottom(getMeasurementBar(Side.BOTTOM));
        localBorderPane.setLeft(getMeasurementBar(Side.LEFT));
        localBorderPane.setRight(getMeasurementBar(Side.RIGHT));

        super.setContent(localBorderPane);

        // hiddenPane.setTriggerDistance(DEFAULT_TRIGGER_DISTANCE);
        hiddenPane.triggerDistanceProperty().bindBidirectional(triggerDistanceProperty());
        hiddenPane.setAnimationDelay(Duration.millis(500));
        // hiddenPane.setMouseTransparent(true);
        hiddenPane.setPickOnBounds(false);

        final StackPane stackPane = new StackPane(getCanvas(), getCanvasForeground(), pluginsArea);
        hiddenPane.setContent(stackPane);

        // alt: canvas resize (default JavaFX Canvas does not automatically
        // resize to pref width/height according to parent constraints
        // canvas.widthProperty().bind(stackPane.widthProperty());
        // canvas.heightProperty().bind(stackPane.heightProperty());
        getCanvasForeground().setManaged(false);
        final ChangeListener canvasSizeChangeListener = (ch, o, n) -> {
            final double width = getCanvas().getWidth();
            final double height = getCanvas().getHeight();

            if (getCanvasForeground().getWidth() != width || getCanvasForeground().getHeight() != height) {
                // workaround needed so that pane within pane does not trigger
                // recursions w.r.t. repainting
                getCanvasForeground().resize(width, height);
            }

            if (!isCanvasChangeRequested) {
                isCanvasChangeRequested = true;
                Platform.runLater(() -> {
                    this.layoutChildren();
                    isCanvasChangeRequested = false;
                });
            }
        };
        canvas.widthProperty().addListener(canvasSizeChangeListener);
        canvas.heightProperty().addListener(canvasSizeChangeListener);

        getCanvasForeground().setMouseTransparent(true);
        getCanvas().toFront();
        getCanvasForeground().toFront();
        pluginsArea.toFront();

        hiddenPane.getStyleClass().setAll("plot-content");

        plotBackground.getStyleClass().setAll("chart-plot-background");

        if (!canvas.isCache()) {
            canvas.setCache(true);
            canvas.setCacheHint(CacheHint.QUALITY);
        }

        axesAndCanvasPane.add(hiddenPane, 2, 2); // centre-centre
        canvas.setStyle("-fx-background-color: rgba(200, 250, 200, 0.5);");

        final int rowSpan1 = 1;
        final int colSpan1 = 1;
        final int rowSpan3 = 3;
        final int colSpan3 = 3;

        // outer title/legend/parameter pane border (outer rim)
        axesAndCanvasPane.add(getTitleLegendPane(Side.LEFT), 0, 1, colSpan1, rowSpan3); // left-centre
        axesAndCanvasPane.add(getTitleLegendPane(Side.RIGHT), 4, 1, colSpan1, rowSpan3); // centre-centre
        axesAndCanvasPane.add(getTitleLegendPane(Side.TOP), 1, 0, colSpan3, rowSpan1); // centre-top
        axesAndCanvasPane.add(getTitleLegendPane(Side.BOTTOM), 1, 4, colSpan3, rowSpan1); // centre-bottom

        // add default axis panes (inner rim)
        axesAndCanvasPane.add(getAxesPane(Side.LEFT), 1, 2); // left-centre
        axesAndCanvasPane.add(getAxesPane(Side.RIGHT), 3, 2); // centre-centre
        axesAndCanvasPane.add(getAxesPane(Side.TOP), 2, 1); // centre-top
        axesAndCanvasPane.add(getAxesPane(Side.BOTTOM), 2, 3); // centre-bottom

        final Pane pane = getAxesPane(Side.CENTER_VER);
        GridPane.setFillHeight(pane, true);
        GridPane.setFillWidth(pane, true);

        axesAndCanvasPane.add(getAxesPane(Side.CENTER_VER), 2, 2); // centre-vertical
        axesAndCanvasPane.add(getAxesPane(Side.CENTER_HOR), 2, 2); // centre-vertical

        // add default corner BorderPane fields -- inner rim
        axesAndCanvasPane.add(getAxesCornerPane(Corner.TOP_LEFT), 1, 1);
        axesAndCanvasPane.add(getAxesCornerPane(Corner.TOP_RIGHT), 3, 1);
        axesAndCanvasPane.add(getAxesCornerPane(Corner.BOTTOM_LEFT), 1, 3);
        axesAndCanvasPane.add(getAxesCornerPane(Corner.BOTTOM_RIGHT), 3, 3);

        // add default corner BorderPane fields -- outer rim
        axesAndCanvasPane.add(getTitleLegendCornerPane(Corner.TOP_LEFT), 0, 0);
        axesAndCanvasPane.add(getTitleLegendCornerPane(Corner.TOP_RIGHT), 4, 0);
        axesAndCanvasPane.add(getTitleLegendCornerPane(Corner.BOTTOM_LEFT), 0, 4);
        axesAndCanvasPane.add(getTitleLegendCornerPane(Corner.BOTTOM_RIGHT), 4, 4);

        // set row/colum constraints for grid pane
        for (int i = 0; i < 4; i++) {
            final RowConstraints rowConstraint = new RowConstraints();
            if (i == 2) {
                rowConstraint.setVgrow(Priority.ALWAYS);
                rowConstraint.setFillHeight(true);
            }
            axesAndCanvasPane.getRowConstraints().add(i, rowConstraint);

            final ColumnConstraints colConstraint = new ColumnConstraints();
            if (i == 2) {
                colConstraint.setHgrow(Priority.ALWAYS);
                colConstraint.setFillWidth(true);
            }
            axesAndCanvasPane.getColumnConstraints().add(i, colConstraint);
        }

        // add plugin handling and listeners
        getPlugins().addListener(pluginsChangedListener);

        // add default chart content ie. ToolBar and Legend
        // can be repositioned via setToolBarSide(...) and setLegendSide(...)
        titleLabel.setAlignment(Pos.CENTER);
        HBox.setHgrow(titleLabel, Priority.ALWAYS);
        VBox.setVgrow(titleLabel, Priority.ALWAYS);
        titleLabel.focusTraversableProperty().bind(Platform.accessibilityActiveProperty());

        // register listener in tool bar FlowPane
        toolBar.registerListener();
        setTop(getToolBar());

        getTitleLegendPane(Side.TOP).getChildren().add(titleLabel);

        legendVisibleProperty().addListener((ch, old, visible) -> {
            if (getLegend() == null) {
                return;
            }
            getLegend().getNode().setVisible(visible);
            if (Boolean.TRUE.equals(visible)) {
                if (!getTitleLegendPane(getLegendSide()).getChildren().contains(getLegend().getNode())) {
                    getTitleLegendPane(getLegendSide()).getChildren().add(getLegend().getNode());
                }
            } else {
                getTitleLegendPane(getLegendSide()).getChildren().remove(getLegend().getNode());
            }
        });

        // set CSS stuff
        titleLabel.getStyleClass().add("chart-title");
        getStyleClass().add("chart");
        axesAndCanvasPane.getStyleClass().add("chart-content");

        registerShowingListener(); // NOPMD - unlikely but allowed override
    }

    @Override
    public String getUserAgentStylesheet() {
        return CHART_CSS;
    }

    @Override
    public void addListener(final InvalidationListener listener) {
        Objects.requireNonNull(listener, "InvalidationListener must not be null");
        listeners.add(listener);
    }

    /**
     * Play a animation involving the given keyframes. On every frame of the animation the chart will be relayed out
     *
     * @param keyFrames Array of KeyFrames to play
     */
    public void animate(final KeyFrame... keyFrames) {
        animator.animate(keyFrames);
    }

    public final BooleanProperty animatedProperty() {
        return animated;
    }

    public BooleanProperty autoNotificationProperty() {
        return autoNotification;
    }

    /**
     * Notifies listeners that the data has been invalidated. If the data is added to the chart, it triggers repaint.
     *
     * @return itself (fluent design)
     */
    public Chart fireInvalidated() {
        synchronized (autoNotification) {
            if (!isAutoNotification() || listeners.isEmpty()) {
                return this;
            }
        }

        if (Platform.isFxApplicationThread()) {
            executeFireInvalidated();
        } else {
            Platform.runLater(this::executeFireInvalidated);
        }

        return this;
    }

    /**
     * @return datasets attached to the chart and datasets attached to all renderers
     */
    public ObservableList getAllDatasets() {
        if (getRenderers() == null) {
            return allDataSets;
        }

        allDataSets.clear();
        allDataSets.addAll(getDatasets());
        getRenderers().stream().filter(renderer -> !(renderer instanceof LabelledMarkerRenderer)).forEach(renderer -> allDataSets.addAll(renderer.getDatasets()));

        return allDataSets;
    }

    public ObservableList getAxes() {
        return axesList;
    }

    public GridPane getAxesAndCanvasPane() {
        return axesAndCanvasPane;
    }

    public final StackPane getAxesCornerPane(final Corner corner) {
        return axesCorner.get(corner);
    }

    public final Pane getAxesPane(final Side side) {
        return axesPane.get(side);
    }

    /**
     * @return the actual canvas the data is being drawn upon
     */
    public final Canvas getCanvas() {
        return canvas;
    }

    public final Pane getCanvasForeground() {
        return canvasForeground;
    }

    /**
     * @return datasets attached to the chart and drawn by all renderers
     */
    public ObservableList getDatasets() {
        return datasets;
    }

    public Axis getFirstAxis(final Orientation orientation) {
        for (final Axis axis : getAxes()) {
            if (axis.getSide() == null) {
                continue;
            }
            switch (orientation) {
            case VERTICAL:
                if (axis.getSide().isVertical()) {
                    return axis;
                }
                break;
            case HORIZONTAL:
            default:
                if (axis.getSide().isHorizontal()) {
                    return axis;
                }
                break;
            }
        }
        // Add default axis if no suitable axis is available
        switch (orientation) {
        case HORIZONTAL:
            DefaultNumericAxis newXAxis = new DefaultNumericAxis("x-Axis");
            newXAxis.setSide(Side.BOTTOM);
            newXAxis.setDimIndex(DataSet.DIM_X);
            getAxes().add(newXAxis);
            return newXAxis;
        case VERTICAL:
        default:
            DefaultNumericAxis newYAxis = new DefaultNumericAxis("y-Axis");
            newYAxis.setSide(Side.LEFT);
            newYAxis.setDimIndex(DataSet.DIM_Y);
            getAxes().add(newYAxis);
            return newYAxis;
        }
    }

    public final Legend getLegend() {
        return legend.getValue();
    }

    public final Side getLegendSide() {
        return legendSide.get();
    }

    public final Pane getMeasurementBar(final Side side) {
        return measurementBar.get(side);
    }

    public final Side getMeasurementBarSide() {
        return measurementBarSide.get();
    }

    public final HiddenSidesPane getPlotArea() {
        return hiddenPane;
    }

    public final Pane getPlotBackground() {
        return plotBackground;
    }

    public final Pane getPlotForeground() {
        return plotForeGround;
    }

    /**
     * Returns a list of plugins added to this chart pane.
     *
     * @return a modifiable list of plugins
     */
    public final ObservableList getPlugins() {
        return plugins;
    }

    /**
     * @return observable list of associated chart renderers
     */
    public ObservableList getRenderers() {
        return renderers;
    }

    public final String getTitle() {
        return title.get();
    }

    public final StackPane getTitleLegendCornerPane(final Corner corner) {
        return titleLegendCorner.get(corner);
    }

    public final Pane getTitleLegendPane(final Side side) {
        return titleLegendPane.get(side);
    }

    public final Side getTitleSide() {
        return titleSide.get();
    }

    public final FlowPane getToolBar() {
        return toolBar;
    }

    public final ObjectProperty getToolBarSideProperty() {
        return toolBarSide;
    }

    public final Side getToolBarSide() {
        return toolBarSideProperty().get();
    }

    /**
     * Indicates whether data changes will be animated or not.
     *
     * @return true if data changes will be animated and false otherwise.
     */
    public final boolean isAnimated() {
        return animated.get();
    }

    public boolean isAutoNotification() {
        return autoNotification.get();
    }

    public final boolean isLegendVisible() {
        return legendVisible.getValue();
    }

    /**
     * @return true: if chart is being visible in Scene/Window
     */
    public boolean isShowing() {
        return showing.get();
    }

    public boolean isToolBarPinned() {
        return toolBarPinned.get();
    }

    @Override
    public void layoutChildren() {
        if (DEBUG && LOGGER.isDebugEnabled()) {
            LOGGER.debug("chart layoutChildren() - pre");
        }
        if (layoutOngoing) {
            return;
        }
        if (DEBUG && LOGGER.isDebugEnabled()) {
            LOGGER.debug("chart layoutChildren() - execute");
        }
        final long start = ProcessingProfiler.getTimeStamp();
        layoutOngoing = true;

        // update axes range first because this may change the overall layout
        updateAxisRange();
        ProcessingProfiler.getTimeDiff(start, "updateAxisRange()");

        // update chart parent according to possible size changes
        super.layoutChildren();

        // request re-layout of canvas
        redrawCanvas();

        ProcessingProfiler.getTimeDiff(start, "updateCanvas()");

        // request re-layout of plugins
        layoutPluginsChildren();
        ProcessingProfiler.getTimeDiff(start, "layoutPluginsChildren()");

        ProcessingProfiler.getTimeDiff(start, "end");

        layoutOngoing = false;
        if (DEBUG && LOGGER.isDebugEnabled()) {
            LOGGER.debug("chart layoutChildren() - done");
        }
        fireInvalidated();
    }

    public final ObjectProperty legendProperty() {
        return legend;
    }

    public final ObjectProperty legendSideProperty() {
        return legendSide;
    }

    public final BooleanProperty legendVisibleProperty() {
        return legendVisible;
    }

    public final ObjectProperty measurementBarSideProperty() {
        return measurementBarSide;
    }

    public boolean removeFromAllAxesPanes(final Axis node) {
        if (!(node instanceof Node)) {
            return false;
        }
        final Node axisNode = (Node) node;
        // remove axis from all axis panes
        for (final Side side : Side.values()) {
            if (getAxesPane(side).getChildren().remove(axisNode)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void removeListener(final InvalidationListener listener) {
        listeners.remove(listener);
    }

    @Override
    public void requestLayout() {
        if (DEBUG && LOGGER.isDebugEnabled()) {
            // normal debugDepth = 1 but for more verbose logging (e.g. recursion) use > 10
            for (int debugDepth = 1; debugDepth < 2; debugDepth++) {
                LOGGER.atDebug().addArgument(debugDepth).addArgument(ProcessingProfiler.getCallingClassMethod(debugDepth)).log("chart requestLayout() - called by {}: {}");
            }
            LOGGER.atDebug().addArgument("[..]").log("chart requestLayout() - called by {}");
        }

        FXUtils.assertJavaFxThread();
        super.requestLayout();
    }

    public final void setAnimated(final boolean value) {
        animated.set(value);
    }

    public void setAutoNotification(final boolean flag) {
        autoNotification.set(flag);
    }

    public final void setLegend(final Legend value) {
        legend.set(value);
    }

    public final void setLegendSide(final Side value) {
        legendSide.set(value);
    }

    public final void setLegendVisible(final boolean value) {
        legendVisible.set(value);
    }

    public final void setMeasurementBarSide(final Side value) {
        measurementBarSide.set(value);
    }

    public final void setTitle(final String value) {
        title.set(value);
    }

    public final void setTitleSide(final Side value) {
        titleSide.set(value);
    }

    public final void setTitlePaint(final Paint paint) {
        titleLabel.setTextFill(paint);
    }

    public Chart setToolBarPinned(boolean value) {
        toolBarPinned.set(value);
        return this;
    }

    public final void setToolBarSide(final Side value) {
        toolBarSide.set(value);
    }

    /**
     * @return property indicating if chart is actively visible in Scene/Window
     */
    public ReadOnlyBooleanProperty showingProperty() {
        return showing;
    }

    public final StringProperty titleProperty() {
        return title;
    }

    public final ObjectProperty titleSideProperty() {
        return titleSide;
    }

    public BooleanProperty toolBarPinnedProperty() {
        return toolBarPinned;
    }

    public final ObjectProperty toolBarSideProperty() {
        return toolBarSide;
    }

    // -------------- CONSTRUCTOR
    // --------------------------------------------------------------------------------------

    /**
     * Translates point from chart pane coordinates to the plot area coordinates.
     *
     * @param xCoord the x coordinate within XYChartPane coordinates system
     * @param yCoord the y coordinate within XYChartPane coordinates system
     * @return point in plot area coordinates
     */
    public final Point2D toPlotArea(final double xCoord, final double yCoord) {
        final Bounds plotAreaBounds = getCanvas().getBoundsInParent();
        return new Point2D(xCoord - plotAreaBounds.getMinX(), yCoord - plotAreaBounds.getMinY());
    }

    // -------------- METHODS
    // ------------------------------------------------------------------------------------------

    /**
     * update axes ranges (if necessary). This is supposed to be implemented in derived classes
     */
    public abstract void updateAxisRange();

    /**
     * Play the given animation on every frame of the animation the chart will be relayed out until the animation
     * finishes. So to add a animation to a chart, create a animation on data model, during layoutChartContent() map
     * data model to nodes then call this method with the animation.
     *
     * @param animation The animation to play
     */
    protected void animate(final Animation animation) {
        animator.animate(animation);
    }

    /**
     * add Chart specific axis handling (ie. placement around charts, add new DefaultNumericAxis if one is missing,
     * etc.)
     *
     * @param change the new axis change that is being added
     */
    protected abstract void axesChanged(final ListChangeListener.Change change);

    /**
     * add Chart specific axis handling (ie. placement around charts, add new DefaultNumericAxis if one is missing,
     * etc.)
     *
     * @param change the new axis change that is being added
     */
    protected void axesChangedLocal(final ListChangeListener.Change change) {
        while (change.next()) {
            change.getRemoved().forEach(set -> {
                AssertUtils.notNull("to be removed axis is null", set);
                // remove axis invalidation listener
                set.removeListener(axisChangeListener);
            });
            for (final Axis set : change.getAddedSubList()) {
                // check if axis is associated with an existing renderer,
                // if yes -> throw an exception
                AssertUtils.notNull("to be added axis is null", set);
                set.addListener(axisChangeListener);
            }
        }

        requestLayout();
    }

    /**
     * function called whenever a axis has been invalidated (e.g. range change or parameter plotting changes). Typically
     * calls 'requestLayout()' but can be overwritten in derived classes.
     *
     * @param axisObj the calling axis object
     */
    protected void axesInvalidated(final Object axisObj) {
        if (!(axisObj instanceof Axis) || layoutOngoing || isAxesUpdate) {
            return;
        }
        FXUtils.assertJavaFxThread();
        isAxesUpdate = true;
        if (DEBUG && LOGGER.isDebugEnabled()) {
            LOGGER.debug("chart axesInvalidated() - called by (1) {}", ProcessingProfiler.getCallingClassMethod(1));
            LOGGER.debug("chart axesInvalidated() - called by (3) {}", ProcessingProfiler.getCallingClassMethod(3));
        }
        requestLayout();
        isAxesUpdate = false;
    }

    protected void dataSetInvalidated() {
        // DataSet has notified and invalidate
        if (DEBUG && LOGGER.isDebugEnabled()) {
            LOGGER.debug("chart dataSetDataListener change notified");
        }
        FXUtils.assertJavaFxThread();
        // updateAxisRange();
        // TODO: check why the following does not always forces a layoutChildren
        requestLayout();
    }

    protected void datasetsChanged(final ListChangeListener.Change change) {
        boolean dataSetChanges = false;
        FXUtils.assertJavaFxThread();
        while (change.next()) {
            for (final DataSet set : change.getRemoved()) {
                // remove Legend listeners from removed datasets
                set.updateEventListener().removeIf(l -> l instanceof DefaultLegend.DatasetVisibilityListener);

                set.removeListener(dataSetDataListener);
                dataSetChanges = true;
            }

            for (final DataSet set : change.getAddedSubList()) {
                set.addListener(dataSetDataListener);
                dataSetChanges = true;
            }
        }

        if (dataSetChanges) {
            if (DEBUG && LOGGER.isDebugEnabled()) {
                LOGGER.debug("chart datasetsChanged(Change) - has dataset changes");
            }
            // updateAxisRange();
            updateLegend(getDatasets(), getRenderers());
            requestLayout();
        }
    }

    protected void executeFireInvalidated() {
        new ArrayList<>(listeners).forEach(listener -> listener.invalidated(this));
    }

    /**
     * @return unmodifiable list of the controls css styleable properties
     * @since JavaFX 8.0
     */
    @Override
    protected List> getControlCssMetaData() {
        return Chart.getClassCssMetaData();
    }

    protected void layoutPluginsChildren() {
        plugins.forEach(ChartPlugin::layoutChildren);
    }

    protected void pluginAdded(final ChartPlugin plugin) {
        plugin.setChart(Chart.this);
        final Group group = Chart.createChildGroup();
        Bindings.bindContent(group.getChildren(), plugin.getChartChildren());
        pluginGroups.put(plugin, group);
    }

    // -------------- STYLESHEET HANDLING
    // ------------------------------------------------------------------------------

    protected void pluginRemoved(final ChartPlugin plugin) {
        plugin.setChart(null);
        final Group group = pluginGroups.remove(plugin);
        Bindings.unbindContent(group, plugin.getChartChildren());
        group.getChildren().clear();
        pluginsArea.getChildren().remove(group);
    }

    protected void pluginsChanged(final ListChangeListener.Change change) {
        while (change.next()) {
            change.getRemoved().forEach(this::pluginRemoved);
            change.getAddedSubList().forEach(this::pluginAdded);
        }
        updatePluginsArea();
    }

    /**
     * (re-)draw canvas (if necessary). This is supposed to be implemented in derived classes
     */
    protected abstract void redrawCanvas();

    // -------------- LISTENER HANDLING
    // ------------------------------------------------------------------------------

    protected void registerShowingListener() {
        sceneProperty().addListener(scenePropertyListener);

        showing.addListener((ch, o, n) -> {
            if (Boolean.TRUE.equals(n)) {
                // requestLayout();

                // alt implementation in case of start-up issues
                final KeyFrame kf1 = new KeyFrame(Duration.millis(20), e -> requestLayout());

                final Timeline timeline = new Timeline(kf1);
                Platform.runLater(timeline::play);
            }
        });
    }

    protected void rendererChanged(final ListChangeListener.Change change) {
        FXUtils.assertJavaFxThread();
        while (change.next()) {
            // handle added renderer
            change.getAddedSubList().forEach(renderer -> {
                // update legend and recalculateLayout on datasetChange
                renderer.getDatasets().addListener(datasetChangeListener);
                // add listeners to all datasets already in the renderer
                renderer.getDatasets().forEach(set -> set.addListener(dataSetDataListener));
            });

            // handle removed renderer
            change.getRemoved().forEach(renderer -> {
                renderer.getDatasets().removeListener(datasetChangeListener);
                renderer.getDatasets().forEach(set -> set.removeListener(dataSetDataListener));
            });
        }
        // reset change to allow derived classes to add additional listeners to renderer changes
        change.reset();

        requestLayout();
        updateLegend(getDatasets(), getRenderers());
    }

    /**
     * This is used to check if any given animation should run. It returns true if animation is enabled and the node is
     * visible and in a scene.
     *
     * @return true if should animate
     */
    protected final boolean shouldAnimate() {
        return isAnimated() && getScene() != null;
    }

    protected void updateLegend(final List dataSets, final List renderers) {
        final Legend legend = getLegend();
        if (legend == null) {
            return;
        }
        legend.updateLegend(dataSets, renderers);
    }

    protected void updatePluginsArea() {
        pluginsArea.getChildren().setAll(plugins.stream().map(pluginGroups::get).collect(Collectors.toList()));
        requestLayout();
    }

    /**
     * @return The CssMetaData associated with this class, which may include the CssMetaData of its super classes.
     * @since JavaFX 8.0
     */
    public static List> getClassCssMetaData() {
        return CSS.getCssMetaData();
    }

    protected static Group createChildGroup() {
        final Group group = new Group();
        group.setManaged(false);
        group.setAutoSizeChildren(false);
        group.relocate(0, 0);
        return group;
    }

    protected static class ChartHBox extends HBox {
        public ChartHBox(Node... nodes) {
            super();
            setAlignment(Pos.CENTER);
            setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE);
            getChildren().addAll(nodes);
            visibleProperty().addListener((obs, o, n) -> getChildren().forEach(node -> node.setVisible(n)));
        }

        public ChartHBox(final boolean fill) {
            this();
            setFillHeight(fill);
        }
    }

    protected static class ChartVBox extends VBox {
        public ChartVBox(Node... nodes) {
            super();
            setAlignment(Pos.CENTER);
            setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE);
            getChildren().addAll(nodes);
            visibleProperty().addListener((obs, o, n) -> getChildren().forEach(node -> node.setVisible(n)));
        }

        public ChartVBox(final boolean fill) {
            this();
            setFillWidth(fill);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy