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

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