refactor common code out of clustering modes

This commit is contained in:
jmillman 2015-09-14 15:53:41 -04:00
parent 2161812bd4
commit 7a85c14b2d
10 changed files with 618 additions and 894 deletions

View File

@ -127,26 +127,26 @@ public class EventCluster implements EventBundle {
/**
* merge two aggregate events into one new aggregate event.
*
* @param aggEvent1
* @param cluster1
* @param aggEVent2
*
* @return a new aggregate event that is the result of merging the given
* events
*/
public static EventCluster merge(EventCluster aggEvent1, EventCluster ag2) {
public static EventCluster merge(EventCluster cluster1, EventCluster cluster2) {
if (aggEvent1.getEventType() != ag2.getEventType()) {
if (cluster1.getEventType() != cluster2.getEventType()) {
throw new IllegalArgumentException("aggregate events are not compatible they have different types");
}
if (!aggEvent1.getDescription().equals(ag2.getDescription())) {
if (!cluster1.getDescription().equals(cluster2.getDescription())) {
throw new IllegalArgumentException("aggregate events are not compatible they have different descriptions");
}
Sets.SetView<Long> idsUnion = Sets.union(aggEvent1.getEventIDs(), ag2.getEventIDs());
Sets.SetView<Long> hashHitsUnion = Sets.union(aggEvent1.getEventIDsWithHashHits(), ag2.getEventIDsWithHashHits());
Sets.SetView<Long> taggedUnion = Sets.union(aggEvent1.getEventIDsWithTags(), ag2.getEventIDsWithTags());
Sets.SetView<Long> idsUnion = Sets.union(cluster1.getEventIDs(), cluster2.getEventIDs());
Sets.SetView<Long> hashHitsUnion = Sets.union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits());
Sets.SetView<Long> taggedUnion = Sets.union(cluster1.getEventIDsWithTags(), cluster2.getEventIDsWithTags());
return new EventCluster(IntervalUtils.span(aggEvent1.span, ag2.span), aggEvent1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, aggEvent1.getDescription(), aggEvent1.lod);
return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod);
}
Range<Long> getRange() {

View File

@ -60,15 +60,15 @@ public final class EventStripe implements EventBundle {
*/
private final Set<Long> hashHits = new HashSet<>();
public EventStripe(EventCluster aggEvent) {
spans.add(aggEvent.getRange());
spanMap.put(aggEvent.getRange(), aggEvent);
type = aggEvent.getEventType();
description = aggEvent.getDescription();
lod = aggEvent.getDescriptionLOD();
eventIDs.addAll(aggEvent.getEventIDs());
tagged.addAll(aggEvent.getEventIDsWithTags());
hashHits.addAll(aggEvent.getEventIDsWithHashHits());
public EventStripe(EventCluster cluster) {
spans.add(cluster.getRange());
spanMap.put(cluster.getRange(), cluster);
type = cluster.getEventType();
description = cluster.getDescription();
lod = cluster.getDescriptionLOD();
eventIDs.addAll(cluster.getEventIDs());
tagged.addAll(cluster.getEventIDsWithTags());
hashHits.addAll(cluster.getEventIDsWithHashHits());
}
private EventStripe(EventStripe u, EventStripe v) {

View File

@ -0,0 +1,452 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.sleuthkit.autopsy.timeline.ui.detailview;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import static javafx.scene.layout.Region.USE_COMPUTED_SIZE;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.LoggedTask;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.filters.RootFilter;
import org.sleuthkit.autopsy.timeline.filters.TextFilter;
import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
import org.sleuthkit.datamodel.SleuthkitCase;
public abstract class AbstractDetailViewNode< T extends EventBundle, S extends AbstractDetailViewNode<T, S>> extends StackPane implements DetailViewNode<AbstractDetailViewNode<T, S>> {
static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png");
static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS
static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS
static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS
static final CornerRadii CORNER_RADII = new CornerRadii(3);
Map<EventType, DropShadow> dropShadowMap = new HashMap<>();
static void configureLODButton(Button b) {
b.setMinSize(16, 16);
b.setMaxSize(16, 16);
b.setPrefSize(16, 16);
show(b, false);
}
/**
* the border to apply when this node is 'selected'
*/
static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2)));
final Color evtColor;
@Override
@SuppressWarnings("unchecked")
public List<S> getSubNodes() {
return subNodePane.getChildrenUnmodifiable().stream()
.map(t -> (S) t)
.collect(Collectors.toList());
}
/**
* apply the 'effect' to visually indicate selection
*
* @param applied true to apply the selection 'effect', false to remove it
*/
@Override
public void applySelectionEffect(boolean applied) {
Platform.runLater(() -> {
if (applied) {
setBorder(selectionBorder);
} else {
setBorder(null);
}
});
}
static void show(Node b, boolean show) {
b.setVisible(show);
b.setManaged(show);
}
final ImageView hashIV = new ImageView(HASH_PIN);
final ImageView tagIV = new ImageView(TAG);
private final S parentNode;
DescriptionVisibility descrVis;
/**
* Pane that contains AggregateEventNodes of any 'subevents' if they are
* displayed
*
* //TODO: move more of the control of subnodes/events here and out of
* EventDetail Chart
*/
final Pane subNodePane = new Pane();
/**
* The ImageView used to show the icon for this node's event's type
*/
private final ImageView eventTypeImageView = new ImageView();
/**
* The label used to display this node's event's description
*/
final Label descrLabel = new Label();
/**
* The label used to display this node's event count
*/
final Label countLabel = new Label();
private final T eventBundle;
private final EventDetailChart chart;
final SleuthkitCase sleuthkitCase;
final FilteredEventsModel eventsModel;
final Button plusButton = new Button(null, new ImageView(PLUS)) {
{
configureLODButton(this);
}
};
final Button minusButton = new Button(null, new ImageView(MINUS)) {
{
configureLODButton(this);
}
};
SimpleObjectProperty<DescriptionLOD> descLOD = new SimpleObjectProperty<>();
final HBox header;
/**
*
* @param showControls the value of par
*/
void showDescriptionLoDControls(final boolean showControls) {
DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(),
eventType -> new DropShadow(10, eventType.getColor()));
getSpanFillNode().setEffect(showControls ? dropShadow : null);
show(minusButton, showControls);
show(plusButton, showControls);
}
final Region spacer = new Region();
RootFilter getSubClusterFilter() {
RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf();
//make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters
combinedFilter.getSubFilters().addAll(new TextFilter(getEventBundle().getDescription()),
new TypeFilter(getEventType()));
return combinedFilter;
}
abstract Collection<T> makeBundlesFromClusters(List<EventCluster> eventClusters);
abstract void showSpans(final boolean showSpans);
public AbstractDetailViewNode(EventDetailChart chart, T bundle, S parentEventNode) {
this.eventBundle = bundle;
this.parentNode = parentEventNode;
this.chart = chart;
descLOD.set(bundle.getDescriptionLOD());
sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase();
eventsModel = chart.getController().getEventsModel();
if (eventBundle.getEventIDsWithHashHits().isEmpty()) {
show(hashIV, false);
}
if (eventBundle.getEventIDsWithTags().isEmpty()) {
show(tagIV, false);
}
HBox.setHgrow(spacer, Priority.ALWAYS);
header = new HBox(getDescrLabel(), getCountLabel(), hashIV, tagIV, /*spacer,*/ minusButton, plusButton);
header.setMinWidth(USE_PREF_SIZE);
header.setPadding(new Insets(2, 5, 2, 5));
header.setAlignment(Pos.CENTER_LEFT);
//setup description label
evtColor = getEventType().getColor();
eventTypeImageView.setImage(getEventType().getFXImage());
descrLabel.setGraphic(eventTypeImageView);
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
descrLabel.setMouseTransparent(true);
//set up subnode pane sizing contraints
subNodePane.setPrefHeight(USE_COMPUTED_SIZE);
subNodePane.setMinHeight(USE_PREF_SIZE);
subNodePane.setMinWidth(USE_PREF_SIZE);
subNodePane.setMaxHeight(USE_PREF_SIZE);
subNodePane.setMaxWidth(USE_PREF_SIZE);
subNodePane.setPickOnBounds(false);
setAlignment(Pos.TOP_LEFT);
setMinHeight(24);
setPrefHeight(USE_COMPUTED_SIZE);
setMaxHeight(USE_PREF_SIZE);
setOnMouseClicked(new EventMouseHandler());
//set up mouse hover effect and tooltip
setOnMouseEntered((MouseEvent e) -> {
//defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart
installTooltip();
showDescriptionLoDControls(true);
toFront();
});
setOnMouseExited((MouseEvent e) -> {
showDescriptionLoDControls(false);
});
setCursor(Cursor.HAND);
plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL));
minusButton.disableProperty().bind(descLOD.isEqualTo(getEventBundle().getDescriptionLOD()));
plusButton.setOnMouseClicked(e -> {
final DescriptionLOD next = descLOD.get().next();
if (next != null) {
loadSubClusters(next);
descLOD.set(next);
}
});
minusButton.setOnMouseClicked(e -> {
final DescriptionLOD previous = descLOD.get().previous();
if (previous != null) {
loadSubClusters(previous);
descLOD.set(previous);
}
});
setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
setLayoutX(getChart().getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation());
}
/**
* @param w the maximum width the description label should have
*/
@Override
public void setDescriptionWidth(double w) {
getDescrLabel().setMaxWidth(w);
}
abstract void installTooltip();
/**
* apply the 'effect' to visually indicate highlighted nodes
*
* @param applied true to apply the highlight 'effect', false to remove it
*/
@Override
public synchronized void applyHighlightEffect(boolean applied) {
if (applied) {
getDescrLabel().setStyle("-fx-font-weight: bold;"); // NON-NLS
getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY)));
setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
} else {
getDescrLabel().setStyle("-fx-font-weight: normal;"); // NON-NLS
getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
}
}
String getDisplayedDescription() {
return getDescrLabel().getText();
}
abstract Region getSpanFillNode();
Button getPlusButton() {
return plusButton;
}
Button getMinusButton() {
return minusButton;
}
public final Label getDescrLabel() {
return descrLabel;
}
public final Label getCountLabel() {
return countLabel;
}
public S getParentNode() {
return parentNode;
}
@Override
public final T getEventBundle() {
return eventBundle;
}
public final EventDetailChart getChart() {
return chart;
}
public DescriptionLOD getDescLOD() {
return descLOD.get();
}
private static final Logger LOGGER = Logger.getLogger(AbstractDetailViewNode.class.getName());
/**
* loads sub-clusters at the given Description LOD
*
* @param newDescriptionLOD
*/
final synchronized void loadSubClusters(DescriptionLOD newDescriptionLOD) {
subNodePane.getChildren().clear();
if (newDescriptionLOD == getEventBundle().getDescriptionLOD()) {
showSpans(true);
getChart().setRequiresLayout(true);
getChart().requestChartLayout();
} else {
showSpans(false);
RootFilter combinedFilter = getSubClusterFilter();
//make a new end inclusive span (to 'filter' with)
final Interval span = new Interval(getEventBundle().getStartMillis(), getEventBundle().getEndMillis() + 1000);
//make a task to load the subnodes
LoggedTask<List<S>> loggedTask = new LoggedTask<List<S>>(
NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) {
@Override
protected List<S> call() throws Exception {
//query for the sub-clusters
List<EventCluster> aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span,
eventsModel.eventTypeZoomProperty().get(),
combinedFilter,
newDescriptionLOD));
return makeBundlesFromClusters(aggregatedEvents).stream()
.map(aggEvent -> {
return getNodeForCluser(aggEvent);
}).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters
}
@Override
protected void succeeded() {
try {
getChart().setCursor(Cursor.WAIT);
//assign subNodes and request chart layout
subNodePane.getChildren().setAll(get());
setDescriptionVisibility(descrVis);
getChart().setRequiresLayout(true);
getChart().requestChartLayout();
getChart().setCursor(null);
} catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex);
}
}
};
//start task
getChart().getController().monitorTask(loggedTask);
}
}
final double getLayoutXCompensation() {
return (getParentNode() != null ? getParentNode().getLayoutXCompensation() : 0)
+ getBoundsInParent().getMinX();
}
@Override
final public void setDescriptionVisibility(DescriptionVisibility descrVis) {
this.descrVis = descrVis;
final int size = getEventBundle().getEventIDs().size();
switch (descrVis) {
case COUNT_ONLY:
descrLabel.setText("");
countLabel.setText(String.valueOf(size));
break;
case HIDDEN:
countLabel.setText("");
descrLabel.setText("");
break;
default:
case SHOWN:
String description = getEventBundle().getDescription();
description = getParentNode() != null
? " ..." + StringUtils.substringAfter(description, getParentNode().getDescription())
: description;
descrLabel.setText(description);
countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS
break;
}
}
/**
* event handler used for mouse events on {@link AggregateEventNode}s
*/
private class EventMouseHandler implements EventHandler<MouseEvent> {
@Override
public void handle(MouseEvent t) {
if (t.getButton() == MouseButton.PRIMARY) {
t.consume();
if (t.isShiftDown()) {
if (chart.selectedNodes.contains(AbstractDetailViewNode.this) == false) {
chart.selectedNodes.add(AbstractDetailViewNode.this);
}
} else if (t.isShortcutDown()) {
chart.selectedNodes.removeAll(AbstractDetailViewNode.this);
} else if (t.getClickCount() > 1) {
final DescriptionLOD next = descLOD.get().next();
if (next != null) {
loadSubClusters(next);
descLOD.set(next);
}
} else {
chart.selectedNodes.setAll(AbstractDetailViewNode.this);
}
}
}
}
abstract S getNodeForCluser(T cluster);
}

