Autopsy  4.21.0
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-2019 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.property.ReadOnlyDoubleProperty;
38 import javafx.beans.property.ReadOnlyDoubleWrapper;
39 import javafx.collections.FXCollections;
40 import javafx.collections.ObservableList;
41 import javafx.geometry.Insets;
42 import javafx.scene.Cursor;
43 import javafx.scene.Group;
44 import javafx.scene.Scene;
45 import javafx.scene.chart.Axis;
46 import javafx.scene.chart.XYChart;
47 import javafx.scene.control.ContextMenu;
48 import javafx.scene.control.Tooltip;
49 import javafx.scene.input.MouseEvent;
50 import static javafx.scene.layout.Region.USE_PREF_SIZE;
51 import org.joda.time.DateTime;
62 import org.sleuthkit.datamodel.TskCoreException;
63 
73 abstract class DetailsChartLane<Y extends DetailViewEvent> 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 
99  final InvalidationListener layoutInvalidationListener = observable -> layoutPlotChildren();
100 
101  boolean quickHideFiltersEnabled() {
102  return useQuickHideFilters;
103  }
104 
105  @Override
106  public void clearContextMenu() {
107  parentChart.clearContextMenu();
108  }
109 
110  @Override
111  public ContextMenu getContextMenu(MouseEvent clickEvent) {
112  return parentChart.getContextMenu(clickEvent);
113  }
114 
115  EventNodeBase<?> createNode(DetailsChartLane<?> chart, DetailViewEvent event) throws TskCoreException {
116  if (event.getEventIDs().size() == 1) {
117  return new SingleEventNode(this, new SingleDetailsViewEvent(controller.getEventsModel().getEventById(Iterables.getOnlyElement(event.getEventIDs()))), null);
118  } else if (event instanceof SingleDetailsViewEvent) {
119  return new SingleEventNode(chart, (SingleDetailsViewEvent) event, null);
120  } else if (event instanceof EventCluster) {
121  return new EventClusterNode(chart, (EventCluster) event, null);
122  } else {
123  return new EventStripeNode(chart, (EventStripe) event, null);
124  }
125  }
126 
127  @Override
128  synchronized protected void layoutPlotChildren() {
129  setCursor(Cursor.WAIT);
130  if (useQuickHideFilters) {
131  //These don't change during a layout pass and are expensive to compute per node. So we do it once at the start
132  activeQuickHidefilters = getController().getQuickHideFilters().stream()
133  .filter(FilterState<DescriptionFilter>::isActive)
134  .map(FilterState<DescriptionFilter>::getFilter)
136  .collect(Collectors.toSet());
137  }
138  //This dosn't change during a layout pass and is expensive to compute per node. So we do it once at the start
139  descriptionWidth = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE;
140 
141  if (layoutSettings.getBandByType()) {
142  maxY.set(0);
143  sortedNodes.stream()
144  .collect(Collectors.groupingBy(EventNodeBase<?>::getEventType)).values()
145  .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get())));
146  } else {
147  maxY.set(layoutEventBundleNodes(sortedNodes, 0));
148  }
149  doAdditionalLayout();
150  setCursor(null);
151  }
152 
153  @Override
154  public TimeLineController getController() {
155  return controller;
156  }
157 
158  public ObservableList<EventNodeBase<?>> getSelectedNodes() {
159  return selectedNodes;
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<>())));
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) throws TskCoreException {
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(dummy -> true);
350  }
351 
355  private synchronized Iterable<EventNodeBase<?>> getNodes(Predicate<EventNodeBase<?>> predicate) {
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(predicate).collect(Collectors.toList());
370  }
371 
387  double computeYTop(double yMin, double h, TreeRangeMap<Double, Double> maxXatY, double xLeft, double xRight) {
388  double yTop = yMin;
389  double yBottom = yTop + h;
390  //until the node is not overlapping any others try moving it down.
391  boolean overlapping = true;
392  while (overlapping) {
393  overlapping = false;
394  //check each pixel from bottom to top.
395  for (double y = yBottom; y >= yTop; y -= MINIMUM_ROW_HEIGHT) {
396  final Double maxX = maxXatY.get(y);
397  if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) {
398  //if that pixel is already used
399  //jump top to this y value and repeat until free slot is found.
400  overlapping = true;
401  yTop = y + MINIMUM_EVENT_NODE_GAP;
402  yBottom = yTop + h;
403  break;
404  }
405  }
406  }
407  maxXatY.put(Range.closed(yTop, yBottom), xRight);
408  return yTop;
409  }
410 
416  void layoutBundleHelper(final EventNodeBase< ?> eventNode) {
417  //make sure it is shown
418  eventNode.setVisible(true);
419  eventNode.setManaged(true);
420  //apply advanced layout description visibility options
421  eventNode.setDescriptionVisibility(layoutSettings.getDescrVisibility());
422  eventNode.setMaxDescriptionWidth(descriptionWidth);
423 
424  //do recursive layout
425  eventNode.layoutChildren();
426  }
427 
428  abstract void doAdditionalLayout();
429 
430  DetailsChart getParentChart() {
431  return parentChart;
432  }
433 }
abstract List< EventNodeBase<?> > getSubNodes()

Copyright © 2012-2022 Basis Technology. Generated on: Tue Feb 6 2024
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.