Autopsy 4.22.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-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 java.util.Arrays;
22import java.util.Optional;
23import java.util.logging.Level;
24import javafx.collections.ObservableList;
25import javafx.event.EventHandler;
26import javafx.scene.Cursor;
27import javafx.scene.Node;
28import javafx.scene.chart.CategoryAxis;
29import javafx.scene.chart.NumberAxis;
30import javafx.scene.chart.StackedBarChart;
31import javafx.scene.chart.XYChart;
32import javafx.scene.control.Alert;
33import javafx.scene.control.ButtonType;
34import javafx.scene.control.ContextMenu;
35import javafx.scene.control.SeparatorMenuItem;
36import javafx.scene.control.Tooltip;
37import javafx.scene.effect.DropShadow;
38import javafx.scene.effect.Effect;
39import javafx.scene.effect.Lighting;
40import javafx.scene.image.ImageView;
41import javafx.scene.input.MouseButton;
42import javafx.scene.input.MouseEvent;
43import javafx.util.StringConverter;
44import org.controlsfx.control.Notifications;
45import org.controlsfx.control.action.Action;
46import org.controlsfx.control.action.ActionUtils;
47import org.joda.time.DateTime;
48import org.joda.time.Interval;
49import org.joda.time.Seconds;
50import org.openide.util.NbBundle;
51import org.sleuthkit.autopsy.coreutils.ColorUtilities;
52import org.sleuthkit.autopsy.coreutils.Logger;
53import org.sleuthkit.autopsy.timeline.EventsModel;
54import org.sleuthkit.autopsy.timeline.PromptDialogManager;
55import org.sleuthkit.autopsy.timeline.TimeLineController;
56import org.sleuthkit.autopsy.timeline.ViewMode;
57import static org.sleuthkit.autopsy.timeline.ui.EventTypeUtils.getColor;
58import static org.sleuthkit.autopsy.timeline.ui.EventTypeUtils.getImagePath;
59import org.sleuthkit.autopsy.timeline.ui.IntervalSelector;
60import org.sleuthkit.autopsy.timeline.ui.TimeLineChart;
61import org.sleuthkit.autopsy.timeline.utils.RangeDivision;
62import org.sleuthkit.datamodel.TskCoreException;
63import org.sleuthkit.datamodel.TimelineEventType;
64
69final class EventCountsChart extends StackedBarChart<String, Number> implements TimeLineChart<String> {
70
71 private static final Logger logger = Logger.getLogger(EventCountsChart.class.getName());
72 private static final Effect SELECTED_NODE_EFFECT = new Lighting();
73 private ContextMenu chartContextMenu;
74
75 private final TimeLineController controller;
76 private final EventsModel filteredEvents;
77
78 private IntervalSelector<? extends String> intervalSelector;
79
80 final ObservableList<Node> selectedNodes;
81
87 private RangeDivision rangeInfo;
88
89 EventCountsChart(TimeLineController controller, CategoryAxis dateAxis, NumberAxis countAxis, ObservableList<Node> selectedNodes) {
90 super(dateAxis, countAxis);
91 this.controller = controller;
92 this.filteredEvents = controller.getEventsModel();
93
94 //configure constant properties on axes and chart
95 dateAxis.setAnimated(true);
96 dateAxis.setLabel(null);
97 dateAxis.setTickLabelsVisible(false);
98 dateAxis.setTickLabelGap(0);
99
100 countAxis.setAutoRanging(false);
101 countAxis.setLowerBound(0);
102 countAxis.setAnimated(true);
103 countAxis.setMinorTickCount(0);
104 countAxis.setTickLabelFormatter(new IntegerOnlyStringConverter());
105
106 setAlternativeRowFillVisible(true);
107 setCategoryGap(2);
108 setLegendVisible(false);
109 setAnimated(true);
110 setTitle(null);
111
113 setOnMousePressed(chartDragHandler);
114 setOnMouseReleased(chartDragHandler);
115 setOnMouseDragged(chartDragHandler);
116
117 setOnMouseClicked(new MouseClickedHandler<>(this));
118
119 this.selectedNodes = selectedNodes;
120
121 getController().getEventsModel().timeRangeProperty().addListener(o -> {
123 });
124 }
125
126 @Override
127 public void clearContextMenu() {
128 chartContextMenu = null;
129 }
130
131 @Override
132 public ContextMenu getContextMenu(MouseEvent clickEvent) {
133 if (chartContextMenu != null) {
134 chartContextMenu.hide();
135 }
136
137 chartContextMenu = ActionUtils.createContextMenu(
138 Arrays.asList(TimeLineChart.newZoomHistoyActionGroup(controller)));
139 chartContextMenu.setAutoHide(true);
140 return chartContextMenu;
141 }
142
143 @Override
144 public TimeLineController getController() {
145 return controller;
146 }
147
148 @Override
149 public void clearIntervalSelector() {
150 getChartChildren().remove(intervalSelector);
151 intervalSelector = null;
152 }
153
154 @Override
155 public IntervalSelector<? extends String> getIntervalSelector() {
156 return intervalSelector;
157 }
158
159 @Override
160 public void setIntervalSelector(IntervalSelector<? extends String> newIntervalSelector) {
161 intervalSelector = newIntervalSelector;
162 //Add a listener that sizes the interval selector to its preferred size.
163 intervalSelector.prefHeightProperty().addListener(observable -> newIntervalSelector.autosize());
164 getChartChildren().add(getIntervalSelector());
165 }
166
167 @Override
169 return new CountsIntervalSelector(this);
170 }
171
172 @Override
173 public ObservableList<Node> getSelectedNodes() {
174 return selectedNodes;
175 }
176
177 void setRangeInfo(RangeDivision rangeInfo) {
178 this.rangeInfo = rangeInfo;
179 }
180
181 Effect getSelectionEffect() {
182 return SELECTED_NODE_EFFECT;
183 }
184
193 @NbBundle.Messages({
194 "# {0} - count",
195 "# {1} - event type displayname",
196 "# {2} - start date time",
197 "# {3} - end date time",
198 "CountsViewPane.tooltip.text={0} {1} events\nbetween {2}\nand {3}"})
199 @Override
200 protected void dataItemAdded(Series<String, Number> series, int itemIndex, Data<String, Number> item) {
201 ExtraData extraValue = (ExtraData) item.getExtraValue();
202 TimelineEventType eventType = extraValue.getEventType();
203 Interval interval = extraValue.getInterval();
204 long count = extraValue.getRawCount();
205
206 item.nodeProperty().addListener(observable -> {
207 final Node node = item.getNode();
208 if (node != null) {
209 node.setStyle("-fx-border-width: 2; "
210 + " -fx-border-color: " + ColorUtilities.getRGBCode(getColor(eventType.getParent())) + "; "
211 + " -fx-bar-fill: " + ColorUtilities.getRGBCode(getColor(eventType))); // NON-NLS
212 node.setCursor(Cursor.HAND);
213
214 final Tooltip tooltip = new Tooltip(Bundle.CountsViewPane_tooltip_text(
215 count, eventType.getDisplayName(),
216 item.getXValue(),
217 interval.getEnd().toString(rangeInfo.getTickFormatter())));
218 tooltip.setGraphic(new ImageView(getImagePath(eventType)));
219 Tooltip.install(node, tooltip);
220
221 node.setOnMouseEntered(mouseEntered -> node.setEffect(new DropShadow(10, getColor(eventType))));
222 node.setOnMouseExited(mouseExited -> node.setEffect(selectedNodes.contains(node) ? SELECTED_NODE_EFFECT : null));
223 node.setOnMouseClicked(new BarClickHandler(item));
224 }
225 });
226 super.dataItemAdded(series, itemIndex, item); //To change body of generated methods, choose Tools | Templates.
227 }
228
232 private static class IntegerOnlyStringConverter extends StringConverter<Number> {
233
234 @Override
235 public String toString(Number n) {
236 //suppress non-integer values
237 return n.intValue() == n.doubleValue()
238 ? Integer.toString(n.intValue()) : "";
239 }
240
241 @Override
242 public Number fromString(String string) {
243 //this is unused but here for symmetry
244 return Double.valueOf(string).intValue();
245 }
246 }
247
252 final static private class CountsIntervalSelector extends IntervalSelector<String> {
253
254 private final EventCountsChart countsChart;
255
256 CountsIntervalSelector(EventCountsChart chart) {
257 super(chart);
258 this.countsChart = chart;
259 }
260
261 @Override
262 protected String formatSpan(String date) {
263 return date;
264 }
265
266 @Override
267 protected Interval adjustInterval(Interval i) {
268 //extend range to block bounderies (ie day, month, year)
270 final long lowerBound = iInfo.getLowerBound();
271 final long upperBound = iInfo.getUpperBound();
272 final DateTime lowerDate = new DateTime(lowerBound, TimeLineController.getJodaTimeZone());
273 final DateTime upperDate = new DateTime(upperBound, TimeLineController.getJodaTimeZone());
274 //add extra block to end that gets cut of by conversion from string/category.
275 return new Interval(lowerDate, upperDate.plus(countsChart.rangeInfo.getPeriodSize().toUnitPeriod()));
276 }
277
278 @Override
279 protected DateTime parseDateTime(String date) {
280 return date == null ? new DateTime(countsChart.rangeInfo.getLowerBound()) : countsChart.rangeInfo.getTickFormatter().parseDateTime(date);
281 }
282 }
283
293 private class BarClickHandler implements EventHandler<MouseEvent> {
294
295 private ContextMenu barContextMenu;
296
297 private final Interval interval;
298
299 private final TimelineEventType type;
300
301 private final Node node;
302
303 private final String startDateString;
304
305 BarClickHandler(XYChart.Data<String, Number> data) {
306 EventCountsChart.ExtraData extraData = (EventCountsChart.ExtraData) data.getExtraValue();
307 this.interval = extraData.getInterval();
308 this.type = extraData.getEventType();
309 this.node = data.getNode();
310 this.startDateString = data.getXValue();
311 }
312
313 @NbBundle.Messages({"Timeline.ui.countsview.menuItem.selectTimeRange=Select Time Range",
314 "SelectIntervalAction.errorMessage=Error selecting interval."})
315 class SelectIntervalAction extends Action {
316
317 SelectIntervalAction() {
318 super(Bundle.Timeline_ui_countsview_menuItem_selectTimeRange());
319 setEventHandler(action -> {
320 try {
321 controller.selectTimeAndType(interval, TimelineEventType.ROOT_EVENT_TYPE);
322
323 } catch (TskCoreException ex) {
324 Notifications.create().owner(getScene().getWindow())
325 .text(Bundle.SelectIntervalAction_errorMessage())
326 .showError();
327 logger.log(Level.SEVERE, "Error selecting interval.", ex);
328 }
329 selectedNodes.clear();
330 for (XYChart.Series<String, Number> s : getData()) {
331 s.getData().forEach((XYChart.Data<String, Number> d) -> {
332 if (startDateString.contains(d.getXValue())) {
333 selectedNodes.add(d.getNode());
334 }
335 });
336 }
337 });
338 }
339 }
340
341 @NbBundle.Messages({"Timeline.ui.countsview.menuItem.selectEventType=Select Event Type",
342 "SelectTypeAction.errorMessage=Error selecting type."})
343 class SelectTypeAction extends Action {
344
345 SelectTypeAction() {
346 super(Bundle.Timeline_ui_countsview_menuItem_selectEventType());
347 setEventHandler(action -> {
348 try {
349 controller.selectTimeAndType(filteredEvents.getSpanningInterval(), type);
350
351 } catch (TskCoreException ex) {
352 Notifications.create().owner(getScene().getWindow())
353 .text(Bundle.SelectTypeAction_errorMessage())
354 .showError();
355 logger.log(Level.SEVERE, "Error selecting type.", ex);
356 }
357 selectedNodes.clear();
358 getData().stream().filter(series -> series.getName().equals(type.getDisplayName()))
359 .findFirst()
360 .ifPresent(series -> series.getData().forEach(data -> selectedNodes.add(data.getNode())));
361 });
362 }
363 }
364
365 @NbBundle.Messages({"Timeline.ui.countsview.menuItem.selectTimeandType=Select Time and Type",
366 "SelectIntervalAndTypeAction.errorMessage=Error selecting interval and type."})
367 class SelectIntervalAndTypeAction extends Action {
368
369 SelectIntervalAndTypeAction() {
370 super(Bundle.Timeline_ui_countsview_menuItem_selectTimeandType());
371 setEventHandler(action -> {
372 try {
373 controller.selectTimeAndType(interval, type);
374
375 } catch (TskCoreException ex) {
376 Notifications.create().owner(getScene().getWindow())
377 .text(Bundle.SelectIntervalAndTypeAction_errorMessage())
378 .showError();
379 logger.log(Level.SEVERE, "Error selecting interval and type.", ex);
380 }
381 selectedNodes.setAll(node);
382 });
383 }
384 }
385
386 @NbBundle.Messages({"Timeline.ui.countsview.menuItem.zoomIntoTimeRange=Zoom into Time Range",
387 "ZoomToIntervalAction.errorMessage=Error zooming to interval."})
388 class ZoomToIntervalAction extends Action {
389
390 ZoomToIntervalAction() {
391 super(Bundle.Timeline_ui_countsview_menuItem_zoomIntoTimeRange());
392 setEventHandler(action -> {
393 try {
394 if (interval.toDuration().isShorterThan(Seconds.ONE.toStandardDuration()) == false) {
395 controller.pushTimeRange(interval);
396 }
397 } catch (TskCoreException ex) {
398 Notifications.create().owner(getScene().getWindow())
399 .text(Bundle.ZoomToIntervalAction_errorMessage())
400 .showError();
401 logger.log(Level.SEVERE, "Error zooming to interval.", ex);
402 }
403 });
404 }
405 }
406
407 @Override
408 @NbBundle.Messages({
409 "CountsViewPane.detailSwitchMessage=There is no temporal resolution smaller than Seconds.\nWould you like to switch to the Details view instead?",
410 "CountsViewPane.detailSwitchTitle=\"Switch to Details View?",
411 "BarClickHandler.selectTimeAndType.errorMessage=Error selecting time and type.",
412 "BarClickHandler_zoomIn_errorMessage=Error zooming in."})
413 public void handle(final MouseEvent e) {
414 e.consume();
415 if (e.getClickCount() == 1) { //single click => selection
416 if (e.getButton().equals(MouseButton.PRIMARY)) {
417 try {
418 controller.selectTimeAndType(interval, type);
419 } catch (TskCoreException ex) {
420 Notifications.create().owner(getScene().getWindow())
421 .text(Bundle.BarClickHandler_selectTimeAndType_errorMessage())
422 .showError();
423 logger.log(Level.SEVERE, "Error selecting time and type.", ex);
424 }
425 selectedNodes.setAll(node);
426 } else if (e.getButton().equals(MouseButton.SECONDARY)) {
427 getContextMenu(e).hide();
428
429 if (barContextMenu == null) {
430 barContextMenu = new ContextMenu();
431 barContextMenu.setAutoHide(true);
432 barContextMenu.getItems().addAll(
433 ActionUtils.createMenuItem(new SelectIntervalAction()),
434 ActionUtils.createMenuItem(new SelectTypeAction()),
435 ActionUtils.createMenuItem(new SelectIntervalAndTypeAction()),
436 new SeparatorMenuItem(),
437 ActionUtils.createMenuItem(new ZoomToIntervalAction()));
438
439 barContextMenu.getItems().addAll(getContextMenu(e).getItems());
440 }
441
442 barContextMenu.show(node, e.getScreenX(), e.getScreenY());
443
444 }
445 } else if (e.getClickCount() >= 2) { //double-click => zoom in time
446 if (interval.toDuration().isLongerThan(Seconds.ONE.toStandardDuration())) {
447 try {
448 controller.pushTimeRange(interval);
449 } catch (TskCoreException ex) {
450 Notifications.create().owner(getScene().getWindow())
451 .text(Bundle.BarClickHandler_zoomIn_errorMessage())
452 .showError();
453 logger.log(Level.SEVERE, "Error zooming in.", ex);
454 }
455 } else {
456 Alert alert = new Alert(Alert.AlertType.CONFIRMATION, Bundle.CountsViewPane_detailSwitchMessage(), ButtonType.YES, ButtonType.NO);
457 alert.setTitle(Bundle.CountsViewPane_detailSwitchTitle());
459
460 alert.showAndWait().ifPresent(response -> {
461 if (response == ButtonType.YES) {
462 controller.setViewMode(ViewMode.DETAIL);
463 }
464 });
465 }
466 }
467 }
468 }
469
474 static class ExtraData {
475
476 private final Interval interval;
477 private final TimelineEventType eventType;
478 private final long rawCount;
479
480 ExtraData(Interval interval, TimelineEventType eventType, long rawCount) {
481 this.interval = interval;
482 this.eventType = eventType;
483 this.rawCount = rawCount;
484 }
485
486 public long getRawCount() {
487 return rawCount;
488 }
489
490 public Interval getInterval() {
491 return interval;
492 }
493
494 public TimelineEventType getEventType() {
495 return eventType;
496 }
497 }
498}
final ReadOnlyObjectWrapper< Interval > timeRangeProperty
IntervalSelector(IntervalSelectorProvider< X > chart)
static RangeDivision getRangeDivision(Interval timeRange, DateTimeZone timeZone)
ObservableList<? extends Node > getSelectedNodes()
IntervalSelector<? extends X > getIntervalSelector()

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