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

Copyright © 2012-2015 Basis Technology. Generated on: Wed Apr 6 2016
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.