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.Observable;
38 import javafx.beans.property.ReadOnlyDoubleProperty;
39 import javafx.beans.property.ReadOnlyDoubleWrapper;
40 import javafx.collections.FXCollections;
41 import javafx.collections.ObservableList;
42 import javafx.geometry.Insets;
43 import javafx.scene.Cursor;
44 import javafx.scene.Group;
45 import javafx.scene.Scene;
46 import javafx.scene.chart.Axis;
47 import javafx.scene.chart.XYChart;
48 import javafx.scene.control.ContextMenu;
49 import javafx.scene.control.Tooltip;
50 import javafx.scene.input.MouseEvent;
51 import static javafx.scene.layout.Region.USE_PREF_SIZE;
52 import org.joda.time.DateTime;
73 abstract class DetailsChartLane<Y
extends TimeLineEvent> 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<>();
98 boolean quickHideFiltersEnabled() {
99 return useQuickHideFilters;
102 public void clearContextMenu() {
107 public ContextMenu getContextMenu(MouseEvent clickEvent) {
108 return parentChart.getContextMenu(clickEvent);
111 EventNodeBase<?> createNode(DetailsChartLane<?> chart, TimeLineEvent event) {
112 if (event.getEventIDs().size() == 1) {
113 return new SingleEventNode(
this, controller.getEventsModel().getEventById(Iterables.getOnlyElement(event.getEventIDs())), null);
114 }
else if (event instanceof SingleEvent) {
115 return new SingleEventNode(chart, (SingleEvent) event, null);
116 }
else if (event instanceof EventCluster) {
117 return new EventClusterNode(chart, (EventCluster) event, null);
119 return new EventStripeNode(chart, (EventStripe) event, null);
124 synchronized protected void layoutPlotChildren() {
125 setCursor(Cursor.WAIT);
126 if (useQuickHideFilters) {
128 activeQuickHidefilters = getController().getQuickHideFilters().stream()
131 .collect(Collectors.toSet());
134 descriptionWidth = layoutSettings.getTruncateAll() ? layoutSettings.getTruncateWidth() : USE_PREF_SIZE;
136 if (layoutSettings.getBandByType()) {
139 .collect(Collectors.groupingBy(EventNodeBase<?>::getEventType)).values()
140 .forEach(inputNodes -> maxY.set(layoutEventBundleNodes(inputNodes, maxY.get())));
142 maxY.set(layoutEventBundleNodes(sortedNodes, 0));
144 doAdditionalLayout();
148 public TimeLineController getController() {
152 public ObservableList<EventNodeBase<?>> getSelectedNodes() {
153 return selectedNodes;
158 final InvalidationListener layoutInvalidationListener = (Observable o) -> {
159 layoutPlotChildren();
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<DateTime, Y>())));
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) {
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((x) ->
true);
355 synchronized Iterable<EventNodeBase<?>> getNodes(Predicate<
EventNodeBase<?>> p) {
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(p).collect(Collectors.toList());
389 double computeYTop(
double yMin,
double h, TreeRangeMap<Double, Double> maxXatY,
double xLeft,
double xRight) {
391 double yBottom = yTop + h;
393 boolean overlapping =
true;
394 while (overlapping) {
397 for (
double y = yBottom; y >= yTop; y -= MINIMUM_ROW_HEIGHT) {
398 final Double maxX = maxXatY.get(y);
399 if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) {
403 yTop = y + MINIMUM_EVENT_NODE_GAP;
409 maxXatY.put(Range.closed(yTop, yBottom), xRight);
418 void layoutBundleHelper(
final EventNodeBase< ?> eventNode) {
420 eventNode.setVisible(
true);
421 eventNode.setManaged(
true);
423 eventNode.setDescriptionVisibility(layoutSettings.getDescrVisibility());
424 eventNode.setMaxDescriptionWidth(descriptionWidth);
427 eventNode.layoutChildren();
430 abstract void doAdditionalLayout();
432 DetailsChart getParentChart() {
abstract List< EventNodeBase<?> > getSubNodes()