Autopsy  4.6.0
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 javafx.beans.Observable;
23 import javafx.collections.ObservableList;
24 import javafx.event.EventHandler;
25 import javafx.scene.Cursor;
26 import javafx.scene.Node;
27 import javafx.scene.chart.CategoryAxis;
28 import javafx.scene.chart.NumberAxis;
29 import javafx.scene.chart.StackedBarChart;
30 import javafx.scene.chart.XYChart;
31 import javafx.scene.control.ContextMenu;
32 import javafx.scene.control.SeparatorMenuItem;
33 import javafx.scene.control.Tooltip;
34 import javafx.scene.effect.DropShadow;
35 import javafx.scene.effect.Effect;
36 import javafx.scene.effect.Lighting;
37 import javafx.scene.image.ImageView;
38 import javafx.scene.input.MouseButton;
39 import javafx.scene.input.MouseEvent;
40 import javafx.util.StringConverter;
41 import javax.swing.JOptionPane;
42 import org.controlsfx.control.action.Action;
43 import org.controlsfx.control.action.ActionUtils;
44 import org.joda.time.DateTime;
45 import org.joda.time.Interval;
46 import org.joda.time.Seconds;
47 import org.openide.util.NbBundle;
48 import org.openide.windows.WindowManager;
58 
63 final class EventCountsChart extends StackedBarChart<String, Number> implements TimeLineChart<String> {
64 
65  private static final Effect SELECTED_NODE_EFFECT = new Lighting();
66  private ContextMenu chartContextMenu;
67 
68  private final TimeLineController controller;
69  private final FilteredEventsModel filteredEvents;
70 
71  private IntervalSelector<? extends String> intervalSelector;
72 
73  final ObservableList<Node> selectedNodes;
74 
80  private RangeDivisionInfo rangeInfo;
81 
82  EventCountsChart(TimeLineController controller, CategoryAxis dateAxis, NumberAxis countAxis, ObservableList<Node> selectedNodes) {
83  super(dateAxis, countAxis);
84  this.controller = controller;
85  this.filteredEvents = controller.getEventsModel();
86 
87  //configure constant properties on axes and chart
88  dateAxis.setAnimated(true);
89  dateAxis.setLabel(null);
90  dateAxis.setTickLabelsVisible(false);
91  dateAxis.setTickLabelGap(0);
92 
93  countAxis.setAutoRanging(false);
94  countAxis.setLowerBound(0);
95  countAxis.setAnimated(true);
96  countAxis.setMinorTickCount(0);
97  countAxis.setTickLabelFormatter(new IntegerOnlyStringConverter());
98 
99  setAlternativeRowFillVisible(true);
100  setCategoryGap(2);
101  setLegendVisible(false);
102  setAnimated(true);
103  setTitle(null);
104 
105  ChartDragHandler<String, EventCountsChart> chartDragHandler = new ChartDragHandler<>(this);
106  setOnMousePressed(chartDragHandler);
107  setOnMouseReleased(chartDragHandler);
108  setOnMouseDragged(chartDragHandler);
109 
110  setOnMouseClicked(new MouseClickedHandler<>(this));
111 
112  this.selectedNodes = selectedNodes;
113 
114  getController().getEventsModel().timeRangeProperty().addListener(o -> {
115  clearIntervalSelector();
116  });
117  }
118 
119  @Override
120  public void clearContextMenu() {
121  chartContextMenu = null;
122  }
123 
124  @Override
125  public ContextMenu getContextMenu(MouseEvent clickEvent) {
126  if (chartContextMenu != null) {
127  chartContextMenu.hide();
128  }
129 
130  chartContextMenu = ActionUtils.createContextMenu(
131  Arrays.asList(TimeLineChart.newZoomHistoyActionGroup(controller)));
132  chartContextMenu.setAutoHide(true);
133  return chartContextMenu;
134  }
135 
136  @Override
137  public TimeLineController getController() {
138  return controller;
139  }
140 
141  @Override
142  public void clearIntervalSelector() {
143  getChartChildren().remove(intervalSelector);
144  intervalSelector = null;
145  }
146 
147  @Override
148  public IntervalSelector<? extends String> getIntervalSelector() {
149  return intervalSelector;
150  }
151 
152  @Override
153  public void setIntervalSelector(IntervalSelector<? extends String> newIntervalSelector) {
154  intervalSelector = newIntervalSelector;
155  //Add a listener that sizes the interval selector to its preferred size.
156  intervalSelector.prefHeightProperty().addListener(observable -> newIntervalSelector.autosize());
157  getChartChildren().add(getIntervalSelector());
158  }
159 
160  @Override
161  public CountsIntervalSelector newIntervalSelector() {
162  return new CountsIntervalSelector(this);
163  }
164 
165  @Override
166  public ObservableList<Node> getSelectedNodes() {
167  return selectedNodes;
168  }
169 
170  void setRangeInfo(RangeDivisionInfo rangeInfo) {
171  this.rangeInfo = rangeInfo;
172  }
173 
174  Effect getSelectionEffect() {
175  return SELECTED_NODE_EFFECT;
176  }
177 
186  @NbBundle.Messages({
187  "# {0} - count",
188  "# {1} - event type displayname",
189  "# {2} - start date time",
190  "# {3} - end date time",
191  "CountsViewPane.tooltip.text={0} {1} events\nbetween {2}\nand {3}"})
192  @Override
193  protected void dataItemAdded(Series<String, Number> series, int itemIndex, Data<String, Number> item) {
194  ExtraData extraValue = (ExtraData) item.getExtraValue();
195  EventType eventType = extraValue.getEventType();
196  Interval interval = extraValue.getInterval();
197  long count = extraValue.getRawCount();
198 
199  item.nodeProperty().addListener((Observable o) -> {
200  final Node node = item.getNode();
201  if (node != null) {
202  node.setStyle("-fx-border-width: 2; -fx-border-color: " + ColorUtilities.getRGBCode(eventType.getSuperType().getColor()) + "; -fx-bar-fill: " + ColorUtilities.getRGBCode(eventType.getColor())); // NON-NLS
203  node.setCursor(Cursor.HAND);
204 
205  final Tooltip tooltip = new Tooltip(Bundle.CountsViewPane_tooltip_text(
206  count, eventType.getDisplayName(),
207  item.getXValue(),
208  interval.getEnd().toString(rangeInfo.getTickFormatter())));
209  tooltip.setGraphic(new ImageView(eventType.getFXImage()));
210  Tooltip.install(node, tooltip);
211 
212  node.setOnMouseEntered(mouseEntered -> node.setEffect(new DropShadow(10, eventType.getColor())));
213  node.setOnMouseExited(mouseExited -> node.setEffect(selectedNodes.contains(node) ? SELECTED_NODE_EFFECT : null));
214  node.setOnMouseClicked(new BarClickHandler(item));
215  }
216  });
217  super.dataItemAdded(series, itemIndex, item); //To change body of generated methods, choose Tools | Templates.
218  }
219 
223  private static class IntegerOnlyStringConverter extends StringConverter<Number> {
224 
225  @Override
226  public String toString(Number n) {
227  //suppress non-integer values
228  return n.intValue() == n.doubleValue()
229  ? Integer.toString(n.intValue()) : "";
230  }
231 
232  @Override
233  public Number fromString(String string) {
234  //this is unused but here for symmetry
235  return Double.valueOf(string).intValue();
236  }
237  }
238 
243  final static private class CountsIntervalSelector extends IntervalSelector<String> {
244 
245  private final EventCountsChart countsChart;
246 
247  CountsIntervalSelector(EventCountsChart chart) {
248  super(chart);
249  this.countsChart = chart;
250  }
251 
252  @Override
253  protected String formatSpan(String date) {
254  return date;
255  }
256 
257  @Override
258  protected Interval adjustInterval(Interval i) {
259  //extend range to block bounderies (ie day, month, year)
261  final long lowerBound = iInfo.getLowerBound();
262  final long upperBound = iInfo.getUpperBound();
263  final DateTime lowerDate = new DateTime(lowerBound, TimeLineController.getJodaTimeZone());
264  final DateTime upperDate = new DateTime(upperBound, TimeLineController.getJodaTimeZone());
265  //add extra block to end that gets cut of by conversion from string/category.
266  return new Interval(lowerDate, upperDate.plus(countsChart.rangeInfo.getPeriodSize().getPeriod()));
267  }
268 
269  @Override
270  protected DateTime parseDateTime(String date) {
271  return date == null ? new DateTime(countsChart.rangeInfo.getLowerBound()) : countsChart.rangeInfo.getTickFormatter().parseDateTime(date);
272  }
273  }
274 
285  private class BarClickHandler implements EventHandler<MouseEvent> {
286 
287  private ContextMenu barContextMenu;
288 
289  private final Interval interval;
290 
291  private final EventType type;
292 
293  private final Node node;
294 
295  private final String startDateString;
296 
297  BarClickHandler(XYChart.Data<String, Number> data) {
298  EventCountsChart.ExtraData extraData = (EventCountsChart.ExtraData) data.getExtraValue();
299  this.interval = extraData.getInterval();
300  this.type = extraData.getEventType();
301  this.node = data.getNode();
302  this.startDateString = data.getXValue();
303  }
304 
305  @NbBundle.Messages({"Timeline.ui.countsview.menuItem.selectTimeRange=Select Time Range"})
306  class SelectIntervalAction extends Action {
307 
308  SelectIntervalAction() {
309  super(Bundle.Timeline_ui_countsview_menuItem_selectTimeRange());
310  setEventHandler(action -> {
311  controller.selectTimeAndType(interval, RootEventType.getInstance());
312  selectedNodes.clear();
313  for (XYChart.Series<String, Number> s : getData()) {
314  s.getData().forEach((XYChart.Data<String, Number> d) -> {
315  if (startDateString.contains(d.getXValue())) {
316  selectedNodes.add(d.getNode());
317  }
318  });
319  }
320  });
321  }
322  }
323 
324  @NbBundle.Messages({"Timeline.ui.countsview.menuItem.selectEventType=Select Event Type"})
325  class SelectTypeAction extends Action {
326 
327  SelectTypeAction() {
328  super(Bundle.Timeline_ui_countsview_menuItem_selectEventType());
329  setEventHandler(action -> {
330  controller.selectTimeAndType(filteredEvents.getSpanningInterval(), type);
331  selectedNodes.clear();
332  getData().stream().filter(series -> series.getName().equals(type.getDisplayName()))
333  .findFirst()
334  .ifPresent(series -> series.getData().forEach(data -> selectedNodes.add(data.getNode())));
335  });
336  }
337  }
338 
339  @NbBundle.Messages({"Timeline.ui.countsview.menuItem.selectTimeandType=Select Time and Type"})
340  class SelectIntervalAndTypeAction extends Action {
341 
342  SelectIntervalAndTypeAction() {
343  super(Bundle.Timeline_ui_countsview_menuItem_selectTimeandType());
344  setEventHandler(action -> {
345  controller.selectTimeAndType(interval, type);
346  selectedNodes.setAll(node);
347  });
348  }
349  }
350 
351  @NbBundle.Messages({"Timeline.ui.countsview.menuItem.zoomIntoTimeRange=Zoom into Time Range"})
352  class ZoomToIntervalAction extends Action {
353 
354  ZoomToIntervalAction() {
355  super(Bundle.Timeline_ui_countsview_menuItem_zoomIntoTimeRange());
356  setEventHandler(action -> {
357  if (interval.toDuration().isShorterThan(Seconds.ONE.toStandardDuration()) == false) {
358  controller.pushTimeRange(interval);
359  }
360  });
361  }
362  }
363 
364  @Override
365  @NbBundle.Messages({
366  "CountsViewPane.detailSwitchMessage=There is no temporal resolution smaller than Seconds.\nWould you like to switch to the Details view instead?",
367  "CountsViewPane.detailSwitchTitle=\"Switch to Details View?"})
368  public void handle(final MouseEvent e) {
369  e.consume();
370  if (e.getClickCount() == 1) { //single click => selection
371  if (e.getButton().equals(MouseButton.PRIMARY)) {
372  controller.selectTimeAndType(interval, type);
373  selectedNodes.setAll(node);
374  } else if (e.getButton().equals(MouseButton.SECONDARY)) {
375  getContextMenu(e).hide();
376 
377  if (barContextMenu == null) {
378  barContextMenu = new ContextMenu();
379  barContextMenu.setAutoHide(true);
380  barContextMenu.getItems().addAll(
381  ActionUtils.createMenuItem(new SelectIntervalAction()),
382  ActionUtils.createMenuItem(new SelectTypeAction()),
383  ActionUtils.createMenuItem(new SelectIntervalAndTypeAction()),
384  new SeparatorMenuItem(),
385  ActionUtils.createMenuItem(new ZoomToIntervalAction()));
386 
387  barContextMenu.getItems().addAll(getContextMenu(e).getItems());
388  }
389 
390  barContextMenu.show(node, e.getScreenX(), e.getScreenY());
391 
392  }
393  } else if (e.getClickCount() >= 2) { //double-click => zoom in time
394  if (interval.toDuration().isLongerThan(Seconds.ONE.toStandardDuration())) {
395  controller.pushTimeRange(interval);
396  } else {
397 
398  int showConfirmDialog = JOptionPane.showConfirmDialog(WindowManager.getDefault().getMainWindow(),
399  Bundle.CountsViewPane_detailSwitchMessage(),
400  Bundle.CountsViewPane_detailSwitchTitle(), JOptionPane.YES_NO_OPTION);
401  if (showConfirmDialog == JOptionPane.YES_OPTION) {
402  controller.setViewMode(ViewMode.DETAIL);
403  }
404  }
405  }
406  }
407  }
408 
413  static class ExtraData {
414 
415  private final Interval interval;
416  private final EventType eventType;
417  private final long rawCount;
418 
419  ExtraData(Interval interval, EventType eventType, long rawCount) {
420  this.interval = interval;
421  this.eventType = eventType;
422  this.rawCount = rawCount;
423  }
424 
425  public long getRawCount() {
426  return rawCount;
427  }
428 
429  public Interval getInterval() {
430  return interval;
431  }
432 
433  public EventType getEventType() {
434  return eventType;
435  }
436 
437  }
438 }
static RangeDivisionInfo getRangeDivisionInfo(Interval timeRange)
void setIntervalSelector(IntervalSelector<?extends X > newIntervalSelector)

Copyright © 2012-2016 Basis Technology. Generated on: Mon May 7 2018
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.