View File

@ -5,6 +5,7 @@
*/
package org.sleuthkit.autopsy.timeline.ui.detailview;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
@ -15,24 +16,14 @@ import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
*/
public interface DetailViewNode<S extends DetailViewNode<S>> {
public long getStartMillis();
public long getEndMillis();
public void setDescriptionVisibility(DescriptionVisibility get);
public List<S> getSubNodes();
public List<? extends S> getSubNodes();
public void setSpanWidths(List<Double> spanWidths);
public void setDescriptionWidth(double max);
public EventType getEventType();
public Set<Long> getEventIDs();
public String getDescription();
public EventBundle getEventBundle();
/**
@ -43,4 +34,34 @@ public interface DetailViewNode<S extends DetailViewNode<S>> {
void applyHighlightEffect(boolean applied);
public void applySelectionEffect(boolean applied);
default String getDescription() {
return getEventBundle().getDescription();
}
default EventType getEventType() {
return getEventBundle().getEventType();
}
default Set<Long> getEventIDs() {
return getEventBundle().getEventIDs();
}
default public long getStartMillis() {
return getEventBundle().getStartMillis();
}
default long getEndMillis() {
return getEventBundle().getEndMillis();
}
static class StartTimeComparator implements Comparator<DetailViewNode<?>> {
@Override
public int compare(DetailViewNode<?> o1, DetailViewNode<?> o2) {
return Long.compare(o1.getStartMillis(), o2.getStartMillis());
}
}
}

View File

@ -70,7 +70,6 @@ import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.FXMLConstructor;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
@ -101,7 +100,7 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;
* TODO: refactor common code out of this class and CountsChartPane into
* {@link AbstractVisualization}
*/
public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster, EventBundle, EventDetailChart> {
public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster, DetailViewNode<?>, EventDetailChart> {
private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName());
@ -358,9 +357,8 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
}
@Override
protected void applySelectionEffect(EventBundle c1, Boolean selected) {
protected void applySelectionEffect(DetailViewNode<?> c1, Boolean selected) {
chart.applySelectionEffect(c1, selected);
}
private class DetailViewSettingsPane extends HBox {

View File

@ -18,120 +18,39 @@
*/
package org.sleuthkit.autopsy.timeline.ui.detailview;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.scene.control.Tooltip;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.ColorUtilities;
import org.sleuthkit.autopsy.coreutils.LoggedTask;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.filters.RootFilter;
import org.sleuthkit.autopsy.timeline.filters.TextFilter;
import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Represents an {@link EventCluster} in a {@link EventDetailChart}.
*/
public class EventClusterNode extends StackPane implements DetailViewNode<EventClusterNode> {
public class EventClusterNode extends AbstractDetailViewNode<EventCluster, EventClusterNode> {
private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName());
private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png");
private final static Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS
private final static Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS
private final static Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS
private static final CornerRadii CORNER_RADII = new CornerRadii(3);
/**
* the border to apply when this node is 'selected'
*/
private static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2)));
/**
* The event this AggregateEventNode represents visually
*/
private EventCluster eventCluster;
private final EventClusterNode parentEventNode;
/**
* the region that represents the time span of this node's event
*/
private final Region spanRegion = new Region();
/**
* The label used to display this node's event's description
*/
private final Label descrLabel = new Label();
/**
* The label used to display this node's event count
*/
private final Label countLabel = new Label();
/**
* The IamgeView used to show the icon for this node's event's type
*/
private final ImageView eventTypeImageView = new ImageView();
/**
* Pane that contains AggregateEventNodes of any 'subevents' if they are
* displayed
*
* //TODO: move more of the control of subnodes/events here and out of
* EventDetail Chart
*/
private final Pane subNodePane = new Pane();
/**
* the context menu that with the slider that controls subnode/event display
*
@ -140,153 +59,34 @@ public class EventClusterNode extends StackPane implements DetailViewNode<EventC
*/
private final SimpleObjectProperty<ContextMenu> contextMenu = new SimpleObjectProperty<>();
/**
* the Background used to fill the spanRegion, this varies epending on the
* selected/highlighted state of this node in its parent EventDetailChart
*/
private Background spanFill;
private final Button plusButton = new Button(null, new ImageView(PLUS)) {
{
configureLODButton(this);
}
};
private final Button minusButton = new Button(null, new ImageView(MINUS)) {
{
configureLODButton(this);
}
};
private static void configureLODButton(Button b) {
b.setMinSize(16, 16);
b.setMaxSize(16, 16);
b.setPrefSize(16, 16);
b.setVisible(false);
b.setManaged(false);
}
private final EventDetailChart chart;
private SimpleObjectProperty<DescriptionLOD> descLOD = new SimpleObjectProperty<>();
private DescriptionVisibility descrVis;
private final SleuthkitCase sleuthkitCase;
private final FilteredEventsModel eventsModel;
private Tooltip tooltip;
private final ImageView hashIV = new ImageView(HASH_PIN);
private final ImageView tagIV = new ImageView(TAG);
public EventClusterNode(final EventCluster aggEvent, EventClusterNode parentEventNode, EventDetailChart chart) {
this.eventCluster = aggEvent;
descLOD.set(aggEvent.getDescriptionLOD());
this.parentEventNode = parentEventNode;
this.chart = chart;
sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase();
eventsModel = chart.getController().getEventsModel();
public EventClusterNode(final EventCluster eventCluster, EventClusterNode parentEventNode, EventDetailChart chart) {
super(chart, eventCluster, parentEventNode);
minWidthProperty().bind(spanRegion.widthProperty());
header.setPrefWidth(USE_COMPUTED_SIZE);
final Region region = new Region();
HBox.setHgrow(region, Priority.ALWAYS);
final HBox hBox = new HBox(descrLabel, countLabel, region, hashIV, tagIV, minusButton, plusButton);
if (aggEvent.getEventIDsWithHashHits().isEmpty()) {
hashIV.setManaged(false);
hashIV.setVisible(false);
}
if (aggEvent.getEventIDsWithTags().isEmpty()) {
tagIV.setManaged(false);
tagIV.setVisible(false);
}
hBox.setPrefWidth(USE_COMPUTED_SIZE);
hBox.setMinWidth(USE_PREF_SIZE);
hBox.setPadding(new Insets(2, 5, 2, 5));
hBox.setAlignment(Pos.CENTER_LEFT);
final BorderPane borderPane = new BorderPane(subNodePane, hBox, null, null, null);
final BorderPane borderPane = new BorderPane(subNodePane, header, null, null, null);
BorderPane.setAlignment(subNodePane, Pos.TOP_LEFT);
borderPane.setPrefWidth(USE_COMPUTED_SIZE);
getChildren().addAll(spanRegion, borderPane);
setAlignment(Pos.TOP_LEFT);
setMinHeight(24);
minWidthProperty().bind(spanRegion.widthProperty());
setPrefHeight(USE_COMPUTED_SIZE);
setMaxHeight(USE_PREF_SIZE);
//set up subnode pane sizing contraints
subNodePane.setPrefHeight(USE_COMPUTED_SIZE);
subNodePane.setMinHeight(USE_PREF_SIZE);
subNodePane.setMinWidth(USE_PREF_SIZE);
subNodePane.setMaxHeight(USE_PREF_SIZE);
subNodePane.setMaxWidth(USE_PREF_SIZE);
subNodePane.setPickOnBounds(false);
//setup description label
eventTypeImageView.setImage(aggEvent.getEventType().getFXImage());
descrLabel.setGraphic(eventTypeImageView);
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
descrLabel.setMouseTransparent(true);
setDescriptionVisibility(chart.getDescrVisibility().get());
//setup backgrounds
final Color evtColor = aggEvent.getEventType().getColor();
spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY));
setBackground(spanFill);
setCursor(Cursor.HAND);
spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor) + ";"); // NON-NLS
spanRegion.setBackground(spanFill);
spanRegion.setBackground(getBackground());
//set up mouse hover effect and tooltip
setOnMouseEntered((MouseEvent e) -> {
//defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart
installTooltip();
spanRegion.setEffect(new DropShadow(10, evtColor));
minusButton.setVisible(true);
plusButton.setVisible(true);
minusButton.setManaged(true);
plusButton.setManaged(true);
toFront();
});
setOnMouseExited((MouseEvent e) -> {
spanRegion.setEffect(null);
minusButton.setVisible(false);
plusButton.setVisible(false);
minusButton.setManaged(false);
plusButton.setManaged(false);
});
setOnMouseClicked(new EventMouseHandler());
plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL));
minusButton.disableProperty().bind(descLOD.isEqualTo(aggEvent.getDescriptionLOD()));
plusButton.setOnMouseClicked(e -> {
final DescriptionLOD next = descLOD.get().next();
if (next != null) {
loadSubClusters(next);
descLOD.set(next);
}
});
minusButton.setOnMouseClicked(e -> {
final DescriptionLOD previous = descLOD.get().previous();
if (previous != null) {
loadSubClusters(previous);
descLOD.set(previous);
}
});
}
synchronized private void installTooltip() {
@Override
synchronized void installTooltip() {
//TODO: all this work should probably go on a background thread...
if (tooltip == null) {
HashMap<String, Long> hashSetCounts = new HashMap<>();
if (!eventCluster.getEventIDsWithHashHits().isEmpty()) {
if (!getEventCluster().getEventIDsWithHashHits().isEmpty()) {
hashSetCounts = new HashMap<>();
try {
for (TimeLineEvent tle : eventsModel.getEventsById(eventCluster.getEventIDsWithHashHits())) {
for (TimeLineEvent tle : eventsModel.getEventsById(getEventCluster().getEventIDsWithHashHits())) {
Set<String> hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames();
for (String hashSetName : hashSetNames) {
hashSetCounts.merge(hashSetName, 1L, Long::sum);
@ -298,8 +98,8 @@ public class EventClusterNode extends StackPane implements DetailViewNode<EventC
}
Map<String, Long> tagCounts = new HashMap<>();
if (!eventCluster.getEventIDsWithTags().isEmpty()) {
tagCounts.putAll(eventsModel.getTagCountsByTagName(eventCluster.getEventIDsWithTags()));
if (!getEventCluster().getEventIDsWithTags().isEmpty()) {
tagCounts.putAll(eventsModel.getTagCountsByTagName(getEventCluster().getEventIDsWithTags()));
}
@ -312,9 +112,9 @@ public class EventClusterNode extends StackPane implements DetailViewNode<EventC
tooltip = new Tooltip(
NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text",
getEvent().getEventIDs().size(), getEvent().getEventType(), getEvent().getDescription(),
getEvent().getSpan().getStart().toString(TimeLineController.getZonedFormatter()),
getEvent().getSpan().getEnd().toString(TimeLineController.getZonedFormatter()))
getEventCluster().getEventIDs().size(), getEventCluster().getEventType(), getEventCluster().getDescription(),
getEventCluster().getSpan().getStart().toString(TimeLineController.getZonedFormatter()),
getEventCluster().getSpan().getEnd().toString(TimeLineController.getZonedFormatter()))
+ (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString)
+ (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString)
);
@ -322,15 +122,8 @@ public class EventClusterNode extends StackPane implements DetailViewNode<EventC
}
}
@Override
public List<EventClusterNode> getSubNodes() {
return subNodePane.getChildrenUnmodifiable().stream()
.map(EventClusterNode.class::cast)
.collect(Collectors.toList());
}
synchronized public EventCluster getEvent() {
return eventCluster;
synchronized public EventCluster getEventCluster() {
return getEventBundle();
}
/**
@ -351,93 +144,9 @@ public class EventClusterNode extends StackPane implements DetailViewNode<EventC
}
/**
*
* @param w the maximum width the description label should have
*/
@Override
public void setDescriptionWidth(double w) {
descrLabel.setMaxWidth(w);
}
/**
* @param descrVis the level of description that should be displayed
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@Override
public final synchronized void setDescriptionVisibility(DescriptionVisibility descrVis) {
this.descrVis = descrVis;
final int size = eventCluster.getEventIDs().size();
switch (descrVis) {
case COUNT_ONLY:
descrLabel.setText("");
countLabel.setText(String.valueOf(size));
break;
case HIDDEN:
countLabel.setText("");
descrLabel.setText("");
break;
default:
case SHOWN:
String description = eventCluster.getDescription();
description = parentEventNode != null
? " ..." + StringUtils.substringAfter(description, parentEventNode.getEvent().getDescription())
: description;
descrLabel.setText(description);
countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS
break;
}
}
/**
* apply the 'effect' to visually indicate selection
*
* @param applied true to apply the selection 'effect', false to remove it
*/
@Override
public void applySelectionEffect(boolean applied) {
Platform.runLater(() -> {
if (applied) {
setBorder(selectionBorder);
} else {
setBorder(null);
}
});
}
@Override
public String getDescription() {
return eventCluster.getDescription();
}
/**
* apply the 'effect' to visually indicate highlighted nodes
*
* @param applied true to apply the highlight 'effect', false to remove it
*/
public synchronized void applyHighlightEffect(boolean applied) {
if (applied) {
descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS
spanFill = new Background(new BackgroundFill(eventCluster.getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY));
spanRegion.setBackground(spanFill);
setBackground(new Background(new BackgroundFill(eventCluster.getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
} else {
descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS
spanFill = new Background(new BackgroundFill(eventCluster.getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY));
spanRegion.setBackground(spanFill);
setBackground(new Background(new BackgroundFill(eventCluster.getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
}
}
String getDisplayedDescription() {
return descrLabel.getText();
}
double getLayoutXCompensation() {
return (parentEventNode != null ? parentEventNode.getLayoutXCompensation() : 0)
+ getBoundsInParent().getMinX();
Region getSpanFillNode() {
return spanRegion;
}
/**
@ -454,115 +163,19 @@ public class EventClusterNode extends StackPane implements DetailViewNode<EventC
this.contextMenu.set(contextMenu);
}
/**
* loads sub-clusters at the given Description LOD
*
* @param newDescriptionLOD
*/
synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) {
subNodePane.getChildren().clear();
if (newDescriptionLOD == eventCluster.getDescriptionLOD()) {
chart.setRequiresLayout(true);
chart.requestChartLayout();
} else {
RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf();
//make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters
combinedFilter.getSubFilters().addAll(new TextFilter(eventCluster.getDescription()),
new TypeFilter(eventCluster.getEventType()));
//make a new end inclusive span (to 'filter' with)
final Interval span = eventCluster.getSpan().withEndMillis(eventCluster.getSpan().getEndMillis() + 1000);
//make a task to load the subnodes
LoggedTask<List<EventClusterNode>> loggedTask = new LoggedTask<List<EventClusterNode>>(
NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) {
@Override
protected List<EventClusterNode> call() throws Exception {
//query for the sub-clusters
List<EventCluster> aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span,
eventsModel.eventTypeZoomProperty().get(),
combinedFilter,
newDescriptionLOD));
//for each sub cluster make an AggregateEventNode to visually represent it, and set x-position
return aggregatedEvents.stream().map(aggEvent -> {
EventClusterNode subNode = new EventClusterNode(aggEvent, EventClusterNode.this, chart);
subNode.setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis())) - getLayoutXCompensation());
return subNode;
}).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters
}
@Override
protected void succeeded() {
try {
chart.setCursor(Cursor.WAIT);
//assign subNodes and request chart layout
subNodePane.getChildren().setAll(get());
setDescriptionVisibility(descrVis);
chart.setRequiresLayout(true);
chart.requestChartLayout();
chart.setCursor(null);
} catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex);
}
}
};
//start task
chart.getController().monitorTask(loggedTask);
}
}
/**
* event handler used for mouse events on {@link EventClusterNode}s
*/
private class EventMouseHandler implements EventHandler<MouseEvent> {
@Override
public void handle(MouseEvent t) {
if (t.getButton() == MouseButton.PRIMARY) {
t.consume();
if (t.isShiftDown()) {
if (chart.selectedBundles.contains(eventCluster) == false) {
chart.selectedBundles.add(eventCluster);
}
} else if (t.isShortcutDown()) {
chart.selectedBundles.removeAll(eventCluster);
} else if (t.getClickCount() > 1) {
final DescriptionLOD next = descLOD.get().next();
if (next != null) {
loadSubClusters(next);
descLOD.set(next);
}
} else {
chart.selectedBundles.setAll(eventCluster);
}
}
}
@Override
void showSpans(boolean showSpans) {
//no-op for now
}
@Override
public long getStartMillis() {
return getEvent().getStartMillis();
Collection<EventCluster> makeBundlesFromClusters(List<EventCluster> eventClusters) {
return eventClusters;
}
@Override
public long getEndMillis() {
return getEvent().getStartMillis();
EventClusterNode getNodeForCluser(EventCluster cluster) {
return new EventClusterNode(cluster, this, getChart());
}
@Override
public EventType getEventType() {
return getEvent().getEventType();
}
@Override
public Set<Long> getEventIDs() {
return getEvent().getEventIDs();
}
@Override
public EventBundle getEventBundle() {
return eventCluster;
}
}

View File

@ -60,7 +60,6 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.scene.shape.Line;
import javafx.scene.shape.StrokeLineCap;
import javafx.util.Duration;
@ -75,7 +74,6 @@ import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.actions.Back;
import org.sleuthkit.autopsy.timeline.actions.Forward;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
@ -175,7 +173,7 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
@GuardedBy(value = "this")
private boolean requiresLayout = true;
final ObservableList<EventBundle> selectedBundles;
final ObservableList<DetailViewNode<?>> selectedNodes;
/**
* list of series of data added to this chart TODO: replace this with a map
@ -205,7 +203,7 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0);
private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true);
EventDetailChart(DateAxis dateAxis, final Axis<EventCluster> verticalAxis, ObservableList<EventBundle> selectedNodes) {
EventDetailChart(DateAxis dateAxis, final Axis<EventCluster> verticalAxis, ObservableList<DetailViewNode<?>> selectedNodes) {
super(dateAxis, verticalAxis);
dateAxis.setAutoRanging(false);
@ -284,38 +282,28 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
setOnMouseReleased(dragHandler);
setOnMouseDragged(dragHandler);
// projectionMap.addListener((MapChangeListener.Change<? extends DetailViewNode<?>, ? extends Line> change) -> {
// final Line valueRemoved = change.getValueRemoved();
// if (valueRemoved != null) {
// getChartChildren().removeAll(valueRemoved);
// }
// final Line valueAdded = change.getValueAdded();
// if (valueAdded != null) {
// getChartChildren().add(valueAdded);
// }
// });
this.selectedBundles = selectedNodes;
this.selectedBundles.addListener((
ListChangeListener.Change<? extends EventBundle> c) -> {
this.selectedNodes = selectedNodes;
this.selectedNodes.addListener((
ListChangeListener.Change<? extends DetailViewNode<?>> c) -> {
while (c.next()) {
c.getRemoved().forEach((EventBundle t) -> {
t.getRanges().forEach((Range<Long> t1) -> {
Line removeAll = projectionMap.remove(t1);
getChartChildren().removeAll(removeAll);
c.getRemoved().forEach((DetailViewNode<?> t) -> {
t.getEventBundle().getRanges().forEach((Range<Long> t1) -> {
Line removedLine = projectionMap.remove(t1);
getChartChildren().removeAll(removedLine);
});
});
c.getAddedSubList().forEach((EventBundle t) -> {
c.getAddedSubList().forEach((DetailViewNode<?> t) -> {
for (Range<Long> r : t.getRanges()) {
for (Range<Long> range : t.getEventBundle().getRanges()) {
Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(r.lowerEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET,
dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(r.upperEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET
Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.lowerEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET,
dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.upperEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET
);
line.setStroke(t.getEventType().getColor().deriveColor(0, 1, 1, .5));
line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH);
line.setStrokeLineCap(StrokeLineCap.ROUND);
projectionMap.put(r, line);
projectionMap.put(range, line);
getChartChildren().add(line);
}
});
@ -353,7 +341,7 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
clearGuideLine();
clearIntervalSelector();
selectedBundles.clear();
selectedNodes.clear();
projectionMap.clear();
controller.selectEventIDs(Collections.emptyList());
});
@ -600,38 +588,38 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
* @param nodes
* @param minY
*/
private synchronized <DVRegion extends Region & DetailViewNode<DVRegion>> double layoutNodes(final Collection<DVRegion> nodes, final double minY, final double xOffset) {
private synchronized double layoutNodes(final Collection<? extends AbstractDetailViewNode<?, ?>> nodes, final double minY, final double xOffset) {
//hash map from y value to right most occupied x value. This tells you for a given 'row' what is the first avaialable slot
Map<Integer, Double> maxXatY = new HashMap<>();
double localMax = minY;
//for each node lay size it and position it in first available slot
for (DVRegion n : nodes) {
n.setDescriptionVisibility(descrVisibility.get());
double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(n.getStartMillis()));
for (AbstractDetailViewNode<?, ?> node : nodes) {
node.setDescriptionVisibility(descrVisibility.get());
double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(node.getStartMillis()));
//position of start and end according to range of axis
double startX = rawDisplayPosition - xOffset;
double layoutNodesResultHeight = 0;
double span = 0;
List<DVRegion> subNodes = n.getSubNodes();
List<? extends AbstractDetailViewNode<?, ?>> subNodes = node.getSubNodes();
if (subNodes.isEmpty() == false) {
subNodes.sort(Comparator.comparing((DVRegion t) -> t.getStartMillis()));
subNodes.sort(new DetailViewNode.StartTimeComparator());
layoutNodesResultHeight = layoutNodes(subNodes, 0, rawDisplayPosition);
}
if (n instanceof EventClusterNode) {
double endX = getXAxis().getDisplayPosition(new DateTime(n.getEndMillis())) - xOffset;
if (alternateLayout.get() == false) {
double endX = getXAxis().getDisplayPosition(new DateTime(node.getEndMillis())) - xOffset;
span = endX - startX;
//size timespan border
n.setSpanWidths(Arrays.asList(span));
node.setSpanWidths(Arrays.asList(span));
} else {
EventStripeNode cn = (EventStripeNode) n;
EventStripeNode stripeNode = (EventStripeNode) node;
List<Double> spanWidths = new ArrayList<>();
double x = getXAxis().getDisplayPosition(new DateTime(cn.getStartMillis()));;
double x = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));;
double x2;
Iterator<Range<Long>> ranges = cn.getStripe().getRanges().iterator();
Iterator<Range<Long>> ranges = stripeNode.getStripe().getRanges().iterator();
Range<Long> range = ranges.next();
do {
x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint()));
@ -644,25 +632,31 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
double gapSpan = x - x2;
span += gapSpan;
spanWidths.add(gapSpan);
if (ranges.hasNext() == false) {
x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint()));
clusterSpan = x2 - x;
span += clusterSpan;
spanWidths.add(clusterSpan);
}
}
} while (ranges.hasNext());
cn.setSpanWidths(spanWidths);
stripeNode.setSpanWidths(spanWidths);
}
if (truncateAll.get()) { //if truncate option is selected limit width of description label
n.setDescriptionWidth(Math.max(span, truncateWidth.get()));
node.setDescriptionWidth(Math.max(span, truncateWidth.get()));
} else { //else set it unbounded
n.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth());
node.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth());
}
n.autosize(); //compute size of tlNode based on constraints and event data
node.autosize(); //compute size of tlNode based on constraints and event data
//get position of right edge of node ( influenced by description label)
double xRight = startX + n.getWidth();
double xRight = startX + node.getWidth();
//get the height of the node
final double h = layoutNodesResultHeight == 0 ? n.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT;
final double h = layoutNodesResultHeight == 0 ? node.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT;
//initial test position
double yPos = minY;
@ -701,8 +695,8 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
localMax = Math.max(yPos2, localMax);
Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0),
new KeyValue(n.layoutXProperty(), startX),
new KeyValue(n.layoutYProperty(), yPos)));
new KeyValue(node.layoutXProperty(), startX),
new KeyValue(node.layoutYProperty(), yPos)));
tm.play();
}
@ -753,15 +747,6 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
return alternateLayout;
}
private static class StartTimeComparator<T extends Node & DetailViewNode<?>> implements Comparator<T> {
@Override
public int compare(T n1, T n2) {
return Long.compare(n1.getStartMillis(), n2.getStartMillis()
);
}
}
private class DetailIntervalSelector extends IntervalSelector<DateTime> {
public DetailIntervalSelector(double x, double height, Axis<DateTime> axis, TimeLineController controller) {
@ -797,12 +782,7 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
super.requestChartLayout();
}
void applySelectionEffect(EventBundle c1, Boolean selected) {
if (alternateLayout.get()) {
EventStripe eventStripe = stripeDescMap.get(ImmutablePair.of(c1.getEventType(), c1.getDescription()));
stripeNodeMap.get(eventStripe).applySelectionEffect(selected);
} else {
clusterNodeMap.get(c1).applySelectionEffect(selected);
}
void applySelectionEffect(DetailViewNode<?> c1, Boolean selected) {
c1.applySelectionEffect(selected);
}
}

View File

@ -6,235 +6,64 @@
package org.sleuthkit.autopsy.timeline.ui.detailview;
import com.google.common.collect.Range;
import java.util.HashMap;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import static javafx.scene.layout.Region.USE_COMPUTED_SIZE;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.ColorUtilities;
import org.sleuthkit.autopsy.coreutils.LoggedTask;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.filters.RootFilter;
import org.sleuthkit.autopsy.timeline.filters.TextFilter;
import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
import org.sleuthkit.datamodel.SleuthkitCase;
import static org.sleuthkit.autopsy.timeline.ui.detailview.AbstractDetailViewNode.show;
/**
*
*/
public class EventStripeNode extends StackPane implements DetailViewNode<EventStripeNode> {
public class EventStripeNode extends AbstractDetailViewNode<EventStripe, EventStripeNode> {
private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName());
private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png");
private final static Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS
private final static Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS
private final static Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS
private final Pane subNodePane = new Pane();
private final EventStripe eventStripe;
private final EventStripeNode parentNode;
private final EventDetailChart chart;
private SimpleObjectProperty<DescriptionLOD> descLOD = new SimpleObjectProperty<>();
private final SleuthkitCase sleuthkitCase;
private final FilteredEventsModel eventsModel;
/**
* The label used to display this node's event's description
*/
private final Label descrLabel = new Label();
/**
* The label used to display this node's event count
*/
private final Label countLabel = new Label();
private final ImageView hashIV = new ImageView(HASH_PIN);
private final ImageView tagIV = new ImageView(TAG);
private final Button plusButton = new Button(null, new ImageView(PLUS)) {
{
configureLODButton(this);
}
};
private final Button minusButton = new Button(null, new ImageView(MINUS)) {
{
configureLODButton(this);
}
};
private static void configureLODButton(Button b) {
b.setMinSize(16, 16);
b.setMaxSize(16, 16);
b.setPrefSize(16, 16);
show(b, false);
}
private static void show(Node b, boolean show) {
b.setVisible(show);
b.setManaged(show);
}
private DescriptionVisibility descrVis;
private final HBox spansHBox = new HBox();
/**
* The IamgeView used to show the icon for this node's event's type
*/
private final ImageView eventTypeImageView = new ImageView();
private Background spanFill;
private static final CornerRadii CORNER_RADII = new CornerRadii(3);
private final HBox rangesHBox = new HBox();
EventStripeNode(EventStripe eventStripe, EventStripeNode parentNode, EventDetailChart chart) {
this.chart = chart;
sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase();
eventsModel = chart.getController().getEventsModel();
this.parentNode = parentNode;
this.eventStripe = eventStripe;
descLOD.set(eventStripe.getDescriptionLOD());
final Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
final HBox header = new HBox(descrLabel, countLabel, hashIV, tagIV, spacer, minusButton, plusButton);
if (eventStripe.getEventIDsWithHashHits().isEmpty()) {
show(hashIV, false);
}
if (eventStripe.getEventIDsWithTags().isEmpty()) {
show(tagIV, false);
}
header.setMinWidth(USE_PREF_SIZE);
header.setPadding(new Insets(2, 5, 2, 5));
header.setAlignment(Pos.CENTER_LEFT);
super(chart, eventStripe, parentNode);
minWidthProperty().bind(rangesHBox.widthProperty());
final VBox internalVBox = new VBox(header, subNodePane);
internalVBox.setAlignment(Pos.CENTER_LEFT);
final Color evtColor = eventStripe.getEventType().getColor();
spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY));
for (Range<Long> r : eventStripe.getRanges()) {
Region spanRegion = new Region();
spanRegion.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS
spanRegion.setBackground(spanFill);
spansHBox.getChildren().addAll(spanRegion, new Region());
for (Range<Long> range : eventStripe.getRanges()) {
Region rangeRegion = new Region();
rangeRegion.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS
rangeRegion.setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
rangesHBox.getChildren().addAll(rangeRegion, new Region());
}
spansHBox.getChildren().remove(spansHBox.getChildren().size() - 1);
spansHBox.setMaxWidth(USE_PREF_SIZE);
rangesHBox.getChildren().remove(rangesHBox.getChildren().size() - 1);
rangesHBox.setMaxWidth(USE_PREF_SIZE);
setMaxWidth(USE_PREF_SIZE);
getChildren().addAll(spansHBox, internalVBox);
setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
setAlignment(Pos.TOP_LEFT);
setMinHeight(24);
minWidthProperty().bind(spansHBox.widthProperty());
setPrefHeight(USE_COMPUTED_SIZE);
setMaxHeight(USE_PREF_SIZE);
//set up subnode pane sizing contraints
subNodePane.setPrefHeight(USE_COMPUTED_SIZE);
subNodePane.setMinHeight(USE_PREF_SIZE);
subNodePane.setMinWidth(USE_PREF_SIZE);
subNodePane.setMaxHeight(USE_PREF_SIZE);
subNodePane.setMaxWidth(USE_PREF_SIZE);
subNodePane.setPickOnBounds(false);
//setup description label
eventTypeImageView.setImage(eventStripe.getEventType().getFXImage());
descrLabel.setGraphic(eventTypeImageView);
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
descrLabel.setMouseTransparent(true);
setDescriptionVisibility(chart.getDescrVisibility().get());
setOnMouseClicked(new EventMouseHandler());
//set up mouse hover effect and tooltip
setOnMouseEntered((MouseEvent e) -> {
//defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart
// installTooltip();
spansHBox.setEffect(new DropShadow(10, evtColor));
show(spacer, true);
show(minusButton, true);
show(plusButton, true);
toFront();
});
setOnMouseExited((MouseEvent e) -> {
spansHBox.setEffect(null);
show(spacer, false);
show(minusButton, false);
show(plusButton, false);
});
plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL));
minusButton.disableProperty().bind(descLOD.isEqualTo(eventStripe.getDescriptionLOD()));
plusButton.setOnMouseClicked(e -> {
final DescriptionLOD next = descLOD.get().next();
if (next != null) {
loadSubClusters(next);
descLOD.set(next);
}
});
minusButton.setOnMouseClicked(e -> {
final DescriptionLOD previous = descLOD.get().previous();
if (previous != null) {
loadSubClusters(previous);
descLOD.set(previous);
}
});
getChildren().addAll(rangesHBox, internalVBox);
}
/**
*
* @param showControls the value of par
*/
@Override
public long getStartMillis() {
return eventStripe.getStartMillis();
void showDescriptionLoDControls(final boolean showControls) {
super.showDescriptionLoDControls(showControls);
show(spacer, showControls);
}
@Override
public void setSpanWidths(List<Double> spanWidths) {
for (int i = 0; i < spanWidths.size(); i++) {
Region spanRegion = (Region) spansHBox.getChildren().get(i);
Region spanRegion = (Region) rangesHBox.getChildren().get(i);
Double w = spanWidths.get(i);
spanRegion.setPrefWidth(w);
spanRegion.setMaxWidth(w);
@ -242,211 +71,42 @@ public class EventStripeNode extends StackPane implements DetailViewNode<EventSt
}
}
public void setDescriptionVisibility(DescriptionVisibility descrVis) {
this.descrVis = descrVis;
final int size = eventStripe.getEventIDs().size();
switch (descrVis) {
case COUNT_ONLY:
descrLabel.setText("");
countLabel.setText(String.valueOf(size));
break;
case HIDDEN:
countLabel.setText("");
descrLabel.setText("");
break;
default:
case SHOWN:
String description = eventStripe.getDescription();
description = parentNode != null
? " ..." + StringUtils.substringAfter(description, parentNode.getDescription())
: description;
descrLabel.setText(description);
countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS
break;
}
}
EventStripe getStripe() {
return eventStripe;
return getEventBundle();
}
@Override
public void setDescriptionWidth(double w) {
descrLabel.setMaxWidth(w);
HBox getSpanFillNode() {
return rangesHBox;
}
@Override
public long getEndMillis() {
return eventStripe.getEndMillis();
}
@Override
public List<EventStripeNode> getSubNodes() {
return subNodePane.getChildrenUnmodifiable().stream()
.map(EventStripeNode.class::cast)
.collect(Collectors.toList());
Collection<EventStripe> makeBundlesFromClusters(List<EventCluster> eventClusters) {
return eventClusters.stream().collect(
Collectors.toMap(
EventCluster::getDescription, //key
EventStripe::new, //value
EventStripe::merge)//merge method
).values();
}
/**
* event handler used for mouse events on {@link AggregateEventNode}s
*/
private class EventMouseHandler implements EventHandler<MouseEvent> {
@Override
public void handle(MouseEvent t) {
if (t.getButton() == MouseButton.PRIMARY) {
t.consume();
if (t.isShiftDown()) {
if (chart.selectedBundles.contains(eventStripe) == false) {
chart.selectedBundles.add(eventStripe);
}
} else if (t.isShortcutDown()) {
chart.selectedBundles.removeAll(eventStripe);
} else if (t.getClickCount() > 1) {
final DescriptionLOD next = descLOD.get().next();
if (next != null) {
loadSubClusters(next);
descLOD.set(next);
}
} else {
chart.selectedBundles.setAll(eventStripe);
}
}
}
}
@Override
public EventType getEventType() {
return eventStripe.getEventType();
}
@Override
public Set<Long> getEventIDs() {
return eventStripe.getEventIDs();
}
private static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2)));
/**
* apply the 'effect' to visually indicate selection
*
* @param applied true to apply the selection 'effect', false to remove it
* @param showSpans the value of showSpans
*/
@Override
public void applySelectionEffect(boolean applied) {
Platform.runLater(() -> {
if (applied) {
setBorder(selectionBorder);
} else {
setBorder(null);
}
});
}
/**
* apply the 'effect' to visually indicate highlighted nodes
*
* @param applied true to apply the highlight 'effect', false to remove it
*/
@Override
public synchronized void applyHighlightEffect(boolean applied) {
if (applied) {
descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS
spanFill = new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY));
spansHBox.setBackground(spanFill);
setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
} else {
descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS
spanFill = new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY));
spansHBox.setBackground(spanFill);
setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
}
void showSpans(final boolean showSpans) {
rangesHBox.setVisible(showSpans);
}
@Override
public String getDescription() {
return eventStripe.getDescription();
void installTooltip() {
// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public EventBundle getEventBundle() {
return getStripe();
EventStripeNode getNodeForCluser(EventStripe cluster) {
return new EventStripeNode(cluster, this, getChart());
}
/**
* loads sub-clusters at the given Description LOD
*
* @param newDescriptionLOD
*/
synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) {
subNodePane.getChildren().clear();
if (newDescriptionLOD == eventStripe.getDescriptionLOD()) {
spansHBox.setVisible(true);
chart.setRequiresLayout(true);
chart.requestChartLayout();
} else {
spansHBox.setVisible(false);
RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf();
//make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters
combinedFilter.getSubFilters().addAll(new TextFilter(eventStripe.getDescription()),
new TypeFilter(eventStripe.getEventType()));
//make a new end inclusive span (to 'filter' with)
final Interval span = new Interval(eventStripe.getStartMillis(), eventStripe.getEndMillis() + 1000);
//make a task to load the subnodes
LoggedTask<List<EventStripeNode>> loggedTask = new LoggedTask<List<EventStripeNode>>(
NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) {
@Override
protected List<EventStripeNode> call() throws Exception {
//query for the sub-clusters
List<EventCluster> aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span,
eventsModel.eventTypeZoomProperty().get(),
combinedFilter,
newDescriptionLOD));
//for each sub cluster make an AggregateEventNode to visually represent it, and set x-position
HashMap<String, EventStripe> stripeDescMap = new HashMap<>();
for (EventCluster subCluster : aggregatedEvents) {
stripeDescMap.merge(subCluster.getDescription(),
new EventStripe(subCluster),
(EventStripe u, EventStripe v) -> {
return EventStripe.merge(u, v);
}
);
}
return stripeDescMap.values().stream().map(subStripe -> {
EventStripeNode subNode = new EventStripeNode(subStripe, EventStripeNode.this, chart);
subNode.setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(subStripe.getStartMillis())) - getLayoutXCompensation());
return subNode;
}).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters
}
@Override
protected void succeeded() {
try {
chart.setCursor(Cursor.WAIT);
//assign subNodes and request chart layout
subNodePane.getChildren().setAll(get());
setDescriptionVisibility(descrVis);
chart.setRequiresLayout(true);
chart.requestChartLayout();
chart.setCursor(null);
} catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex);
}
}
};
//start task
chart.getController().monitorTask(loggedTask);
}
}
double getLayoutXCompensation() {
return (parentNode != null ? parentNode.getLayoutXCompensation() : 0)
+ getBoundsInParent().getMinX();
}
}

