Autopsy  4.20.0
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 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 com.google.common.collect.ImmutableList;
22 import com.google.common.collect.Lists;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.function.Function;
26 import javafx.application.Platform;
27 import javafx.beans.Observable;
28 import javafx.beans.binding.BooleanBinding;
29 import javafx.beans.property.SimpleObjectProperty;
30 import javafx.collections.FXCollections;
31 import javafx.concurrent.Task;
32 import javafx.fxml.FXML;
33 import javafx.geometry.Insets;
34 import javafx.scene.Cursor;
35 import javafx.scene.Node;
36 import javafx.scene.chart.CategoryAxis;
37 import javafx.scene.chart.NumberAxis;
38 import javafx.scene.chart.XYChart;
39 import javafx.scene.control.Label;
40 import javafx.scene.control.RadioButton;
41 import javafx.scene.control.ToggleGroup;
42 import javafx.scene.control.Tooltip;
43 import javafx.scene.image.Image;
44 import javafx.scene.image.ImageView;
45 import javafx.scene.layout.BorderPane;
46 import javafx.scene.layout.HBox;
47 import javafx.scene.layout.Pane;
48 import javafx.scene.text.Font;
49 import javafx.scene.text.FontPosture;
50 import javafx.scene.text.FontWeight;
51 import javafx.scene.text.Text;
52 import javafx.scene.text.TextFlow;
53 import org.controlsfx.control.PopOver;
54 import org.joda.time.Interval;
55 import org.openide.util.NbBundle;
64 import org.sleuthkit.datamodel.TimelineEventType;
65 
82 public class CountsViewPane extends AbstractTimelineChart<String, Number, Node, EventCountsChart> {
83 
84  private static final Logger logger = Logger.getLogger(CountsViewPane.class.getName());
85 
86  private final NumberAxis countAxis = new NumberAxis();
87  private final CategoryAxis dateAxis = new CategoryAxis(FXCollections.<String>observableArrayList());
88 
89  private final SimpleObjectProperty<Scale> scaleProp = new SimpleObjectProperty<>(Scale.LOGARITHMIC);
90 
91  @Override
92  protected String getTickMarkLabel(String labelValueString) {
93  return labelValueString;
94  }
95 
96  @Override
97  protected Boolean isTickBold(String value) {
98  return dataSeries.stream().flatMap(series -> series.getData().stream())
99  .anyMatch(data -> data.getXValue().equals(value) && data.getYValue().intValue() > 0);
100  }
101 
102  @Override
103  protected Task<Boolean> getNewUpdateTask() {
104  return new CountsUpdateTask();
105  }
106 
112  @NbBundle.Messages({
113  "# {0} - scale name",
114  "CountsViewPane.numberOfEvents=Number of Events ({0})"})
116  super(controller);
117 
118  setChart(new EventCountsChart(controller, dateAxis, countAxis, getSelectedNodes()));
119  getChart().setData(dataSeries);
120  Tooltip.install(getChart(), getDefaultTooltip());
121 
122  dateAxis.getTickMarks().addListener((Observable tickMarks) -> layoutDateLabels());
123  dateAxis.categorySpacingProperty().addListener((Observable spacing) -> layoutDateLabels());
124  dateAxis.getCategories().addListener((Observable categories) -> layoutDateLabels());
125 
126  //bind tick visibility to scaleProp
127  BooleanBinding scaleIsLinear = scaleProp.isEqualTo(Scale.LINEAR);
128  countAxis.tickLabelsVisibleProperty().bind(scaleIsLinear);
129  countAxis.tickMarkVisibleProperty().bind(scaleIsLinear);
130  countAxis.minorTickVisibleProperty().bind(scaleIsLinear);
131  scaleProp.addListener(scale -> {
132  refresh();
134  });
136  }
137 
138  @Override
139  final protected NumberAxis getYAxis() {
140  return countAxis;
141  }
142 
143  @Override
144  final protected CategoryAxis getXAxis() {
145  return dateAxis;
146  }
147 
148  @Override
149  protected double getTickSpacing() {
150  return dateAxis.getCategorySpacing();
151  }
152 
153  @Override
154  protected void applySelectionEffect(Node c1, Boolean applied) {
155  c1.setEffect(applied ? getChart().getSelectionEffect() : null);
156  }
157 
159  @Override
160  protected void clearData() {
161  for (XYChart.Series<String, Number> series : dataSeries) {
162  series.getData().clear();
163  }
164  dataSeries.clear();
165  eventTypeToSeriesMap.clear();
166  createSeries();
167  }
168 
169  @Override
170  final protected ViewMode getViewMode() {
171  return ViewMode.COUNTS;
172  }
173 
174  @Override
175  protected ImmutableList<Node> getSettingsControls() {
176  return ImmutableList.copyOf(new CountsViewSettingsPane().getChildrenUnmodifiable());
177  }
178 
179  @Override
180  protected boolean hasCustomTimeNavigationControls() {
181  return false;
182  }
183 
184  @Override
185  protected ImmutableList<Node> getTimeNavigationControls() {
186  return ImmutableList.of();
187  }
188 
193  private void syncAxisScaleLabel() {
194  countAxis.setLabel(Bundle.CountsViewPane_numberOfEvents(scaleProp.get().getDisplayName()));
195  }
196 
200  @NbBundle.Messages({
201  "ScaleType.Linear=Linear",
202  "ScaleType.Logarithmic=Logarithmic"})
203  private static enum Scale implements Function<Long, Double> {
204 
205  LINEAR(Bundle.ScaleType_Linear()) {
206  @Override
207  public Double apply(Long inValue) {
208  return inValue.doubleValue();
209  }
210  },
211  LOGARITHMIC(Bundle.ScaleType_Logarithmic()) {
212  @Override
213  public Double apply(Long inValue) {
214  return Math.log10(inValue) + 1;
215  }
216  };
217 
218  private final String displayName;
219 
225  Scale(String displayName) {
226  this.displayName = displayName;
227  }
228 
234  private String getDisplayName() {
235  return displayName;
236  }
237  }
238 
239  @Override
240  protected double getAxisMargin() {
241  return dateAxis.getStartMargin() + dateAxis.getEndMargin();
242  }
243 
244  /*
245  * A Pane that contains widgets to adjust settings specific to a
246  * CountsViewPane
247  */
248  private class CountsViewSettingsPane extends HBox {
249 
250  @FXML
251  private RadioButton logRadio;
252  @FXML
253  private RadioButton linearRadio;
254  @FXML
255  private ToggleGroup scaleGroup;
256 
257  @FXML
258  private Label scaleLabel;
259 
260  @FXML
261  private ImageView logImageView;
262  @FXML
263  private ImageView linearImageView;
264 
265  @FXML
266  @NbBundle.Messages({
267  "CountsViewPane.logRadio.text=Logarithmic",
268  "CountsViewPane.scaleLabel.text=Scale:",
269  "CountsViewPane.scaleHelp.label.text=Scales: ",
270  "CountsViewPane.linearRadio.text=Linear",
271  "CountsViewPane.scaleHelpLinear=The linear scale is good for many use cases. When this scale is selected, the height of the bars represents the counts in a linear, one-to-one fashion, and the y-axis is labeled with values. When the range of values is very large, time periods with low counts may have a bar that is too small to see. To help the user detect this, the labels for date ranges with events are bold. To see bars that are too small, there are three options: adjust the window size so that the timeline has more vertical space, adjust the time range shown so that time periods with larger bars are excluded, or adjust the scale setting to logarithmic.",
272  "CountsViewPane.scaleHelpLog=The logarithmic scale represents the number of events in a non-linear way that compresses the difference between large and small numbers. Note that even with the logarithmic scale, an extremely large difference in counts may still produce bars too small to see. In this case the only option may be to filter events to reduce the difference in counts. NOTE: Because the logarithmic scale is applied to each event type separately, the meaning of the height of the combined bar is not intuitive, and to emphasize this, no labels are shown on the y-axis with the logarithmic scale. The logarithmic scale should be used to quickly compare the counts ",
273  "CountsViewPane.scaleHelpLog2=across time within a type, or across types for one time period, but not both.",
274  "CountsViewPane.scaleHelpLog3= The actual counts (available in tooltips or the result viewer) should be used for absolute comparisons. Use the logarithmic scale with care."})
275  void initialize() {
276  assert logRadio != null : "fx:id=\"logRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'."; // NON-NLS
277  assert linearRadio != null : "fx:id=\"linearRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'."; // NON-NLS
278  scaleLabel.setText(Bundle.CountsViewPane_scaleLabel_text());
279  linearRadio.setText(Bundle.CountsViewPane_linearRadio_text());
280  logRadio.setText(Bundle.CountsViewPane_logRadio_text());
281 
282  scaleGroup.selectedToggleProperty().addListener((observable, oldToggle, newToggle) -> {
283  if (newToggle == linearRadio) {
284  scaleProp.set(Scale.LINEAR);
285  } else if (newToggle == logRadio) {
286  scaleProp.set(Scale.LOGARITHMIC);
287  }
288  });
289  logRadio.setSelected(true);
290 
291  //make a popup help "window" with a description of the log scale.
292  logImageView.setCursor(Cursor.HAND);
293  logImageView.setOnMouseClicked(clicked -> {
294  Text text = new Text(Bundle.CountsViewPane_scaleHelpLog());
295  Text text2 = new Text(Bundle.CountsViewPane_scaleHelpLog2());
296  Font baseFont = text.getFont();
297  text2.setFont(Font.font(baseFont.getFamily(), FontWeight.BOLD, FontPosture.ITALIC, baseFont.getSize()));
298  Text text3 = new Text(Bundle.CountsViewPane_scaleHelpLog3());
299  showPopoverHelp(logImageView,
300  Bundle.CountsViewPane_logRadio_text(),
301  logImageView.getImage(),
302  new TextFlow(text, text2, text3));
303  });
304 
305  //make a popup help "window" with a description of the linear scale.
306  linearImageView.setCursor(Cursor.HAND);
307  linearImageView.setOnMouseClicked(clicked -> {
308  Text text = new Text(Bundle.CountsViewPane_scaleHelpLinear());
309  text.setWrappingWidth(480); //This is a hack to fix the layout.
310  showPopoverHelp(linearImageView,
311  Bundle.CountsViewPane_linearRadio_text(),
312  linearImageView.getImage(), text);
313  });
314  }
315 
319  CountsViewSettingsPane() {
320  FXMLConstructor.construct(this, "CountsViewSettingsPane.fxml"); // NON-NLS
321  }
322  }
323 
337  private static void showPopoverHelp(final Node owner, final String headerText, final Image headerImage, final Node content) {
338  Pane borderPane = new BorderPane(null, null, new ImageView(headerImage),
339  content,
340  new Label(headerText));
341  borderPane.setPadding(new Insets(10));
342  borderPane.setPrefWidth(500);
343 
344  PopOver popOver = new PopOver(borderPane);
345  popOver.setDetachable(false);
346  popOver.setArrowLocation(PopOver.ArrowLocation.TOP_CENTER);
347 
348  popOver.show(owner);
349  }
350 
356  @NbBundle.Messages({
357  "CountsViewPane.loggedTask.name=Updating Counts View",
358  "CountsViewPane.loggedTask.updatingCounts=Populating view"})
359  private class CountsUpdateTask extends ViewRefreshTask<List<String>> {
360 
361  CountsUpdateTask() {
362  super(Bundle.CountsViewPane_loggedTask_name(), true);
363  }
364 
365  @Override
366  protected void succeeded() {
367  super.succeeded();
369  }
370 
371  @Override
372  protected Boolean call() throws Exception {
373  super.call();
374  if (isCancelled()) {
375  return null;
376  }
377  EventsModel eventsModel = getEventsModel();
378 
380  getChart().setRangeInfo(rangeInfo); //do we need this. It seems like a hack.
381  List<Interval> intervals = rangeInfo.getIntervals(TimeLineController.getJodaTimeZone());
382 
383  //clear old data, and reset ranges and series
384  resetView(Lists.transform(intervals, interval -> interval.getStart().toString(rangeInfo.getTickFormatter())));
385 
386  updateMessage(Bundle.CountsViewPane_loggedTask_updatingCounts());
387  int chartMax = 0;
388  int numIntervals = intervals.size();
389  Scale activeScale = scaleProp.get();
390 
391  /*
392  * For each interval, query the database for event counts and add
393  * the counts to the chart. Doing this in chunks might seem
394  * inefficient but it lets us reuse more cached results as the user
395  * navigates to overlapping views.
396  */
397  for (int i = 0; i < numIntervals; i++) {
398  if (isCancelled()) {
399  return null;
400  }
401  updateProgress(i, numIntervals);
402  final Interval interval = intervals.get(i);
403  int maxPerInterval = 0;
404 
405  //query for current interval
406  Map<TimelineEventType, Long> eventCounts = eventsModel.getEventCounts(interval);
407 
408  //for each type add data to graph
409  for (final TimelineEventType eventType : eventCounts.keySet()) {
410  if (isCancelled()) {
411  return null;
412  }
413 
414  final Long count = eventCounts.get(eventType);
415  if (count > 0) {
416  final String intervalCategory = interval.getStart().toString(rangeInfo.getTickFormatter());
417  final double adjustedCount = activeScale.apply(count);
418 
419  final XYChart.Data<String, Number> dataItem
420  = new XYChart.Data<>(intervalCategory, adjustedCount,
421  new EventCountsChart.ExtraData(interval, eventType, count));
422  Platform.runLater(() -> getSeries(eventType).getData().add(dataItem));
423  maxPerInterval += adjustedCount;
424  }
425  }
426  chartMax = Math.max(chartMax, maxPerInterval);
427  }
428 
429  //adjust vertical axis according to scale type and max counts
430  double countAxisUpperbound = 1 + chartMax * 1.2;
431  double tickUnit = Scale.LINEAR.equals(activeScale)
432  ? Math.pow(10, Math.max(0, Math.floor(Math.log10(chartMax)) - 1))
433  : Double.MAX_VALUE;
434 
435  Platform.runLater(() -> {
436  countAxis.setTickUnit(tickUnit);
437  countAxis.setUpperBound(countAxisUpperbound);
438  });
439 
440  return chartMax > 0; // are there events
441  }
442 
443  @Override
444  protected void setDateValues(List<String> categories) {
445  dateAxis.getCategories().setAll(categories);
446  }
447  }
448 }
final Map< TimelineEventType, XYChart.Series< X, Y > > eventTypeToSeriesMap
final XYChart.Series< X, Y > getSeries(final TimelineEventType eventType)
final ObservableList< XYChart.Series< X, Y > > dataSeries
synchronized List< Interval > getIntervals(DateTimeZone tz)
static void showPopoverHelp(final Node owner, final String headerText, final Image headerImage, final Node content)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
static RangeDivision getRangeDivision(Interval timeRange, DateTimeZone timeZone)
Map< TimelineEventType, Long > getEventCounts(Interval timeRange)

Copyright © 2012-2022 Basis Technology. Generated on: Tue Aug 1 2023
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.