org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane.java Source code

Java tutorial

Introduction

Here is the source code for org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane.java

Source

/*
 * Autopsy Forensic Browser
 *
 * Copyright 2014 Basis Technology Corp.
 * Contact: carrier <at> sleuthkit <dot> org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.sleuthkit.autopsy.timeline.ui;

import com.google.common.eventbus.Subscribe;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.chart.Axis;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.Chart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.scene.control.Tooltip;
import javafx.scene.effect.Effect;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javax.annotation.concurrent.Immutable;
import org.apache.commons.lang3.StringUtils;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent;

/**
 * Abstract base class for {@link Chart} based {@link TimeLineView}s used in the
 * main visualization area.
 *
 * @param <X> the type of data plotted along the x axis
 * @param <Y> the type of data plotted along the y axis
 * @param <N> the type of nodes used to represent data items
 * @param <C> the type of the {@link XYChart<X,Y>} this class uses to plot the
 *            data.
 *
 * TODO: this is becoming (too?) closely tied to the notion that their is a
 * {@link XYChart} doing the rendering. Is this a good idea? -jm TODO: pull up
 * common history context menu items out of derived classes? -jm
 */
public abstract class AbstractVisualizationPane<X, Y, N, C extends XYChart<X, Y> & TimeLineChart<X>>
        extends BorderPane {

    @NbBundle.Messages("AbstractVisualization.Drag_Tooltip.text=Drag the mouse to select a time interval to zoom into.")
    private static final Tooltip DRAG_TOOLTIP = new Tooltip(Bundle.AbstractVisualization_Drag_Tooltip_text());
    private static final Logger LOGGER = Logger.getLogger(AbstractVisualizationPane.class.getName());

    public static Tooltip getDragTooltip() {
        return DRAG_TOOLTIP;
    }

    protected final SimpleBooleanProperty hasEvents = new SimpleBooleanProperty(true);

    protected final ObservableList<BarChart.Series<X, Y>> dataSets = FXCollections.<BarChart.Series<X, Y>>observableArrayList();

    protected C chart;

    //// replacement axis label componenets
    private final Pane leafPane; // container for the leaf lables in the declutterd axis

    private final Pane branchPane;// container for the branch lables in the declutterd axis

    protected final Region spacer;

    /**
     * task used to reload the content of this visualization
     */
    private Task<Boolean> updateTask;

    final protected TimeLineController controller;

    final protected FilteredEventsModel filteredEvents;

    final protected ObservableList<N> selectedNodes = FXCollections.observableArrayList();

    private InvalidationListener invalidationListener = (Observable observable) -> {
        update();
    };

    public ObservableList<N> getSelectedNodes() {
        return selectedNodes;
    }

    /**
     * list of {@link Node}s to insert into the toolbar. This should be set in
     * an implementations constructor.
     */
    protected List<Node> settingsNodes;

    /**
     * @return the list of nodes containing settings widgets to insert into this
     *         visualization's header
     */
    protected List<Node> getSettingsNodes() {
        return Collections.unmodifiableList(settingsNodes);
    }

    /**
     * @param value a value along this visualization's x axis
     *
     * @return true if the tick label for the given value should be bold ( has
     *         relevant data), false* otherwise
     */
    protected abstract Boolean isTickBold(X value);

    /**
     * apply this visualization's 'selection effect' to the given node
     *
     * @param node    the node to apply the 'effect' to
     * @param applied true if the effect should be applied, false if the effect
     *                should
     */
    protected abstract void applySelectionEffect(N node, Boolean applied);

    /**
     * @return a task to execute on a background thread to reload this
     *         visualization with different data.
     */
    protected abstract Task<Boolean> getUpdateTask();

    /**
     * @return return the {@link Effect} applied to 'selected nodes' in this
     *         visualization, or null if selection is visualized via another
     *         mechanism
     */
    protected abstract Effect getSelectionEffect();

    /**
     * @param tickValue
     *
     * @return a String to use for a tick mark label given a tick value
     */
    protected abstract String getTickMarkLabel(X tickValue);

    /**
     * the spacing (in pixels) between tick marks of the horizontal axis. This
     * will be used to layout the decluttered replacement labels.
     *
     * @return the spacing in pixels between tick marks of the horizontal axis
     */
    protected abstract double getTickSpacing();

    /**
     * @return the horizontal axis used by this Visualization's chart
     */
    protected abstract Axis<X> getXAxis();

    /**
     * @return the vertical axis used by this Visualization's chart
     */
    protected abstract Axis<Y> getYAxis();

    /**
     * update this visualization based on current state of zoom / filters.
     * Primarily this invokes the background {@link Task} returned by
     * {@link #getUpdateTask()} which derived classes must implement.
     */
    final synchronized public void update() {
        if (updateTask != null) {
            updateTask.cancel(true);
            updateTask = null;
        }
        updateTask = getUpdateTask();
        updateTask.stateProperty().addListener((Observable observable) -> {
            switch (updateTask.getState()) {
            case CANCELLED:
            case FAILED:
            case READY:
            case RUNNING:
            case SCHEDULED:
                break;
            case SUCCEEDED:
                try {
                    this.hasEvents.set(updateTask.get());
                } catch (InterruptedException | ExecutionException ex) {
                    LOGGER.log(Level.SEVERE, "Unexpected exception updating visualization", ex); //NOI18N
                }
                break;
            }
        });
        controller.monitorTask(updateTask);
    }

    final synchronized public void dispose() {
        if (updateTask != null) {
            updateTask.cancel(true);
        }
        this.filteredEvents.zoomParametersProperty().removeListener(invalidationListener);
        invalidationListener = null;
    }

    protected AbstractVisualizationPane(TimeLineController controller, Pane partPane, Pane contextPane,
            Region spacer) {
        this.controller = controller;

        this.filteredEvents = controller.getEventsModel();
        this.filteredEvents.registerForEvents(this);
        this.filteredEvents.zoomParametersProperty().addListener(invalidationListener);
        this.leafPane = partPane;
        this.branchPane = contextPane;
        this.spacer = spacer;
        selectedNodes.addListener((ListChangeListener.Change<? extends N> c) -> {
            while (c.next()) {
                c.getRemoved().forEach((N n) -> {
                    applySelectionEffect(n, false);
                });

                c.getAddedSubList().forEach((N c1) -> {
                    applySelectionEffect(c1, true);
                });
            }
        });

        TimeLineController.getTimeZone().addListener(invalidationListener);

        //show tooltip text in status bar
        hoverProperty().addListener((observable, oldActivated, newActivated) -> {
            if (newActivated) {
                controller.setStatus(DRAG_TOOLTIP.getText());
            } else {
                controller.setStatus("");
            }
        });

        update();
    }

    @Subscribe
    public void handleRefreshRequested(RefreshRequestedEvent event) {
        update();
    }

    /**
     * iterate through the list of tick-marks building a two level structure of
     * replacement tick marl labels. (Visually) upper level has most
     * detailed/highest frequency part of date/time. Second level has rest of
     * date/time grouped by unchanging part. eg:
     *
     *
     * october-30_october-31_september-01_september-02_september-03
     *
     * becomes
     *
     * _________30_________31___________01___________02___________03
     *
     * _________october___________|_____________september___________
     *
     *
     * NOTE: This method should only be invoked on the JFX thread
     */
    public synchronized void layoutDateLabels() {

        //clear old labels
        branchPane.getChildren().clear();
        leafPane.getChildren().clear();
        //since the tickmarks aren't necessarily in value/position order,
        //make a clone of the list sorted by position along axis
        ObservableList<Axis.TickMark<X>> tickMarks = FXCollections.observableArrayList(getXAxis().getTickMarks());
        tickMarks.sort(
                (Axis.TickMark<X> t, Axis.TickMark<X> t1) -> Double.compare(t.getPosition(), t1.getPosition()));

        if (tickMarks.isEmpty() == false) {
            //get the spacing between ticks in the underlying axis
            double spacing = getTickSpacing();

            //initialize values from first tick
            TwoPartDateTime dateTime = new TwoPartDateTime(getTickMarkLabel(tickMarks.get(0).getValue()));
            String lastSeenBranchLabel = dateTime.branch;
            //cumulative width of the current branch label

            //x-positions (pixels) of the current branch and leaf labels
            double leafLabelX = 0;

            if (dateTime.branch.isEmpty()) {
                //if there is only one part to the date (ie only year), just add a label for each tick
                for (Axis.TickMark<X> t : tickMarks) {
                    assignLeafLabel(new TwoPartDateTime(getTickMarkLabel(t.getValue())).leaf, spacing, leafLabelX,
                            isTickBold(t.getValue()));

                    leafLabelX += spacing; //increment x
                }
            } else {
                //there are two parts so ...
                //initialize additional state
                double branchLabelX = 0;
                double branchLabelWidth = 0;

                for (Axis.TickMark<X> t : tickMarks) { //for each tick

                    //split the label into a TwoPartDateTime
                    dateTime = new TwoPartDateTime(getTickMarkLabel(t.getValue()));

                    //if we are still on the same branch
                    if (lastSeenBranchLabel.equals(dateTime.branch)) {
                        //increment branch width
                        branchLabelWidth += spacing;
                    } else {// we are on to a new branch, so ...
                        assignBranchLabel(lastSeenBranchLabel, branchLabelWidth, branchLabelX);
                        //and then update label, x-pos, and width
                        lastSeenBranchLabel = dateTime.branch;
                        branchLabelX += branchLabelWidth;
                        branchLabelWidth = spacing;
                    }
                    //add the label for the leaf (highest frequency part)
                    assignLeafLabel(dateTime.leaf, spacing, leafLabelX, isTickBold(t.getValue()));

                    //increment leaf position
                    leafLabelX += spacing;
                }
                //we have reached end so add branch label for current branch
                assignBranchLabel(lastSeenBranchLabel, branchLabelWidth, branchLabelX);
            }
        }
        //request layout since we have modified scene graph structure
        requestParentLayout();
    }

    protected void setChartClickHandler() {
        chart.addEventHandler(MouseEvent.MOUSE_CLICKED, (MouseEvent event) -> {
            if (event.getButton() == MouseButton.PRIMARY && event.isStillSincePress()) {
                selectedNodes.clear();
            }
        });
    }

    /**
     * add a {@link Text} node to the leaf container for the decluttered axis
     * labels
     *
     * @param labelText  the string to add
     * @param labelWidth the width of the space available for the text
     * @param labelX     the horizontal position in the partPane of the text
     * @param bold       true if the text should be bold, false otherwise
     */
    private synchronized void assignLeafLabel(String labelText, double labelWidth, double labelX, boolean bold) {

        Text label = new Text(" " + labelText + " "); //NOI18N
        label.setTextAlignment(TextAlignment.CENTER);
        label.setFont(Font.font(null, bold ? FontWeight.BOLD : FontWeight.NORMAL, 10));
        //position label accounting for width
        label.relocate(labelX + labelWidth / 2 - label.getBoundsInLocal().getWidth() / 2, 0);
        label.autosize();

        if (leafPane.getChildren().isEmpty()) {
            //just add first label
            leafPane.getChildren().add(label);
        } else {
            //otherwise don't actually add the label if it would intersect with previous label
            final Text lastLabel = (Text) leafPane.getChildren().get(leafPane.getChildren().size() - 1);

            if (!lastLabel.getBoundsInParent().intersects(label.getBoundsInParent())) {
                leafPane.getChildren().add(label);
            }
        }
    }

    /**
     * add a {@link Label} node to the branch container for the decluttered axis
     * labels
     *
     * @param labelText  the string to add
     * @param labelWidth the width of the space to use for the label
     * @param labelX     the horizontal position in the partPane of the text
     */
    private synchronized void assignBranchLabel(String labelText, double labelWidth, double labelX) {

        Label label = new Label(labelText);
        label.setAlignment(Pos.CENTER);
        label.setTextAlignment(TextAlignment.CENTER);
        label.setFont(Font.font(10));
        //use a leading ellipse since that is the lowest frequency part,
        //and can be infered more easily from other surrounding labels
        label.setTextOverrun(OverrunStyle.LEADING_ELLIPSIS);
        //force size
        label.setMinWidth(labelWidth);
        label.setPrefWidth(labelWidth);
        label.setMaxWidth(labelWidth);
        label.relocate(labelX, 0);

        if (labelX == 0) { // first label has no border
            label.setStyle("-fx-border-width: 0 0 0 0 ; -fx-border-color:black;"); // NON-NLS //NOI18N
        } else { // subsequent labels have border on left to create dividers
            label.setStyle("-fx-border-width: 0 0 0 1; -fx-border-color:black;"); // NON-NLS //NOI18N
        }

        branchPane.getChildren().add(label);
    }

    /**
     * A simple data object used to represent a partial date as up to two parts.
     * A low frequency part (branch) containing all but the most specific
     * element, and a highest frequency part (leaf) containing the most specific
     * element. The branch and leaf names come from thinking of the space of all
     * date times as a tree with higher frequency information further from the
     * root. If there is only one part, it will be in the branch and the leaf
     * will equal an empty string
     */
    @Immutable
    private static final class TwoPartDateTime {

        /**
         * the low frequency part of a date/time eg 2001-May-4
         */
        private final String branch;

        /**
         * the highest frequency part of a date/time eg 14 (2pm)
         */
        private final String leaf;

        TwoPartDateTime(String dateString) {
            //find index of separator to spit on
            int splitIndex = StringUtils.lastIndexOfAny(dateString, " ", "-", ":"); //NOI18N
            if (splitIndex < 0) { // there is only one part
                leaf = dateString;
                branch = ""; //NOI18N
            } else { //split at index
                leaf = StringUtils.substring(dateString, splitIndex + 1);
                branch = StringUtils.substring(dateString, 0, splitIndex);
            }
        }
    }
}