19 package org.sleuthkit.autopsy.timeline.ui.detailview;
21 import com.google.common.collect.Collections2;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collections;
25 import java.util.Comparator;
26 import java.util.HashMap;
27 import java.util.List;
29 import java.util.TreeMap;
30 import java.util.function.Predicate;
31 import java.util.stream.Collectors;
32 import javafx.animation.KeyFrame;
33 import javafx.animation.KeyValue;
34 import javafx.animation.Timeline;
35 import javafx.beans.InvalidationListener;
36 import javafx.beans.Observable;
37 import javafx.beans.property.ReadOnlyDoubleProperty;
38 import javafx.beans.property.ReadOnlyDoubleWrapper;
39 import javafx.beans.property.SimpleBooleanProperty;
40 import javafx.beans.property.SimpleDoubleProperty;
41 import javafx.beans.property.SimpleObjectProperty;
42 import javafx.collections.FXCollections;
43 import javafx.collections.ListChangeListener;
44 import javafx.collections.MapChangeListener;
45 import javafx.collections.ObservableList;
46 import javafx.collections.ObservableMap;
47 import javafx.event.ActionEvent;
48 import javafx.event.EventHandler;
49 import javafx.geometry.Insets;
50 import javafx.scene.Cursor;
51 import javafx.scene.Group;
52 import javafx.scene.Node;
53 import javafx.scene.chart.Axis;
54 import javafx.scene.chart.NumberAxis;
55 import javafx.scene.chart.XYChart;
56 import javafx.scene.control.ContextMenu;
57 import javafx.scene.image.Image;
58 import javafx.scene.image.ImageView;
59 import javafx.scene.input.MouseButton;
60 import javafx.scene.input.MouseEvent;
61 import javafx.scene.shape.Line;
62 import javafx.scene.shape.StrokeLineCap;
63 import javafx.util.Duration;
64 import javax.annotation.concurrent.GuardedBy;
65 import org.controlsfx.control.action.Action;
66 import org.controlsfx.control.action.ActionGroup;
67 import org.controlsfx.control.action.ActionUtils;
68 import org.joda.time.DateTime;
69 import org.joda.time.Interval;
70 import org.openide.util.NbBundle;
100 private final SimpleBooleanProperty
bandByType =
new SimpleBooleanProperty(
false);
110 private final SimpleObjectProperty<DescriptionVisibility>
descrVisibility =
new SimpleObjectProperty<>(DescriptionVisibility.SHOWN);
130 private final ReadOnlyDoubleWrapper
maxY =
new ReadOnlyDoubleWrapper(0.0);
139 private final Map<AggregateEvent, AggregateEventNode>
nodeMap =
new TreeMap<>((
142 int comp = Long.compare(o1.getSpan().getStartMillis(), o2.getSpan().getStartMillis());
146 return Comparator.comparing(AggregateEvent::hashCode).compare(o1, o2);
153 private final SimpleBooleanProperty
oneEventPerRow =
new SimpleBooleanProperty(
false);
155 private final ObservableMap<AggregateEventNode, Line>
projectionMap = FXCollections.observableHashMap();
158 @GuardedBy(value =
"this")
171 .sorted((s1, s2) -> {
173 return Integer.compare(collect.indexOf(s1.getName()), collect.indexOf(s2.getName()));
180 private final SimpleBooleanProperty
truncateAll =
new SimpleBooleanProperty(
false);
184 private final SimpleDoubleProperty
truncateWidth =
new SimpleDoubleProperty(200.0);
186 EventDetailChart(DateAxis dateAxis,
final Axis<AggregateEvent> verticalAxis, ObservableList<AggregateEventNode> selectedNodes) {
187 super(dateAxis, verticalAxis);
188 dateAxis.setAutoRanging(
false);
191 verticalAxis.setTickLabelsVisible(
false);
192 verticalAxis.setTickMarkVisible(
false);
194 setLegendVisible(
false);
195 setPadding(Insets.EMPTY);
196 setAlternativeColumnFillVisible(
true);
199 getPlotChildren().add(nodeGroup);
202 widthProperty().addListener(layoutInvalidationListener);
203 heightProperty().addListener(layoutInvalidationListener);
205 bandByType.addListener(layoutInvalidationListener);
206 oneEventPerRow.addListener(layoutInvalidationListener);
207 truncateAll.addListener(layoutInvalidationListener);
208 truncateWidth.addListener(layoutInvalidationListener);
209 descrVisibility.addListener(layoutInvalidationListener);
212 boundsInLocalProperty().addListener((Observable observable) -> {
213 setPrefHeight(boundsInLocalProperty().
get().getHeight());
217 final EventHandler<MouseEvent> clickHandler = (MouseEvent clickEvent) -> {
218 if (chartContextMenu != null) {
219 chartContextMenu.hide();
221 if (clickEvent.getButton() == MouseButton.SECONDARY && clickEvent.isStillSincePress()) {
223 chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(
new Action(
224 NbBundle.getMessage(
this.getClass(),
"EventDetailChart.chartContextMenu.placeMarker.name")) {
226 setGraphic(
new ImageView(
new Image(
"/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16,
true,
true,
true)));
227 setEventHandler((ActionEvent t) -> {
228 if (guideLine == null) {
229 guideLine =
new GuideLine(0, 0, 0, getHeight(), dateAxis);
230 guideLine.relocate(clickEvent.getX(), 0);
231 guideLine.endYProperty().bind(heightProperty().subtract(dateAxis.heightProperty().subtract(dateAxis.tickLengthProperty())));
233 getChartChildren().add(guideLine);
235 guideLine.setOnMouseClicked((MouseEvent event) -> {
236 if (event.getButton() == MouseButton.SECONDARY) {
242 guideLine.relocate(clickEvent.getX(), 0);
248 NbBundle.getMessage(
this.getClass(),
"EventDetailChart.contextMenu.zoomHistory.name"),
249 new Back(controller),
250 new Forward(controller))));
251 chartContextMenu.setAutoHide(
true);
252 chartContextMenu.show(EventDetailChart.this, clickEvent.getScreenX(), clickEvent.getScreenY());
253 clickEvent.consume();
257 setOnMouseClicked(clickHandler);
260 final ChartDragHandler<DateTime, EventDetailChart> dragHandler =
new ChartDragHandler<>(
this, getXAxis());
261 setOnMousePressed(dragHandler);
262 setOnMouseReleased(dragHandler);
263 setOnMouseDragged(dragHandler);
265 projectionMap.addListener((MapChangeListener.Change<? extends AggregateEventNode, ? extends Line> change) -> {
266 final Line valueRemoved = change.getValueRemoved();
267 if (valueRemoved != null) {
268 getChartChildren().removeAll(valueRemoved);
270 final Line valueAdded = change.getValueAdded();
271 if (valueAdded != null) {
272 getChartChildren().add(valueAdded);
276 this.selectedNodes = selectedNodes;
277 this.selectedNodes.addListener((
278 ListChangeListener.Change<? extends AggregateEventNode> c) -> {
280 c.getRemoved().forEach((AggregateEventNode t) -> {
281 projectionMap.remove(t);
283 c.getAddedSubList().forEach((AggregateEventNode t) -> {
284 Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET,
285 dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET
287 line.setStroke(t.getEvent().getType().getColor().deriveColor(0, 1, 1, .5));
288 line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH);
289 line.setStrokeLineCap(StrokeLineCap.ROUND);
290 projectionMap.put(t, line);
295 this.controller.selectEventIDs(selectedNodes.stream()
296 .flatMap((AggregateEventNode aggNode) -> aggNode.getEvent().getEventIDs().stream())
297 .collect(Collectors.toList()));
300 requestChartLayout();
305 getChartChildren().remove(intervalSelector);
306 intervalSelector = null;
315 this.controller = controller;
321 this.filteredEvents = filteredEvents;
324 clearIntervalSelector();
326 selectedNodes.clear();
327 projectionMap.clear();
334 return new DetailIntervalSelector(x, getHeight() - axis.getHeight() - axis.getTickLength(), axis, controller);
337 synchronized void setBandByType(Boolean t1) {
350 return getXAxis().getValueForDisplay(getXAxis().parentToLocal(x, 0).getX());
355 return intervalSelector;
360 intervalSelector = newIntervalSelector;
361 getChartChildren().add(getIntervalSelector());
365 return oneEventPerRow;
372 synchronized void setEventOnePerRow(Boolean t1) {
373 oneEventPerRow.set(t1);
376 synchronized void setTruncateAll(Boolean t1) {
382 protected synchronized void dataItemAdded(Series<DateTime, AggregateEvent> series,
int i, Data<DateTime, AggregateEvent> data) {
384 AggregateEventNode eventNode = nodeMap.get(aggEvent);
385 if (eventNode == null) {
386 eventNode =
new AggregateEventNode(aggEvent, null,
this);
388 eventNode.setLayoutX(getXAxis().getDisplayPosition(
new DateTime(aggEvent.
getSpan().getStartMillis())));
389 data.setNode(eventNode);
390 nodeMap.put(aggEvent, eventNode);
391 nodeGroup.getChildren().add(eventNode);
392 requiresLayout =
true;
399 throw new UnsupportedOperationException(
"Not supported yet.");
403 protected synchronized void dataItemRemoved(Data<DateTime, AggregateEvent> data, Series<DateTime, AggregateEvent> series) {
404 nodeMap.remove(data.getYValue());
405 nodeGroup.getChildren().remove(data.getNode());
411 super.layoutChildren();
433 if (requiresLayout) {
434 setCursor(Cursor.WAIT);
439 if (bandByType.get() ==
false) {
441 ObservableList<Node> nodes = FXCollections.observableArrayList(nodeMap.values());
443 layoutNodes(nodes, minY, 0);
446 for (Series<DateTime, AggregateEvent> s : sortedSeriesList) {
447 ObservableList<Node> nodes = FXCollections.observableArrayList(Collections2.transform(s.getData(), Data::getNode));
450 layoutNodes(nodes.filtered((Node n) -> n != null), minY, 0);
455 requiresLayout =
false;
457 layoutProjectionMap();
461 protected synchronized void seriesAdded(Series<DateTime, AggregateEvent> series,
int i) {
462 for (
int j = 0; j < series.getData().size(); j++) {
463 dataItemAdded(series, j, series.getData().get(j));
465 seriesList.add(series);
466 requiresLayout =
true;
470 protected synchronized void seriesRemoved(Series<DateTime, AggregateEvent> series) {
471 for (
int j = 0; j < series.getData().size(); j++) {
472 dataItemRemoved(series.getData().get(j), series);
474 seriesList.remove(series);
475 requiresLayout =
true;
478 synchronized SimpleObjectProperty<DescriptionVisibility> getDescrVisibility() {
479 return descrVisibility;
482 synchronized ReadOnlyDoubleProperty getMaxVScroll() {
483 return maxY.getReadOnlyProperty();
486 Iterable<AggregateEventNode> getNodes(Predicate<AggregateEventNode> p) {
487 List<AggregateEventNode> nodes =
new ArrayList<>();
489 for (AggregateEventNode node : nodeMap.values()) {
490 checkNode(node, p, nodes);
496 synchronized SimpleDoubleProperty getTruncateWidth() {
497 return truncateWidth;
500 synchronized void setVScroll(
double d) {
501 final double h = maxY.get() - (getHeight() * .9);
502 nodeGroup.setTranslateY(-d * h);
505 private void checkNode(AggregateEventNode node, Predicate<AggregateEventNode> p, List<AggregateEventNode> nodes) {
512 checkNode((AggregateEventNode) n, p, nodes);
518 getChartChildren().remove(guideLine);
529 private synchronized double layoutNodes(
final List<Node> nodes,
final double minY,
final double xOffset) {
531 Map<Integer, Double> maxXatY =
new HashMap<>();
532 double localMax = minY;
534 for (Node n : nodes) {
535 final AggregateEventNode tlNode = (AggregateEventNode) n;
536 tlNode.setDescriptionVisibility(descrVisibility.get());
539 final double rawDisplayPosition = getXAxis().getDisplayPosition(
new DateTime(ie.
getSpan().getStartMillis()));
541 double xPos = rawDisplayPosition - xOffset;
542 double layoutNodesResultHeight = 0;
545 layoutNodesResultHeight = layoutNodes(tlNode.
getSubNodePane().getChildren(), 0, rawDisplayPosition);
547 double xPos2 = getXAxis().getDisplayPosition(
new DateTime(ie.
getSpan().getEndMillis())) - xOffset;
548 double span = xPos2 - xPos;
552 if (truncateAll.get()) {
560 double xRight = xPos + tlNode.getWidth();
563 final double h = layoutNodesResultHeight == 0 ? tlNode.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT;
567 double yPos2 = yPos + h;
569 if (oneEventPerRow.get()) {
571 yPos = (localMax + 2);
576 boolean overlapping =
true;
577 while (overlapping) {
582 for (
double y = yPos2; y >= yPos; y--) {
583 final Double maxX = maxXatY.get((
int) y);
584 if (maxX != null && maxX >= xPos - 4) {
595 for (
double y = yPos; y <= yPos2; y++) {
596 maxXatY.put((
int) y, xRight);
599 localMax = Math.max(yPos2, localMax);
601 Timeline tm =
new Timeline(
new KeyFrame(Duration.seconds(1.0),
602 new KeyValue(tlNode.layoutXProperty(), xPos),
603 new KeyValue(tlNode.layoutYProperty(), yPos)));
608 maxY.set(Math.max(maxY.get(), localMax));
609 return localMax - minY;
611 private static final int DEFAULT_ROW_HEIGHT = 24;
614 for (
final Map.Entry<AggregateEventNode, Line> entry : projectionMap.entrySet()) {
615 final AggregateEventNode aggNode = entry.getKey();
616 final Line line = entry.getValue();
620 line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
621 line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
626 return getXAxis().localToParent(getXAxis().getDisplayPosition(dt), 0).getX();
640 return filteredEvents;
647 return chartContextMenu;
657 }
else if (n2 == null) {
661 return Long.compare(((AggregateEventNode) n1).getEvent().getSpan().getStartMillis(),
662 (((AggregateEventNode) n2).getEvent().getSpan().getStartMillis()));
671 super(x, height, axis, controller);
691 synchronized void setRequiresLayout(
boolean b) {
692 requiresLayout =
true;
697 super.requestChartLayout();
static final int PROJECTED_LINE_Y_OFFSET
synchronized void setController(TimeLineController controller)
void setDescriptionWidth(double w)
final InvalidationListener layoutInvalidationListener
synchronized void layoutPlotChildren()
IntervalSelector<?extends DateTime > intervalSelector
FilteredEventsModel getEventsModel()
void setIntervalSelector(IntervalSelector<?extends DateTime > newIntervalSelector)
DateTime getDateTimeForPosition(double x)
final SimpleBooleanProperty truncateAll
TimeLineController controller
void requestChartLayout()
synchronized void dataItemRemoved(Data< DateTime, AggregateEvent > data, Series< DateTime, AggregateEvent > series)
double getParentXForValue(DateTime dt)
final ReadOnlyDoubleWrapper maxY
TimeLineController getController()
DateTime parseDateTime(DateTime date)
void selectEventIDs(Collection< Long > events)
int compare(Node n1, Node n2)
final SimpleBooleanProperty bandByType
synchronized void dataItemChanged(Data< DateTime, AggregateEvent > data)
ContextMenu chartContextMenu
final SimpleObjectProperty< DescriptionVisibility > descrVisibility
synchronized void seriesAdded(Series< DateTime, AggregateEvent > series, int i)
final ObservableMap< AggregateEventNode, Line > projectionMap
void checkNode(AggregateEventNode node, Predicate< AggregateEventNode > p, List< AggregateEventNode > nodes)
FilteredEventsModel getFilteredEvents()
String formatSpan(DateTime date)
ContextMenu getChartContextMenu()
void setModel(FilteredEventsModel filteredEvents)
IntervalSelector< DateTime > newIntervalSelector(double x, Axis< DateTime > axis)
void setSpanWidth(double w)
final SimpleDoubleProperty truncateWidth
Interval adjustInterval(Interval i)
DetailIntervalSelector(double x, double height, Axis< DateTime > axis, TimeLineController controller)
static DateTimeZone getJodaTimeZone()
final Map< AggregateEvent, AggregateEventNode > nodeMap
IntervalSelector<?extends DateTime > getIntervalSelector()
void layoutProjectionMap()
final SimpleBooleanProperty oneEventPerRow
synchronized SimpleBooleanProperty getBandByType()
static final int PROJECTED_LINE_STROKE_WIDTH
AggregateEvent getEvent()
void clearIntervalSelector()
synchronized SimpleBooleanProperty getTruncateAll()
static DateTimeFormatter getZonedFormatter()
synchronized double layoutNodes(final List< Node > nodes, final double minY, final double xOffset)
synchronized void dataItemAdded(Series< DateTime, AggregateEvent > series, int i, Data< DateTime, AggregateEvent > data)
final ObservableList< Series< DateTime, AggregateEvent > > sortedSeriesList
FilteredEventsModel filteredEvents
synchronized SimpleBooleanProperty getOneEventPerRow()
final ObservableList< Series< DateTime, AggregateEvent > > seriesList
synchronized ReadOnlyObjectProperty< ZoomParams > getRequestedZoomParamters()
static final List<?extends EventType > allTypes
synchronized void seriesRemoved(Series< DateTime, AggregateEvent > series)