org.sleuthkit.autopsy.timeline.ui.detailview.DetailsChartLane.java Source code

Java tutorial

Introduction

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

Source

/*
 * Autopsy Forensic Browser
 *
 * Copyright 2016 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.detailview;

import com.google.common.collect.Iterables;
import com.google.common.collect.Range;
import com.google.common.collect.TreeRangeMap;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.chart.Axis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
import org.joda.time.DateTime;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.filters.AbstractFilter;
import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter;
import org.sleuthkit.autopsy.timeline.ui.AbstractTimelineChart;
import org.sleuthkit.autopsy.timeline.ui.ContextMenuProvider;

/**
 * One "lane" of a the details view, contains all the core logic and layout
 * code.
 *
 * NOTE: It was too hard to control the threading of this chart via the
 * complicated default listeners. Instead clients should use
 * addDataItem(javafx.scene.chart.XYChart.Data) and
 * removeDataItem(javafx.scene.chart.XYChart.Data) to add and remove data.
 */
abstract class DetailsChartLane<Y extends TimeLineEvent> extends XYChart<DateTime, Y>
        implements ContextMenuProvider {

    private static final String STYLE_SHEET = GuideLine.class.getResource("EventsDetailsChart.css")
            .toExternalForm(); //NON-NLS

    static final int MINIMUM_EVENT_NODE_GAP = 4;
    static final int MINIMUM_ROW_HEIGHT = 24;

    private final DetailsChart parentChart;
    private final TimeLineController controller;
    private final DetailsChartLayoutSettings layoutSettings;
    private final ObservableList<EventNodeBase<?>> selectedNodes;

    private final Map<Y, EventNodeBase<?>> eventMap = new HashMap<>();

    @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
    final ObservableList<EventNodeBase<?>> nodes = FXCollections.observableArrayList();
    final ObservableList<EventNodeBase<?>> sortedNodes = nodes
            .sorted(Comparator.comparing(EventNodeBase::getStartMillis));

    private final boolean useQuickHideFilters;

    @ThreadConfined(type = ThreadConfined.ThreadType.JFX) //at start of layout pass
    private double descriptionWidth;
    @ThreadConfined(type = ThreadConfined.ThreadType.JFX) //at start of layout pass
    private Set<String> activeQuickHidefilters = new HashSet<>();

    boolean quickHideFiltersEnabled() {
        return useQuickHideFilters;
    }

    public void clearContextMenu() {
        parentChart.clearContextMenu();
    }

    @Override
    public ContextMenu getContextMenu(MouseEvent clickEvent) {
        return parentChart.getContextMenu(clickEvent);
    }

    EventNodeBase<?> createNode(DetailsChartLane<?> chart, TimeLineEvent event) {
        if (event.getEventIDs().size() == 1) {
            return new SingleEventNode(this,
                    controller.getEventsModel().getEventById(Iterables.getOnlyElement(event.getEventIDs())), null);
        } else if (event instanceof SingleEvent) {
            return new SingleEventNode(chart, (SingleEvent) event, null);
        } else if (event instanceof EventCluster) {
            return new EventClusterNode(chart, (EventCluster) event, null);
        } else {
            return new EventStripeNode(chart, (EventStripe) event, null);
        }
    }

    @Override
    synchronized protected void layoutPlotChildren() {
        setCursor(Cursor.WAIT);
        if (useQuickHideFilters) {
            //These don't change during a layout pass and are expensive to compute per node.  So we do it once at the start
            activeQuickHidefilters = getController().getQuickHideFilters().stream().filter(AbstractFilter::isActive)
                    .map(DescriptionFilter::getDescription).collect(Collectors.toSet());
        }
        //This dosn't change during a layout pass and is expensive to compute per node.  So we do it once at the start
        descriptionWidth = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE;

        if (layoutSettings.getBandByType()) {
            maxY.set(0);
            sortedNodes.stream().collect(Collectors.groupingBy(EventNodeBase<?>::getEventType)).values()
                    .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get())));
        } else {
            maxY.set(layoutEventBundleNodes(sortedNodes, 0));
        }
        doAdditionalLayout();
        setCursor(null);
    }

    public TimeLineController getController() {
        return controller;
    }

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

    /**
     * listener that triggers chart layout pass
     */
    final InvalidationListener layoutInvalidationListener = (Observable o) -> {
        layoutPlotChildren();
    };

    public ReadOnlyDoubleProperty maxVScrollProperty() {
        return maxY.getReadOnlyProperty();
    }

    /**
     * the maximum y value used so far during the most recent layout pass
     */
    private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0);

    DetailsChartLane(DetailsChart parentChart, Axis<DateTime> dateAxis, Axis<Y> verticalAxis,
            boolean useQuickHideFilters) {
        super(dateAxis, verticalAxis);
        this.parentChart = parentChart;
        this.layoutSettings = parentChart.getLayoutSettings();
        this.controller = parentChart.getController();
        this.selectedNodes = parentChart.getSelectedNodes();
        this.useQuickHideFilters = useQuickHideFilters;

        //add a dummy series or the chart is never rendered
        setData(FXCollections.observableList(Arrays.asList(new Series<DateTime, Y>())));

        Tooltip.install(this, AbstractTimelineChart.getDefaultTooltip());

        dateAxis.setAutoRanging(false);
        setLegendVisible(false);
        setPadding(Insets.EMPTY);
        setAlternativeColumnFillVisible(true);

        sceneProperty().addListener(observable -> {
            Scene scene = getScene();
            if (scene != null && scene.getStylesheets().contains(STYLE_SHEET) == false) {
                scene.getStylesheets().add(STYLE_SHEET);
            }
        });

        //add listener for events that should trigger layout
        layoutSettings.bandByTypeProperty().addListener(layoutInvalidationListener);
        layoutSettings.oneEventPerRowProperty().addListener(layoutInvalidationListener);
        layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener);
        layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener);
        layoutSettings.descrVisibilityProperty().addListener(layoutInvalidationListener);
        controller.getQuickHideFilters().addListener(layoutInvalidationListener);

        //all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly
        getPlotChildren().add(nodeGroup);
    }

    /**
     * Layout the nodes in the given list, starting form the given minimum y
     * coordinate via the following algorithm:
     *
     * We start with a list of nodes (each representing an event) sorted by span
     * start time of the underlying event
     *
     * - initialize empty map (maxXatY) from y-ranges to max used x-value
     *
     * - for each node:
     *
     * -- size the node based on its children (use this algorithm recursively)
     *
     * -- get the event's start position from the dateaxis
     *
     * -- to position node: check if maxXatY is to the left of the left x coord:
     * if maxXatY is less than the left x coord, good, put the current node
     * here, mark right x coord as maxXatY, go to next node ; if maxXatY is
     * greater than the left x coord, increment y position, do check again until
     * maxXatY less than left x coord.
     *
     * @param nodes collection of nodes to layout, sorted by event start time
     * @param minY  the minimum y coordinate to position the nodes at.
     *
     * @return the maximum y coordinate used by any of the layed out nodes.
     */
    public double layoutEventBundleNodes(final Collection<? extends EventNodeBase<?>> nodes, final double minY) {
        // map from y-ranges to maximum x
        TreeRangeMap<Double, Double> maxXatY = TreeRangeMap.create();

        // maximum y values occupied by any of the given nodes,  updated as nodes are layed out.
        double localMax = minY;

        //for each node do a recursive layout to size it and then position it in first available slot
        for (EventNodeBase<?> bundleNode : nodes) {
            if (useQuickHideFilters && activeQuickHidefilters.contains(bundleNode.getDescription())) {
                //if the node hiden is hidden by  quick hide filter, hide it and skip layout
                bundleNode.setVisible(false);
                bundleNode.setManaged(false);
            } else {
                layoutBundleHelper(bundleNode);
                //get computed height and width
                double h = bundleNode.getBoundsInLocal().getHeight();
                double w = bundleNode.getBoundsInLocal().getWidth();
                //get left and right x coords from axis plus computed width
                double xLeft = getXForEpochMillis(bundleNode.getStartMillis())
                        - bundleNode.getLayoutXCompensation();
                double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP;

                //initial test position
                double yTop = (layoutSettings.getOneEventPerRow()) ? (localMax + MINIMUM_EVENT_NODE_GAP)// if onePerRow, just put it at end
                        : computeYTop(minY, h, maxXatY, xLeft, xRight);

                localMax = Math.max(yTop + h, localMax);

                //animate node to new position
                bundleNode.animateTo(xLeft, yTop);
            }
        }
        return localMax; //return new max
    }

    @Override
    final public void requestChartLayout() {
        super.requestChartLayout();
    }

    double getXForEpochMillis(Long millis) {
        DateTime dateTime = new DateTime(millis);
        return getXAxis().getDisplayPosition(dateTime);
    }

    @Deprecated
    @Override
    protected void dataItemAdded(Series<DateTime, Y> series, int itemIndex, Data<DateTime, Y> item) {
    }

    @Deprecated
    @Override
    protected void dataItemRemoved(Data<DateTime, Y> item, Series<DateTime, Y> series) {
    }

    @Deprecated
    @Override
    protected void dataItemChanged(Data<DateTime, Y> item) {
    }

    @Deprecated
    @Override
    protected void seriesAdded(Series<DateTime, Y> series, int seriesIndex) {
    }

    @Deprecated
    @Override
    protected void seriesRemoved(Series<DateTime, Y> series) {
    }

    /**
     * add an event to this chart
     *
     * @see note in main section of class JavaDoc
     *
     * @param event
     */
    void addEvent(Y event) {
        EventNodeBase<?> eventNode = createNode(this, event);
        eventMap.put(event, eventNode);
        Platform.runLater(() -> {
            nodes.add(eventNode);
            nodeGroup.getChildren().add(eventNode);
        });
    }

    /**
     * remove an event from this chart
     *
     * @see note in main section of class JavaDoc
     *
     * @param event
     */
    void removeEvent(Y event) {
        EventNodeBase<?> removedNode = eventMap.remove(event);
        Platform.runLater(() -> {
            nodes.remove(removedNode);
            nodeGroup.getChildren().removeAll(removedNode);
        });
    }

    /**
     * the group that all event nodes are added to. This facilitates scrolling
     * by allowing a single translation of this group.
     */
    final Group nodeGroup = new Group();

    public synchronized void setVScroll(double vScrollValue) {
        nodeGroup.setTranslateY(-vScrollValue);
    }

    /**
     * @return all the nodes that pass the given predicate
     */
    synchronized Iterable<EventNodeBase<?>> getAllNodes() {
        return getNodes((x) -> true);
    }

    /**
     * @return all the nodes that pass the given predicate
     */
    synchronized Iterable<EventNodeBase<?>> getNodes(Predicate<EventNodeBase<?>> p) {
        //use this recursive function to flatten the tree of nodes into an single stream.
        Function<EventNodeBase<?>, Stream<EventNodeBase<?>>> stripeFlattener = new Function<EventNodeBase<?>, Stream<EventNodeBase<?>>>() {
            @Override
            public Stream<EventNodeBase<?>> apply(EventNodeBase<?> node) {
                return Stream.concat(Stream.of(node), node.getSubNodes().stream().flatMap(this::apply));
            }
        };

        return sortedNodes.stream().flatMap(stripeFlattener).filter(p).collect(Collectors.toList());
    }

    /**
     * Given information about the current layout pass so far and about a
     * particular node, compute the y position of that node.
     *
     *
     * @param yMin    the smallest (towards the top of the screen) y position to
     *                consider
     * @param h       the height of the node we are trying to position
     * @param maxXatY a map from y ranges to the max x within that range. NOTE:
     *                This map will be updated to include the node in question.
     * @param xLeft   the left x-cord of the node to position
     * @param xRight  the left x-cord of the node to position
     *
     * @return the y position for the node in question.
     *
     *
     */
    double computeYTop(double yMin, double h, TreeRangeMap<Double, Double> maxXatY, double xLeft, double xRight) {
        double yTop = yMin;
        double yBottom = yTop + h;
        //until the node is not overlapping any others try moving it down.
        boolean overlapping = true;
        while (overlapping) {
            overlapping = false;
            //check each pixel from bottom to top.
            for (double y = yBottom; y >= yTop; y -= MINIMUM_ROW_HEIGHT) {
                final Double maxX = maxXatY.get(y);
                if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) {
                    //if that pixel is already used
                    //jump top to this y value and repeat until free slot is found.
                    overlapping = true;
                    yTop = y + MINIMUM_EVENT_NODE_GAP;
                    yBottom = yTop + h;
                    break;
                }
            }
        }
        maxXatY.put(Range.closed(yTop, yBottom), xRight);
        return yTop;
    }

    /**
     * Set layout parameters on the given node and layout its children
     *
     * @param eventNode the Node to layout
     */
    void layoutBundleHelper(final EventNodeBase<?> eventNode) {
        //make sure it is shown
        eventNode.setVisible(true);
        eventNode.setManaged(true);
        //apply advanced layout description visibility options
        eventNode.setDescriptionVisibility(layoutSettings.getDescrVisibility());
        eventNode.setMaxDescriptionWidth(descriptionWidth);

        //do recursive layout
        eventNode.layoutChildren();
    }

    abstract void doAdditionalLayout();

    DetailsChart getParentChart() {
        return parentChart;
    }
}