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)