Autopsy 4.22.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
DetailsChartLane.java
Go to the documentation of this file.
1/*
2 * Autopsy Forensic Browser
3 *
4 * Copyright 2016-2019 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.detailview;
20
21import com.google.common.collect.Iterables;
22import com.google.common.collect.Range;
23import com.google.common.collect.TreeRangeMap;
24import java.util.Arrays;
25import java.util.Collection;
26import java.util.Comparator;
27import java.util.HashMap;
28import java.util.HashSet;
29import java.util.Map;
30import java.util.Set;
31import java.util.function.Function;
32import java.util.function.Predicate;
33import java.util.stream.Collectors;
34import java.util.stream.Stream;
35import javafx.application.Platform;
36import javafx.beans.InvalidationListener;
37import javafx.beans.property.ReadOnlyDoubleProperty;
38import javafx.beans.property.ReadOnlyDoubleWrapper;
39import javafx.collections.FXCollections;
40import javafx.collections.ObservableList;
41import javafx.geometry.Insets;
42import javafx.scene.Cursor;
43import javafx.scene.Group;
44import javafx.scene.Scene;
45import javafx.scene.chart.Axis;
46import javafx.scene.chart.XYChart;
47import javafx.scene.control.ContextMenu;
48import javafx.scene.control.Tooltip;
49import javafx.scene.input.MouseEvent;
50import static javafx.scene.layout.Region.USE_PREF_SIZE;
51import org.joda.time.DateTime;
52import org.sleuthkit.autopsy.coreutils.ThreadConfined;
53import org.sleuthkit.autopsy.timeline.TimeLineController;
54import org.sleuthkit.autopsy.timeline.ui.AbstractTimelineChart;
55import org.sleuthkit.autopsy.timeline.ui.ContextMenuProvider;
56import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.DetailViewEvent;
57import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventCluster;
58import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventStripe;
59import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.SingleDetailsViewEvent;
60import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.DescriptionFilter;
61import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.FilterState;
62import org.sleuthkit.datamodel.TskCoreException;
63
73abstract class DetailsChartLane<Y extends DetailViewEvent> extends XYChart<DateTime, Y> implements ContextMenuProvider {
74
75 private static final String STYLE_SHEET = GuideLine.class.getResource("EventsDetailsChart.css").toExternalForm(); //NON-NLS
76
77 static final int MINIMUM_EVENT_NODE_GAP = 4;
78 static final int MINIMUM_ROW_HEIGHT = 24;
79
80 private final DetailsChart parentChart;
81 private final TimeLineController controller;
82 private final DetailsChartLayoutSettings layoutSettings;
83 private final ObservableList<EventNodeBase<?>> selectedNodes;
84
85 private final Map<Y, EventNodeBase<?>> eventMap = new HashMap<>();
86
87 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
88 final ObservableList< EventNodeBase<?>> nodes = FXCollections.observableArrayList();
89 final ObservableList< EventNodeBase<?>> sortedNodes = nodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis));
90
91 private final boolean useQuickHideFilters;
92
93 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass
94 private double descriptionWidth;
95 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)//at start of layout pass
96 private Set<String> activeQuickHidefilters = new HashSet<>();
97
99 final InvalidationListener layoutInvalidationListener = observable -> layoutPlotChildren();
100
101 boolean quickHideFiltersEnabled() {
102 return useQuickHideFilters;
103 }
104
105 @Override
106 public void clearContextMenu() {
107 parentChart.clearContextMenu();
108 }
109
110 @Override
111 public ContextMenu getContextMenu(MouseEvent clickEvent) {
112 return parentChart.getContextMenu(clickEvent);
113 }
114
115 EventNodeBase<?> createNode(DetailsChartLane<?> chart, DetailViewEvent event) throws TskCoreException {
116 if (event.getEventIDs().size() == 1) {
117 return new SingleEventNode(this, new SingleDetailsViewEvent(controller.getEventsModel().getEventById(Iterables.getOnlyElement(event.getEventIDs()))), null);
118 } else if (event instanceof SingleDetailsViewEvent) {
119 return new SingleEventNode(chart, (SingleDetailsViewEvent) event, null);
120 } else if (event instanceof EventCluster) {
121 return new EventClusterNode(chart, (EventCluster) event, null);
122 } else {
123 return new EventStripeNode(chart, (EventStripe) event, null);
124 }
125 }
126
127 @Override
128 synchronized protected void layoutPlotChildren() {
129 setCursor(Cursor.WAIT);
130 if (useQuickHideFilters) {
131 //These don't change during a layout pass and are expensive to compute per node. So we do it once at the start
132 activeQuickHidefilters = getController().getQuickHideFilters().stream()
136 .collect(Collectors.toSet());
137 }
138 //This dosn't change during a layout pass and is expensive to compute per node. So we do it once at the start
139 descriptionWidth = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE;
140
141 if (layoutSettings.getBandByType()) {
142 maxY.set(0);
143 sortedNodes.stream()
144 .collect(Collectors.groupingBy(EventNodeBase<?>::getEventType)).values()
145 .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get())));
146 } else {
147 maxY.set(layoutEventBundleNodes(sortedNodes, 0));
148 }
149 doAdditionalLayout();
150 setCursor(null);
151 }
152
153 @Override
154 public TimeLineController getController() {
155 return controller;
156 }
157
158 public ObservableList<EventNodeBase<?>> getSelectedNodes() {
159 return selectedNodes;
160 }
161
162 public ReadOnlyDoubleProperty maxVScrollProperty() {
163 return maxY.getReadOnlyProperty();
164 }
168 private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0);
169
170 DetailsChartLane(DetailsChart parentChart, Axis<DateTime> dateAxis, Axis<Y> verticalAxis, boolean useQuickHideFilters) {
171 super(dateAxis, verticalAxis);
172 this.parentChart = parentChart;
173 this.layoutSettings = parentChart.getLayoutSettings();
174 this.controller = parentChart.getController();
175 this.selectedNodes = parentChart.getSelectedNodes();
176 this.useQuickHideFilters = useQuickHideFilters;
177
178 //add a dummy series or the chart is never rendered
179 setData(FXCollections.observableList(Arrays.asList(new Series<>())));
180
181 Tooltip.install(this, AbstractTimelineChart.getDefaultTooltip());
182
183 dateAxis.setAutoRanging(false);
184 setLegendVisible(false);
185 setPadding(Insets.EMPTY);
186 setAlternativeColumnFillVisible(true);
187
188 sceneProperty().addListener(observable -> {
189 Scene scene = getScene();
190 if (scene != null && scene.getStylesheets().contains(STYLE_SHEET) == false) {
191 scene.getStylesheets().add(STYLE_SHEET);
192 }
193 });
194
195 //add listener for events that should trigger layout
196 layoutSettings.bandByTypeProperty().addListener(layoutInvalidationListener);
197 layoutSettings.oneEventPerRowProperty().addListener(layoutInvalidationListener);
198 layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener);
199 layoutSettings.truncateAllProperty().addListener(layoutInvalidationListener);
200 layoutSettings.descrVisibilityProperty().addListener(layoutInvalidationListener);
201 controller.getQuickHideFilters().addListener(layoutInvalidationListener);
202
203 //all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly
204 getPlotChildren().add(nodeGroup);
205 }
206
233 public double layoutEventBundleNodes(final Collection<? extends EventNodeBase<?>> nodes, final double minY) {
234 // map from y-ranges to maximum x
235 TreeRangeMap<Double, Double> maxXatY = TreeRangeMap.create();
236
237 // maximum y values occupied by any of the given nodes, updated as nodes are layed out.
238 double localMax = minY;
239
240 //for each node do a recursive layout to size it and then position it in first available slot
241 for (EventNodeBase<?> bundleNode : nodes) {
242 if (useQuickHideFilters && activeQuickHidefilters.contains(bundleNode.getDescription())) {
243 //if the node hiden is hidden by quick hide filter, hide it and skip layout
244 bundleNode.setVisible(false);
245 bundleNode.setManaged(false);
246 } else {
247 layoutBundleHelper(bundleNode);
248 //get computed height and width
249 double h = bundleNode.getBoundsInLocal().getHeight();
250 double w = bundleNode.getBoundsInLocal().getWidth();
251 //get left and right x coords from axis plus computed width
252 double xLeft = getXForEpochMillis(bundleNode.getStartMillis()) - bundleNode.getLayoutXCompensation();
253 double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP;
254
255 //initial test position
256 double yTop = (layoutSettings.getOneEventPerRow())
257 ? (localMax + MINIMUM_EVENT_NODE_GAP)// if onePerRow, just put it at end
258 : computeYTop(minY, h, maxXatY, xLeft, xRight);
259
260 localMax = Math.max(yTop + h, localMax);
261
262 //animate node to new position
263 bundleNode.animateTo(xLeft, yTop);
264 }
265 }
266 return localMax; //return new max
267 }
268
269 @Override
270 final public void requestChartLayout() {
271 super.requestChartLayout();
272 }
273
274 double getXForEpochMillis(Long millis) {
275 DateTime dateTime = new DateTime(millis);
276 return getXAxis().getDisplayPosition(dateTime);
277 }
278
279 @Deprecated
280 @Override
281 protected void dataItemAdded(Series<DateTime, Y> series, int itemIndex, Data<DateTime, Y> item) {
282 }
283
284 @Deprecated
285 @Override
286 protected void dataItemRemoved(Data<DateTime, Y> item, Series<DateTime, Y> series) {
287 }
288
289 @Deprecated
290 @Override
291 protected void dataItemChanged(Data<DateTime, Y> item) {
292 }
293
294 @Deprecated
295 @Override
296 protected void seriesAdded(Series<DateTime, Y> series, int seriesIndex) {
297 }
298
299 @Deprecated
300 @Override
301 protected void seriesRemoved(Series<DateTime, Y> series) {
302 }
303
311 void addEvent(Y event) throws TskCoreException {
312 EventNodeBase<?> eventNode = createNode(this, event);
313 eventMap.put(event, eventNode);
314 Platform.runLater(() -> {
315 nodes.add(eventNode);
316 nodeGroup.getChildren().add(eventNode);
317 });
318 }
319
327 void removeEvent(Y event) {
328 EventNodeBase<?> removedNode = eventMap.remove(event);
329 Platform.runLater(() -> {
330 nodes.remove(removedNode);
331 nodeGroup.getChildren().removeAll(removedNode);
332 });
333 }
334
339 final Group nodeGroup = new Group();
340
341 public synchronized void setVScroll(double vScrollValue) {
342 nodeGroup.setTranslateY(-vScrollValue);
343 }
344
348 synchronized Iterable<EventNodeBase<?>> getAllNodes() {
349 return getNodes(dummy -> true);
350 }
351
355 private synchronized Iterable<EventNodeBase<?>> getNodes(Predicate<EventNodeBase<?>> predicate) {
356 //use this recursive function to flatten the tree of nodes into an single stream.
357 Function<EventNodeBase<?>, Stream<EventNodeBase<?>>> stripeFlattener
358 = new Function<EventNodeBase<?>, Stream<EventNodeBase<?>>>() {
359 @Override
360 public Stream<EventNodeBase<?>> apply(EventNodeBase<?> node) {
361 return Stream.concat(
362 Stream.of(node),
363 node.getSubNodes().stream().flatMap(this::apply));
364 }
365 };
366
367 return sortedNodes.stream()
368 .flatMap(stripeFlattener)
369 .filter(predicate).collect(Collectors.toList());
370 }
371
387 double computeYTop(double yMin, double h, TreeRangeMap<Double, Double> maxXatY, double xLeft, double xRight) {
388 double yTop = yMin;
389 double yBottom = yTop + h;
390 //until the node is not overlapping any others try moving it down.
391 boolean overlapping = true;
392 while (overlapping) {
393 overlapping = false;
394 //check each pixel from bottom to top.
395 for (double y = yBottom; y >= yTop; y -= MINIMUM_ROW_HEIGHT) {
396 final Double maxX = maxXatY.get(y);
397 if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) {
398 //if that pixel is already used
399 //jump top to this y value and repeat until free slot is found.
400 overlapping = true;
401 yTop = y + MINIMUM_EVENT_NODE_GAP;
402 yBottom = yTop + h;
403 break;
404 }
405 }
406 }
407 maxXatY.put(Range.closed(yTop, yBottom), xRight);
408 return yTop;
409 }
410
416 void layoutBundleHelper(final EventNodeBase< ?> eventNode) {
417 //make sure it is shown
418 eventNode.setVisible(true);
419 eventNode.setManaged(true);
420 //apply advanced layout description visibility options
421 eventNode.setDescriptionVisibility(layoutSettings.getDescrVisibility());
422 eventNode.setMaxDescriptionWidth(descriptionWidth);
423
424 //do recursive layout
425 eventNode.layoutChildren();
426 }
427
428 abstract void doAdditionalLayout();
429
430 DetailsChart getParentChart() {
431 return parentChart;
432 }
433}
ObservableList< DescriptionFilterState > getQuickHideFilters()

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