Autopsy  4.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
AbstractVisualizationPane.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2014-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;
20 
21 import com.google.common.eventbus.Subscribe;
22 import java.util.Collections;
23 import java.util.Comparator;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.concurrent.ExecutionException;
28 import java.util.logging.Level;
29 import javafx.application.Platform;
30 import javafx.beans.InvalidationListener;
31 import javafx.beans.Observable;
32 import javafx.beans.property.SimpleBooleanProperty;
33 import javafx.collections.FXCollections;
34 import javafx.collections.ListChangeListener;
35 import javafx.collections.ObservableList;
36 import javafx.concurrent.Service;
37 import javafx.concurrent.Task;
38 import javafx.geometry.Pos;
39 import javafx.scene.Cursor;
40 import javafx.scene.Node;
41 import javafx.scene.chart.Axis;
42 import javafx.scene.chart.Chart;
43 import javafx.scene.chart.XYChart;
44 import javafx.scene.control.Label;
45 import javafx.scene.control.OverrunStyle;
46 import javafx.scene.control.Tooltip;
47 import javafx.scene.effect.Effect;
48 import javafx.scene.input.MouseButton;
49 import javafx.scene.input.MouseEvent;
50 import javafx.scene.layout.BorderPane;
51 import javafx.scene.layout.Pane;
52 import javafx.scene.layout.Region;
53 import javafx.scene.layout.StackPane;
54 import javafx.scene.text.Font;
55 import javafx.scene.text.FontWeight;
56 import javafx.scene.text.Text;
57 import javafx.scene.text.TextAlignment;
58 import javax.annotation.concurrent.Immutable;
59 import org.apache.commons.lang3.StringUtils;
60 import org.controlsfx.control.MaskerPane;
61 import org.joda.time.Interval;
62 import org.openide.util.NbBundle;
70 
85 public abstract class AbstractVisualizationPane<X, Y, NodeType extends Node, ChartType extends XYChart<X, Y> & TimeLineChart<X>> extends BorderPane {
86 
87  @NbBundle.Messages("AbstractVisualization.Default_Tooltip.text=Drag the mouse to select a time interval to zoom into.\nRight-click for more actions.")
88  private static final Tooltip DEFAULT_TOOLTIP = new Tooltip(Bundle.AbstractVisualization_Default_Tooltip_text());
89  private static final Logger LOGGER = Logger.getLogger(AbstractVisualizationPane.class.getName());
90 
91  public static Tooltip getDefaultTooltip() {
92  return DEFAULT_TOOLTIP;
93  }
94 
95  protected final SimpleBooleanProperty hasEvents = new SimpleBooleanProperty(true);
96 
100  protected final ObservableList<XYChart.Series<X, Y>> dataSeries = FXCollections.<XYChart.Series<X, Y>>observableArrayList();
101  protected final Map<EventType, XYChart.Series<X, Y>> eventTypeToSeriesMap = new HashMap<>();
102 
103  protected ChartType chart;
104 
106  private final Pane leafPane; // container for the leaf lables in the declutterd axis
107  private final Pane branchPane;// container for the branch lables in the declutterd axis
108  protected final Region spacer;
109 
113  private Task<Boolean> updateTask;
114 
116 
118 
119  final protected ObservableList<NodeType> selectedNodes = FXCollections.observableArrayList();
120 
121  private InvalidationListener invalidationListener = (Observable observable) -> {
122  update();
123  };
124 
125  public ObservableList<NodeType> getSelectedNodes() {
126  return selectedNodes;
127  }
128 
133  protected List<Node> settingsNodes;
134 
139  protected List<Node> getSettingsNodes() {
140  return Collections.unmodifiableList(settingsNodes);
141  }
142 
149  abstract protected Boolean isTickBold(X value);
150 
158  abstract protected void applySelectionEffect(NodeType node, Boolean applied);
159 
164  abstract protected Task<Boolean> getUpdateTask();
165 
171  abstract protected Effect getSelectionEffect();
172 
178  abstract protected String getTickMarkLabel(X tickValue);
179 
186  abstract protected double getTickSpacing();
187 
191  abstract protected Axis<X> getXAxis();
192 
196  abstract protected Axis<Y> getYAxis();
197 
198  abstract protected void resetData();
199 
208  final synchronized public void update() {
209  if (updateTask != null) {
210  updateTask.cancel(true);
211  updateTask = null;
212  }
213  updateTask = getUpdateTask();
214  updateTask.stateProperty().addListener((Observable observable) -> {
215  switch (updateTask.getState()) {
216  case CANCELLED:
217  case FAILED:
218  case READY:
219  case RUNNING:
220  case SCHEDULED:
221  break;
222  case SUCCEEDED:
223  try {
224  this.hasEvents.set(updateTask.get());
225  } catch (InterruptedException | ExecutionException ex) {
226  LOGGER.log(Level.SEVERE, "Unexpected exception updating visualization", ex); //NOI18N NON-NLS
227  }
228  break;
229  }
230  });
231  controller.monitorTask(updateTask);
232  }
233 
234  final synchronized public void dispose() {
235  if (updateTask != null) {
236  updateTask.cancel(true);
237  }
238  this.filteredEvents.zoomParametersProperty().removeListener(invalidationListener);
239  invalidationListener = null;
240  }
241 
245  protected final void createSeries() {
246  for (EventType eventType : EventType.allTypes) {
247  XYChart.Series<X, Y> series = new XYChart.Series<>();
248  series.setName(eventType.getDisplayName());
249  eventTypeToSeriesMap.put(eventType, series);
250  dataSeries.add(series);
251  }
252  }
253 
261  protected final XYChart.Series<X, Y> getSeries(final EventType et) {
262  return eventTypeToSeriesMap.get(et);
263  }
264 
265  protected AbstractVisualizationPane(TimeLineController controller, Pane partPane, Pane contextPane, Region spacer) {
266  this.controller = controller;
267  this.filteredEvents = controller.getEventsModel();
268  this.filteredEvents.registerForEvents(this);
269  this.filteredEvents.zoomParametersProperty().addListener(invalidationListener);
270  this.leafPane = partPane;
271  this.branchPane = contextPane;
272  this.spacer = spacer;
273 
274  createSeries();
275 
276  selectedNodes.addListener((ListChangeListener.Change<? extends NodeType> c) -> {
277  while (c.next()) {
278  c.getRemoved().forEach(n -> applySelectionEffect(n, false));
279  c.getAddedSubList().forEach(n -> applySelectionEffect(n, true));
280  }
281  });
282 
283  TimeLineController.getTimeZone().addListener(invalidationListener);
284 
285  //show tooltip text in status bar
286  hoverProperty().addListener(observable -> controller.setStatus(isHover() ? DEFAULT_TOOLTIP.getText() : ""));
287 
288  }
289 
290  @Subscribe
292  update();
293  }
294 
312  public synchronized void layoutDateLabels() {
313 
314  //clear old labels
315  branchPane.getChildren().clear();
316  leafPane.getChildren().clear();
317  //since the tickmarks aren't necessarily in value/position order,
318  //make a clone of the list sorted by position along axis
319  ObservableList<Axis.TickMark<X>> tickMarks = FXCollections.observableArrayList(getXAxis().getTickMarks());
320  tickMarks.sort(Comparator.comparing(Axis.TickMark::getPosition));
321 
322  if (tickMarks.isEmpty() == false) {
323  //get the spacing between ticks in the underlying axis
324  double spacing = getTickSpacing();
325 
326  //initialize values from first tick
327  TwoPartDateTime dateTime = new TwoPartDateTime(getTickMarkLabel(tickMarks.get(0).getValue()));
328  String lastSeenBranchLabel = dateTime.branch;
329  //cumulative width of the current branch label
330 
331  //x-positions (pixels) of the current branch and leaf labels
332  double leafLabelX = 0;
333 
334  if (dateTime.branch.isEmpty()) {
335  //if there is only one part to the date (ie only year), just add a label for each tick
336  for (Axis.TickMark<X> t : tickMarks) {
337  assignLeafLabel(new TwoPartDateTime(getTickMarkLabel(t.getValue())).leaf,
338  spacing,
339  leafLabelX,
340  isTickBold(t.getValue())
341  );
342 
343  leafLabelX += spacing; //increment x
344  }
345  } else {
346  //there are two parts so ...
347  //initialize additional state
348  double branchLabelX = 0;
349  double branchLabelWidth = 0;
350 
351  for (Axis.TickMark<X> t : tickMarks) { //for each tick
352 
353  //split the label into a TwoPartDateTime
354  dateTime = new TwoPartDateTime(getTickMarkLabel(t.getValue()));
355 
356  //if we are still on the same branch
357  if (lastSeenBranchLabel.equals(dateTime.branch)) {
358  //increment branch width
359  branchLabelWidth += spacing;
360  } else {// we are on to a new branch, so ...
361  assignBranchLabel(lastSeenBranchLabel, branchLabelWidth, branchLabelX);
362  //and then update label, x-pos, and width
363  lastSeenBranchLabel = dateTime.branch;
364  branchLabelX += branchLabelWidth;
365  branchLabelWidth = spacing;
366  }
367  //add the label for the leaf (highest frequency part)
368  assignLeafLabel(dateTime.leaf, spacing, leafLabelX, isTickBold(t.getValue()));
369 
370  //increment leaf position
371  leafLabelX += spacing;
372  }
373  //we have reached end so add branch label for current branch
374  assignBranchLabel(lastSeenBranchLabel, branchLabelWidth, branchLabelX);
375  }
376  }
377  //request layout since we have modified scene graph structure
378  requestParentLayout();
379  }
380 
381  protected void setChartClickHandler() {
382  chart.addEventHandler(MouseEvent.MOUSE_CLICKED, (MouseEvent event) -> {
383  if (event.getButton() == MouseButton.PRIMARY && event.isStillSincePress()) {
384  selectedNodes.clear();
385  }
386  });
387  }
388 
398  private synchronized void assignLeafLabel(String labelText, double labelWidth, double labelX, boolean bold) {
399 
400  Text label = new Text(" " + labelText + " "); //NOI18N
401  label.setTextAlignment(TextAlignment.CENTER);
402  label.setFont(Font.font(null, bold ? FontWeight.BOLD : FontWeight.NORMAL, 10));
403  //position label accounting for width
404  label.relocate(labelX + labelWidth / 2 - label.getBoundsInLocal().getWidth() / 2, 0);
405  label.autosize();
406 
407  if (leafPane.getChildren().isEmpty()) {
408  //just add first label
409  leafPane.getChildren().add(label);
410  } else {
411  //otherwise don't actually add the label if it would intersect with previous label
412  final Text lastLabel = (Text) leafPane.getChildren().get(leafPane.getChildren().size() - 1);
413 
414  if (!lastLabel.getBoundsInParent().intersects(label.getBoundsInParent())) {
415  leafPane.getChildren().add(label);
416  }
417  }
418  }
419 
428  private synchronized void assignBranchLabel(String labelText, double labelWidth, double labelX) {
429 
430  Label label = new Label(labelText);
431  label.setAlignment(Pos.CENTER);
432  label.setTextAlignment(TextAlignment.CENTER);
433  label.setFont(Font.font(10));
434  //use a leading ellipse since that is the lowest frequency part,
435  //and can be infered more easily from other surrounding labels
436  label.setTextOverrun(OverrunStyle.LEADING_ELLIPSIS);
437  //force size
438  label.setMinWidth(labelWidth);
439  label.setPrefWidth(labelWidth);
440  label.setMaxWidth(labelWidth);
441  label.relocate(labelX, 0);
442 
443  if (labelX == 0) { // first label has no border
444  label.setStyle("-fx-border-width: 0 0 0 0 ; -fx-border-color:black;"); // NON-NLS //NOI18N
445  } else { // subsequent labels have border on left to create dividers
446  label.setStyle("-fx-border-width: 0 0 0 1; -fx-border-color:black;"); // NON-NLS //NOI18N
447  }
448 
449  branchPane.getChildren().add(label);
450  }
451 
461  @Immutable
462  private static final class TwoPartDateTime {
463 
467  private final String branch;
468 
472  private final String leaf;
473 
474  TwoPartDateTime(String dateString) {
475  //find index of separator to spit on
476  int splitIndex = StringUtils.lastIndexOfAny(dateString, " ", "-", ":"); //NOI18N
477  if (splitIndex < 0) { // there is only one part
478  leaf = dateString;
479  branch = ""; //NOI18N
480  } else { //split at index
481  leaf = StringUtils.substring(dateString, splitIndex + 1);
482  branch = StringUtils.substring(dateString, 0, splitIndex);
483  }
484  }
485  }
486 
487  protected Interval getTimeRange() {
488  return filteredEvents.timeRangeProperty().get();
489  }
490 
491  abstract protected class VisualizationUpdateTask<AxisValuesType> extends LoggedTask<Boolean> {
492 
493  protected VisualizationUpdateTask(String taskName, boolean logStateChanges) {
494  super(taskName, logStateChanges);
495  }
496 
506  @NbBundle.Messages({"VisualizationUpdateTask.preparing=Analyzing zoom and filter settings"})
507  @Override
508  protected Boolean call() throws Exception {
509  updateProgress(-1, 1);
510  updateMessage(Bundle.VisualizationUpdateTask_preparing());
511  Platform.runLater(() -> {
512  MaskerPane maskerPane = new MaskerPane();
513  maskerPane.textProperty().bind(messageProperty());
514  maskerPane.progressProperty().bind(progressProperty());
515  setCenter(new StackPane(chart, maskerPane));
516  setCursor(Cursor.WAIT);
517  });
518 
519  return true;
520  }
521 
527  @Override
528  protected void succeeded() {
529  super.succeeded();
530  layoutDateLabels();
531 
532  Platform.runLater(() -> {
533  setCenter(chart); //clear masker pane
534  setCursor(Cursor.DEFAULT);
535  });
536  }
537 
545  protected void resetChart(AxisValuesType axisValues) {
546  resetData();
547  Platform.runLater(() -> {
548  setDateAxisValues(axisValues);
549  });
550 
551  }
552 
553  abstract protected void setDateAxisValues(AxisValuesType values);
554  }
555 }
AbstractVisualizationPane(TimeLineController controller, Pane partPane, Pane contextPane, Region spacer)
static ReadOnlyObjectProperty< TimeZone > getTimeZone()
final Map< EventType, XYChart.Series< X, Y > > eventTypeToSeriesMap
synchronized ReadOnlyObjectProperty< ZoomParams > zoomParametersProperty()
synchronized void assignLeafLabel(String labelText, double labelWidth, double labelX, boolean bold)
synchronized void assignBranchLabel(String labelText, double labelWidth, double labelX)
synchronized void monitorTask(final Task<?> task)
final XYChart.Series< X, Y > getSeries(final EventType et)
synchronized static Logger getLogger(String name)
Definition: Logger.java:166
abstract void applySelectionEffect(NodeType node, Boolean applied)
synchronized ReadOnlyObjectProperty< Interval > timeRangeProperty()

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.