Autopsy  4.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
DetailsChartLane.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2016 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.timeline.ui.detailview;
20 
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;
29 import java.util.Map;
30 import java.util.Set;
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;
63 
73 abstract class DetailsChartLane<Y extends TimeLineEvent> extends XYChart<DateTime, Y> implements ContextMenuProvider {
74 
75  private static final String STYLE_SHEET = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //NON-NLS
76 
77  static final int MINIMUM_EVENT_NODE_GAP = 4;
78  static final int MINIMUM_ROW_HEIGHT = 24;
79 
80  private final DetailsChart parentChart;
81  private final TimeLineController controller;
82  private final DetailsChartLayoutSettings layoutSettings;
83  private final ObservableList<EventNodeBase<?>> selectedNodes;
84 
85  private final Map<Y, EventNodeBase<?>> eventMap = new HashMap<>();
86 
87  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
88  final ObservableList< EventNodeBase<?>> nodes = FXCollections.observableArrayList();
89  final ObservableList< EventNodeBase<?>> sortedNodes = nodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis));
90 
91  private final boolean useQuickHideFilters;
92 
93  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass
94  private double descriptionWidth;
95  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass
96  private Set<String> activeQuickHidefilters = new HashSet<>();
97 
98  boolean quickHideFiltersEnabled() {
99  return useQuickHideFilters;
100  }
101 
102  public void clearContextMenu() {
103  parentChart.clearContextMenu();
104  }
105 
106  @Override
107  public ContextMenu getContextMenu(MouseEvent clickEvent) {
108  return parentChart.getContextMenu(clickEvent);
109  }
110 
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);
118  } else {
119  return new EventStripeNode(chart, (EventStripe) event, null);
120  }
121  }
122 
123  @Override
124  synchronized protected void layoutPlotChildren() {
125  setCursor(Cursor.WAIT);
126  if (useQuickHideFilters) {
127  //These don't change during a layout pass and are expensive to compute per node. So we do it once at the start
128  activeQuickHidefilters = getController().getQuickHideFilters().stream()
129  .filter(AbstractFilter::isActive)
131  .collect(Collectors.toSet());
132  }
133  //This dosn't change during a layout pass and is expensive to compute per node. So we do it once at the start
134  descriptionWidth = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE;
135 
136  if (layoutSettings.getBandByType()) {
137  maxY.set(0);
138  sortedNodes.stream()
139  .collect(Collectors.groupingBy(EventNodeBase<?>::getEventType)).values()
140  .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get())));
141  } else {
142  maxY.set(layoutEventBundleNodes(sortedNodes, 0));
143  }
144  doAdditionalLayout();
145  setCursor(null);
146  }
147 
148  public TimeLineController getController() {
149  return controller;
150  }
151 
152  public ObservableList<EventNodeBase<?>> getSelectedNodes() {
153  return selectedNodes;
154  }
158  final InvalidationListener layoutInvalidationListener = (Observable o) -> {
159  layoutPlotChildren();
160  };
161 
162  public ReadOnlyDoubleProperty maxVScrollProperty() {
163  return maxY.getReadOnlyProperty();
164  }
168  private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0);
169 
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;
177 
178  //add a dummy series or the chart is never rendered
179  setData(FXCollections.observableList(Arrays.asList(new Series<DateTime, Y>())));
180 
181  Tooltip.install(this, AbstractTimelineChart.getDefaultTooltip());
182 
183  dateAxis.setAutoRanging(false);
184  setLegendVisible(false);
185  setPadding(Insets.EMPTY);
186  setAlternativeColumnFillVisible(true);
187 
188  sceneProperty().addListener(observable -> {
189  Scene scene = getScene();
190  if (scene != null && scene.getStylesheets().contains(STYLE_SHEET) == false) {
191  scene.getStylesheets().add(STYLE_SHEET);
192  }
193  });
194 
195  //add listener for events that should trigger layout
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);
202 
203  //all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly
204  getPlotChildren().add(nodeGroup);
205  }
206 
233  public double layoutEventBundleNodes(final Collection<? extends EventNodeBase<?>> nodes, final double minY) {
234  // map from y-ranges to maximum x
235  TreeRangeMap<Double, Double> maxXatY = TreeRangeMap.create();
236 
237  // maximum y values occupied by any of the given nodes, updated as nodes are layed out.
238  double localMax = minY;
239 
240  //for each node do a recursive layout to size it and then position it in first available slot
241  for (EventNodeBase<?> bundleNode : nodes) {
242  if (useQuickHideFilters && activeQuickHidefilters.contains(bundleNode.getDescription())) {
243  //if the node hiden is hidden by quick hide filter, hide it and skip layout
244  bundleNode.setVisible(false);
245  bundleNode.setManaged(false);
246  } else {
247  layoutBundleHelper(bundleNode);
248  //get computed height and width
249  double h = bundleNode.getBoundsInLocal().getHeight();
250  double w = bundleNode.getBoundsInLocal().getWidth();
251  //get left and right x coords from axis plus computed width
252  double xLeft = getXForEpochMillis(bundleNode.getStartMillis()) - bundleNode.getLayoutXCompensation();
253  double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP;
254 
255  //initial test position
256  double yTop = (layoutSettings.getOneEventPerRow())
257  ? (localMax + MINIMUM_EVENT_NODE_GAP)// if onePerRow, just put it at end
258  : computeYTop(minY, h, maxXatY, xLeft, xRight);
259 
260  localMax = Math.max(yTop + h, localMax);
261 
262  //animate node to new position
263  bundleNode.animateTo(xLeft, yTop);
264  }
265  }
266  return localMax; //return new max
267  }
268 
269  @Override
270  final public void requestChartLayout() {
271  super.requestChartLayout();
272  }
273 
274  double getXForEpochMillis(Long millis) {
275  DateTime dateTime = new DateTime(millis);
276  return getXAxis().getDisplayPosition(dateTime);
277  }
278 
279  @Deprecated
280  @Override
281  protected void dataItemAdded(Series<DateTime, Y> series, int itemIndex, Data<DateTime, Y> item) {
282  }
283 
284  @Deprecated
285  @Override
286  protected void dataItemRemoved(Data<DateTime, Y> item, Series<DateTime, Y> series) {
287  }
288 
289  @Deprecated
290  @Override
291  protected void dataItemChanged(Data<DateTime, Y> item) {
292  }
293 
294  @Deprecated
295  @Override
296  protected void seriesAdded(Series<DateTime, Y> series, int seriesIndex) {
297  }
298 
299  @Deprecated
300  @Override
301  protected void seriesRemoved(Series<DateTime, Y> series) {
302  }
303 
311  void addEvent(Y event) {
312  EventNodeBase<?> eventNode = createNode(this, event);
313  eventMap.put(event, eventNode);
314  Platform.runLater(() -> {
315  nodes.add(eventNode);
316  nodeGroup.getChildren().add(eventNode);
317  });
318  }
319 
327  void removeEvent(Y event) {
328  EventNodeBase<?> removedNode = eventMap.remove(event);
329  Platform.runLater(() -> {
330  nodes.remove(removedNode);
331  nodeGroup.getChildren().removeAll(removedNode);
332  });
333  }
334 
339  final Group nodeGroup = new Group();
340 
341  public synchronized void setVScroll(double vScrollValue) {
342  nodeGroup.setTranslateY(-vScrollValue);
343  }
344 
348  synchronized Iterable<EventNodeBase<?>> getAllNodes() {
349  return getNodes((x) -> true);
350  }
351 
355  synchronized Iterable<EventNodeBase<?>> getNodes(Predicate<EventNodeBase<?>> p) {
356  //use this recursive function to flatten the tree of nodes into an single stream.
357  Function<EventNodeBase<?>, Stream<EventNodeBase<?>>> stripeFlattener
358  = new Function<EventNodeBase<?>, Stream<EventNodeBase<?>>>() {
359  @Override
360  public Stream<EventNodeBase<?>> apply(EventNodeBase<?> node) {
361  return Stream.concat(
362  Stream.of(node),
363  node.getSubNodes().stream().flatMap(this::apply));
364  }
365  };
366 
367  return sortedNodes.stream()
368  .flatMap(stripeFlattener)
369  .filter(p).collect(Collectors.toList());
370  }
371 
389  double computeYTop(double yMin, double h, TreeRangeMap<Double, Double> maxXatY, double xLeft, double xRight) {
390  double yTop = yMin;
391  double yBottom = yTop + h;
392  //until the node is not overlapping any others try moving it down.
393  boolean overlapping = true;
394  while (overlapping) {
395  overlapping = false;
396  //check each pixel from bottom to top.
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) {
400  //if that pixel is already used
401  //jump top to this y value and repeat until free slot is found.
402  overlapping = true;
403  yTop = y + MINIMUM_EVENT_NODE_GAP;
404  yBottom = yTop + h;
405  break;
406  }
407  }
408  }
409  maxXatY.put(Range.closed(yTop, yBottom), xRight);
410  return yTop;
411  }
412 
418  void layoutBundleHelper(final EventNodeBase< ?> eventNode) {
419  //make sure it is shown
420  eventNode.setVisible(true);
421  eventNode.setManaged(true);
422  //apply advanced layout description visibility options
423  eventNode.setDescriptionVisibility(layoutSettings.getDescrVisibility());
424  eventNode.setMaxDescriptionWidth(descriptionWidth);
425 
426  //do recursive layout
427  eventNode.layoutChildren();
428  }
429 
430  abstract void doAdditionalLayout();
431 
432  DetailsChart getParentChart() {
433  return parentChart;
434  }
435 }

Copyright © 2012-2016 Basis Technology. Generated on: Mon Apr 24 2017
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.