Autopsy  4.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
EventDetailsChart.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2013-15 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.Range;
22 import com.google.common.collect.TreeRangeMap;
23 import java.util.Arrays;
24 import java.util.Collection;
25 import java.util.Collections;
26 import java.util.Comparator;
27 import java.util.Map;
28 import java.util.MissingResourceException;
29 import java.util.Set;
30 import java.util.concurrent.ConcurrentHashMap;
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.beans.property.SimpleBooleanProperty;
41 import javafx.beans.property.SimpleDoubleProperty;
42 import javafx.beans.property.SimpleObjectProperty;
43 import javafx.collections.FXCollections;
44 import javafx.collections.ListChangeListener;
45 import javafx.collections.ObservableList;
46 import javafx.event.ActionEvent;
47 import javafx.geometry.Insets;
48 import javafx.scene.Cursor;
49 import javafx.scene.Group;
50 import javafx.scene.Scene;
51 import javafx.scene.chart.Axis;
52 import javafx.scene.chart.NumberAxis;
53 import javafx.scene.chart.XYChart;
54 import javafx.scene.control.ContextMenu;
55 import javafx.scene.control.Tooltip;
56 import javafx.scene.image.Image;
57 import javafx.scene.image.ImageView;
58 import javafx.scene.input.MouseEvent;
59 import javafx.scene.shape.Line;
60 import javafx.scene.shape.StrokeLineCap;
61 import org.controlsfx.control.action.Action;
62 import org.controlsfx.control.action.ActionUtils;
63 import org.joda.time.DateTime;
64 import org.joda.time.Interval;
65 import org.openide.util.NbBundle;
77 
97 public final class EventDetailsChart extends XYChart<DateTime, EventStripe> implements TimeLineChart<DateTime> {
98 
99  private static final String styleSheet = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //NON-NLS
100  private static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS
101  private static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS
102  private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); //NON-NLS
103  private static final int PROJECTED_LINE_Y_OFFSET = 5;
104  private static final int PROJECTED_LINE_STROKE_WIDTH = 5;
105  private static final int MINIMUM_EVENT_NODE_GAP = 4;
106  private final static int MINIMUM_ROW_HEIGHT = 24;
107 
110 
111  private ContextMenu chartContextMenu;
112 
113  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass
114  private Set<String> activeQuickHidefilters;
115  @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass
116  private double descriptionWidth;
117 
118  @Override
119  public ContextMenu getChartContextMenu() {
120  return chartContextMenu;
121  }
122 
127  private Line guideLine;
128 
134  private IntervalSelector<? extends DateTime> intervalSelector;
135 
139  private final InvalidationListener layoutInvalidationListener = (Observable o) -> {
141  };
142 
146  private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0);
147 
148  final ObservableList<EventBundleNodeBase<?, ?, ?>> selectedNodes;
153  private final Group nodeGroup = new Group();
154 
156  private final ObservableList<EventStripe> eventStripes = FXCollections.observableArrayList();
157  private final ObservableList< EventStripeNode> stripeNodes = FXCollections.observableArrayList();
158  private final ObservableList< EventStripeNode> sortedStripeNodes = stripeNodes.sorted(Comparator.comparing(EventStripeNode::getStartMillis));
159  private final Map<EventCluster, Line> projectionMap = new ConcurrentHashMap<>();
160 
165  private final SimpleBooleanProperty bandByType = new SimpleBooleanProperty(false);
171  private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false);
172 
176  private final SimpleObjectProperty<DescriptionVisibility> descrVisibility =
177  new SimpleObjectProperty<>(DescriptionVisibility.SHOWN);
178 
185  final SimpleBooleanProperty truncateAll = new SimpleBooleanProperty(false);
186 
191  final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0);
192 
193  EventDetailsChart(TimeLineController controller, DateAxis dateAxis, final Axis<EventStripe> verticalAxis, ObservableList<EventBundleNodeBase<?, ?, ?>> selectedNodes) {
194  super(dateAxis, verticalAxis);
195 
196  this.controller = controller;
197  this.filteredEvents = this.controller.getEventsModel();
198 
199  sceneProperty().addListener(observable -> {
200  Scene scene = getScene();
201  if (scene != null && scene.getStylesheets().contains(styleSheet) == false) {
202  scene.getStylesheets().add(styleSheet);
203  }
204  });
205 
206  filteredEvents.zoomParametersProperty().addListener(o -> {
207  clearGuideLine();
209  selectedNodes.clear();
210  projectionMap.clear();
211  controller.selectEventIDs(Collections.emptyList());
212  });
213 
214  Tooltip.install(this, AbstractVisualizationPane.getDefaultTooltip());
215 
216  dateAxis.setAutoRanging(false);
217  verticalAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm
218  verticalAxis.setTickLabelsVisible(false);
219  verticalAxis.setTickMarkVisible(false);
220  setLegendVisible(false);
221  setPadding(Insets.EMPTY);
222  setAlternativeColumnFillVisible(true);
223 
224  //all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly
225  getPlotChildren().add(nodeGroup);
226 
227  //add listener for events that should trigger layout
230  truncateAll.addListener(layoutInvalidationListener);
231  truncateWidth.addListener(layoutInvalidationListener);
234 
235  //this is needed to allow non circular binding of the guideline and timerangeRect heights to the height of the chart
236  //TODO: seems like a hack, can we remove? -jm
237  boundsInLocalProperty().addListener((Observable observable) -> {
238  setPrefHeight(boundsInLocalProperty().get().getHeight());
239  });
240 
242  setOnMousePressed(chartDragHandler);
243  setOnMouseReleased(chartDragHandler);
244  setOnMouseDragged(chartDragHandler);
245 
246  setOnMouseClicked(new MouseClickedHandler<>(this));
247 
248  this.selectedNodes = selectedNodes;
249  this.selectedNodes.addListener(new SelectionChangeHandler());
250  }
251 
252  ObservableList<EventStripe> getEventStripes() {
253  return eventStripes;
254  }
255 
256  @Override
258  return controller;
259  }
260 
261  @Override
262  public ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException {
263  if (chartContextMenu != null) {
264  chartContextMenu.hide();
265  }
266 
267  chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new PlaceMarkerAction(clickEvent),
269  chartContextMenu.setAutoHide(true);
270  return chartContextMenu;
271  }
272 
273  @Override
274  public void clearIntervalSelector() {
275  getChartChildren().remove(intervalSelector);
276  intervalSelector = null;
277  }
278 
279  public synchronized SimpleBooleanProperty bandByTypeProperty() {
280  return bandByType;
281  }
282 
283  @Override
285  return new DetailIntervalSelector(this);
286  }
287 
288  synchronized void setBandByType(Boolean t1) {
289  bandByType.set(t1);
290  }
291 
301  public DateTime getDateTimeForPosition(double x) {
302  return getXAxis().getValueForDisplay(getXAxis().parentToLocal(x, 0).getX());
303  }
304 
305  @Override
307  return intervalSelector;
308  }
309 
310  @Override
313  getChartChildren().add(getIntervalSelector());
314  }
315 
316  SimpleBooleanProperty oneEventPerRowProperty() {
317  return oneEventPerRow;
318  }
319 
320  SimpleDoubleProperty getTruncateWidth() {
321  return truncateWidth;
322  }
323 
324  SimpleBooleanProperty truncateAllProperty() {
325  return truncateAll;
326  }
327 
328  SimpleObjectProperty< DescriptionVisibility> descrVisibilityProperty() {
329  return descrVisibility;
330  }
331 
338  @Override
339  protected void seriesAdded(Series<DateTime, EventStripe> series, int i) {
340 
341  }
342 
348  @Override
349  protected void seriesRemoved(Series<DateTime, EventStripe> series) {
350 
351  }
352 
360  @Override
361  protected void dataItemAdded(Series<DateTime, EventStripe> series, int itemIndex, Data<DateTime, EventStripe> item) {
362  }
363 
371  @Override
372  protected void dataItemRemoved(Data<DateTime, EventStripe> item, Series<DateTime, EventStripe> series) {
373  }
374 
380  @Override
381  protected void dataItemChanged(Data<DateTime, EventStripe> item) {
382  }
383 
391  void addDataItem(Data<DateTime, EventStripe> data) {
392  final EventStripe eventStripe = data.getYValue();
393 
394  EventStripeNode stripeNode = new EventStripeNode(EventDetailsChart.this, eventStripe, null);
395 
396  Platform.runLater(() -> {
397  eventStripes.add(eventStripe);
398  stripeNodes.add(stripeNode);
399  nodeGroup.getChildren().add(stripeNode);
400  data.setNode(stripeNode);
401  });
402  }
403 
411  void removeDataItem(Data<DateTime, EventStripe> data) {
412  Platform.runLater(() -> {
413  EventStripeNode removedNode = (EventStripeNode) data.getNode();
414  eventStripes.removeAll(new StripeFlattener().apply(removedNode).collect(Collectors.toList()));
415  stripeNodes.removeAll(removedNode);
416  nodeGroup.getChildren().removeAll(removedNode);
417  data.setNode(null);
418  });
419  }
420 
421  @Override
422  protected void layoutPlotChildren() {
423  setCursor(Cursor.WAIT);
424  maxY.set(0);
425 
426  //These don't change during a layout pass and are expensive to compute per node. So we do it once at the start
428  .filter(AbstractFilter::isActive)
430  .collect(Collectors.toSet());
431 
432  //This dosn't change during a layout pass and is expensive to compute per node. So we do it once at the start
433  descriptionWidth = truncateAll.get() ? truncateWidth.get() : USE_PREF_SIZE;
434 
435  if (bandByType.get()) {
436  sortedStripeNodes.stream()
437  .collect(Collectors.groupingBy(EventStripeNode::getEventType)).values()
438  .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get())));
439  } else {
440  maxY.set(layoutEventBundleNodes(sortedStripeNodes.sorted(Comparator.comparing(EventStripeNode::getStartMillis)), 0));
441  }
443  setCursor(null);
444  }
445 
446  ReadOnlyDoubleProperty maxVScrollProperty() {
447  return maxY.getReadOnlyProperty();
448  }
449 
453  synchronized Iterable<EventBundleNodeBase<?, ?, ?>> getNodes(Predicate<EventBundleNodeBase<?, ?, ?>> p) {
454  //use this recursive function to flatten the tree of nodes into an single stream.
455  Function<EventBundleNodeBase<?, ?, ?>, Stream<EventBundleNodeBase<?, ?, ?>>> stripeFlattener =
456  new Function<EventBundleNodeBase<?, ?, ?>, Stream<EventBundleNodeBase<?, ?, ?>>>() {
457  @Override
458  public Stream<EventBundleNodeBase<?, ?, ?>> apply(EventBundleNodeBase<?, ?, ?> node) {
459  return Stream.concat(
460  Stream.of(node),
461  node.getSubNodes().stream().flatMap(this::apply));
462  }
463  };
464 
465  return sortedStripeNodes.stream()
466  .flatMap(stripeFlattener)
467  .filter(p).collect(Collectors.toList());
468  }
469 
470  synchronized void setVScroll(double vScrollValue) {
471  nodeGroup.setTranslateY(-vScrollValue);
472  }
473 
474  void clearGuideLine() {
475  getChartChildren().remove(guideLine);
476  guideLine = null;
477  }
478 
509  double layoutEventBundleNodes(final Collection<? extends EventBundleNodeBase<?, ?, ?>> nodes, final double minY) {
510  // map from y-ranges to maximum x
511  TreeRangeMap<Double, Double> maxXatY = TreeRangeMap.create();
512 
513  // maximum y values occupied by any of the given nodes, updated as nodes are layed out.
514  double localMax = minY;
515 
516  //for each node do a recursive layout to size it and then position it in first available slot
517  for (EventBundleNodeBase<?, ?, ?> bundleNode : nodes) {
518  //is the node hiden by a quick hide filter?
519  boolean quickHide = activeQuickHidefilters.contains(bundleNode.getDescription());
520  if (quickHide) {
521  //hide it and skip layout
522  bundleNode.setVisible(false);
523  bundleNode.setManaged(false);
524  } else {
525  layoutBundleHelper(bundleNode);
526  //get computed height and width
527  double h = bundleNode.getBoundsInLocal().getHeight();
528  double w = bundleNode.getBoundsInLocal().getWidth();
529  //get left and right x coords from axis plus computed width
530  double xLeft = getXForEpochMillis(bundleNode.getStartMillis()) - bundleNode.getLayoutXCompensation();
531  double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP;
532 
533  //initial test position
534  double yTop = (oneEventPerRow.get())
535  ? (localMax + MINIMUM_EVENT_NODE_GAP)// if onePerRow, just put it at end
536  : computeYTop(minY, h, maxXatY, xLeft, xRight);
537 
538  localMax = Math.max(yTop + h, localMax);
539 
540  if ((xLeft != bundleNode.getLayoutX()) || (yTop != bundleNode.getLayoutY())) {
541  //animate node to new position
542  bundleNode.animateTo(xLeft, yTop);
543  }
544  }
545  }
546  return localMax; //return new max
547  }
548 
566  private double computeYTop(double yMin, double h, TreeRangeMap<Double, Double> maxXatY, double xLeft, double xRight) {
567  double yTop = yMin;
568  double yBottom = yTop + h;
569  //until the node is not overlapping any others try moving it down.
570  boolean overlapping = true;
571  while (overlapping) {
572  overlapping = false;
573  //check each pixel from bottom to top.
574  for (double y = yBottom; y >= yTop; y -= MINIMUM_ROW_HEIGHT) {
575  final Double maxX = maxXatY.get(y);
576  if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) {
577  //if that pixel is already used
578  //jump top to this y value and repeat until free slot is found.
579  overlapping = true;
580  yTop = y + MINIMUM_EVENT_NODE_GAP;
581  yBottom = yTop + h;
582  break;
583  }
584  }
585  }
586  maxXatY.put(Range.closed(yTop, yBottom), xRight);
587  return yTop;
588  }
589 
597  private void layoutBundleHelper(final EventBundleNodeBase<?, ?, ?> bundleNode) {
598  //make sure it is shown
599  bundleNode.setVisible(true);
600  bundleNode.setManaged(true);
601  //apply advanced layout description visibility options
602  bundleNode.setDescriptionVisibility(descrVisibility.get());
603  bundleNode.setMaxDescriptionWidth(descriptionWidth);
604 
605  //do recursive layout
606  bundleNode.layoutChildren();
607  }
608 
612  @Override
613  protected void requestChartLayout() {
614  super.requestChartLayout();
615  }
616 
617  private double getXForEpochMillis(Long millis) {
618  DateTime dateTime = new DateTime(millis);
619  return getXAxis().getDisplayPosition(dateTime);
620  }
621 
622  private double getParentXForEpochMillis(Long epochMillis) {
623  return getXAxis().localToParent(getXForEpochMillis(epochMillis), 0).getX();
624  }
625 
626  private void layoutProjectionMap() {
627  for (final Map.Entry<EventCluster, Line> entry : projectionMap.entrySet()) {
628  final EventCluster cluster = entry.getKey();
629  final Line line = entry.getValue();
630 
631  line.setStartX(getParentXForEpochMillis(cluster.getStartMillis()));
632  line.setEndX(getParentXForEpochMillis(cluster.getEndMillis()));
633 
634  line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
635  line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
636  }
637  }
638 
643  return filteredEvents;
644 
645  }
646 
647  static private class DetailIntervalSelector extends IntervalSelector<DateTime> {
648 
650  super(chart);
651  }
652 
653  @Override
654  protected String formatSpan(DateTime date) {
655  return date.toString(TimeLineController.getZonedFormatter());
656  }
657 
658  @Override
659  protected Interval adjustInterval(Interval i) {
660  return i;
661  }
662 
663  @Override
664  protected DateTime parseDateTime(DateTime date) {
665  return date;
666  }
667  }
668 
669  private class PlaceMarkerAction extends Action {
670 
671  @NbBundle.Messages({"EventDetailChart.chartContextMenu.placeMarker.name=Place Marker"})
672  PlaceMarkerAction(MouseEvent clickEvent) {
673  super(Bundle.EventDetailChart_chartContextMenu_placeMarker_name());
674 
675  setGraphic(new ImageView(MARKER)); // NON-NLS
676  setEventHandler(actionEvent -> {
677  if (guideLine == null) {
678  guideLine = new GuideLine(EventDetailsChart.this);
679  guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0);
680  getChartChildren().add(guideLine);
681 
682  } else {
683  guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0);
684  }
685  });
686  }
687  }
688 
689  private class SelectionChangeHandler implements ListChangeListener<EventBundleNodeBase<?, ?, ?>> {
690 
691  private final Axis<DateTime> dateAxis;
692 
694  dateAxis = getXAxis();
695  }
696 
697  @Override
698  public void onChanged(ListChangeListener.Change<? extends EventBundleNodeBase<?, ?, ?>> change) {
699  while (change.next()) {
700  change.getRemoved().forEach((EventBundleNodeBase<?, ?, ?> removedNode) -> {
701  removedNode.getEventBundle().getClusters().forEach(cluster -> {
702  Line removedLine = projectionMap.remove(cluster);
703  getChartChildren().removeAll(removedLine);
704  });
705 
706  });
707  change.getAddedSubList().forEach((EventBundleNodeBase<?, ?, ?> addedNode) -> {
708 
709  for (EventCluster range : addedNode.getEventBundle().getClusters()) {
710 
711  Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET,
712  dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET
713  );
714  line.setStroke(addedNode.getEventType().getColor().deriveColor(0, 1, 1, .5));
715  line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH);
716  line.setStrokeLineCap(StrokeLineCap.ROUND);
717  projectionMap.put(range, line);
718  getChartChildren().add(line);
719  }
720  });
721  }
722  EventDetailsChart.this.controller.selectEventIDs(selectedNodes.stream()
723  .flatMap(detailNode -> detailNode.getEventIDs().stream())
724  .collect(Collectors.toList()));
725  }
726  }
727 
728  @NbBundle.Messages({"HideDescriptionAction.displayName=Hide",
729  "HideDescriptionAction.displayMsg=Hide this group from the details view."})
730  class HideDescriptionAction extends Action {
731 
732  HideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
733  super(Bundle.HideDescriptionAction_displayName());
734  setLongText(Bundle.HideDescriptionAction_displayMsg());
735  setGraphic(new ImageView(HIDE));
736  setEventHandler((ActionEvent t) -> {
737  final DescriptionFilter testFilter = new DescriptionFilter(
738  descriptionLoD,
739  description,
741 
742  DescriptionFilter descriptionFilter = getController().getQuickHideFilters().stream()
743  .filter(testFilter::equals)
744  .findFirst().orElseGet(() -> {
745  testFilter.selectedProperty().addListener(observable -> requestChartLayout());
746  getController().getQuickHideFilters().add(testFilter);
747  return testFilter;
748  });
749  descriptionFilter.setSelected(true);
750  });
751  }
752  }
753 
754  @NbBundle.Messages({"UnhideDescriptionAction.displayName=Unhide"})
755  class UnhideDescriptionAction extends Action {
756 
757  UnhideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
758  super(Bundle.UnhideDescriptionAction_displayName());
759  setGraphic(new ImageView(SHOW));
760  setEventHandler((ActionEvent t) ->
761  getController().getQuickHideFilters().stream()
762  .filter(descriptionFilter -> descriptionFilter.getDescriptionLoD().equals(descriptionLoD)
763  && descriptionFilter.getDescription().equals(description))
764  .forEach(descriptionfilter -> descriptionfilter.setSelected(false))
765  );
766  }
767  }
768 }
void dataItemRemoved(Data< DateTime, EventStripe > item, Series< DateTime, EventStripe > series)
synchronized ReadOnlyObjectProperty< ZoomParams > zoomParametersProperty()
void setIntervalSelector(IntervalSelector<?extends DateTime > newIntervalSelector)
void dataItemAdded(Series< DateTime, EventStripe > series, int itemIndex, Data< DateTime, EventStripe > item)
static ActionGroup newZoomHistoyActionGroup(TimeLineController controller)
double computeYTop(double yMin, double h, TreeRangeMap< Double, Double > maxXatY, double xLeft, double xRight)
void seriesRemoved(Series< DateTime, EventStripe > series)
void seriesAdded(Series< DateTime, EventStripe > series, int i)
final SimpleObjectProperty< DescriptionVisibility > descrVisibility
void layoutBundleHelper(final EventBundleNodeBase<?,?,?> bundleNode)
void onChanged(ListChangeListener.Change<?extends EventBundleNodeBase<?,?,?>> change)
ObservableList< DescriptionFilter > getQuickHideFilters()

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