19 package org.sleuthkit.autopsy.timeline.ui.detailview;
 
   21 import com.google.common.collect.Iterables;
 
   22 import com.google.common.collect.Range;
 
   23 import com.google.common.collect.TreeRangeMap;
 
   24 import java.util.Arrays;
 
   25 import java.util.Collection;
 
   26 import java.util.Comparator;
 
   27 import java.util.HashMap;
 
   28 import java.util.HashSet;
 
   31 import java.util.function.Function;
 
   32 import java.util.function.Predicate;
 
   33 import java.util.stream.Collectors;
 
   34 import java.util.stream.Stream;
 
   35 import javafx.application.Platform;
 
   36 import javafx.beans.InvalidationListener;
 
   37 import javafx.beans.Observable;
 
   38 import javafx.beans.property.ReadOnlyDoubleProperty;
 
   39 import javafx.beans.property.ReadOnlyDoubleWrapper;
 
   40 import javafx.collections.FXCollections;
 
   41 import javafx.collections.ObservableList;
 
   42 import javafx.geometry.Insets;
 
   43 import javafx.scene.Cursor;
 
   44 import javafx.scene.Group;
 
   45 import javafx.scene.Scene;
 
   46 import javafx.scene.chart.Axis;
 
   47 import javafx.scene.chart.XYChart;
 
   48 import javafx.scene.control.ContextMenu;
 
   49 import javafx.scene.control.Tooltip;
 
   50 import javafx.scene.input.MouseEvent;
 
   51 import static javafx.scene.layout.Region.USE_PREF_SIZE;
 
   52 import org.joda.time.DateTime;
 
   73 abstract class DetailsChartLane<Y 
extends TimeLineEvent> extends XYChart<DateTime, Y> implements ContextMenuProvider {
 
   75     private static final String STYLE_SHEET = GuideLine.class.getResource(
"EventsDetailsChart.css").toExternalForm(); 
 
   77     static final int MINIMUM_EVENT_NODE_GAP = 4;
 
   78     static final int MINIMUM_ROW_HEIGHT = 24;
 
   80     private final DetailsChart parentChart;
 
   81     private final TimeLineController controller;
 
   82     private final DetailsChartLayoutSettings layoutSettings;
 
   83     private final ObservableList<EventNodeBase<?>> selectedNodes;
 
   85     private final Map<Y, EventNodeBase<?>> eventMap = 
new HashMap<>();
 
   87     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
   88     final ObservableList< EventNodeBase<?>> nodes = FXCollections.observableArrayList();
 
   89     final ObservableList< EventNodeBase<?>> sortedNodes = nodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis));
 
   91     private final 
boolean useQuickHideFilters;
 
   93     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
   94     private 