View File

@ -41,9 +41,9 @@ import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.timeline.FXMLConstructor;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.TimeLineView;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewNode;
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane;
/**
@ -91,8 +91,8 @@ public class NavPanel extends BorderPane implements TimeLineView {
});
detailViewPane.getSelectedNodes().addListener((Observable observable) -> {
eventsTree.getSelectionModel().clearSelection();
detailViewPane.getSelectedNodes().forEach((EventBundle t) -> {
eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t));
detailViewPane.getSelectedNodes().forEach((DetailViewNode<?> t) -> {
eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getEventBundle()));
});
});
@ -131,7 +131,7 @@ public class NavPanel extends BorderPane implements TimeLineView {
sortByBox.getItems().setAll(Arrays.asList(TreeComparator.Description, TreeComparator.Count));
sortByBox.getSelectionModel().select(TreeComparator.Description);
sortByBox.getSelectionModel().selectedItemProperty().addListener((Observable o) -> {
((RootItem) eventsTree.getRoot()).resort(sortByBox.getSelectionModel().getSelectedItem());
((NavTreeItem) eventsTree.getRoot()).resort(sortByBox.getSelectionModel().getSelectedItem());
});
eventsTree.setShowRoot(false);
eventsTree.setCellFactory((TreeView<NavTreeNode> p) -> new EventTreeCell());

View File

@ -20,8 +20,8 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
import java.util.Comparator;
import javafx.scene.control.TreeItem;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
/**
* A node in the nav tree. Manages inserts and resorts. Has parents and