Autopsy  4.20.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
AbstractTimelineChart.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2011-2018 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 java.util.Comparator;
22 import java.util.HashMap;
23 import java.util.Map;
24 import javafx.application.Platform;
25 import javafx.beans.binding.DoubleBinding;
26 import javafx.collections.FXCollections;
27 import javafx.collections.ListChangeListener;
28 import javafx.collections.ObservableList;
29 import javafx.collections.transformation.SortedList;
30 import javafx.geometry.Pos;
31 import javafx.scene.Node;
32 import javafx.scene.chart.Axis;
33 import javafx.scene.chart.XYChart;
34 import javafx.scene.control.Label;
35 import javafx.scene.control.OverrunStyle;
36 import javafx.scene.control.Tooltip;
37 import javafx.scene.layout.Border;
38 import javafx.scene.layout.BorderStroke;
39 import javafx.scene.layout.BorderStrokeStyle;
40 import javafx.scene.layout.BorderWidths;
41 import javafx.scene.layout.CornerRadii;
42 import javafx.scene.layout.HBox;
43 import javafx.scene.layout.Pane;
44 import javafx.scene.layout.Region;
45 import javafx.scene.layout.VBox;
46 import javafx.scene.paint.Color;
47 import javafx.scene.text.Font;
48 import javafx.scene.text.FontWeight;
49 import javafx.scene.text.Text;
50 import javafx.scene.text.TextAlignment;
51 import javax.annotation.concurrent.Immutable;
52 import org.apache.commons.lang3.StringUtils;
53 import org.openide.util.NbBundle;
57 import org.sleuthkit.datamodel.TimelineEventType;
58 
73 public abstract class AbstractTimelineChart<X, Y, NodeType extends Node, ChartType extends Region & TimeLineChart<X>> extends AbstractTimeLineView {
74 
75  private static final Logger logger = Logger.getLogger(AbstractTimelineChart.class.getName());
76 
77  @NbBundle.Messages("AbstractTimelineChart.defaultTooltip.text=Drag the mouse to select a time interval to zoom into.\nRight-click for more actions.")
78  private static final Tooltip DEFAULT_TOOLTIP = new Tooltip(Bundle.AbstractTimelineChart_defaultTooltip_text());
79  private static final Border ONLY_LEFT_BORDER = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(0, 0, 0, 1)));
80 
87  static public Tooltip getDefaultTooltip() {
88  return DEFAULT_TOOLTIP;
89  }
90 
97  protected ObservableList<NodeType> getSelectedNodes() {
98  return selectedNodes;
99  }
100 
104  protected final ObservableList<XYChart.Series<X, Y>> dataSeries = FXCollections.<XYChart.Series<X, Y>>observableArrayList();
105  protected final Map<TimelineEventType, XYChart.Series<X, Y>> eventTypeToSeriesMap = new HashMap<>();
106 
107  private ChartType chart;
108 
110  private final Pane specificLabelPane = new Pane(); // container for the specfic labels in the decluttered axis
111  private final Pane contextLabelPane = new Pane();// container for the contextual labels in the decluttered axis
112 // container for the contextual labels in the decluttered axis
113  private final Region spacer = new Region();
114 
115  final private ObservableList<NodeType> selectedNodes = FXCollections.observableArrayList();
116 
117  public Pane getSpecificLabelPane() {
118  return specificLabelPane;
119  }
120 
121  public Pane getContextLabelPane() {
122  return contextLabelPane;
123  }
124 
125  public Region getSpacer() {
126  return spacer;
127  }
128 
134  protected ChartType getChart() {
135  return chart;
136  }
137 
144  protected void setChart(ChartType chart) {
145  this.chart = chart;
146  setCenter(chart);
147  }
148 
154  protected void applySelectionEffect(NodeType node) {
155  applySelectionEffect(node, true);
156  }
157 
163  protected void removeSelectionEffect(NodeType node) {
164  applySelectionEffect(node, Boolean.FALSE);
165  }
166 
176  abstract protected Boolean isTickBold(X value);
177 
186  abstract protected void applySelectionEffect(NodeType node, Boolean applied);
187 
195  abstract protected String getTickMarkLabel(X tickValue);
196 
203  abstract protected double getTickSpacing();
204 
210  abstract protected Axis<X> getXAxis();
211 
217  abstract protected Axis<Y> getYAxis();
218 
226  abstract protected double getAxisMargin();
227 
231  protected final void createSeries() {
232  for (TimelineEventType eventType : getController().getEventsModel().getEventTypes()) {
233  XYChart.Series<X, Y> series = new XYChart.Series<>();
234  series.setName(eventType.getDisplayName());
235  eventTypeToSeriesMap.put(eventType, series);
236  dataSeries.add(series);
237  }
238  }
239 
248  protected final XYChart.Series<X, Y> getSeries(final TimelineEventType eventType) {
249  return eventTypeToSeriesMap.get(eventType);
250  }
251 
258  super(controller);
259  Platform.runLater(new Runnable() {
260  @Override
261  public void run() {
262  VBox vBox = new VBox(getSpecificLabelPane(), getContextLabelPane());
263  vBox.setFillWidth(false);
264  HBox hBox = new HBox(getSpacer(), vBox);
265  hBox.setFillHeight(false);
266  setBottom(hBox);
267  DoubleBinding spacerSize = getYAxis().widthProperty().add(getYAxis().tickLengthProperty()).add(getAxisMargin());
268  getSpacer().minWidthProperty().bind(spacerSize);
269  getSpacer().prefWidthProperty().bind(spacerSize);
270  getSpacer().maxWidthProperty().bind(spacerSize);
271  }
272  });
273 
274  createSeries();
275 
276  selectedNodes.addListener((ListChangeListener.Change<? extends NodeType> change) -> {
277  while (change.next()) {
278  change.getRemoved().forEach(node -> applySelectionEffect(node, false));
279  change.getAddedSubList().forEach(node -> applySelectionEffect(node, true));
280  }
281  });
282 
283  //show tooltip text in status bar
284  hoverProperty().addListener(hoverProp -> controller.setStatusMessage(isHover() ? getDefaultTooltip().getText() : ""));
285 
286  }
287 
307  protected synchronized void layoutDateLabels() {
308  //clear old labels
309  contextLabelPane.getChildren().clear();
310  specificLabelPane.getChildren().clear();
311  //since the tickmarks aren't necessarily in value/position order,
312  //make a copy of the list sorted by position along axis
313  SortedList<Axis.TickMark<X>> tickMarks = getXAxis().getTickMarks().sorted(Comparator.comparing(Axis.TickMark::getPosition));
314 
315  if (tickMarks.isEmpty()) {
316  /*
317  * Since StackedBarChart does some funky animation/background thread
318  * stuff, sometimes there are no tick marks even though there is
319  * data. Dispatching another call to layoutDateLables() allows that
320  * stuff time to run before we check a gain.
321  */
322  Platform.runLater(this::layoutDateLabels);
323  } else {
324  //get the spacing between ticks in the underlying axis
325  double spacing = getTickSpacing();
326 
327  //initialize values from first tick
328  TwoPartDateTime dateTime = new TwoPartDateTime(getTickMarkLabel(tickMarks.get(0).getValue()));
329  String lastSeenContextLabel = dateTime.context;
330 
331  //x-positions (pixels) of the current branch and leaf labels
332  double specificLabelX = 0;
333 
334  if (dateTime.context.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  addSpecificLabel(new TwoPartDateTime(getTickMarkLabel(t.getValue())).specifics,
338  spacing,
339  specificLabelX,
340  isTickBold(t.getValue())
341  );
342  specificLabelX += spacing; //increment x
343  }
344  } else {
345  //there are two parts so ...
346  //initialize additional state
347  double contextLabelX = 0;
348  double contextLabelWidth = 0;
349 
350  for (Axis.TickMark<X> t : tickMarks) {
351  //split the label into a TwoPartDateTime
352  dateTime = new TwoPartDateTime(getTickMarkLabel(t.getValue()));
353 
354  //if we are still in the same context
355  if (lastSeenContextLabel.equals(dateTime.context)) {
356  //increment context width
357  contextLabelWidth += spacing;
358  } else {// we are on to a new context, so ...
359  addContextLabel(lastSeenContextLabel, contextLabelWidth, contextLabelX);
360  //and then update label, x-pos, and width
361  lastSeenContextLabel = dateTime.context;
362  contextLabelX += contextLabelWidth;
363  contextLabelWidth = spacing;
364  }
365  //add the specific label (highest frequency part)
366  addSpecificLabel(dateTime.specifics, spacing, specificLabelX, isTickBold(t.getValue()));
367 
368  //increment specific position
369  specificLabelX += spacing;
370  }
371  //we have reached end so add label for current context
372  addContextLabel(lastSeenContextLabel, contextLabelWidth, contextLabelX);
373  }
374  }
375  //request layout since we have modified scene graph structure
376  requestParentLayout();
377  }
378 
390  private synchronized void addSpecificLabel(String labelText, double labelWidth, double labelX, boolean bold) {
391  Text label = new Text(" " + labelText + " "); //NON-NLS
392  label.setTextAlignment(TextAlignment.CENTER);
393  label.setFont(Font.font(null, bold ? FontWeight.BOLD : FontWeight.NORMAL, 10));
394  //position label accounting for width
395  label.relocate(labelX + labelWidth / 2 - label.getBoundsInLocal().getWidth() / 2, 0);
396  label.autosize();
397 
398  if (specificLabelPane.getChildren().isEmpty()) {
399  //just add first label
400  specificLabelPane.getChildren().add(label);
401  } else {
402  //otherwise don't actually add the label if it would intersect with previous label
403 
404  final Node lastLabel = specificLabelPane.getChildren().get(specificLabelPane.getChildren().size() - 1);
405 
406  if (false == lastLabel.getBoundsInParent().intersects(label.getBoundsInParent())) {
407  specificLabelPane.getChildren().add(label);
408  }
409  }
410  }
411 
421  private synchronized void addContextLabel(String labelText, double labelWidth, double labelX) {
422  Label label = new Label(labelText);
423  label.setAlignment(Pos.CENTER);
424  label.setTextAlignment(TextAlignment.CENTER);
425  label.setFont(Font.font(10));
426  //use a leading ellipse since that is the lowest frequency part,
427  //and can be infered more easily from other surrounding labels
428  label.setTextOverrun(OverrunStyle.LEADING_ELLIPSIS);
429  //force size
430  label.setMinWidth(labelWidth);
431  label.setPrefWidth(labelWidth);
432  label.setMaxWidth(labelWidth);
433  label.relocate(labelX, 0);
434 
435  if (labelX == 0) { // first label has no border
436  label.setBorder(null);
437  } else { // subsequent labels have border on left to create dividers
438  label.setBorder(ONLY_LEFT_BORDER);
439  }
440 
441  contextLabelPane.getChildren().add(label);
442  }
443 
451  @Immutable
452  private static final class TwoPartDateTime {
453 
457  private final String context;
458 
462  private final String specifics;
463 
470  TwoPartDateTime(String dateString) {
471  //find index of separator to split on
472  int splitIndex = StringUtils.lastIndexOfAny(dateString, " ", "-", ":"); //NON-NLS
473  if (splitIndex < 0) { // there is only one part
474  specifics = dateString;
475  context = ""; //NON-NLS
476  } else { //split at index
477  specifics = StringUtils.substring(dateString, splitIndex + 1);
478  context = StringUtils.substring(dateString, 0, splitIndex);
479  }
480  }
481  }
482 
483 }
final Map< TimelineEventType, XYChart.Series< X, Y > > eventTypeToSeriesMap
synchronized void addSpecificLabel(String labelText, double labelWidth, double labelX, boolean bold)
synchronized void addContextLabel(String labelText, double labelWidth, double labelX)
final XYChart.Series< X, Y > getSeries(final TimelineEventType eventType)
final ObservableList< XYChart.Series< X, Y > > dataSeries
synchronized static Logger getLogger(String name)
Definition: Logger.java:124

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