diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java index ba3333e7f7..8714452725 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java @@ -18,16 +18,16 @@ */ package org.sleuthkit.autopsy.timeline.datamodel; +import com.google.common.collect.Range; import java.util.Optional; import java.util.Set; -import java.util.SortedSet; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** - * A interface for groups of events that share some attributes in common. + * */ -public interface EventBundle> { +public interface EventBundle { String getDescription(); @@ -35,6 +35,7 @@ public interface EventBundle> { Set getEventIDs(); + Set getEventIDsWithHashHits(); Set getEventIDsWithTags(); @@ -45,11 +46,11 @@ public interface EventBundle> { long getStartMillis(); - Optional getParentBundle(); + Iterable> getRanges(); - default long getCount() { + Optional getParentBundle(); + + default long getCount() { return getEventIDs().size(); } - - SortedSet getClusters(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java index 592c4674f6..57d5cec0db 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java @@ -18,14 +18,12 @@ */ package org.sleuthkit.autopsy.timeline.datamodel; -import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Range; import com.google.common.collect.Sets; import java.util.Collections; -import java.util.Comparator; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.SortedSet; import javax.annotation.concurrent.Immutable; import org.joda.time.Interval; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; @@ -38,7 +36,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; * designated 'zoom level', and be 'close together' in time. */ @Immutable -public class EventCluster implements EventBundle { +public class EventCluster implements EventBundle { /** * merge two event clusters into one new event cluster. @@ -64,7 +62,7 @@ public class EventCluster implements EventBundle { return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod); } - final private EventStripe parent; + final private EventBundle parent; /** * the smallest time interval containing all the clustered events @@ -103,7 +101,7 @@ public class EventCluster implements EventBundle { */ private final Set hashHits; - private EventCluster(Interval spanningInterval, EventType type, Set eventIDs, Set hashHits, Set tagged, String description, DescriptionLoD lod, EventStripe parent) { + private EventCluster(Interval spanningInterval, EventType type, Set eventIDs, Set hashHits, Set tagged, String description, DescriptionLoD lod, EventBundle parent) { this.span = spanningInterval; this.type = type; @@ -120,7 +118,7 @@ public class EventCluster implements EventBundle { } @Override - public Optional getParentBundle() { + public Optional getParentBundle() { return Optional.ofNullable(parent); } @@ -168,6 +166,19 @@ public class EventCluster implements EventBundle { return lod; } + Range getRange() { + if (getEndMillis() > getStartMillis()) { + return Range.closedOpen(getSpan().getStartMillis(), getSpan().getEndMillis()); + } else { + return Range.singleton(getStartMillis()); + } + } + + @Override + public Iterable> getRanges() { + return Collections.singletonList(getRange()); + } + /** * return a new EventCluster identical to this one, except with the given * EventBundle as the parent. @@ -177,15 +188,11 @@ public class EventCluster implements EventBundle { * @return a new EventCluster identical to this one, except with the given * EventBundle as the parent. */ - public EventCluster withParent(EventStripe parent) { + public EventCluster withParent(EventBundle parent) { if (Objects.nonNull(this.parent)) { throw new IllegalStateException("Event Cluster already has a parent!"); } return new EventCluster(span, type, eventIDs, hashHits, tagged, description, lod, parent); } - @Override - public SortedSet< EventCluster> getClusters() { - return ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis)).add(this).build(); - } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java index cd457d336e..8a465de1cd 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -1,31 +1,20 @@ /* - * Autopsy Forensic Browser - * - * Copyright 2015 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.datamodel; import com.google.common.base.Preconditions; +import com.google.common.collect.Range; +import com.google.common.collect.RangeMap; +import com.google.common.collect.RangeSet; +import com.google.common.collect.TreeRangeMap; +import com.google.common.collect.TreeRangeSet; import java.util.Collections; -import java.util.Comparator; import java.util.HashSet; import java.util.Optional; import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; import javax.annotation.concurrent.Immutable; import org.python.google.common.base.Objects; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; @@ -33,10 +22,10 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** * A 'collection' of {@link EventCluster}s, all having the same type, - * description, and zoom levels, but not necessarily close together in time. + * description, and zoom levels. */ @Immutable -public final class EventStripe implements EventBundle { +public final class EventStripe implements EventBundle { public static EventStripe merge(EventStripe u, EventStripe v) { Preconditions.checkNotNull(u); @@ -48,9 +37,10 @@ public final class EventStripe implements EventBundle { return new EventStripe(u, v); } - private final EventCluster parent; + private final EventBundle parent; - private final SortedSet clusters = new TreeSet<>(Comparator.comparing(EventCluster::getStartMillis)); + private final RangeSet spans = TreeRangeSet.create(); + private final RangeMap spanMap = TreeRangeMap.create(); /** * the type of all the events @@ -83,21 +73,23 @@ public final class EventStripe implements EventBundle { */ private final Set hashHits = new HashSet<>(); - public EventStripe(EventCluster cluster, EventCluster parent) { - clusters.add(cluster); - + 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()); - this.parent = parent; + parent = cluster.getParentBundle().orElse(null); } private EventStripe(EventStripe u, EventStripe v) { - clusters.addAll(u.clusters); - clusters.addAll(v.clusters); + spans.addAll(u.spans); + spans.addAll(v.spans); + spanMap.putAll(u.spanMap); + spanMap.putAll(v.spanMap); type = u.getEventType(); description = u.getDescription(); lod = u.getDescriptionLoD(); @@ -107,11 +99,11 @@ public final class EventStripe implements EventBundle { tagged.addAll(v.getEventIDsWithTags()); hashHits.addAll(u.getEventIDsWithHashHits()); hashHits.addAll(v.getEventIDsWithHashHits()); - parent = u.getParentBundle().orElse(v.getParentBundle().orElse(null)); + parent = u.getParentBundle().orElse(null); } @Override - public Optional getParentBundle() { + public Optional getParentBundle() { return Optional.ofNullable(parent); } @@ -147,15 +139,16 @@ public final class EventStripe implements EventBundle { @Override public long getStartMillis() { - return clusters.first().getStartMillis(); + return spans.span().lowerEndpoint(); } @Override public long getEndMillis() { - return clusters.last().getEndMillis(); + return spans.span().upperEndpoint(); } - public SortedSet< EventCluster> getClusters() { - return Collections.unmodifiableSortedSet(clusters); + @Override + public Iterable> getRanges() { + return spans.asRanges(); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java index 8d16c7a86f..9ba8977757 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java @@ -25,8 +25,6 @@ import java.util.concurrent.ExecutionException; import java.util.logging.Level; import javafx.beans.InvalidationListener; import javafx.beans.Observable; -import javafx.beans.property.ReadOnlyListProperty; -import javafx.beans.property.ReadOnlyListWrapper; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; @@ -104,14 +102,14 @@ public abstract class AbstractVisualizationPane final protected FilteredEventsModel filteredEvents; - final protected ReadOnlyListWrapper selectedNodes = new ReadOnlyListWrapper<>(FXCollections.observableArrayList()); + final protected ObservableList selectedNodes = FXCollections.observableArrayList(); private InvalidationListener invalidationListener = (Observable observable) -> { update(); }; - public ReadOnlyListProperty getSelectedNodes() { - return selectedNodes.getReadOnlyProperty(); + public ObservableList getSelectedNodes() { + return selectedNodes; } /** diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 55dda9bf19..6d671d1591 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -89,11 +89,11 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; * EventTypeMap, and dataSets is all linked directly to the ClusterChart which * must only be manipulated on the JavaFx thread. */ -public class DetailViewPane extends AbstractVisualizationPane, EventDetailsChart> { +public class DetailViewPane extends AbstractVisualizationPane { private final static Logger LOGGER = Logger.getLogger(DetailViewPane.class.getName()); - private MultipleSelectionModel>> treeSelectionModel; + private MultipleSelectionModel> treeSelectionModel; //these three could be injected from fxml but it was causing npe's private final DateAxis dateAxis = new DateAxis(); @@ -107,13 +107,12 @@ public class DetailViewPane extends AbstractVisualizationPane> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ObservableList highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - public ObservableList> getEventBundles() { + public ObservableList getEventBundles() { return chart.getEventBundles(); } - public DetailViewPane(TimeLineController controller, Pane partPane, Pane contextPane, Region spacer) { super(controller, partPane, contextPane, spacer); chart = new EventDetailsChart(controller, dateAxis, verticalAxis, selectedNodes); @@ -121,7 +120,6 @@ public class DetailViewPane extends AbstractVisualizationPane(new DetailViewSettingsPane().getChildrenUnmodifiable()); @@ -138,7 +136,7 @@ public class DetailViewPane extends AbstractVisualizationPane> change) -> { + highlightedNodes.addListener((ListChangeListener.Change change) -> { while (change.next()) { change.getAddedSubList().forEach(node -> { @@ -203,7 +201,7 @@ public class DetailViewPane extends AbstractVisualizationPane { - for (EventBundleNodeBase n : chart.getNodes((EventBundleNodeBase t) -> + for (EventStripeNode n : chart.getNodes((EventStripeNode t) -> t.getDescription().equals(tn.getDescription()))) { highlightedNodes.add(n); } @@ -216,14 +214,14 @@ public class DetailViewPane extends AbstractVisualizationPane>> selectionModel) { + public void setSelectionModel(MultipleSelectionModel> selectionModel) { this.treeSelectionModel = selectionModel; treeSelectionModel.getSelectedItems().addListener((Observable observable) -> { highlightedNodes.clear(); - for (TreeItem> tn : treeSelectionModel.getSelectedItems()) { + for (TreeItem tn : treeSelectionModel.getSelectedItems()) { - for (EventBundleNodeBase n : chart.getNodes((EventBundleNodeBase t) -> + for (EventStripeNode n : chart.getNodes((EventStripeNode t) -> t.getDescription().equals(tn.getValue().getDescription()))) { highlightedNodes.add(n); } @@ -348,7 +346,7 @@ public class DetailViewPane extends AbstractVisualizationPane c1, Boolean selected) { + protected void applySelectionEffect(EventStripeNode c1, Boolean selected) { c1.applySelectionEffect(selected); } @@ -472,5 +470,4 @@ public class DetailViewPane extends AbstractVisualizationPane sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.timeline.ui.detailview; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; -import java.util.stream.Collectors; -import javafx.beans.Observable; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; -import javafx.concurrent.Task; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.Tooltip; -import javafx.scene.effect.DropShadow; -import javafx.scene.effect.Effect; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -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 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.joda.time.DateTime; -import org.openide.util.NbBundle; -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.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; -import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.autopsy.timeline.ui.AbstractVisualizationPane; -import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.show; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TskCoreException; - -/** - * - */ -public abstract class EventBundleNodeBase, ParentType extends EventBundle, ParentNodeType extends EventBundleNodeBase> extends StackPane { - - private static final Logger LOGGER = Logger.getLogger(EventBundleNodeBase.class.getName()); - private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N - private static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS //NOI18N - - static final CornerRadii CORNER_RADII_3 = new CornerRadii(3); - static final CornerRadii CORNER_RADII_1 = new CornerRadii(1); - - private final Border SELECTION_BORDER; - private static final Map dropShadowMap = new ConcurrentHashMap<>(); - - static void configureLoDButton(Button b) { - b.setMinSize(16, 16); - b.setMaxSize(16, 16); - b.setPrefSize(16, 16); - show(b, false); - } - - static void show(Node b, boolean show) { - b.setVisible(show); - b.setManaged(show); - } - - protected final EventDetailsChart chart; - final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); - final SimpleObjectProperty descVisibility = new SimpleObjectProperty<>(DescriptionVisibility.SHOWN); - protected final BundleType eventBundle; - - protected final ParentNodeType parentNode; - - final SleuthkitCase sleuthkitCase; - final FilteredEventsModel eventsModel; - - final Background highlightedBackground; - final Background defaultBackground; - final Color evtColor; - - final List subNodes = new ArrayList<>(); - final Pane subNodePane = new Pane(); - final Label descrLabel = new Label(); - final Label countLabel = new Label(); - - final ImageView hashIV = new ImageView(HASH_PIN); - final ImageView tagIV = new ImageView(TAG); - final HBox infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV); - - private final Tooltip tooltip = new Tooltip("loading..."); - - public EventBundleNodeBase(EventDetailsChart chart, BundleType eventBundle, ParentNodeType parentNode) { - this.eventBundle = eventBundle; - this.parentNode = parentNode; - this.chart = chart; - - this.descLOD.set(eventBundle.getDescriptionLoD()); - sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); - eventsModel = chart.getController().getEventsModel(); - evtColor = getEventType().getColor(); - defaultBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII_3, Insets.EMPTY)); - highlightedBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1.1, 1.1, .3), CORNER_RADII_3, Insets.EMPTY)); - SELECTION_BORDER = new Border(new BorderStroke(evtColor.darker().desaturate(), BorderStrokeStyle.SOLID, CORNER_RADII_3, new BorderWidths(2))); - if (eventBundle.getEventIDsWithHashHits().isEmpty()) { - show(hashIV, false); - } - if (eventBundle.getEventIDsWithTags().isEmpty()) { - show(tagIV, false); - } - - setBackground(defaultBackground); - setAlignment(Pos.TOP_LEFT); - - setPrefHeight(USE_COMPUTED_SIZE); - heightProperty().addListener((Observable observable) -> { - chart.layoutPlotChildren(); - }); - setMaxHeight(USE_PREF_SIZE); - setMaxWidth(USE_PREF_SIZE); - setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation()); - - //initialize info hbox - infoHBox.setMinWidth(USE_PREF_SIZE); - infoHBox.setMaxWidth(USE_PREF_SIZE); - infoHBox.setPadding(new Insets(2, 5, 2, 5)); - infoHBox.setAlignment(Pos.TOP_LEFT); - - //set up subnode pane sizing contraints - subNodePane.setPrefHeight(USE_COMPUTED_SIZE); - subNodePane.setMaxHeight(USE_PREF_SIZE); - subNodePane.setPrefWidth(USE_COMPUTED_SIZE); - subNodePane.setMinWidth(USE_PREF_SIZE); - subNodePane.setMaxWidth(USE_PREF_SIZE); - - Tooltip.install(this, this.tooltip); - - //set up mouse hover effect and tooltip - setOnMouseEntered((MouseEvent e) -> { - /* - * defer tooltip content creation till needed, this had a - * surprisingly large impact on speed of loading the chart - */ - installTooltip(); - Tooltip.uninstall(chart, AbstractVisualizationPane.getDragTooltip()); - showHoverControls(true); - toFront(); - - }); - setOnMouseExited((MouseEvent event) -> { - showHoverControls(false); - if (parentNode != null) { - parentNode.showHoverControls(true); - } else { - Tooltip.install(chart, AbstractVisualizationPane.getDragTooltip()); - } - }); - - descVisibility.addListener((ObservableValue observable, DescriptionVisibility oldValue, DescriptionVisibility newValue) -> { - setDescriptionVisibiltiyImpl(newValue); - }); - setDescriptionVisibiltiyImpl(DescriptionVisibility.SHOWN); - - } - - final DescriptionLoD getDescriptionLoD() { - return descLOD.get(); - } - - public final BundleType getEventBundle() { - return eventBundle; - } - - final double getLayoutXCompensation() { - return parentNode != null - ? chart.getXAxis().getDisplayPosition(new DateTime(parentNode.getStartMillis())) - : 0; - } - - @NbBundle.Messages({"# {0} - counts", - "# {1} - event type", - "# {2} - description", - "# {3} - start date/time", - "# {4} - end date/time", - "EventBundleNodeBase.tooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand \t{4}"}) - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - private void installTooltip() { - if (tooltip.getText().equalsIgnoreCase("loading...")) { - final Task tooltTipTask = new Task() { - { - updateTitle("loading tooltip"); - } - - @Override - protected String call() throws Exception { - HashMap hashSetCounts = new HashMap<>(); - if (eventBundle.getEventIDsWithHashHits().isEmpty() == false) { - try { - //TODO:push this to DB - for (TimeLineEvent tle : eventsModel.getEventsById(eventBundle.getEventIDsWithHashHits())) { - Set hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames(); - for (String hashSetName : hashSetNames) { - hashSetCounts.merge(hashSetName, 1L, Long::sum); - } - } - } catch (TskCoreException ex) { - LOGGER.log(Level.SEVERE, "Error getting hashset hit info for event.", ex); - } - } - String hashSetCountsString = hashSetCounts.entrySet().stream() - .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) - .collect(Collectors.joining("\n")); - - Map tagCounts = new HashMap<>(); - if (eventBundle.getEventIDsWithTags().isEmpty() == false) { - tagCounts.putAll(eventsModel.getTagCountsByTagName(eventBundle.getEventIDsWithTags())); - } - String tagCountsString = tagCounts.entrySet().stream() - .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) - .collect(Collectors.joining("\n")); - - return Bundle.EventBundleNodeBase_tooltip_text(getEventIDs().size(), getEventType(), getDescription(), - TimeLineController.getZonedFormatter().print(getStartMillis()), - TimeLineController.getZonedFormatter().print(getEndMillis() + 1000)) - + (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString) - + (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString); - } - - @Override - protected void succeeded() { - super.succeeded(); - try { - tooltip.setText(get()); - tooltip.setGraphic(null); - } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, "Tooltip generation failed.", ex); - } - } - }; - - chart.getController().monitorTask(tooltTipTask); - } - } - - /** - * apply the 'effect' to visually indicate selection - * - * @param applied true to apply the selection 'effect', false to remove it - */ - public void applySelectionEffect(boolean applied) { - setBorder(applied ? SELECTION_BORDER : null); - } - - /** - * apply the 'effect' to visually indicate highlighted nodes - * - * @param applied true to apply the highlight 'effect', false to remove it - */ - abstract void applyHighlightEffect(boolean applied); - - @SuppressWarnings("unchecked") - public List getSubNodes() { - return subNodes; - } - - abstract void setDescriptionVisibiltiyImpl(DescriptionVisibility get); - - void showHoverControls(final boolean showControls) { - Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(), - eventType -> new DropShadow(-10, eventType.getColor())); - setEffect(showControls ? dropShadow : null); - enableTooltip(showControls); - if (parentNode != null) { - parentNode.enableTooltip(false); - parentNode.showHoverControls(false); - } - - } - - final EventType getEventType() { - return getEventBundle().getEventType(); - } - - final String getDescription() { - return getEventBundle().getDescription(); - } - - final long getStartMillis() { - return getEventBundle().getStartMillis(); - } - - final long getEndMillis() { - return getEventBundle().getEndMillis(); - } - - final Set getEventIDs() { - return getEventBundle().getEventIDs(); - } - - @Override - protected void layoutChildren() { - chart.layoutEventBundleNodes(subNodes, 0); - super.layoutChildren(); - } - - /** - * @param w the maximum width the description label should have - */ - abstract void setDescriptionWidth(double w); - - void setDescriptionVisibility(DescriptionVisibility get) { - descVisibility.set(get); - } - - void enableTooltip(boolean toolTipEnabled) { - if (toolTipEnabled) { - Tooltip.install(this, tooltip); - } else { - Tooltip.uninstall(this, tooltip); - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java deleted file mode 100644 index 838445948c..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013-15 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.timeline.ui.detailview; - -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import static java.util.Objects.nonNull; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; -import java.util.stream.Collectors; -import javafx.beans.binding.Bindings; -import javafx.concurrent.Task; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.geometry.Pos; -import javafx.scene.Cursor; -import javafx.scene.control.Button; -import javafx.scene.control.ContextMenu; -import javafx.scene.control.SeparatorMenuItem; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.Border; -import javafx.scene.layout.BorderStroke; -import javafx.scene.layout.BorderStrokeStyle; -import javafx.scene.layout.BorderWidths; -import javafx.scene.layout.VBox; -import org.controlsfx.control.action.Action; -import org.controlsfx.control.action.ActionUtils; -import org.joda.time.DateTime; -import org.joda.time.Interval; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; -import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; -import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; -import org.sleuthkit.autopsy.timeline.filters.RootFilter; -import org.sleuthkit.autopsy.timeline.filters.TypeFilter; -import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.configureLoDButton; -import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.show; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; -import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; -import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; - -/** - * - */ -final public class EventClusterNode extends EventBundleNodeBase { - - private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); - private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(2, 1, 2, 1); - private static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS //NOI18N - private static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS //NOI18N - private final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS)); - - final Button plusButton = ActionUtils.createButton(new ExpandClusterAction(), ActionUtils.ActionTextBehavior.HIDE); - final Button minusButton = ActionUtils.createButton(new CollapseClusterAction(), ActionUtils.ActionTextBehavior.HIDE); - - public EventClusterNode(EventDetailsChart chart, EventCluster eventCluster, EventStripeNode parentNode) { - super(chart, eventCluster, parentNode); - setMinHeight(24); - - subNodePane.setBorder(clusterBorder); - subNodePane.setBackground(defaultBackground); - subNodePane.setMaxHeight(USE_COMPUTED_SIZE); - subNodePane.setMaxWidth(USE_PREF_SIZE); - subNodePane.setMinWidth(1); - - setCursor(Cursor.HAND); - setOnMouseClicked(new MouseClickHandler()); - - configureLoDButton(plusButton); - configureLoDButton(minusButton); - - setAlignment(Pos.CENTER_LEFT); - infoHBox.getChildren().addAll(minusButton, plusButton); - getChildren().addAll(subNodePane, infoHBox); - - } - - @Override - void showHoverControls(final boolean showControls) { - super.showHoverControls(showControls); - show(plusButton, showControls); - show(minusButton, showControls); - } - - @Override - void applyHighlightEffect(boolean applied) { -// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - void setDescriptionWidth(double max) { -// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - void setDescriptionVisibiltiyImpl(DescriptionVisibility descrVis) { - final int size = getEventBundle().getEventIDs().size(); - switch (descrVis) { - case HIDDEN: - countLabel.setText(""); - descrLabel.setText(""); - break; - case COUNT_ONLY: - descrLabel.setText(""); - countLabel.setText(String.valueOf(size)); - break; - default: - case SHOWN: - countLabel.setText(String.valueOf(size)); - break; - } - } - - /** - * loads sub-bundles at the given Description LOD, continues - * - * @param requestedDescrLoD - * @param expand - */ - @NbBundle.Messages(value = "EventStripeNode.loggedTask.name=Load sub clusters") - private synchronized void loadSubBundles(DescriptionLoD.RelativeDetail relativeDetail) { - chart.setCursor(Cursor.WAIT); - chart.getEventBundles().removeIf(bundle -> - subNodes.stream().anyMatch(subNode -> - bundle.equals(subNode.getEventStripe())) - ); - subNodes.clear(); - - /* - * make new ZoomParams to query with - * - * We need to extend end time because for the query by one second, - * because it is treated as an open interval but we want to include - * events at exactly the time of the last event in this cluster - */ - final RootFilter subClusterFilter = getSubClusterFilter(); - final Interval subClusterSpan = new Interval(getStartMillis(), getEndMillis() + 1000); - final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get(); - final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD()); - - Task> loggedTask = new Task>() { - - private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); - - { - updateTitle(Bundle.EventStripeNode_loggedTask_name()); - } - - @Override - protected Collection call() throws Exception { - Collection bundles; - DescriptionLoD next = loadedDescriptionLoD; - do { - loadedDescriptionLoD = next; - if (loadedDescriptionLoD == getEventBundle().getDescriptionLoD()) { - return Collections.emptySet(); - } - bundles = eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD)).stream() - .collect(Collectors.toMap(EventCluster::getDescription, //key - (eventCluster) -> new EventStripe(eventCluster, getEventCluster()), //value - EventStripe::merge) //merge method - ).values(); - next = loadedDescriptionLoD.withRelativeDetail(relativeDetail); - } while (bundles.size() == 1 && nonNull(next)); - - // return list of AbstractEventStripeNodes representing sub-bundles - return bundles; - - } - - @Override - protected void succeeded() { - - try { - Collection bundles = get(); - - if (bundles.isEmpty()) { - subNodePane.getChildren().clear(); - getChildren().setAll(subNodePane, infoHBox); - descLOD.set(getEventBundle().getDescriptionLoD()); - } else { - chart.getEventBundles().addAll(bundles); - subNodes.addAll(bundles.stream() - .map(EventClusterNode.this::createStripeNode) - .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) - .collect(Collectors.toList())); - subNodePane.getChildren().setAll(subNodes); - getChildren().setAll(new VBox(infoHBox, subNodePane)); - descLOD.set(loadedDescriptionLoD); - } - } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); - } - chart.layoutPlotChildren(); - chart.setCursor(null); - } - }; - - //start task - chart.getController().monitorTask(loggedTask); - } - - private EventStripeNode createStripeNode(EventStripe stripe) { - return new EventStripeNode(chart, stripe, this); - } - - EventCluster getEventCluster() { - return getEventBundle(); - } - - @Override - protected void layoutChildren() { - double chartX = chart.getXAxis().getDisplayPosition(new DateTime(getStartMillis())); - double w = chart.getXAxis().getDisplayPosition(new DateTime(getEndMillis())) - chartX; - subNodePane.setPrefWidth(w); - subNodePane.setMinWidth(Math.max(1, w)); - super.layoutChildren(); - } - - /** - * make a new filter intersecting the global filter with description and - * type filters to restrict sub-clusters - * - */ - RootFilter getSubClusterFilter() { - RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf(); - subClusterFilter.getSubFilters().addAll( - new DescriptionFilter(getEventBundle().getDescriptionLoD(), getDescription(), DescriptionFilter.FilterMode.INCLUDE), - new TypeFilter(getEventType())); - return subClusterFilter; - } - - /** - * event handler used for mouse events on {@link EventStripeNode}s - */ - private class MouseClickHandler implements EventHandler { - - private ContextMenu contextMenu; - - @Override - public void handle(MouseEvent t) { - - if (t.getButton() == MouseButton.PRIMARY) { - - if (t.isShiftDown()) { - if (chart.selectedNodes.contains(EventClusterNode.this) == false) { - chart.selectedNodes.add(EventClusterNode.this); - } - } else if (t.isShortcutDown()) { - chart.selectedNodes.removeAll(EventClusterNode.this); - } else if (t.getClickCount() > 1) { - final DescriptionLoD next = descLOD.get().moreDetailed(); - if (next != null) { - loadSubBundles(DescriptionLoD.RelativeDetail.MORE); - } - } else { - chart.selectedNodes.setAll(EventClusterNode.this); - } - t.consume(); - } else if (t.getButton() == MouseButton.SECONDARY) { - ContextMenu chartContextMenu = chart.getChartContextMenu(t); - if (contextMenu == null) { - contextMenu = new ContextMenu(); - contextMenu.setAutoHide(true); - - contextMenu.getItems().add(ActionUtils.createMenuItem(new ExpandClusterAction())); - contextMenu.getItems().add(ActionUtils.createMenuItem(new CollapseClusterAction())); - - contextMenu.getItems().add(new SeparatorMenuItem()); - contextMenu.getItems().addAll(chartContextMenu.getItems()); - } - contextMenu.show(EventClusterNode.this, t.getScreenX(), t.getScreenY()); - t.consume(); - } - } - } - - private class ExpandClusterAction extends Action { - - @NbBundle.Messages(value = "ExpandClusterAction.text=Expand") - ExpandClusterAction() { - super(Bundle.ExpandClusterAction_text()); - - setGraphic(new ImageView(PLUS)); - setEventHandler((ActionEvent t) -> { - final DescriptionLoD next = descLOD.get().moreDetailed(); - if (next != null) { - loadSubBundles(DescriptionLoD.RelativeDetail.MORE); - } - }); - disabledProperty().bind(descLOD.isEqualTo(DescriptionLoD.FULL)); - } - } - - private class CollapseClusterAction extends Action { - - @NbBundle.Messages(value = "CollapseClusterAction.text=Collapse") - CollapseClusterAction() { - super(Bundle.CollapseClusterAction_text()); - - setGraphic(new ImageView(MINUS)); - setEventHandler((ActionEvent t) -> { - final DescriptionLoD previous = descLOD.get().lessDetailed(); - if (previous != null) { - loadSubBundles(DescriptionLoD.RelativeDetail.LESS); - } - }); - disabledProperty().bind(Bindings.createBooleanBinding(() -> nonNull(getEventCluster()) && descLOD.get() == getEventCluster().getDescriptionLoD(), descLOD)); - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java index 093d9e0137..2ca1243563 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailsChart.java @@ -19,12 +19,13 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import com.google.common.collect.Range; -import com.google.common.collect.TreeRangeMap; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.MissingResourceException; @@ -37,6 +38,7 @@ import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.InvalidationListener; import javafx.beans.Observable; +import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.beans.property.SimpleBooleanProperty; @@ -61,6 +63,7 @@ import javafx.scene.input.MouseEvent; import javafx.scene.shape.Line; import javafx.scene.shape.StrokeLineCap; import javafx.util.Duration; +import javax.annotation.concurrent.GuardedBy; import org.apache.commons.lang3.tuple.ImmutablePair; import org.controlsfx.control.action.Action; import org.controlsfx.control.action.ActionUtils; @@ -96,12 +99,12 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; */ public final class EventDetailsChart extends XYChart implements TimeLineChart { - private static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS - private static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS + static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS + static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); private static final int PROJECTED_LINE_Y_OFFSET = 5; private static final int PROJECTED_LINE_STROKE_WIDTH = 5; - private static final int MINIMUM_EVENT_NODE_GAP = 4; + private static final int DEFAULT_ROW_HEIGHT = 24; private final TimeLineController controller; @@ -116,7 +119,7 @@ public final class EventDetailsChart extends XYChart imp } /** - * a user positionable vertical line to help compare events + * a user position-able vertical line to help the compare events */ private Line guideLine; @@ -128,27 +131,35 @@ public final class EventDetailsChart extends XYChart imp private IntervalSelector intervalSelector; /** - * listener that triggers chart layout pass + * listener that triggers layout pass */ private final InvalidationListener layoutInvalidationListener = (Observable o) -> { - layoutPlotChildren(); + synchronized (EventDetailsChart.this) { + requiresLayout = true; + requestChartLayout(); + } }; /** * the maximum y value used so far during the most recent layout pass */ private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); + /** + * flag indicating whether this chart actually needs a layout pass + */ + @GuardedBy(value = "this") + private boolean requiresLayout = true; - final ObservableList> selectedNodes; + final ObservableList selectedNodes; /** * the group that all event nodes are added to. This facilitates scrolling * by allowing a single translation of this group. */ private final Group nodeGroup = new Group(); - private final ObservableList> bundles = FXCollections.observableArrayList(); + private final ObservableList bundles = FXCollections.observableArrayList(); private final Map, EventStripe> stripeDescMap = new HashMap<>(); private final Map stripeNodeMap = new HashMap<>(); - private final Map projectionMap = new HashMap<>(); + private final Map, Line> projectionMap = new HashMap<>(); /** * list of series of data added to this chart @@ -158,6 +169,11 @@ public final class EventDetailsChart extends XYChart imp private final ObservableList> seriesList = FXCollections.>observableArrayList(); + private final ObservableList> sortedSeriesList = seriesList + .sorted((s1, s2) -> { + final List collect = EventType.allTypes.stream().map(EventType::getDisplayName).collect(Collectors.toList()); + return Integer.compare(collect.indexOf(s1.getName()), collect.indexOf(s2.getName())); + }); /** * true == layout each event type in its own band, false == mix all the * events together during layout @@ -182,15 +198,16 @@ public final class EventDetailsChart extends XYChart imp * the labels, alow them to extend past the timespan indicator and off the * edge of the screen */ - final SimpleBooleanProperty truncateAll = new SimpleBooleanProperty(false); + private final SimpleBooleanProperty truncateAll = new SimpleBooleanProperty(false); /** * the width to truncate all labels to if truncateAll is true. adjustable * via slider if truncateAll is true */ - final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); + private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); + private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true); - EventDetailsChart(TimeLineController controller, DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes) { + EventDetailsChart(TimeLineController controller, DateAxis dateAxis, final Axis verticalAxis, ObservableList selectedNodes) { super(dateAxis, verticalAxis); this.controller = controller; this.filteredEvents = this.controller.getEventsModel(); @@ -208,10 +225,11 @@ public final class EventDetailsChart extends XYChart imp dateAxis.setAutoRanging(false); verticalAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm + verticalAxis.setTickLabelsVisible(false); verticalAxis.setTickMarkVisible(false); - setLegendVisible(false); + setLegendVisible(false); setPadding(Insets.EMPTY); setAlternativeColumnFillVisible(true); @@ -219,6 +237,8 @@ public final class EventDetailsChart extends XYChart imp getPlotChildren().add(nodeGroup); //add listener for events that should trigger layout + widthProperty().addListener(layoutInvalidationListener); + heightProperty().addListener(layoutInvalidationListener); bandByType.addListener(layoutInvalidationListener); oneEventPerRow.addListener(layoutInvalidationListener); truncateAll.addListener(layoutInvalidationListener); @@ -240,10 +260,42 @@ public final class EventDetailsChart extends XYChart imp setOnMouseClicked(new MouseClickedHandler<>(this)); this.selectedNodes = selectedNodes; - this.selectedNodes.addListener(new SelectionChangeHandler()); + this.selectedNodes.addListener(( + ListChangeListener.Change c) -> { + while (c.next()) { + c.getRemoved().forEach((EventStripeNode t) -> { + t.getEventStripe().getRanges().forEach((Range t1) -> { + + Line removedLine = projectionMap.remove(t1); + getChartChildren().removeAll(removedLine); + }); + + }); + c.getAddedSubList().forEach((EventStripeNode t) -> { + + for (Range range : t.getEventStripe().getRanges()) { + + 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(range, line); + getChartChildren().add(line); + } + }); + } + + this.controller.selectEventIDs(selectedNodes.stream() + .flatMap(detailNode -> detailNode.getEventsIDs().stream()) + .collect(Collectors.toList())); + }); + + requestChartLayout(); } - ObservableList> getEventBundles() { + ObservableList getEventBundles() { return bundles; } @@ -329,7 +381,7 @@ public final class EventDetailsChart extends XYChart imp final EventCluster eventCluster = data.getYValue(); bundles.add(eventCluster); EventStripe eventStripe = stripeDescMap.merge(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()), - new EventStripe(eventCluster, null), + new EventStripe(eventCluster), (EventStripe u, EventStripe v) -> { EventStripeNode remove = stripeNodeMap.remove(u); nodeGroup.getChildren().remove(remove); @@ -342,7 +394,7 @@ public final class EventDetailsChart extends XYChart imp stripeNodeMap.put(eventStripe, stripeNode); nodeGroup.getChildren().add(stripeNode); data.setNode(stripeNode); - layoutPlotChildren(); + } @Override @@ -358,32 +410,103 @@ public final class EventDetailsChart extends XYChart imp EventStripe removedStripe = stripeDescMap.remove(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription())); EventStripeNode removedNode = stripeNodeMap.remove(removedStripe); nodeGroup.getChildren().remove(removedNode); + data.setNode(null); - layoutPlotChildren(); + } + + synchronized void setRequiresLayout(boolean b) { + requiresLayout = true; + } + + /** + * make this accessible to {@link EventStripeNode} + */ + @Override + protected void requestChartLayout() { + super.requestChartLayout(); } @Override - protected synchronized void layoutPlotChildren() { - setCursor(Cursor.WAIT); - maxY.set(0); - if (bandByType.get()) { - stripeNodeMap.values().stream() - .collect(Collectors.groupingBy(EventStripeNode::getEventType)).values() - .forEach(inputNodes -> { - List stripeNodes = inputNodes.stream() - .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) - .collect(Collectors.toList()); + protected void layoutChildren() { + super.layoutChildren(); - maxY.set(layoutEventBundleNodes(stripeNodes, maxY.get())); - }); - } else { - List stripeNodes = stripeNodeMap.values().stream() - .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) - .collect(Collectors.toList()); - maxY.set(layoutEventBundleNodes(stripeNodes, 0)); + } + + /** + * Layout the nodes representing events via the following algorithm. + * + * we start with a list of nodes (each representing an event) - sort the + * list of nodes by span start time of the underlying event - initialize + * empty map (maxXatY) from y-position to max used x-value - for each node: + * -- autosize the node (based on text label) -- get the event's start and + * end positions from the dateaxis -- size the capsule representing event + * duration -- starting from the top of the chart: --- (1)check if maxXatY + * is to the left of the start position: -------if maxXatY less than start + * position , good, put the current node here, mark end position as maxXatY, + * go to next node -------if maxXatY greater than start position, increment + * y position, do -------------check(1) again until maxXatY less than start + * position + */ + @Override + protected synchronized void layoutPlotChildren() { + if (requiresLayout) { + setCursor(Cursor.WAIT); + + maxY.set(0.0); + + Map> hiddenPartition; + if (bandByType.get()) { + double minY = 0; + for (Series series : sortedSeriesList) { + hiddenPartition = series.getData().stream().map(Data::getNode).map(EventStripeNode.class::cast) + .collect(Collectors.partitioningBy(node -> getController().getQuickHideFilters().stream() + .filter(AbstractFilter::isActive) + .anyMatch(filter -> filter.getDescription().equals(node.getDescription())))); + + layoutNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), minY, 0); + minY = maxY.get(); + } + } else { + hiddenPartition = stripeNodeMap.values().stream() + .collect(Collectors.partitioningBy(node -> getController().getQuickHideFilters().stream() + .filter(AbstractFilter::isActive) + .anyMatch(filter -> filter.getDescription().equals(node.getDescription())))); + layoutNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), 0, 0); + } + setCursor(null); + requiresLayout = false; } layoutProjectionMap(); - setCursor(null); + } + + /** + * + * @param hiddenNodes the value of hiddenNodes + * @param shownNodes the value of shownNodes + * @param minY the value of minY + * @param children the value of children + * @param xOffset the value of xOffset + * + * @return the double + */ + private double layoutNodesHelper(List hiddenNodes, List shownNodes, double minY, final double xOffset) { + + hiddenNodes.forEach((EventStripeNode t) -> { +// children.remove(t); + t.setVisible(false); + t.setManaged(false); + }); + + shownNodes.forEach((EventStripeNode t) -> { +// if (false == children.contains(t)) { +// children.add(t); +// } + t.setVisible(true); + t.setManaged(true); + }); + + shownNodes.sort(Comparator.comparing(EventStripeNode::getStartMillis)); + return layoutNodes(shownNodes, minY, xOffset); } @Override @@ -392,6 +515,7 @@ public final class EventDetailsChart extends XYChart imp dataItemAdded(series, j, series.getData().get(j)); } seriesList.add(series); + requiresLayout = true; } @Override @@ -400,21 +524,18 @@ public final class EventDetailsChart extends XYChart imp dataItemRemoved(series.getData().get(j), series); } seriesList.remove(series); + requiresLayout = true; } ReadOnlyDoubleProperty maxVScrollProperty() { return maxY.getReadOnlyProperty(); } - /** - * @return all the nodes that pass the given predicate - */ - Iterable> getNodes(Predicate> p) { - //use this recursive function to flatten the tree of nodes into an iterable. - Function, Stream>> stripeFlattener = - new Function, Stream>>() { + Iterable getNodes(Predicate p) { + Function> flattener = + new Function>() { @Override - public Stream> apply(EventBundleNodeBase node) { + public Stream apply(EventStripeNode node) { return Stream.concat( Stream.of(node), node.getSubNodes().stream().flatMap(this::apply)); @@ -422,11 +543,11 @@ public final class EventDetailsChart extends XYChart imp }; return stripeNodeMap.values().stream() - .flatMap(stripeFlattener) + .flatMap(flattener) .filter(p).collect(Collectors.toList()); } - Iterable> getAllNodes() { + Iterable getAllNodes() { return getNodes(x -> true); } @@ -444,114 +565,132 @@ public final class EventDetailsChart extends XYChart imp * layout the nodes in the given list, starting form the given minimum y * coordinate. * - * Layout the nodes representing events via the following algorithm. - * - * we start with a list of nodes (each representing an event) - sort the - * list of nodes by span start time of the underlying event - initialize - * empty map (maxXatY) from y-position to max used x-value - for each node: - * - * -- size the node based on its children (recursively) - * - * -- get the event's start position from the dateaxis - * - * -- to position node (1)check if maxXatY is to the left of the left x - * coord: if maxXatY is less than the left x coord, good, put the current - * node here, mark right x coord as maxXatY, go to next node ; if maxXatY - * greater than start position, increment y position, do check(1) again - * until maxXatY less than start position - * - * @param nodes collection of nodes to layout - * @param minY the minimum y coordinate to position the nodes at. + * @param nodes + * @param minY */ - synchronized double layoutEventBundleNodes(final Collection> nodes, final double minY) { - // map from y value (ranges) to right most occupied x value. - TreeRangeMap treeRangeMap = TreeRangeMap.create(); - // maximum y values occupied by any of the given nodes, updated as nodes are layed out. + private synchronized double layoutNodes(final Collection< EventStripeNode> 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 maxXatY = new HashMap<>(); double localMax = minY; + //for each node lay size it and position it in first available slot - //for each node do a recursive layout to size it and then position it in first available slot - for (final EventBundleNodeBase bundleNode : nodes) { - //is the node hiden by a quick hide filter? - boolean quickHide = getController().getQuickHideFilters().stream() - .filter(AbstractFilter::isActive) - .anyMatch(filter -> filter.getDescription().equals(bundleNode.getDescription())); - if (quickHide) { - //hide it and skip layout - bundleNode.setVisible(false); - bundleNode.setManaged(false); - } else { - //make sure it is shown - bundleNode.setVisible(true); - bundleNode.setManaged(true); - //apply advanced layout description visibility options - bundleNode.setDescriptionVisibility(descrVisibility.get()); - bundleNode.setDescriptionWidth(truncateAll.get() ? truncateWidth.get() : USE_PREF_SIZE); + for (EventStripeNode stripeNode : nodes) { - //do recursive layout - bundleNode.layout(); - //get computed height and width - double h = bundleNode.getBoundsInLocal().getHeight(); - double w = bundleNode.getBoundsInLocal().getWidth(); - //get left and right x coords from axis plus computed width - double xLeft = getXForEpochMillis(bundleNode.getStartMillis()) - bundleNode.getLayoutXCompensation(); - double xRight = xLeft + w; + stripeNode.setDescriptionVisibility(descrVisibility.get()); + double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis())); - //initial test position - double yTop = minY; - double yBottom = yTop + h; + //position of start and end according to range of axis + double startX = rawDisplayPosition - xOffset; + double layoutNodesResultHeight = 0; - if (oneEventPerRow.get()) { - // if onePerRow, just put it at end - yTop = (localMax + MINIMUM_EVENT_NODE_GAP); - yBottom = yTop + h; - } else { - //until the node is not overlapping any others try moving it down. - boolean overlapping = true; - while (overlapping) { - overlapping = false; - //check each pixel from bottom to top. - for (double y = yBottom; y >= yTop; y--) { - final Double maxX = treeRangeMap.get(y); - if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) { - //if that pixel is already used - //jump top to this y value and repeat until free slot is found. - overlapping = true; - yTop = y + MINIMUM_EVENT_NODE_GAP; - yBottom = yTop + h; - break; - } - } + double span = 0; + + List subNodes = stripeNode.getSubNodes(); + if (subNodes.isEmpty() == false) { + Map> hiddenPartition = subNodes.stream() + .collect(Collectors.partitioningBy(testNode -> getController().getQuickHideFilters().stream() + .filter(AbstractFilter::isActive) + .anyMatch(filter -> filter.getDescription().equals(testNode.getDescription())))); + stripeNode.setDescriptionVisibility(descrVisibility.get()); + + layoutNodesResultHeight = layoutNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), minY, rawDisplayPosition); + } + + List spanWidths = new ArrayList<>(); + double x = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));; + double x2; + Iterator> ranges = stripeNode.getEventStripe().getRanges().iterator(); + Range range = ranges.next(); + do { + x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint())); + double clusterSpan = x2 - x; + span += clusterSpan; + spanWidths.add(clusterSpan); + if (ranges.hasNext()) { + range = ranges.next(); + x = getXAxis().getDisplayPosition(new DateTime(range.lowerEndpoint())); + 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); } - treeRangeMap.put(Range.closed(yTop, yBottom), xRight); } - localMax = Math.max(yBottom, localMax); + } while (ranges.hasNext()); - //animate node to new position - Timeline timeline = new Timeline(new KeyFrame(Duration.millis(100), - new KeyValue(bundleNode.layoutXProperty(), xLeft), - new KeyValue(bundleNode.layoutYProperty(), yTop))); - timeline.setOnFinished((ActionEvent event) -> { - requestChartLayout(); - }); - timeline.play(); + stripeNode.setSpanWidths(spanWidths); + + if (truncateAll.get()) { //if truncate option is selected limit width of description label + stripeNode.setDescriptionWidth(Math.max(span, truncateWidth.get())); + } else { //else set it unbounded + stripeNode.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); } - } - return localMax; //return new max - } - private double getXForEpochMillis(Long millis) { - DateTime dateTime = new DateTime(millis, TimeLineController.getJodaTimeZone()); - return getXAxis().getDisplayPosition(new DateTime(dateTime)); + stripeNode.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 + stripeNode.getWidth(); + + //get the height of the node + final double h = layoutNodesResultHeight == 0 ? stripeNode.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT; + //initial test position + double yPos = minY; + + double yPos2 = yPos + h; + + if (oneEventPerRow.get()) { + // if onePerRow, just put it at end + yPos = (localMax + 2); + yPos2 = yPos + h; + + } else {//else + + boolean overlapping = true; + while (overlapping) { + //loop through y values looking for available slot. + + overlapping = false; + //check each pixel from bottom to top. + for (double y = yPos2; y >= yPos; y--) { + final Double maxX = maxXatY.get((int) y); + if (maxX != null && maxX >= startX - 4) { + //if that pixel is already used + //jump top to this y value and repeat until free slot is found. + overlapping = true; + yPos = y + 4; + yPos2 = yPos + h; + break; + } + } + } + //mark used y values + for (double y = yPos; y <= yPos2; y++) { + maxXatY.put((int) y, xRight); + } + } + localMax = Math.max(yPos2, localMax); + + Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0), + new KeyValue(stripeNode.layoutXProperty(), startX), + new KeyValue(stripeNode.layoutYProperty(), yPos))); + + tm.play(); + } + maxY.set(Math.max(maxY.get(), localMax)); + return localMax - minY; } private void layoutProjectionMap() { - for (final Map.Entry entry : projectionMap.entrySet()) { - final EventCluster cluster = entry.getKey(); + for (final Map.Entry, Line> entry : projectionMap.entrySet()) { + final Range range = entry.getKey(); final Line line = entry.getValue(); - line.setStartX(getParentXForEpochMillis(cluster.getStartMillis())); - line.setEndX(getParentXForEpochMillis(cluster.getEndMillis())); + line.setStartX(getParentXForEpochMillis(range.lowerEndpoint())); + line.setEndX(getParentXForEpochMillis(range.upperEndpoint())); line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); @@ -570,6 +709,10 @@ public final class EventDetailsChart extends XYChart imp return filteredEvents; } + Property alternateLayoutProperty() { + return alternateLayout; + } + static private class DetailIntervalSelector extends IntervalSelector { DetailIntervalSelector(EventDetailsChart chart) { @@ -618,45 +761,6 @@ public final class EventDetailsChart extends XYChart imp } } - private class SelectionChangeHandler implements ListChangeListener> { - - private final Axis dateAxis; - - SelectionChangeHandler() { - dateAxis = getXAxis(); - } - - @Override - public void onChanged(ListChangeListener.Change> change) { - while (change.next()) { - change.getRemoved().forEach((EventBundleNodeBase removedNode) -> { - removedNode.getEventBundle().getClusters().forEach(cluster -> { - Line removedLine = projectionMap.remove(cluster); - getChartChildren().removeAll(removedLine); - }); - - }); - change.getAddedSubList().forEach((EventBundleNodeBase addedNode) -> { - - for (EventCluster range : addedNode.getEventBundle().getClusters()) { - - Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, - dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET - ); - line.setStroke(addedNode.getEventType().getColor().deriveColor(0, 1, 1, .5)); - line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH); - line.setStrokeLineCap(StrokeLineCap.ROUND); - projectionMap.put(range, line); - getChartChildren().add(line); - } - }); - } - EventDetailsChart.this.controller.selectEventIDs(selectedNodes.stream() - .flatMap(detailNode -> detailNode.getEventIDs().stream()) - .collect(Collectors.toList())); - } - } - class HideDescriptionAction extends Action { HideDescriptionAction(String description, DescriptionLoD descriptionLoD) { @@ -672,13 +776,12 @@ public final class EventDetailsChart extends XYChart imp DescriptionFilter descriptionFilter = getController().getQuickHideFilters().stream() .filter(testFilter::equals) .findFirst().orElseGet(() -> { - testFilter.selectedProperty().addListener((Observable observable) -> { - layoutPlotChildren(); - }); + testFilter.selectedProperty().addListener(layoutInvalidationListener); getController().getQuickHideFilters().add(testFilter); return testFilter; }); descriptionFilter.setSelected(true); + }); } } @@ -686,6 +789,7 @@ public final class EventDetailsChart extends XYChart imp class UnhideDescriptionAction extends Action { UnhideDescriptionAction(String description, DescriptionLoD descriptionLoD) { + super("Unhide"); setGraphic(new ImageView(SHOW)); setEventHandler((ActionEvent t) -> diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index db3a0456f2..824eb4e9ea 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -19,30 +19,107 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; +import com.google.common.collect.Range; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import static java.util.Objects.nonNull; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleObjectProperty; +import javafx.concurrent.Task; +import javafx.event.ActionEvent; 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.ContextMenu; -import javafx.scene.control.MenuItem; +import javafx.scene.control.Label; import javafx.scene.control.OverrunStyle; +import javafx.scene.control.SeparatorMenuItem; +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.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.Region; +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.controlsfx.control.action.Action; import org.controlsfx.control.action.ActionUtils; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; -import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.configureLoDButton; +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.DescriptionFilter; +import org.sleuthkit.autopsy.timeline.filters.RootFilter; +import org.sleuthkit.autopsy.timeline.filters.TypeFilter; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; +import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; /** * Node used in {@link EventDetailsChart} to represent an EventStripe. */ -final public class EventStripeNode extends EventBundleNodeBase { +final public class EventStripeNode extends StackPane { private static final Logger LOGGER = Logger.getLogger(EventStripeNode.class.getName()); - final Button hideButton; + private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N + private static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS //NOI18N + private static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS //NOI18N + private static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS //NOI18N + + private static final CornerRadii CORNER_RADII_3 = new CornerRadii(3); + private static final CornerRadii CORNER_RADII_1 = new CornerRadii(1); + private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(2, 1, 2, 1); + private final static Map dropShadowMap = new ConcurrentHashMap<>(); + private static final Border SELECTION_BORDER = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII_3, new BorderWidths(2))); + + static void configureLoDButton(Button b) { + b.setMinSize(16, 16); + b.setMaxSize(16, 16); + b.setPrefSize(16, 16); + show(b, false); + } + + static void show(Node b, boolean show) { + b.setVisible(show); + b.setManaged(show); + } + + private final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); + private DescriptionVisibility descrVis; + private Tooltip tooltip; + /** * Pane that contains EventStripeNodes for any 'subevents' if they are * displayed @@ -50,60 +127,267 @@ final public class EventStripeNode extends EventBundleNodeBase range : eventStripe.getRanges()) { + Region clusterRegion = new Region(); + clusterRegion.setBorder(clusterBorder); + clusterRegion.setBackground(highlightedBackground); + clustersHBox.getChildren().addAll(clusterRegion, new Region()); } + clustersHBox.getChildren().remove(clustersHBox.getChildren().size() - 1); + clustersHBox.setMaxWidth(USE_PREF_SIZE); - getChildren().addAll(new VBox(infoHBox, subNodePane)); + final VBox internalVBox = new VBox(infoHBox, subNodePane); + internalVBox.setAlignment(Pos.CENTER_LEFT); + getChildren().addAll(clustersHBox, internalVBox); + + setCursor(Cursor.HAND); setOnMouseClicked(new MouseClickHandler()); + + //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); + }); } - @Override - void showHoverControls(final boolean showControls) { - super.showHoverControls(showControls); + void showDescriptionLoDControls(final boolean showControls) { + DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(), + eventType -> new DropShadow(10, eventType.getColor())); + clustersHBox.setEffect(showControls ? dropShadow : null); + show(minusButton, showControls); + show(plusButton, showControls); show(hideButton, showControls); } + public void setSpanWidths(List spanWidths) { + for (int i = 0; i < spanWidths.size(); i++) { + Region spanRegion = (Region) clustersHBox.getChildren().get(i); + + Double w = spanWidths.get(i); + spanRegion.setPrefWidth(w); + spanRegion.setMaxWidth(w); + spanRegion.setMinWidth(Math.max(2, w)); + } + } + public EventStripe getEventStripe() { - return getEventBundle(); + return eventStripe; + } + + Collection makeBundlesFromClusters(List eventClusters) { + return eventClusters.stream().collect( + Collectors.toMap( + EventCluster::getDescription, //key + EventStripe::new, //value + EventStripe::merge)//merge method + ).values(); + } + + @NbBundle.Messages({"# {0} - counts", + "# {1} - event type", + "# {2} - description", + "# {3} - start date/time", + "# {4} - end date/time", + "EventStripeNode.tooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand \t{4}"}) + synchronized void installTooltip() { + if (tooltip == null) { + final Task tooltTipTask = new Task() { + + @Override + protected String call() throws Exception { + HashMap hashSetCounts = new HashMap<>(); + if (!eventStripe.getEventIDsWithHashHits().isEmpty()) { + hashSetCounts = new HashMap<>(); + try { + for (TimeLineEvent tle : eventsModel.getEventsById(eventStripe.getEventIDsWithHashHits())) { + Set hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames(); + for (String hashSetName : hashSetNames) { + hashSetCounts.merge(hashSetName, 1L, Long::sum); + } + } + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Error getting hashset hit info for event.", ex); + } + } + + Map tagCounts = new HashMap<>(); + if (getEventStripe().getEventIDsWithTags().isEmpty() == false) { + tagCounts.putAll(eventsModel.getTagCountsByTagName(getEventStripe().getEventIDsWithTags())); + } + + String hashSetCountsString = hashSetCounts.entrySet().stream() + .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) + .collect(Collectors.joining("\n")); + String tagCountsString = tagCounts.entrySet().stream() + .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) + .collect(Collectors.joining("\n")); + return Bundle.EventStripeNode_tooltip_text(getEventStripe().getEventIDs().size(), getEventStripe().getEventType(), getEventStripe().getDescription(), + TimeLineController.getZonedFormatter().print(getEventStripe().getStartMillis()), + TimeLineController.getZonedFormatter().print(getEventStripe().getEndMillis() + 1000)) + + (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString) + + (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString); + } + + @Override + protected void succeeded() { + super.succeeded(); + try { + tooltip = new Tooltip(get()); + Tooltip.install(EventStripeNode.this, tooltip); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "Tooltip generation failed.", ex); + Tooltip.uninstall(EventStripeNode.this, tooltip); + tooltip = null; + } + } + }; + + chart.getController().monitorTask(tooltTipTask); + } + } + + EventStripeNode getNodeForBundle(EventStripe cluster) { + return new EventStripeNode(chart, cluster, this); + } + + EventType getEventType() { + return eventStripe.getEventType(); + } + + String getDescription() { + return eventStripe.getDescription(); + } + + long getStartMillis() { + return eventStripe.getStartMillis(); + } + + @SuppressWarnings("unchecked") + public List getSubNodes() { + return subNodePane.getChildrenUnmodifiable().stream() + .map(t -> (EventStripeNode) t) + .collect(Collectors.toList()); + } + + /** + * make a new filter intersecting the global filter with description and + * type filters to restrict sub-clusters + * + */ + RootFilter getSubClusterFilter() { + RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf(); + subClusterFilter.getSubFilters().addAll( + new DescriptionFilter(eventStripe.getDescriptionLoD(), eventStripe.getDescription(), DescriptionFilter.FilterMode.INCLUDE), + new TypeFilter(getEventType())); + return subClusterFilter; } /** * @param w the maximum width the description label should have */ - @Override public void setDescriptionWidth(double w) { descrLabel.setMaxWidth(w); } + /** + * apply the 'effect' to visually indicate selection + * + * @param applied true to apply the selection 'effect', false to remove it + */ + public void applySelectionEffect(boolean applied) { + setBorder(applied ? SELECTION_BORDER : 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 @@ -114,11 +398,116 @@ final public class EventStripeNode extends EventBundleNodeBase + getSubNodes().stream().anyMatch(subNode -> + bundle.equals(subNode.getEventStripe())) + ); + subNodePane.getChildren().clear(); + if (descLOD.get().withRelativeDetail(relativeDetail) == eventStripe.getDescriptionLoD()) { + descLOD.set(eventStripe.getDescriptionLoD()); + clustersHBox.setVisible(true); + chart.setRequiresLayout(true); + chart.requestChartLayout(); + } else { + clustersHBox.setVisible(false); + + // make new ZoomParams to query with + final RootFilter subClusterFilter = getSubClusterFilter(); + /* + * We need to extend end time because for the query by one second, + * because it is treated as an open interval but we want to include + * events at exactly the time of the last event in this cluster + */ + final Interval subClusterSpan = new Interval(eventStripe.getStartMillis(), eventStripe.getEndMillis() + 1000); + final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get(); + final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD()); + + Task> loggedTask = new Task>() { + + private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); + + { + updateTitle(Bundle.EventStripeNode_loggedTask_name()); + } + + @Override + protected Collection call() throws Exception { + Collection bundles; + DescriptionLoD next = loadedDescriptionLoD; + do { + loadedDescriptionLoD = next; + if (loadedDescriptionLoD == eventStripe.getDescriptionLoD()) { + return Collections.emptySet(); + } + bundles = eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD)).stream() + .map(cluster -> cluster.withParent(getEventStripe())) + .collect(Collectors.toMap( + EventCluster::getDescription, //key + EventStripe::new, //value + EventStripe::merge) //merge method + ).values(); + next = loadedDescriptionLoD.withRelativeDetail(relativeDetail); + } while (bundles.size() == 1 && nonNull(next)); + + // return list of AbstractEventStripeNodes representing sub-bundles + return bundles; + + } + + @Override + protected void succeeded() { + chart.setCursor(Cursor.WAIT); + try { + Collection bundles = get(); + + if (bundles.isEmpty()) { + clustersHBox.setVisible(true); + } else { + clustersHBox.setVisible(false); + chart.getEventBundles().addAll(bundles); + subNodePane.getChildren().setAll(bundles.stream() + .map(EventStripeNode.this::getNodeForBundle) + .collect(Collectors.toSet())); + } + descLOD.set(loadedDescriptionLoD); + //assign subNodes and request chart layout + + chart.setRequiresLayout(true); + chart.requestChartLayout(); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); + } + chart.setCursor(null); + } + }; + +//start task + chart.getController().monitorTask(loggedTask); + } + } + + private double getLayoutXCompensation() { + return (parentNode != null ? parentNode.getLayoutXCompensation() : 0) + + getBoundsInParent().getMinX(); + } + + public void setDescriptionVisibility(DescriptionVisibility descrVis){ + this.descrVis = descrVis; + final int size = eventStripe.getEventIDs().size(); + + switch (this.descrVis) { case HIDDEN: countLabel.setText(""); descrLabel.setText(""); @@ -129,7 +518,7 @@ final public class EventStripeNode extends EventBundleNodeBase getEventsIDs() { + return eventStripe.getEventIDs(); + } + /** * event handler used for mouse events on {@link EventStripeNode}s */ @@ -150,13 +543,19 @@ final public class EventStripeNode extends EventBundleNodeBase 1) { + final DescriptionLoD next = descLOD.get().moreDetailed(); + if (next != null) { + loadSubBundles(DescriptionLoD.RelativeDetail.MORE); + + } } else { chart.selectedNodes.setAll(EventStripeNode.this); } @@ -167,9 +566,10 @@ final public class EventStripeNode extends EventBundleNodeBase { + final DescriptionLoD next = descLOD.get().moreDetailed(); + if (next != null) { + loadSubBundles(DescriptionLoD.RelativeDetail.MORE); + + } + }); + disabledProperty().bind(descLOD.isEqualTo(DescriptionLoD.FULL)); + } + } + + private class CollapseClusterAction extends Action { + + @NbBundle.Messages("CollapseClusterAction.text=Collapse") + CollapseClusterAction() { + super(Bundle.CollapseClusterAction_text()); + + setGraphic(new ImageView(MINUS)); + setEventHandler((ActionEvent t) -> { + final DescriptionLoD previous = descLOD.get().lessDetailed(); + if (previous != null) { + loadSubBundles(DescriptionLoD.RelativeDetail.LESS); + } + }); + disabledProperty().bind(Bindings.createBooleanBinding(() -> nonNull(eventStripe) && descLOD.get() == eventStripe.getDescriptionLoD(), descLOD)); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java index 6feef28f8f..b3e80eb827 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java @@ -35,13 +35,13 @@ class EventDescriptionTreeItem extends NavTreeItem { * maps a description to the child item of this item with that description */ private final Map childMap = new ConcurrentHashMap<>(); - private final EventBundle bundle; + private final EventBundle bundle; - public EventBundle getEventBundle() { + public EventBundle getEventBundle() { return bundle; } - EventDescriptionTreeItem(EventBundle g) { + EventDescriptionTreeItem(EventBundle g) { bundle = g; setValue(g); } @@ -51,8 +51,8 @@ class EventDescriptionTreeItem extends NavTreeItem { return getValue().getCount(); } - public void insert(Deque> path) { - EventBundle head = path.removeFirst(); + public void insert(Deque path) { + EventBundle head = path.removeFirst(); EventDescriptionTreeItem treeItem = childMap.get(head.getDescription()); if (treeItem == null) { treeItem = new EventDescriptionTreeItem(head); @@ -68,12 +68,12 @@ class EventDescriptionTreeItem extends NavTreeItem { } @Override - public void resort(Comparator>> comp) { + public void resort(Comparator> comp) { FXCollections.sort(getChildren(), comp); } @Override - public NavTreeItem findTreeItemForEvent(EventBundle t) { + public NavTreeItem findTreeItemForEvent(EventBundle t) { if (getValue().getEventType() == t.getEventType() && getValue().getDescription().equals(t.getDescription())) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java index e921991191..f1b91bf202 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java @@ -33,9 +33,9 @@ class EventTypeTreeItem extends NavTreeItem { */ private final Map childMap = new ConcurrentHashMap<>(); - private final Comparator>> comparator = TreeComparator.Description; + private final Comparator> comparator = TreeComparator.Description; - EventTypeTreeItem(EventBundle g) { + EventTypeTreeItem(EventBundle g) { setValue(g); } @@ -44,8 +44,8 @@ class EventTypeTreeItem extends NavTreeItem { return getValue().getCount(); } - public void insert(Deque> path) { - EventBundle head = path.removeFirst(); + public void insert(Deque path) { + EventBundle head = path.removeFirst(); EventDescriptionTreeItem treeItem = childMap.get(head.getDescription()); if (treeItem == null) { treeItem = new EventDescriptionTreeItem(head); @@ -61,7 +61,7 @@ class EventTypeTreeItem extends NavTreeItem { } @Override - public NavTreeItem findTreeItemForEvent(EventBundle t) { + public NavTreeItem findTreeItemForEvent(EventBundle t) { if (t.getEventType().getBaseType() == getValue().getEventType().getBaseType()) { for (EventDescriptionTreeItem child : childMap.values()) { @@ -75,7 +75,7 @@ class EventTypeTreeItem extends NavTreeItem { } @Override - public void resort(Comparator>> comp) { + public void resort(Comparator> comp) { FXCollections.sort(getChildren(), comp); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java index 154ab9fab0..774cf51f7f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventsTree.java @@ -64,13 +64,13 @@ final public class EventsTree extends BorderPane { private DetailViewPane detailViewPane; @FXML - private TreeView> eventsTree; + private TreeView eventsTree; @FXML private Label eventsTreeLabel; @FXML - private ComboBox>>> sortByBox; + private ComboBox>> sortByBox; public EventsTree(TimeLineController controller) { this.controller = controller; @@ -89,8 +89,8 @@ final public class EventsTree extends BorderPane { detailViewPane.getSelectedNodes().addListener((Observable observable) -> { eventsTree.getSelectionModel().clearSelection(); - detailViewPane.getSelectedNodes().forEach(eventBundleNode -> { - eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventBundleNode.getEventBundle())); + detailViewPane.getSelectedNodes().forEach(eventStripeNode -> { + eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventStripeNode.getEventStripe())); }); }); @@ -103,7 +103,7 @@ final public class EventsTree extends BorderPane { @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private void setRoot() { RootItem root = new RootItem(); - for (EventBundle bundle : detailViewPane.getEventBundles()) { + for (EventBundle bundle : detailViewPane.getEventBundles()) { root.insert(bundle); } eventsTree.setRoot(root); @@ -121,7 +121,7 @@ final public class EventsTree extends BorderPane { getRoot().resort(sortByBox.getSelectionModel().getSelectedItem()); }); eventsTree.setShowRoot(false); - eventsTree.setCellFactory((TreeView> p) -> new EventBundleTreeCell()); + eventsTree.setCellFactory((TreeView p) -> new EventBundleTreeCell()); eventsTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); eventsTreeLabel.setText(Bundle.EventsTree_Label_text()); @@ -131,7 +131,7 @@ final public class EventsTree extends BorderPane { * A tree cell to display {@link EventBundle}s. Shows the description, and * count, as well a a "legend icon" for the event type. */ - private class EventBundleTreeCell extends TreeCell> { + private class EventBundleTreeCell extends TreeCell { private static final double HIDDEN_MULTIPLIER = .6; private final Rectangle rect = new Rectangle(24, 24); @@ -145,7 +145,7 @@ final public class EventsTree extends BorderPane { } @Override - protected void updateItem(EventBundle item, boolean empty) { + protected void updateItem(EventBundle item, boolean empty) { super.updateItem(item, empty); if (item == null || empty) { setText(null); @@ -164,7 +164,7 @@ final public class EventsTree extends BorderPane { }); registerListeners(controller.getQuickHideFilters(), item); String text = item.getDescription() + " (" + item.getCount() + ")"; // NON-NLS - TreeItem> parent = getTreeItem().getParent(); + TreeItem parent = getTreeItem().getParent(); if (parent != null && parent.getValue() != null && (parent instanceof EventDescriptionTreeItem)) { text = StringUtils.substringAfter(text, parent.getValue().getDescription()); } @@ -176,7 +176,7 @@ final public class EventsTree extends BorderPane { } } - private void registerListeners(Collection filters, EventBundle item) { + private void registerListeners(Collection filters, EventBundle item) { for (DescriptionFilter filter : filters) { if (filter.getDescription().equals(item.getDescription())) { filter.activeProperty().addListener(filterStateChangeListener); @@ -192,8 +192,8 @@ final public class EventsTree extends BorderPane { } } - private void updateHiddenState(EventBundle item) { - TreeItem> treeItem = getTreeItem(); + private void updateHiddenState(EventBundle item) { + TreeItem treeItem = getTreeItem(); ContextMenu newMenu; if (controller.getQuickHideFilters().stream(). filter(AbstractFilter::isActive) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java index bed99d2270..d1df7a5b38 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java @@ -28,11 +28,11 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; * {@link EventTreeCell}. Each NavTreeItem has a EventBundle which has a type, * description , count, etc. */ -abstract class NavTreeItem extends TreeItem> { +abstract class NavTreeItem extends TreeItem { abstract long getCount(); - abstract void resort(Comparator>> comp); + abstract void resort(Comparator> comp); - abstract NavTreeItem findTreeItemForEvent(EventBundle t); + abstract NavTreeItem findTreeItemForEvent(EventBundle t); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java index 571758a037..a96dfaae20 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java @@ -56,7 +56,7 @@ class RootItem extends NavTreeItem { * * @param g Group to add */ - public void insert(EventBundle g) { + public void insert(EventBundle g) { EventTypeTreeItem treeItem = childMap.computeIfAbsent(g.getEventType().getBaseType(), baseType -> { @@ -69,12 +69,12 @@ class RootItem extends NavTreeItem { treeItem.insert(getTreePath(g)); } - static Deque< EventBundle> getTreePath(EventBundle g) { - Deque> path = new ArrayDeque<>(); - Optional> p = Optional.of(g); + static Deque getTreePath(EventBundle g) { + Deque path = new ArrayDeque<>(); + Optional p = Optional.of(g); while (p.isPresent()) { - EventBundle parent = p.get(); + EventBundle parent = p.get(); path.addFirst(parent); p = parent.getParentBundle(); } @@ -83,12 +83,12 @@ class RootItem extends NavTreeItem { } @Override - public void resort(Comparator>> comp) { + public void resort(Comparator> comp) { childMap.values().forEach(ti -> ti.resort(comp)); } @Override - public NavTreeItem findTreeItemForEvent(EventBundle t) { + public NavTreeItem findTreeItemForEvent(EventBundle t) { for (EventTypeTreeItem child : childMap.values()) { final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); if (findTreeItemForEvent != null) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java index 195a286ed9..aad194f61c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java @@ -23,23 +23,23 @@ import javafx.scene.control.TreeItem; import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -enum TreeComparator implements Comparator>> { +enum TreeComparator implements Comparator> { Description { @Override - public int compare(TreeItem> o1, TreeItem> o2) { + public int compare(TreeItem o1, TreeItem o2) { return o1.getValue().getDescription().compareTo(o2.getValue().getDescription()); } }, Count { @Override - public int compare(TreeItem> o1, TreeItem> o2) { + public int compare(TreeItem o1, TreeItem o2) { return Long.compare(o2.getValue().getCount(), o1.getValue().getCount()); } }, Type { @Override - public int compare(TreeItem> o1, TreeItem> o2) { + public int compare(TreeItem o1, TreeItem o2) { return EventType.getComparator().compare(o1.getValue().getEventType(), o2.getValue().getEventType()); } }; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLoD.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLoD.java index 0fab0442c8..50d612b210 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLoD.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLoD.java @@ -43,7 +43,7 @@ public enum DescriptionLoD { try { return values()[ordinal() + 1]; } catch (ArrayIndexOutOfBoundsException e) { - return FULL; + return null; } } @@ -51,7 +51,7 @@ public enum DescriptionLoD { try { return values()[ordinal() - 1]; } catch (ArrayIndexOutOfBoundsException e) { - return SHORT; + return null; } }