Autopsy 4.22.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 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 */
19package org.sleuthkit.autopsy.timeline.ui.countsview;
20
21import com.google.common.collect.ImmutableList;
22import com.google.common.collect.Lists;
23import java.util.List;
24import java.util.Map;
25import java.util.function.Function;
26import javafx.application.Platform;
27import javafx.beans.Observable;
28import javafx.beans.binding.BooleanBinding;
29import javafx.beans.property.SimpleObjectProperty;
30import javafx.collections.FXCollections;
31import javafx.concurrent.Task;
32import javafx.fxml.FXML;
33import javafx.geometry.Insets;
34import javafx.scene.Cursor;
35import javafx.scene.Node;
36import javafx.scene.chart.CategoryAxis;
37import javafx.scene.chart.NumberAxis;
38import javafx.scene.chart.XYChart;
39import javafx.scene.control.Label;
40import javafx.scene.control.RadioButton;
41import javafx.scene.control.ToggleGroup;
42import javafx.scene.control.Tooltip;
43import javafx.scene.image.Image;
44import javafx.scene.image.ImageView;
45import javafx.scene.layout.BorderPane;
46import javafx.scene.layout.HBox;
47import javafx.scene.layout.Pane;
48import javafx.scene.text.Font;
49import javafx.scene.text.FontPosture;
50import javafx.scene.text.FontWeight;
51import javafx.scene.text.Text;
52import javafx.scene.text.TextFlow;
53import org.controlsfx.control.PopOver;
54import org.joda.time.Interval;
55import org.openide.util.NbBundle;
56import org.sleuthkit.autopsy.coreutils.Logger;
57import org.sleuthkit.autopsy.coreutils.ThreadConfined;
58import org.sleuthkit.autopsy.timeline.FXMLConstructor;
59import org.sleuthkit.autopsy.timeline.EventsModel;
60import org.sleuthkit.autopsy.timeline.TimeLineController;
61import org.sleuthkit.autopsy.timeline.ViewMode;
62import org.sleuthkit.autopsy.timeline.ui.AbstractTimelineChart;
63import org.sleuthkit.autopsy.timeline.utils.RangeDivision;
64import org.sleuthkit.datamodel.TimelineEventType;
65
82public 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
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
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) {
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());
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.
311 Bundle.CountsViewPane_linearRadio_text(),
312 linearImageView.getImage(), text);
313 });
314 }
315
319 @SuppressWarnings("this-escape")
320 CountsViewSettingsPane() {
321 FXMLConstructor.construct(this, "CountsViewSettingsPane.fxml"); // NON-NLS
322 }
323 }
324
338 private static void showPopoverHelp(final Node owner, final String headerText, final Image headerImage, final Node content) {
339 Pane borderPane = new BorderPane(null, null, new ImageView(headerImage),
340 content,
341 new Label(headerText));
342 borderPane.setPadding(new Insets(10));
343 borderPane.setPrefWidth(500);
344
345 PopOver popOver = new PopOver(borderPane);
346 popOver.setDetachable(false);
347 popOver.setArrowLocation(PopOver.ArrowLocation.TOP_CENTER);
348
349 popOver.show(owner);
350 }
351
357 @NbBundle.Messages({
358 "CountsViewPane.loggedTask.name=Updating Counts View",
359 "CountsViewPane.loggedTask.updatingCounts=Populating view"})
360 private class CountsUpdateTask extends ViewRefreshTask<List<String>> {
361
362 CountsUpdateTask() {
363 super(Bundle.CountsViewPane_loggedTask_name(), true);
364 }
365
366 @Override
367 protected void succeeded() {
368 super.succeeded();
370 }
371
372 @Override
373 protected Boolean call() throws Exception {
374 super.call();
375 if (isCancelled()) {
376 return null;
377 }
378 EventsModel eventsModel = getEventsModel();
379
381 getChart().setRangeInfo(rangeInfo); //do we need this. It seems like a hack.
382 List<Interval> intervals = rangeInfo.getIntervals(TimeLineController.getJodaTimeZone());
383
384 //clear old data, and reset ranges and series
385 resetView(Lists.transform(intervals, interval -> interval.getStart().toString(rangeInfo.getTickFormatter())));
386
387 updateMessage(Bundle.CountsViewPane_loggedTask_updatingCounts());
388 int chartMax = 0;
389 int numIntervals = intervals.size();
390 Scale activeScale = scaleProp.get();
391
392 /*
393 * For each interval, query the database for event counts and add
394 * the counts to the chart. Doing this in chunks might seem
395 * inefficient but it lets us reuse more cached results as the user
396 * navigates to overlapping views.
397 */
398 for (int i = 0; i < numIntervals; i++) {
399 if (isCancelled()) {
400 return null;
401 }
402 updateProgress(i, numIntervals);
403 final Interval interval = intervals.get(i);
404 int maxPerInterval = 0;
405
406 //query for current interval
407 Map<TimelineEventType, Long> eventCounts = eventsModel.getEventCounts(interval);
408
409 //for each type add data to graph
410 for (final TimelineEventType eventType : eventCounts.keySet()) {
411 if (isCancelled()) {
412 return null;
413 }
414
415 final Long count = eventCounts.get(eventType);
416 if (count > 0) {
417 final String intervalCategory = interval.getStart().toString(rangeInfo.getTickFormatter());
418 final double adjustedCount = activeScale.apply(count);
419
420 final XYChart.Data<String, Number> dataItem
421 = new XYChart.Data<>(intervalCategory, adjustedCount,
422 new EventCountsChart.ExtraData(interval, eventType, count));
423 Platform.runLater(() -> getSeries(eventType).getData().add(dataItem));
424 maxPerInterval += adjustedCount;
425 }
426 }
427 chartMax = Math.max(chartMax, maxPerInterval);
428 }
429
430 //adjust vertical axis according to scale type and max counts
431 double countAxisUpperbound = 1 + chartMax * 1.2;
432 double tickUnit = Scale.LINEAR.equals(activeScale)
433 ? Math.pow(10, Math.max(0, Math.floor(Math.log10(chartMax)) - 1))
434 : Double.MAX_VALUE;
435
436 Platform.runLater(() -> {
437 countAxis.setTickUnit(tickUnit);
438 countAxis.setUpperBound(countAxisUpperbound);
439 });
440
441 return chartMax > 0; // are there events
442 }
443
444 @Override
445 protected void setDateValues(List<String> categories) {
446 dateAxis.getCategories().setAll(categories);
447 }
448 }
449}
synchronized static Logger getLogger(String name)
Definition Logger.java:124
Map< TimelineEventType, Long > getEventCounts(Interval timeRange)
final ObservableList< XYChart.Series< X, Y > > dataSeries
final Map< TimelineEventType, XYChart.Series< X, Y > > eventTypeToSeriesMap
final XYChart.Series< X, Y > getSeries(final TimelineEventType eventType)
static void showPopoverHelp(final Node owner, final String headerText, final Image headerImage, final Node content)
synchronized List< Interval > getIntervals(DateTimeZone tz)
static RangeDivision getRangeDivision(Interval timeRange, DateTimeZone timeZone)

Copyright © 2012-2024 Sleuth Kit Labs. Generated on:
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.