19 package org.sleuthkit.autopsy.timeline.ui.detailview;
21 import com.google.common.collect.Iterables;
22 import com.google.common.collect.Range;
23 import com.google.common.collect.TreeRangeMap;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.Comparator;
27 import java.util.HashMap;
28 import java.util.HashSet;
31 import java.util.function.Function;
32 import java.util.function.Predicate;
33 import java.util.stream.Collectors;
34 import java.util.stream.Stream;
35 import javafx.application.Platform;
36 import javafx.beans.InvalidationListener;
37 import javafx.beans.property.ReadOnlyDoubleProperty;
38 import javafx.beans.property.ReadOnlyDoubleWrapper;
39 import javafx.collections.FXCollections;
40 import javafx.collections.ObservableList;
41 import javafx.geometry.Insets;
42 import javafx.scene.Cursor;
43 import javafx.scene.Group;
44 import javafx.scene.Scene;
45 import javafx.scene.chart.Axis;
46 import javafx.scene.chart.XYChart;
47 import javafx.scene.control.ContextMenu;
48 import javafx.scene.control.Tooltip;
49 import javafx.scene.input.MouseEvent;
50 import static javafx.scene.layout.Region.USE_PREF_SIZE;
51 import org.joda.time.DateTime;
73 abstract class DetailsChartLane<Y
extends DetailViewEvent> extends XYChart<DateTime, Y> implements ContextMenuProvider {
75 private static final String STYLE_SHEET = GuideLine.class.getResource(
"EventsDetailsChart.css").toExternalForm();
77 static final int MINIMUM_EVENT_NODE_GAP = 4;
78 static final int MINIMUM_ROW_HEIGHT = 24;
80 private final DetailsChart parentChart;
81 private final TimeLineController controller;
82 private final DetailsChartLayoutSettings layoutSettings;
83 private final ObservableList<EventNodeBase<?>> selectedNodes;
85 private final Map<Y, EventNodeBase<?>> eventMap =
new HashMap<>();
87 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
88 final ObservableList< EventNodeBase<?>> nodes = FXCollections.observableArrayList();
89 final ObservableList< EventNodeBase<?>> sortedNodes = nodes.sorted(Comparator.comparing(EventNodeBase::getStartMillis));
91 private final
boolean useQuickHideFilters;
93 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
94 private
double descriptionWidth;
95 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
96 private Set<String> activeQuickHidefilters = new HashSet<>();
99 final InvalidationListener layoutInvalidationListener = observable -> layoutPlotChildren();
101 boolean quickHideFiltersEnabled() {
102 return useQuickHideFilters;
106 public void clearContextMenu() {
107 parentChart.clearContextMenu();
111 public ContextMenu getContextMenu(MouseEvent clickEvent) {
112 return parentChart.getContextMenu(clickEvent);
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);
123 return new EventStripeNode(chart, (EventStripe) event, null);
128 synchronized protected void layoutPlotChildren() {
129 setCursor(Cursor.WAIT);
130 if (useQuickHideFilters) {
132 activeQuickHidefilters = getController().getQuickHideFilters().stream()
133 .filter(FilterState<DescriptionFilter>::isActive)
134 .map(FilterState<DescriptionFilter>::getFilter)
136 .collect(Collectors.toSet());
139 descriptionWidth = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE;
141 if (layoutSettings.getBandByType()) {
144 .collect(Collectors.groupingBy(EventNodeBase<?>::getEventType)).values()
145 .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get())));
147 maxY.set(layoutEventBundleNodes(sortedNodes, 0));
149 doAdditionalLayout();
154 public TimeLineController getController() {
158 public ObservableList<EventNodeBase<?>> getSelectedNodes() {
159 return selectedNodes;
162 public ReadOnlyDoubleProperty maxVScrollProperty() {
163 return maxY.getReadOnlyProperty();
168 private final ReadOnlyDoubleWrapper maxY =
new ReadOnlyDoubleWrapper(0.0);
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;
179 setData(FXCollections.observableList(Arrays.asList(
new Series<>())));
181 Tooltip.install(
this, AbstractTimelineChart.getDefaultTooltip());
183 dateAxis.setAutoRanging(
false);
184 setLegendVisible(
false);
185 setPadding(Insets.EMPTY);
186 setAlternativeColumnFillVisible(
true);
188 sceneProperty().addListener(observable -> {
189 Scene scene = getScene();
190 if (scene != null && scene.getStylesheets().contains(STYLE_SHEET) ==
false) {
191 scene.getStylesheets().add(STYLE_SHEET);
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);
204 getPlotChildren().add(nodeGroup);
233 public double layoutEventBundleNodes(
final Collection<? extends
EventNodeBase<?>> nodes,
final double minY) {
235 TreeRangeMap<Double, Double> maxXatY = TreeRangeMap.create();
238 double localMax = minY;
242 if (useQuickHideFilters && activeQuickHidefilters.contains(bundleNode.getDescription())) {
244 bundleNode.setVisible(
false);
245 bundleNode.setManaged(
false);
247 layoutBundleHelper(bundleNode);
249 double h = bundleNode.getBoundsInLocal().getHeight();
250 double w = bundleNode.getBoundsInLocal().getWidth();
252 double xLeft = getXForEpochMillis(bundleNode.getStartMillis()) - bundleNode.getLayoutXCompensation();
253 double xRight = xLeft + w + MINIMUM_EVENT_NODE_GAP;
256 double yTop = (layoutSettings.getOneEventPerRow())
257 ? (localMax + MINIMUM_EVENT_NODE_GAP)
258 : computeYTop(minY, h, maxXatY, xLeft, xRight);
260 localMax = Math.max(yTop + h, localMax);
263 bundleNode.animateTo(xLeft, yTop);
270 final public void requestChartLayout() {
271 super.requestChartLayout();
274 double getXForEpochMillis(Long millis) {
275 DateTime dateTime =
new DateTime(millis);
276 return getXAxis().getDisplayPosition(dateTime);
281 protected void dataItemAdded(Series<DateTime, Y> series,
int itemIndex, Data<DateTime, Y> item) {
286 protected void dataItemRemoved(Data<DateTime, Y> item, Series<DateTime, Y> series) {
291 protected void dataItemChanged(Data<DateTime, Y> item) {
296 protected void seriesAdded(Series<DateTime, Y> series,
int seriesIndex) {
301 protected void seriesRemoved(Series<DateTime, Y> series) {
311 void addEvent(Y event)
throws TskCoreException {
313 eventMap.put(event, eventNode);
314 Platform.runLater(() -> {
315 nodes.add(eventNode);
316 nodeGroup.getChildren().add(eventNode);
327 void removeEvent(Y event) {
329 Platform.runLater(() -> {
330 nodes.remove(removedNode);
331 nodeGroup.getChildren().removeAll(removedNode);
339 final Group nodeGroup =
new Group();
341 public synchronized void setVScroll(
double vScrollValue) {
342 nodeGroup.setTranslateY(-vScrollValue);
348 synchronized Iterable<EventNodeBase<?>> getAllNodes() {
349 return getNodes(dummy ->
true);
355 private synchronized Iterable<EventNodeBase<?>> getNodes(Predicate<
EventNodeBase<?>> predicate) {
357 Function<EventNodeBase<?>, Stream<EventNodeBase<?>>> stripeFlattener
358 =
new Function<EventNodeBase<?>, Stream<EventNodeBase<?>>>() {
361 return Stream.concat(
367 return sortedNodes.stream()
368 .flatMap(stripeFlattener)
369 .filter(predicate).collect(Collectors.toList());
387 double computeYTop(
double yMin,
double h, TreeRangeMap<Double, Double> maxXatY,
double xLeft,
double xRight) {
389 double yBottom = yTop + h;
391 boolean overlapping =
true;
392 while (overlapping) {
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) {
401 yTop = y + MINIMUM_EVENT_NODE_GAP;
407 maxXatY.put(Range.closed(yTop, yBottom), xRight);
416 void layoutBundleHelper(
final EventNodeBase< ?> eventNode) {
418 eventNode.setVisible(
true);
419 eventNode.setManaged(
true);
421 eventNode.setDescriptionVisibility(layoutSettings.getDescrVisibility());
422 eventNode.setMaxDescriptionWidth(descriptionWidth);
425 eventNode.layoutChildren();
428 abstract void doAdditionalLayout();
430 DetailsChart getParentChart() {
abstract List< EventNodeBase<?> > getSubNodes()