double descriptionWidth;
 
   95     @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
 
   96     private Set<String> activeQuickHidefilters = new HashSet<>();
 
   98     boolean quickHideFiltersEnabled() {
 
   99         return useQuickHideFilters;
 
  102     public void clearContextMenu() {
 
  107     public ContextMenu getContextMenu(MouseEvent clickEvent) {
 
  108         return parentChart.getContextMenu(clickEvent);
 
  111     EventNodeBase<?> createNode(DetailsChartLane<?> chart, TimeLineEvent event) {
 
  112         if (event.getEventIDs().size() == 1) {
 
  113             return new SingleEventNode(
this, controller.getEventsModel().getEventById(Iterables.getOnlyElement(event.getEventIDs())), null);
 
  114         } 
else if (event instanceof SingleEvent) {
 
  115             return new SingleEventNode(chart, (SingleEvent) event, null);
 
  116         } 
else if (event instanceof EventCluster) {
 
  117             return new EventClusterNode(chart, (EventCluster) event, null);
 
  119             return new EventStripeNode(chart, (EventStripe) event, null);
 
  124     synchronized protected void layoutPlotChildren() {
 
  125         setCursor(Cursor.WAIT);
 
  126         if (useQuickHideFilters) {
 
  128             activeQuickHidefilters = getController().getQuickHideFilters().stream()
 
  131                     .collect(Collectors.toSet());
 
  134         descriptionWidth = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE;
 
  136         if (layoutSettings.getBandByType()) {
 
  139                     .collect(Collectors.groupingBy(EventNodeBase<?>::getEventType)).values()
 
  140                     .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get())));
 
  142             maxY.set(layoutEventBundleNodes(sortedNodes, 0));
 
  144         doAdditionalLayout();
 
  148     public TimeLineController getController() {
 
  152     public ObservableList<EventNodeBase<?>> getSelectedNodes() {
 
  153         return selectedNodes;
 
  158     final InvalidationListener layoutInvalidationListener = (Observable o) -> {
 
  159         layoutPlotChildren();
 
  162     public ReadOnlyDoubleProperty maxVScrollProperty() {
 
  163         return maxY.getReadOnlyProperty();
 
  168     private final ReadOnlyDoubleWrapper maxY = 
new ReadOnlyDoubleWrapper(0.0);
 
  170     DetailsChartLane(DetailsChart parentChart, Axis<DateTime> dateAxis, Axis<Y> verticalAxis, 
boolean useQuickHideFilters) {
 
  171         super(dateAxis, verticalAxis);
 
  172         this.parentChart = parentChart;
 
  173         this.layoutSettings = parentChart.getLayoutSettings();
 
  174         this.controller = parentChart.getController();
 
  175         this.selectedNodes = parentChart.getSelectedNodes();
 
  176         this.useQuickHideFilters = useQuickHideFilters;
 
  179         setData(FXCollections.observableList(Arrays.asList(
new Series<DateTime, Y>())));
 
  181         Tooltip.install(
this, AbstractTimelineChart.getDefaultTooltip());
 
  183         dateAxis.setAutoRanging(
false);
 
  184         setLegendVisible(
false);
 
  185         setPadding(Insets.EMPTY);
 
  186         setAlternativeColumnFillVisible(
true);
 
  188         sceneProperty().addListener(observable -> {
 
  189             Scene scene = getScene();
 
  190             if (scene != null && scene.getStylesheets().contains(STYLE_SHEET) == 
false) {
 
  191                 scene.getStylesheets().add(STYLE_SHEET);
 
  196         layoutSettings.bandByTypeProperty().addListener(layoutInvalidationListener);
 
  197         layoutSettings.oneEventPerRowProperty().addListener(layoutInvalidationListener);
 
  198         layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener);
 
  199         layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener);
 
  200         layoutSettings.descrVisibilityProperty().addListener(layoutInvalidationListener);
 
  201         controller.getQuickHideFilters().addListener(layoutInvalidationListener);
 
  204         getPlotChildren().add(nodeGroup);
 
  233     public double layoutEventBundleNodes(
final Collection<? extends 
EventNodeBase<?>> nodes, 
final double minY) {
 
  235         TreeRangeMap<Double, Double> maxXatY = TreeRangeMap.create();
 
  238         double localMax = minY;
 
  242             if (useQuickHideFilters && activeQuickHidefilters.contains(bundleNode.getDescription())) {
 
  244                 bundleNode.setVisible(
false);
 
  245                 bundleNode.setManaged(
false);
 
  247                 layoutBundleHelper(bundleNode);
 
  249                 double h = bundleNode.getBoundsInLocal().getHeight();
 
  250                 double w = bundleNode.getBoundsInLocal().getWidth();
 
  252                 double xLeft = getXForEpochMillis(bundleNode.getStartMillis()) - bundleNode.getLayoutXCompensation();
 
  253                 double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP;
 
  256                 double yTop = (layoutSettings.getOneEventPerRow())
 
  257                         ? (localMax + MINIMUM_EVENT_NODE_GAP)
 
  258                         : computeYTop(minY, h, maxXatY, xLeft, xRight);
 
  260                 localMax = Math.max(yTop + h, localMax);
 
  263                 bundleNode.animateTo(xLeft, yTop);
 
  270     final public void requestChartLayout() {
 
  271         super.requestChartLayout();
 
  274     double getXForEpochMillis(Long millis) {
 
  275         DateTime dateTime = 
new DateTime(millis);
 
  276         return getXAxis().getDisplayPosition(dateTime);
 
  281     protected void dataItemAdded(Series<DateTime, Y> series, 
int itemIndex, Data<DateTime, Y> item) {
 
  286     protected void dataItemRemoved(Data<DateTime, Y> item, Series<DateTime, Y> series) {
 
  291     protected void dataItemChanged(Data<DateTime, Y> item) {
 
  296     protected void seriesAdded(Series<DateTime, Y> series, 
int seriesIndex) {
 
  301     protected void seriesRemoved(Series<DateTime, Y> series) {
 
  311     void addEvent(Y event) {
 
  313         eventMap.put(event, eventNode);
 
  314         Platform.runLater(() -> {
 
  315             nodes.add(eventNode);
 
  316             nodeGroup.getChildren().add(eventNode);
 
  327     void removeEvent(Y event) {
 
  329         Platform.runLater(() -> {
 
  330             nodes.remove(removedNode);
 
  331             nodeGroup.getChildren().removeAll(removedNode);
 
  339     final Group nodeGroup = 
new Group();
 
  341     public synchronized void setVScroll(
double vScrollValue) {
 
  342         nodeGroup.setTranslateY(-vScrollValue);
 
  348     synchronized Iterable<EventNodeBase<?>> getAllNodes() {
 
  349         return getNodes((x) -> 
true);
 
  355     synchronized Iterable<EventNodeBase<?>> getNodes(Predicate<
EventNodeBase<?>> p) {
 
  357         Function<EventNodeBase<?>, Stream<EventNodeBase<?>>> stripeFlattener
 
  358                 = 
new Function<EventNodeBase<?>, Stream<EventNodeBase<?>>>() {
 
  361                 return Stream.concat(
 
  367         return sortedNodes.stream()
 
  368                 .flatMap(stripeFlattener)
 
  369                 .filter(p).collect(Collectors.toList());
 
  389     double computeYTop(
double yMin, 
double h, TreeRangeMap<Double, Double> maxXatY, 
double xLeft, 
double xRight) {
 
  391         double yBottom = yTop + h;
 
  393         boolean overlapping = 
true;
 
  394         while (overlapping) {
 
  397             for (
double y = yBottom; y >= yTop; y -= MINIMUM_ROW_HEIGHT) {
 
  398                 final Double maxX = maxXatY.get(y);
 
  399                 if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) {
 
  403                     yTop = y + MINIMUM_EVENT_NODE_GAP;
 
  409         maxXatY.put(Range.closed(yTop, yBottom), xRight);
 
  418     void layoutBundleHelper(
final EventNodeBase< ?> eventNode) {
 
  420         eventNode.setVisible(
true);
 
  421         eventNode.setManaged(
true);
 
  423         eventNode.setDescriptionVisibility(layoutSettings.getDescrVisibility());
 
  424         eventNode.setMaxDescriptionWidth(descriptionWidth);
 
  427         eventNode.layoutChildren();
 
  430     abstract void doAdditionalLayout();
 
  432     DetailsChart getParentChart() {
 
abstract List< EventNodeBase<?> > getSubNodes()