Autopsy  4.19.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
EventCountsChart.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.countsview;
20 
21 import java.util.Arrays;
22 import java.util.Optional;
23 import java.util.logging.Level;
24 import javafx.collections.ObservableList;
25 import javafx.event.EventHandler;
26 import javafx.scene.Cursor;
27 import javafx.scene.Node;
28 import javafx.scene.chart.CategoryAxis;
29 import javafx.scene.chart.NumberAxis;
30 import javafx.scene.chart.StackedBarChart;
31 import javafx.scene.chart.XYChart;
32 import javafx.scene.control.Alert;
33 import javafx.scene.control.ButtonType;
34 import javafx.scene.control.ContextMenu;
35 import javafx.scene.control.SeparatorMenuItem;
36 import javafx.scene.control.Tooltip;
37 import javafx.scene.effect.DropShadow;
38 import javafx.scene.effect.Effect;
39 import javafx.scene.effect.Lighting;
40 import javafx.scene.image.ImageView;
41 import javafx.scene.input.MouseButton;
42 import javafx.scene.input.MouseEvent;
43 import javafx.util.StringConverter;
44 import org.controlsfx.control.Notifications;
45 import org.controlsfx.control.action.Action;
46 import org.controlsfx.control.action.ActionUtils;
47 import org.joda.time.DateTime;
48 import org.joda.time.Interval;
49 import org.joda.time.Seconds;
50 import org.openide.util.NbBundle;
62 import org.sleuthkit.datamodel.TskCoreException;
63 import org.sleuthkit.datamodel.TimelineEventType;
64 
69 final class EventCountsChart extends StackedBarChart<String, Number> implements TimeLineChart<String> {
70 
71  private static final Logger logger = Logger.getLogger(EventCountsChart.class.getName());
72  private static final Effect SELECTED_NODE_EFFECT = new Lighting();
73  private ContextMenu chartContextMenu;
74 
75  private final TimeLineController controller;
76  private final EventsModel filteredEvents;
77 
78  private IntervalSelector<? extends String> intervalSelector;
79 
80  final ObservableList<Node> selectedNodes;
81 
87  private RangeDivision rangeInfo;
88 
89  EventCountsChart(TimeLineController controller, CategoryAxis dateAxis, NumberAxis countAxis, ObservableList<Node> selectedNodes) {
90  super(dateAxis, countAxis);
91  this.controller = controller;
92  this.filteredEvents = controller.getEventsModel();
93 
94  //configure constant properties on axes and chart
95  dateAxis.setAnimated(true);
96  dateAxis.setLabel(null);
97  dateAxis.setTickLabelsVisible(false);
98  dateAxis.setTickLabelGap(0);
99 
100  countAxis.setAutoRanging(false);
101  countAxis.setLowerBound(0);
102  countAxis.setAnimated(true);
103  countAxis.setMinorTickCount(0);
104  countAxis.setTickLabelFormatter(new IntegerOnlyStringConverter());
105 
106  setAlternativeRowFillVisible(true);
107  setCategoryGap(2);
108  setLegendVisible(false);
109  setAnimated(true);
110  setTitle(null);
111 
112  ChartDragHandler<String, EventCountsChart> chartDragHandler = new ChartDragHandler<>(this);
113  setOnMousePressed(chartDragHandler);
114  setOnMouseReleased(chartDragHandler);
115  setOnMouseDragged(chartDragHandler);
116 
117  setOnMouseClicked(new MouseClickedHandler<>(this));
118 
119  this.selectedNodes = selectedNodes;
120 
121  getController().getEventsModel().timeRangeProperty().addListener(o -> {
122  clearIntervalSelector();
123  });
124  }
125 
126  @Override
127  public void clearContextMenu() {
128  chartContextMenu = null;
129  }
130 
131  @Override
132  public ContextMenu getContextMenu(MouseEvent clickEvent) {
133  if (chartContextMenu != null) {
134  chartContextMenu.hide();
135  }
136 
137  chartContextMenu = ActionUtils.createContextMenu(
138  Arrays.asList(TimeLineChart.newZoomHistoyActionGroup(controller)));
139  chartContextMenu.setAutoHide(true);
140  return chartContextMenu;
141  }
142 
143  @Override
144  public TimeLineController getController() {
145  return controller;
146  }
147 
148  @Override
149  public void clearIntervalSelector() {
150  getChartChildren().remove(intervalSelector);
151  intervalSelector = null;
152  }
153 
154  @Override
155  public IntervalSelector<? extends String> getIntervalSelector() {
156  return intervalSelector;
157  }
158 
159  @Override
160  public void setIntervalSelector(IntervalSelector<? extends String> newIntervalSelector) {
161  intervalSelector = newIntervalSelector;
162  //Add a listener that sizes the interval selector to its preferred size.
163  intervalSelector.prefHeightProperty().addListener(observable -> newIntervalSelector.autosize());
164  getChartChildren().add(getIntervalSelector());
165  }
166 
167  @Override
168  public CountsIntervalSelector newIntervalSelector() {
169  return new CountsIntervalSelector(this);
170  }
171 
172  @Override
173  public ObservableList<Node> getSelectedNodes() {
174  return selectedNodes;
175  }
176 
177  void setRangeInfo(RangeDivision rangeInfo) {
178  this.rangeInfo = rangeInfo;
179  }
180 
181  Effect getSelectionEffect() {
182  return SELECTED_NODE_EFFECT;
183  }
184 
193  @NbBundle.Messages({
194  "# {0} - count",
195  "# {1} - event type displayname",
196  "# {2} - start date time",
197  "# {3} - end date time",
198  "CountsViewPane.tooltip.text={0} {1} events\nbetween {2}\nand {3}"})
199  @Override
200  protected void dataItemAdded(Series<String, Number> series, int itemIndex, Data<String, Number> item) {
201  ExtraData extraValue = (ExtraData) item.getExtraValue();
202  TimelineEventType eventType = extraValue.getEventType();
203  Interval interval = extraValue.getInterval();
204  long count = extraValue.getRawCount();
205 
206  item.nodeProperty().addListener(observable -> {
207  final Node node = item.getNode();
208  if (node != null) {
209  node.setStyle("-fx-border-width: 2; "
210  + " -fx-border-color: " + ColorUtilities.getRGBCode(getColor(eventType.getParent())) + "; "
211  + " -fx-bar-fill: " + ColorUtilities.getRGBCode(getColor(eventType))); // NON-NLS
212  node.setCursor(Cursor.HAND);
213 
214  final Tooltip tooltip = new Tooltip(Bundle.CountsViewPane_tooltip_text(
215  count, eventType.getDisplayName(),
216  item.getXValue(),
217  interval.getEnd().toString(rangeInfo.getTickFormatter())));
218  tooltip.setGraphic(new ImageView(getImagePath(eventType)));
219  Tooltip.install(node, tooltip);
220 
221  node.setOnMouseEntered(mouseEntered -> node.setEffect(new DropShadow(10, getColor(eventType))));
222  node.setOnMouseExited(mouseExited -> node.setEffect(selectedNodes.contains(node) ? SELECTED_NODE_EFFECT : null));
223  node.setOnMouseClicked(new BarClickHandler(item));
224  }
225  });
226  super.dataItemAdded(series, itemIndex, item); //To change body of generated methods, choose Tools | Templates.
227  }
228 
232  private static class IntegerOnlyStringConverter extends StringConverter<Number> {
233 
234  @Override
235  public String toString(Number n) {
236  //suppress non-integer values
237  return n.intValue() == n.doubleValue()
238  ? Integer.toString(n.intValue()) : "";
239  }
240 
241  @Override
242  public Number fromString(String string) {
243  //this is unused but here for symmetry
244  return Double.valueOf(string).intValue();
245  }
246  }
247 
252  final static private class CountsIntervalSelector extends IntervalSelector<String> {
253 
254  private final EventCountsChart countsChart;
255 
256  CountsIntervalSelector(EventCountsChart chart) {
257  super(chart);
258  this.countsChart = chart;
259  }
260 
261  @Override
262  protected String formatSpan(String date) {
263  return date;
264  }
265 
266  @Override
267  protected Interval adjustInterval(Interval i) {
268  //extend range to block bounderies (ie day, month, year)
270  final long lowerBound = iInfo.getLowerBound();
271  final long upperBound = iInfo.getUpperBound();
272  final DateTime lowerDate = new DateTime(lowerBound, TimeLineController.getJodaTimeZone());
273  final DateTime upperDate = new DateTime(upperBound, TimeLineController.getJodaTimeZone());
274  //add extra block to end that gets cut of by conversion from string/category.
275  return new Interval(lowerDate, upperDate.plus(countsChart.rangeInfo.getPeriodSize().toUnitPeriod()));
276  }
277 
278  @Override
279  protected DateTime parseDateTime(String date) {
280  return date == null ? new DateTime(countsChart.rangeInfo.getLowerBound()) : countsChart.rangeInfo.getTickFormatter().parseDateTime(date);
281  }
282  }
283 
293  private class BarClickHandler implements EventHandler<MouseEvent> {
294 
295  private ContextMenu barContextMenu;
296 
297  private final Interval interval;
298 
299  private final TimelineEventType type;
300 
301  private final Node node;
302 
303  private final String startDateString;
304 
305  BarClickHandler(XYChart.Data<String, Number> data) {
306  EventCountsChart.ExtraData extraData = (EventCountsChart.ExtraData) data.getExtraValue();
307  this.interval = extraData.getInterval();
308  this.type = extraData.getEventType();
309  this.node = data.getNode();
310  this.startDateString = data.getXValue();
311  }
312 
313  @NbBundle.Messages({"Timeline.ui.countsview.menuItem.selectTimeRange=Select Time Range",
314  "SelectIntervalAction.errorMessage=Error selecting interval."})
315  class SelectIntervalAction extends Action {
316 
317  SelectIntervalAction() {
318  super(Bundle.Timeline_ui_countsview_menuItem_selectTimeRange());
319  setEventHandler(action -> {
320  try {
321  controller.selectTimeAndType(interval, TimelineEventType.ROOT_EVENT_TYPE);
322 
323  } catch (TskCoreException ex) {
324  Notifications.create().owner(getScene().getWindow())
325  .text(Bundle.SelectIntervalAction_errorMessage())
326  .showError();
327  logger.log(Level.SEVERE, "Error selecting interval.", ex);
328  }
329  selectedNodes.clear();
330  for (XYChart.Series<String, Number> s : getData()) {
331  s.getData().forEach((XYChart.Data<String, Number> d) -> {
332  if (startDateString.contains(d.getXValue())) {
333  selectedNodes.add(d.getNode());
334  }
335  });
336  }
337  });
338  }
339  }
340 
341  @NbBundle.Messages({"Timeline.ui.countsview.menuItem.selectEventType=Select Event Type",
342  "SelectTypeAction.errorMessage=Error selecting type."})
343  class SelectTypeAction extends Action {
344 
345  SelectTypeAction() {
346  super(Bundle.Timeline_ui_countsview_menuItem_selectEventType());
347  setEventHandler(action -> {
348  try {
349  controller.selectTimeAndType(filteredEvents.getSpanningInterval(), type);
350 
351  } catch (TskCoreException ex) {
352  Notifications.create().owner(getScene().getWindow())
353  .text(Bundle.SelectTypeAction_errorMessage())
354  .showError();
355  logger.log(Level.SEVERE, "Error selecting type.", ex);
356  }
357  selectedNodes.clear();
358  getData().stream().filter(series -> series.getName().equals(type.getDisplayName()))
359  .findFirst()
360  .ifPresent(series -> series.getData().forEach(data -> selectedNodes.add(data.getNode())));
361  });
362  }
363  }
364 
365  @NbBundle.Messages({"Timeline.ui.countsview.menuItem.selectTimeandType=Select Time and Type",
366  "SelectIntervalAndTypeAction.errorMessage=Error selecting interval and type."})
367  class SelectIntervalAndTypeAction extends Action {
368 
369  SelectIntervalAndTypeAction() {
370  super(Bundle.Timeline_ui_countsview_menuItem_selectTimeandType());
371  setEventHandler(action -> {
372  try {
373  controller.selectTimeAndType(interval, type);
374 
375  } catch (TskCoreException ex) {
376  Notifications.create().owner(getScene().getWindow())
377  .text(Bundle.SelectIntervalAndTypeAction_errorMessage())
378  .showError();
379  logger.log(Level.SEVERE, "Error selecting interval and type.", ex);
380  }
381  selectedNodes.setAll(node);
382  });
383  }
384  }
385 
386  @NbBundle.Messages({"Timeline.ui.countsview.menuItem.zoomIntoTimeRange=Zoom into Time Range",
387  "ZoomToIntervalAction.errorMessage=Error zooming to interval."})
388  class ZoomToIntervalAction extends Action {
389 
390  ZoomToIntervalAction() {
391  super(Bundle.Timeline_ui_countsview_menuItem_zoomIntoTimeRange());
392  setEventHandler(action -> {
393  try {
394  if (interval.toDuration().isShorterThan(Seconds.ONE.toStandardDuration()) == false) {
395  controller.pushTimeRange(interval);
396  }
397  } catch (TskCoreException ex) {
398  Notifications.create().owner(getScene().getWindow())
399  .text(Bundle.ZoomToIntervalAction_errorMessage())
400  .showError();
401  logger.log(Level.SEVERE, "Error zooming to interval.", ex);
402  }
403  });
404  }
405  }
406 
407  @Override
408  @NbBundle.Messages({
409  "CountsViewPane.detailSwitchMessage=There is no temporal resolution smaller than Seconds.\nWould you like to switch to the Details view instead?",
410  "CountsViewPane.detailSwitchTitle=\"Switch to Details View?",
411  "BarClickHandler.selectTimeAndType.errorMessage=Error selecting time and type.",
412  "BarClickHandler_zoomIn_errorMessage=Error zooming in."})
413  public void handle(final MouseEvent e) {
414  e.consume();
415  if (e.getClickCount() == 1) { //single click => selection
416  if (e.getButton().equals(MouseButton.PRIMARY)) {
417  try {
418  controller.selectTimeAndType(interval, type);
419  } catch (TskCoreException ex) {
420  Notifications.create().owner(getScene().getWindow())
421  .text(Bundle.BarClickHandler_selectTimeAndType_errorMessage())
422  .showError();
423  logger.log(Level.SEVERE, "Error selecting time and type.", ex);
424  }
425  selectedNodes.setAll(node);
426  } else if (e.getButton().equals(MouseButton.SECONDARY)) {
427  getContextMenu(e).hide();
428 
429  if (barContextMenu == null) {
430  barContextMenu = new ContextMenu();
431  barContextMenu.setAutoHide(true);
432  barContextMenu.getItems().addAll(
433  ActionUtils.createMenuItem(new SelectIntervalAction()),
434  ActionUtils.createMenuItem(new SelectTypeAction()),
435  ActionUtils.createMenuItem(new SelectIntervalAndTypeAction()),
436  new SeparatorMenuItem(),
437  ActionUtils.createMenuItem(new ZoomToIntervalAction()));
438 
439  barContextMenu.getItems().addAll(getContextMenu(e).getItems());
440  }
441 
442  barContextMenu.show(node, e.getScreenX(), e.getScreenY());
443 
444  }
445  } else if (e.getClickCount() >= 2) { //double-click => zoom in time
446  if (interval.toDuration().isLongerThan(Seconds.ONE.toStandardDuration())) {
447  try {
448  controller.pushTimeRange(interval);
449  } catch (TskCoreException ex) {
450  Notifications.create().owner(getScene().getWindow())
451  .text(Bundle.BarClickHandler_zoomIn_errorMessage())
452  .showError();
453  logger.log(Level.SEVERE, "Error zooming in.", ex);
454  }
455  } else {
456  Alert alert = new Alert(Alert.AlertType.CONFIRMATION, Bundle.CountsViewPane_detailSwitchMessage(), ButtonType.YES, ButtonType.NO);
457  alert.setTitle(Bundle.CountsViewPane_detailSwitchTitle());
459 
460  alert.showAndWait().ifPresent(response -> {
461  if (response == ButtonType.YES) {
462  controller.setViewMode(ViewMode.DETAIL);
463  }
464  });
465  }
466  }
467  }
468  }
469 
474  static class ExtraData {
475 
476  private final Interval interval;
477  private final TimelineEventType eventType;
478  private final long rawCount;
479 
480  ExtraData(Interval interval, TimelineEventType eventType, long rawCount) {
481  this.interval = interval;
482  this.eventType = eventType;
483  this.rawCount = rawCount;
484  }
485 
486  public long getRawCount() {
487  return rawCount;
488  }
489 
490  public Interval getInterval() {
491  return interval;
492  }
493 
494  public TimelineEventType getEventType() {
495  return eventType;
496  }
497  }
498 }
static String getImagePath(TimelineEventType type)
static RangeDivision getRangeDivision(Interval timeRange, DateTimeZone timeZone)
static Color getColor(TimelineEventType type)
void setIntervalSelector(IntervalSelector<?extends X > newIntervalSelector)

Copyright © 2012-2021 Basis Technology. Generated on: Thu Sep 30 2021
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.