Autopsy  4.9.1
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-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;
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;
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<EventType, 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 (EventType eventType : EventType.allTypes) {
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 EventType et) {
249  return eventTypeToSeriesMap.get(et);
250  }
251 
258  super(controller);
259  Platform.runLater(() -> {
260  VBox vBox = new VBox(getSpecificLabelPane(), getContextLabelPane());
261  vBox.setFillWidth(false);
262  HBox hBox = new HBox(getSpacer(), vBox);
263  hBox.setFillHeight(false);
264  setBottom(hBox);
265  DoubleBinding spacerSize = getYAxis().widthProperty().add(getYAxis().tickLengthProperty()).add(getAxisMargin());
266  getSpacer().minWidthProperty().bind(spacerSize);
267  getSpacer().prefWidthProperty().bind(spacerSize);
268  getSpacer().maxWidthProperty().bind(spacerSize);
269  });
270 
271  createSeries();
272 
273  selectedNodes.addListener((ListChangeListener.Change<? extends NodeType> change) -> {
274  while (change.next()) {
275  change.getRemoved().forEach(node -> applySelectionEffect(node, false));
276  change.getAddedSubList().forEach(node -> applySelectionEffect(node, true));
277  }
278  });
279 
280  //show tooltip text in status bar
281  hoverProperty().addListener(hoverProp -> controller.setStatusMessage(isHover() ? getDefaultTooltip().getText() : ""));
282 
283  }
284 
304  protected synchronized void layoutDateLabels() {
305  //clear old labels
306  contextLabelPane.getChildren().clear();
307  specificLabelPane.getChildren().clear();
308  //since the tickmarks aren't necessarily in value/position order,
309  //make a copy of the list sorted by position along axis
310  SortedList<Axis.TickMark<X>> tickMarks = getXAxis().getTickMarks().sorted(Comparator.comparing(Axis.TickMark::getPosition));
311 
312  if (tickMarks.isEmpty()) {
313  /*
314  * Since StackedBarChart does some funky animation/background thread
315  * stuff, sometimes there are no tick marks even though there is
316  * data. Dispatching another call to layoutDateLables() allows that
317  * stuff time to run before we check a gain.
318  */
319  Platform.runLater(this::layoutDateLabels);
320  } else {
321  //get the spacing between ticks in the underlying axis
322  double spacing = getTickSpacing();
323 
324  //initialize values from first tick
325  TwoPartDateTime dateTime = new TwoPartDateTime(getTickMarkLabel(tickMarks.get(0).getValue()));
326  String lastSeenContextLabel = dateTime.context;
327 
328  //x-positions (pixels) of the current branch and leaf labels
329  double specificLabelX = 0;
330 
331  if (dateTime.context.isEmpty()) {
332  //if there is only one part to the date (ie only year), just add a label for each tick
333  for (Axis.TickMark<X> t : tickMarks) {
334  addSpecificLabel(new TwoPartDateTime(getTickMarkLabel(t.getValue())).specifics,
335  spacing,
336  specificLabelX,
337  isTickBold(t.getValue())
338  );
339  specificLabelX += spacing; //increment x
340  }
341  } else {
342  //there are two parts so ...
343  //initialize additional state
344  double contextLabelX = 0;
345  double contextLabelWidth = 0;
346 
347  for (Axis.TickMark<X> t : tickMarks) {
348  //split the label into a TwoPartDateTime
349  dateTime = new TwoPartDateTime(getTickMarkLabel(t.getValue()));
350 
351  //if we are still in the same context
352  if (lastSeenContextLabel.equals(dateTime.context)) {
353  //increment context width
354  contextLabelWidth += spacing;
355  } else {// we are on to a new context, so ...
356  addContextLabel(lastSeenContextLabel, contextLabelWidth, contextLabelX);
357  //and then update label, x-pos, and width
358  lastSeenContextLabel = dateTime.context;
359  contextLabelX += contextLabelWidth;
360  contextLabelWidth = spacing;
361  }
362  //add the specific label (highest frequency part)
363  addSpecificLabel(dateTime.specifics, spacing, specificLabelX, isTickBold(t.getValue()));
364 
365  //increment specific position
366  specificLabelX += spacing;
367  }
368  //we have reached end so add label for current context
369  addContextLabel(lastSeenContextLabel, contextLabelWidth, contextLabelX);
370  }
371  }
372  //request layout since we have modified scene graph structure
373  requestParentLayout();
374  }
375 
387  private synchronized void addSpecificLabel(String labelText, double labelWidth, double labelX, boolean bold) {
388  Text label = new Text(" " + labelText + " "); //NON-NLS
389  label.setTextAlignment(TextAlignment.CENTER);
390  label.setFont(Font.font(null, bold ? FontWeight.BOLD : FontWeight.NORMAL, 10));
391  //position label accounting for width
392  label.relocate(labelX + labelWidth / 2 - label.getBoundsInLocal().getWidth() / 2, 0);
393  label.autosize();
394 
395  if (specificLabelPane.getChildren().isEmpty()) {
396  //just add first label
397  specificLabelPane.getChildren().add(label);
398  } else {
399  //otherwise don't actually add the label if it would intersect with previous label
400 
401  final Node lastLabel = specificLabelPane.getChildren().get(specificLabelPane.getChildren().size() - 1);
402 
403  if (false == lastLabel.getBoundsInParent().intersects(label.getBoundsInParent())) {
404  specificLabelPane.getChildren().add(label);
405  }
406  }
407  }
408 
418  private synchronized void addContextLabel(String labelText, double labelWidth, double labelX) {
419  Label label = new Label(labelText);
420  label.setAlignment(Pos.CENTER);
421  label.setTextAlignment(TextAlignment.CENTER);
422  label.setFont(Font.font(10));
423  //use a leading ellipse since that is the lowest frequency part,
424  //and can be infered more easily from other surrounding labels
425  label.setTextOverrun(OverrunStyle.LEADING_ELLIPSIS);
426  //force size
427  label.setMinWidth(labelWidth);
428  label.setPrefWidth(labelWidth);
429  label.setMaxWidth(labelWidth);
430  label.relocate(labelX, 0);
431 
432  if (labelX == 0) { // first label has no border
433  label.setBorder(null);
434  } else { // subsequent labels have border on left to create dividers
435  label.setBorder(ONLY_LEFT_BORDER);
436  }
437 
438  contextLabelPane.getChildren().add(label);
439  }
440 
448  @Immutable
449  private static final class TwoPartDateTime {
450 
454  private final String context;
455 
459  private final String specifics;
460 
467  TwoPartDateTime(String dateString) {
468  //find index of separator to split on
469  int splitIndex = StringUtils.lastIndexOfAny(dateString, " ", "-", ":"); //NON-NLS
470  if (splitIndex < 0) { // there is only one part
471  specifics = dateString;
472  context = ""; //NON-NLS
473  } else { //split at index
474  specifics = StringUtils.substring(dateString, splitIndex + 1);
475  context = StringUtils.substring(dateString, 0, splitIndex);
476  }
477  }
478  }
479 
480 }
final XYChart.Series< X, Y > getSeries(final EventType et)
final Map< EventType, 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 ObservableList< XYChart.Series< X, Y > > dataSeries
synchronized static Logger getLogger(String name)
Definition: Logger.java:124

Copyright © 2012-2018 Basis Technology. Generated on: Tue Dec 18 2018
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.