Autopsy  3.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
CountsViewPane.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2014 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.ArrayList;
22 import java.util.Arrays;
23 import java.util.Map;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.function.Function;
26 import javafx.application.Platform;
27 import javafx.beans.Observable;
28 import javafx.beans.property.SimpleObjectProperty;
29 import javafx.collections.FXCollections;
30 import javafx.concurrent.Task;
31 import javafx.event.ActionEvent;
32 import javafx.event.EventHandler;
33 import javafx.fxml.FXML;
34 import javafx.scene.Cursor;
35 import javafx.scene.Node;
36 import javafx.scene.chart.BarChart;
37 import javafx.scene.chart.CategoryAxis;
38 import javafx.scene.chart.NumberAxis;
39 import javafx.scene.chart.StackedBarChart;
40 import javafx.scene.chart.XYChart;
41 import javafx.scene.control.*;
42 import javafx.scene.effect.DropShadow;
43 import javafx.scene.effect.Effect;
44 import javafx.scene.effect.Lighting;
45 import javafx.scene.image.ImageView;
46 import javafx.scene.input.MouseButton;
47 import javafx.scene.input.MouseEvent;
48 import javafx.scene.layout.HBox;
49 import javafx.scene.layout.Pane;
50 import javafx.scene.layout.Region;
51 import javax.swing.JOptionPane;
52 import org.controlsfx.control.action.ActionGroup;
53 import org.controlsfx.control.action.ActionUtils;
54 import org.joda.time.DateTime;
55 import org.joda.time.Interval;
56 import org.joda.time.Seconds;
57 import org.openide.util.NbBundle;
72 
94 public class CountsViewPane extends AbstractVisualization<String, Number, Node, EventCountsChart> {
95 
96  private static final Effect SELECTED_NODE_EFFECT = new Lighting();
97 
98  private static final Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName());
99 
100  private final NumberAxis countAxis = new NumberAxis();
101 
102  private final CategoryAxis dateAxis = new CategoryAxis(FXCollections.<String>observableArrayList());
103 
104  private final SimpleObjectProperty<ScaleType> scale = new SimpleObjectProperty<>(ScaleType.LOGARITHMIC);
105 
106  //private access to barchart data
107  private final Map<EventType, XYChart.Series<String, Number>> eventTypeMap = new ConcurrentHashMap<>();
108 
109  @Override
110  protected String getTickMarkLabel(String labelValueString) {
111  return labelValueString;
112  }
113 
114  @Override
115  protected Boolean isTickBold(String value) {
116  return dataSets.stream().flatMap((series) -> series.getData().stream())
117  .anyMatch((data) -> data.getXValue().equals(value) && data.getYValue().intValue() > 0);
118  }
119 
120  private ContextMenu getContextMenu() {
121 
122  ContextMenu chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new ActionGroup(
123  NbBundle.getMessage(this.getClass(), "Timeline.ui.countsview.contextMenu.ActionGroup.zoomHistory.title"),
124  new Back(controller), new Forward(controller))));
125  chartContextMenu.setAutoHide(true);
126  return chartContextMenu;
127  }
128 
129  @Override
130  protected Task<Boolean> getUpdateTask() {
131  return new LoggedTask<Boolean>(NbBundle.getMessage(this.getClass(), "CountsViewPane.loggedTask.name"), true) {
132 
133  @Override
134  protected Boolean call() throws Exception {
135  if (isCancelled()) {
136  return null;
137  }
138  updateProgress(-1, 1);
139  updateMessage(NbBundle.getMessage(this.getClass(), "CountsViewPane.loggedTask.prepUpdate"));
140  Platform.runLater(() -> {
141  setCursor(Cursor.WAIT);
142  });
143 
145  chart.setRangeInfo(rangeInfo);
146  //extend range to block bounderies (ie day, month, year)
147  final long lowerBound = rangeInfo.getLowerBound();
148  final long upperBound = rangeInfo.getUpperBound();
149  final Interval timeRange = new Interval(new DateTime(lowerBound, TimeLineController.getJodaTimeZone()), new DateTime(upperBound, TimeLineController.getJodaTimeZone()));
150 
151  int max = 0;
152  int p = 0; // progress counter
153 
154  //clear old data, and reset ranges and series
155  Platform.runLater(() -> {
156  updateMessage(NbBundle.getMessage(this.getClass(), "CountsViewPane.loggedTask.resetUI"));
157  eventTypeMap.clear();
158  dataSets.clear();
159  dateAxis.getCategories().clear();
160 
161  DateTime start = timeRange.getStart();
162  while (timeRange.contains(start)) {
163  //add bar/'category' label for the current interval
164  final String dateString = start.toString(rangeInfo.getTickFormatter());
165  dateAxis.getCategories().add(dateString);
166 
167  //increment for next iteration
168  start = start.plus(rangeInfo.getPeriodSize().getPeriod());
169  }
170 
171  //make all series to ensure they get created in consistent order
172  EventType.allTypes.forEach(CountsViewPane.this::getSeries);
173  });
174 
175  DateTime start = timeRange.getStart();
176  while (timeRange.contains(start)) {
177 
178  final String dateString = start.toString(rangeInfo.getTickFormatter());
179  DateTime end = start.plus(rangeInfo.getPeriodSize().getPeriod());
180  final Interval interval = new Interval(start, end);
181 
182  //query for current range
183  Map<EventType, Long> eventCounts = filteredEvents.getEventCounts(interval);
184 
185  //increment for next iteration
186  start = end;
187 
188  int dateMax = 0; //used in max tracking
189 
190  //for each type add data to graph
191  for (final EventType et : eventCounts.keySet()) {
192  if (isCancelled()) {
193  return null;
194  }
195 
196  final Long count = eventCounts.get(et);
197  final int fp = p++;
198  if (count > 0) {
199  final double adjustedCount = count == 0 ? 0 : scale.get().adjust(count);
200 
201  dateMax += adjustedCount;
202  final XYChart.Data<String, Number> xyData = new BarChart.Data<>(dateString, adjustedCount);
203 
204  xyData.nodeProperty().addListener((Observable o) -> {
205  final Node node = xyData.getNode();
206  if (node != null) {
207  node.setStyle("-fx-border-width: 2; -fx-border-color: " + ColorUtilities.getRGBCode(et.getSuperType().getColor()) + "; -fx-bar-fill: " + ColorUtilities.getRGBCode(et.getColor())); // NON-NLS
208  node.setCursor(Cursor.HAND);
209 
210  node.setOnMouseEntered((MouseEvent event) -> {
211  //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart
212  final Tooltip tooltip = new Tooltip(
213  NbBundle.getMessage(this.getClass(), "CountsViewPane.tooltip.text",
214  count,
215  et.getDisplayName(),
216  dateString,
217  interval.getEnd().toString(
218  rangeInfo.getTickFormatter())));
219  tooltip.setGraphic(new ImageView(et.getFXImage()));
220  Tooltip.install(node, tooltip);
221  node.setEffect(new DropShadow(10, et.getColor()));
222  });
223  node.setOnMouseExited((MouseEvent event) -> {
224  if (selectedNodes.contains(node)) {
225  node.setEffect(SELECTED_NODE_EFFECT);
226  } else {
227  node.setEffect(null);
228  }
229  });
230 
231  node.addEventHandler(MouseEvent.MOUSE_CLICKED, new BarClickHandler(node, dateString, interval, et));
232  }
233  });
234 
235  max = Math.max(max, dateMax);
236 
237  final double fmax = max;
238 
239  Platform.runLater(() -> {
240  updateMessage(
241  NbBundle.getMessage(this.getClass(), "CountsViewPane.loggedTask.updatingCounts"));
242  getSeries(et).getData().add(xyData);
243  if (scale.get().equals(ScaleType.LINEAR)) {
244  countAxis.setTickUnit(Math.pow(10, Math.max(0, Math.floor(Math.log10(fmax)) - 1)));
245  } else {
246  countAxis.setTickUnit(Double.MAX_VALUE);
247  }
248  countAxis.setUpperBound(1 + fmax * 1.2);
250  updateProgress(fp, rangeInfo.getPeriodsInRange());
251  });
252  } else {
253  final double fmax = max;
254 
255  Platform.runLater(() -> {
256  updateMessage(
257  NbBundle.getMessage(this.getClass(), "CountsViewPane.loggedTask.updatingCounts"));
258  updateProgress(fp, rangeInfo.getPeriodsInRange());
259  });
260  }
261  }
262  }
263 
264  Platform.runLater(() -> {
265  updateMessage(NbBundle.getMessage(this.getClass(), "CountsViewPane.loggedTask.wrappingUp"));
266  updateProgress(1, 1);
268  setCursor(Cursor.NONE);
269  });
270 
271  return max > 0;
272  }
273  };
274  }
275 
276  public CountsViewPane(Pane partPane, Pane contextPane, Region spacer) {
277  super(partPane, contextPane, spacer);
278  chart = new EventCountsChart(dateAxis, countAxis);
280  chart.setData(dataSets);
281  setCenter(chart);
282 
283  settingsNodes = new ArrayList<>(new CountsViewSettingsPane().getChildrenUnmodifiable());
284 
285  dateAxis.getTickMarks().addListener((Observable observable) -> {
287  });
288  dateAxis.categorySpacingProperty().addListener((Observable observable) -> {
290  });
291  dateAxis.getCategories().addListener((Observable observable) -> {
293  });
294 
295  spacer.minWidthProperty().bind(countAxis.widthProperty().add(countAxis.tickLengthProperty()).add(dateAxis.startMarginProperty().multiply(2)));
296  spacer.prefWidthProperty().bind(countAxis.widthProperty().add(countAxis.tickLengthProperty()).add(dateAxis.startMarginProperty().multiply(2)));
297  spacer.maxWidthProperty().bind(countAxis.widthProperty().add(countAxis.tickLengthProperty()).add(dateAxis.startMarginProperty().multiply(2)));
298 
299  scale.addListener(o -> {
300  countAxis.tickLabelsVisibleProperty().bind(scale.isEqualTo(ScaleType.LINEAR));
301  countAxis.tickMarkVisibleProperty().bind(scale.isEqualTo(ScaleType.LINEAR));
302  countAxis.minorTickVisibleProperty().bind(scale.isEqualTo(ScaleType.LINEAR));
303  update();
304  });
305  }
306 
307  @Override
308  protected NumberAxis getYAxis() {
309  return countAxis;
310  }
311 
312  @Override
313  protected CategoryAxis getXAxis() {
314  return dateAxis;
315  }
316 
317  @Override
318  protected double getTickSpacing() {
319  return dateAxis.getCategorySpacing();
320  }
321 
322  @Override
323  protected Effect getSelectionEffect() {
324  return SELECTED_NODE_EFFECT;
325  }
326 
327  @Override
328  protected void applySelectionEffect(Node c1, Boolean applied) {
329  if (applied) {
330  c1.setEffect(getSelectionEffect());
331  } else {
332  c1.setEffect(null);
333  }
334  }
335 
345  private XYChart.Series<String, Number> getSeries(final EventType et) {
346  XYChart.Series<String, Number> series = eventTypeMap.get(et);
347  if (series == null) {
348  series = new XYChart.Series<>();
349  series.setName(et.getDisplayName());
350  eventTypeMap.put(et, series);
351 
352  dataSets.add(series);
353  }
354  return series;
355 
356  }
357 
368  private class BarClickHandler implements EventHandler<MouseEvent> {
369 
370  private ContextMenu barContextMenu;
371 
372  private final Interval interval;
373 
374  private final EventType type;
375 
376  private final Node node;
377 
378  private final String startDateString;
379 
380  public BarClickHandler(Node node, String dateString, Interval countInterval, EventType type) {
381  this.interval = countInterval;
382  this.type = type;
383  this.node = node;
384  this.startDateString = dateString;
385  }
386 
387  @Override
388  public void handle(final MouseEvent e) {
389  e.consume();
390  if (e.getClickCount() == 1) { //single click => selection
391  if (e.getButton().equals(MouseButton.PRIMARY)) {
392  controller.selectTimeAndType(interval, type);
393  selectedNodes.setAll(node);
394  } else if (e.getButton().equals(MouseButton.SECONDARY)) {
395  Platform.runLater(() -> {
396  chart.getContextMenu().hide();
397 
398  if (barContextMenu == null) {
399  barContextMenu = new ContextMenu();
400  barContextMenu.setAutoHide(true);
401  barContextMenu.getItems().addAll(
402  new MenuItem(NbBundle.getMessage(this.getClass(),
403  "Timeline.ui.countsview.menuItem.selectTimeRange")) {
404  {
405  setOnAction((ActionEvent t) -> {
407 
408  selectedNodes.clear();
409  for (XYChart.Series<String, Number> s : dataSets) {
410  s.getData().forEach((XYChart.Data<String, Number> d) -> {
411  if (startDateString.contains(d.getXValue())) {
412  selectedNodes.add(d.getNode());
413  }
414  });
415  }
416  });
417  }
418  },
419  new MenuItem(NbBundle.getMessage(this.getClass(),
420  "Timeline.ui.countsview.menuItem.selectEventType")) {
421  {
422  setOnAction((ActionEvent t) -> {
424 
425  selectedNodes.clear();
426  eventTypeMap.get(type).getData().forEach((d) -> {
427  selectedNodes.add(d.getNode());
428 
429  });
430  });
431  }
432  },
433  new MenuItem(NbBundle.getMessage(this.getClass(),
434  "Timeline.ui.countsview.menuItem.selectTimeandType")) {
435  {
436  setOnAction((ActionEvent t) -> {
437  controller.selectTimeAndType(interval, type);
438  selectedNodes.setAll(node);
439  });
440  }
441  },
442  new SeparatorMenuItem(),
443  new MenuItem(NbBundle.getMessage(this.getClass(),
444  "Timeline.ui.countsview.menuItem.zoomIntoTimeRange")) {
445  {
446  setOnAction((ActionEvent t) -> {
447  if (interval.toDuration().isShorterThan(Seconds.ONE.toStandardDuration()) == false) {
448  controller.pushTimeRange(interval);
449  }
450  });
451  }
452  });
453  barContextMenu.getItems().addAll(getContextMenu().getItems());
454  }
455 
456  barContextMenu.show(node, e.getScreenX(), e.getScreenY());
457  });
458 
459  }
460  } else if (e.getClickCount() >= 2) { //double-click => zoom in time
461  if (interval.toDuration().isLongerThan(Seconds.ONE.toStandardDuration())) {
462  controller.pushTimeRange(interval);
463  } else {
464 
465  int showConfirmDialog = JOptionPane.showConfirmDialog(null,
466  NbBundle.getMessage(CountsViewPane.class, "CountsViewPane.detailSwitchMessage"),
467  NbBundle.getMessage(CountsViewPane.class, "CountsViewPane.detailSwitchTitle"), JOptionPane.YES_NO_OPTION);
468  if (showConfirmDialog == JOptionPane.YES_OPTION) {
470  }
471 
472  /* //I would like to use the JAvafx dialog, but it doesn't
473  * block the ui (because it is embeded in a TopComponent)
474  * -jm
475  *
476  * final Dialogs.CommandLink yes = new
477  * Dialogs.CommandLink("Yes", "switch to Details view");
478  * final Dialogs.CommandLink no = new
479  * Dialogs.CommandLink("No", "return to Counts view with a
480  * resolution of Seconds");
481  * Action choice = Dialogs.create()
482  * .title("Switch to Details View?")
483  * .masthead("There is no temporal resolution smaller than
484  * Seconds.")
485  * .message("Would you like to switch to the Details view
486  * instead?")
487  * .showCommandLinks(Arrays.asList(yes, no));
488  *
489  * if (choice == yes) {
490  * controller.setViewMode(VisualizationMode.DETAIL);
491  * } */
492  }
493  }
494  }
495  }
496 
497  private class CountsViewSettingsPane extends HBox {
498 
499  @FXML
500  private RadioButton logRadio;
501 
502  @FXML
503  private RadioButton linearRadio;
504 
505  @FXML
506  private ToggleGroup scaleGroup;
507 
508  @FXML
509  private Label scaleLabel;
510 
511  @FXML
512  void initialize() {
513  assert logRadio != null : "fx:id=\"logRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'."; // NON-NLS
514  assert linearRadio != null : "fx:id=\"linearRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'."; // NON-NLS
515  logRadio.setSelected(true);
516  scaleGroup.selectedToggleProperty().addListener(observable -> {
517  if (scaleGroup.getSelectedToggle() == linearRadio) {
518  scale.set(ScaleType.LINEAR);
519  }
520  if (scaleGroup.getSelectedToggle() == logRadio) {
521  scale.set(ScaleType.LOGARITHMIC);
522  }
523  });
524 
525  logRadio.setText(NbBundle.getMessage(this.getClass(), "CountsViewPane.logRadio.text"));
526  linearRadio.setText(NbBundle.getMessage(this.getClass(), "CountsViewPane.linearRadio.text"));
527  scaleLabel.setText(NbBundle.getMessage(this.getClass(), "CountsViewPane.scaleLabel.text"));
528  }
529 
531  FXMLConstructor.construct(this, "CountsViewSettingsPane.fxml"); // NON-NLS
532  }
533  }
534 
535  private static enum ScaleType {
536 
537  LINEAR(t -> t.doubleValue()),
538  LOGARITHMIC(t -> Math.log10(t) + 1);
539 
540  private final Function<Long, Double> func;
541 
542  ScaleType(Function<Long, Double> func) {
543  this.func = func;
544  }
545 
546  double adjust(Long c) {
547  return func.apply(c);
548  }
549  }
550 }
final Map< EventType, XYChart.Series< String, Number > > eventTypeMap
CountsViewPane(Pane partPane, Pane contextPane, Region spacer)
void selectTimeAndType(Interval interval, EventType type)
BarClickHandler(Node node, String dateString, Interval countInterval, EventType type)
final ObservableList< BarChart.Series< X, Y > > dataSets
static void construct(Node n, String fxmlFileName)
static RangeDivisionInfo getRangeDivisionInfo(Interval timeRange)
synchronized ReadOnlyObjectProperty< Interval > timeRange()
Map< EventType, Long > getEventCounts(Interval timeRange)
XYChart.Series< String, Number > getSeries(final EventType et)
synchronized void pushTimeRange(Interval timeRange)
synchronized void setViewMode(VisualizationMode visualizationMode)
static Logger getLogger(String name)
Definition: Logger.java:131

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