mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-15 09:17:42 +00:00
Merge pull request #1615 from millmanorama/subcluster_expansion
Subcluster expansion
This commit is contained in:
commit
dbf954d183
@ -18,16 +18,16 @@
|
|||||||
*/
|
*/
|
||||||
package org.sleuthkit.autopsy.timeline.datamodel;
|
package org.sleuthkit.autopsy.timeline.datamodel;
|
||||||
|
|
||||||
import com.google.common.collect.Range;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.SortedSet;
|
||||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||||
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
|
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<ParentType extends EventBundle<?>> {
|
||||||
|
|
||||||
String getDescription();
|
String getDescription();
|
||||||
|
|
||||||
@ -35,7 +35,6 @@ public interface EventBundle {
|
|||||||
|
|
||||||
Set<Long> getEventIDs();
|
Set<Long> getEventIDs();
|
||||||
|
|
||||||
|
|
||||||
Set<Long> getEventIDsWithHashHits();
|
Set<Long> getEventIDsWithHashHits();
|
||||||
|
|
||||||
Set<Long> getEventIDsWithTags();
|
Set<Long> getEventIDsWithTags();
|
||||||
@ -46,11 +45,11 @@ public interface EventBundle {
|
|||||||
|
|
||||||
long getStartMillis();
|
long getStartMillis();
|
||||||
|
|
||||||
Iterable<Range<Long>> getRanges();
|
Optional<ParentType> getParentBundle();
|
||||||
|
|
||||||
Optional<EventBundle> getParentBundle();
|
default long getCount() {
|
||||||
|
|
||||||
default long getCount() {
|
|
||||||
return getEventIDs().size();
|
return getEventIDs().size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SortedSet<EventCluster> getClusters();
|
||||||
}
|
}
|
||||||
|
@ -18,12 +18,14 @@
|
|||||||
*/
|
*/
|
||||||
package org.sleuthkit.autopsy.timeline.datamodel;
|
package org.sleuthkit.autopsy.timeline.datamodel;
|
||||||
|
|
||||||
import com.google.common.collect.Range;
|
import com.google.common.collect.ImmutableSortedSet;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.SortedSet;
|
||||||
import javax.annotation.concurrent.Immutable;
|
import javax.annotation.concurrent.Immutable;
|
||||||
import org.joda.time.Interval;
|
import org.joda.time.Interval;
|
||||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||||
@ -36,7 +38,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
|
|||||||
* designated 'zoom level', and be 'close together' in time.
|
* designated 'zoom level', and be 'close together' in time.
|
||||||
*/
|
*/
|
||||||
@Immutable
|
@Immutable
|
||||||
public class EventCluster implements EventBundle {
|
public class EventCluster implements EventBundle<EventStripe> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* merge two event clusters into one new event cluster.
|
* merge two event clusters into one new event cluster.
|
||||||
@ -62,7 +64,7 @@ public class EventCluster implements EventBundle {
|
|||||||
return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod);
|
return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod);
|
||||||
}
|
}
|
||||||
|
|
||||||
final private EventBundle parent;
|
final private EventStripe parent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the smallest time interval containing all the clustered events
|
* the smallest time interval containing all the clustered events
|
||||||
@ -101,7 +103,7 @@ public class EventCluster implements EventBundle {
|
|||||||
*/
|
*/
|
||||||
private final Set<Long> hashHits;
|
private final Set<Long> hashHits;
|
||||||
|
|
||||||
private EventCluster(Interval spanningInterval, EventType type, Set<Long> eventIDs, Set<Long> hashHits, Set<Long> tagged, String description, DescriptionLoD lod, EventBundle parent) {
|
private EventCluster(Interval spanningInterval, EventType type, Set<Long> eventIDs, Set<Long> hashHits, Set<Long> tagged, String description, DescriptionLoD lod, EventStripe parent) {
|
||||||
|
|
||||||
this.span = spanningInterval;
|
this.span = spanningInterval;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
@ -118,7 +120,7 @@ public class EventCluster implements EventBundle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<EventBundle> getParentBundle() {
|
public Optional<EventStripe> getParentBundle() {
|
||||||
return Optional.ofNullable(parent);
|
return Optional.ofNullable(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,19 +168,6 @@ public class EventCluster implements EventBundle {
|
|||||||
return lod;
|
return lod;
|
||||||
}
|
}
|
||||||
|
|
||||||
Range<Long> getRange() {
|
|
||||||
if (getEndMillis() > getStartMillis()) {
|
|
||||||
return Range.closedOpen(getSpan().getStartMillis(), getSpan().getEndMillis());
|
|
||||||
} else {
|
|
||||||
return Range.singleton(getStartMillis());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterable<Range<Long>> getRanges() {
|
|
||||||
return Collections.singletonList(getRange());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* return a new EventCluster identical to this one, except with the given
|
* return a new EventCluster identical to this one, except with the given
|
||||||
* EventBundle as the parent.
|
* EventBundle as the parent.
|
||||||
@ -188,11 +177,15 @@ public class EventCluster implements EventBundle {
|
|||||||
* @return a new EventCluster identical to this one, except with the given
|
* @return a new EventCluster identical to this one, except with the given
|
||||||
* EventBundle as the parent.
|
* EventBundle as the parent.
|
||||||
*/
|
*/
|
||||||
public EventCluster withParent(EventBundle parent) {
|
public EventCluster withParent(EventStripe parent) {
|
||||||
if (Objects.nonNull(this.parent)) {
|
if (Objects.nonNull(this.parent)) {
|
||||||
throw new IllegalStateException("Event Cluster already has a parent!");
|
throw new IllegalStateException("Event Cluster already has a parent!");
|
||||||
}
|
}
|
||||||
return new EventCluster(span, type, eventIDs, hashHits, tagged, description, lod, 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,31 @@
|
|||||||
/*
|
/*
|
||||||
* To change this license header, choose License Headers in Project Properties.
|
* Autopsy Forensic Browser
|
||||||
* To change this template file, choose Tools | Templates
|
*
|
||||||
* and open the template in the editor.
|
* Copyright 2015 Basis Technology Corp.
|
||||||
|
* Contact: carrier <at> sleuthkit <dot> 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.datamodel;
|
package org.sleuthkit.autopsy.timeline.datamodel;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
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.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeSet;
|
||||||
import javax.annotation.concurrent.Immutable;
|
import javax.annotation.concurrent.Immutable;
|
||||||
import org.python.google.common.base.Objects;
|
import org.python.google.common.base.Objects;
|
||||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||||
@ -22,10 +33,10 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A 'collection' of {@link EventCluster}s, all having the same type,
|
* A 'collection' of {@link EventCluster}s, all having the same type,
|
||||||
* description, and zoom levels.
|
* description, and zoom levels, but not necessarily close together in time.
|
||||||
*/
|
*/
|
||||||
@Immutable
|
@Immutable
|
||||||
public final class EventStripe implements EventBundle {
|
public final class EventStripe implements EventBundle<EventCluster> {
|
||||||
|
|
||||||
public static EventStripe merge(EventStripe u, EventStripe v) {
|
public static EventStripe merge(EventStripe u, EventStripe v) {
|
||||||
Preconditions.checkNotNull(u);
|
Preconditions.checkNotNull(u);
|
||||||
@ -37,10 +48,9 @@ public final class EventStripe implements EventBundle {
|
|||||||
return new EventStripe(u, v);
|
return new EventStripe(u, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final EventBundle parent;
|
private final EventCluster parent;
|
||||||
|
|
||||||
private final RangeSet<Long> spans = TreeRangeSet.create();
|
private final SortedSet<EventCluster> clusters = new TreeSet<>(Comparator.comparing(EventCluster::getStartMillis));
|
||||||
private final RangeMap<Long, EventCluster> spanMap = TreeRangeMap.create();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the type of all the events
|
* the type of all the events
|
||||||
@ -73,23 +83,21 @@ public final class EventStripe implements EventBundle {
|
|||||||
*/
|
*/
|
||||||
private final Set<Long> hashHits = new HashSet<>();
|
private final Set<Long> hashHits = new HashSet<>();
|
||||||
|
|
||||||
public EventStripe(EventCluster cluster) {
|
public EventStripe(EventCluster cluster, EventCluster parent) {
|
||||||
spans.add(cluster.getRange());
|
clusters.add(cluster);
|
||||||
spanMap.put(cluster.getRange(), cluster);
|
|
||||||
type = cluster.getEventType();
|
type = cluster.getEventType();
|
||||||
description = cluster.getDescription();
|
description = cluster.getDescription();
|
||||||
lod = cluster.getDescriptionLoD();
|
lod = cluster.getDescriptionLoD();
|
||||||
eventIDs.addAll(cluster.getEventIDs());
|
eventIDs.addAll(cluster.getEventIDs());
|
||||||
tagged.addAll(cluster.getEventIDsWithTags());
|
tagged.addAll(cluster.getEventIDsWithTags());
|
||||||
hashHits.addAll(cluster.getEventIDsWithHashHits());
|
hashHits.addAll(cluster.getEventIDsWithHashHits());
|
||||||
parent = cluster.getParentBundle().orElse(null);
|
this.parent = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private EventStripe(EventStripe u, EventStripe v) {
|
private EventStripe(EventStripe u, EventStripe v) {
|
||||||
spans.addAll(u.spans);
|
clusters.addAll(u.clusters);
|
||||||
spans.addAll(v.spans);
|
clusters.addAll(v.clusters);
|
||||||
spanMap.putAll(u.spanMap);
|
|
||||||
spanMap.putAll(v.spanMap);
|
|
||||||
type = u.getEventType();
|
type = u.getEventType();
|
||||||
description = u.getDescription();
|
description = u.getDescription();
|
||||||
lod = u.getDescriptionLoD();
|
lod = u.getDescriptionLoD();
|
||||||
@ -99,11 +107,11 @@ public final class EventStripe implements EventBundle {
|
|||||||
tagged.addAll(v.getEventIDsWithTags());
|
tagged.addAll(v.getEventIDsWithTags());
|
||||||
hashHits.addAll(u.getEventIDsWithHashHits());
|
hashHits.addAll(u.getEventIDsWithHashHits());
|
||||||
hashHits.addAll(v.getEventIDsWithHashHits());
|
hashHits.addAll(v.getEventIDsWithHashHits());
|
||||||
parent = u.getParentBundle().orElse(null);
|
parent = u.getParentBundle().orElse(v.getParentBundle().orElse(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<EventBundle> getParentBundle() {
|
public Optional<EventCluster> getParentBundle() {
|
||||||
return Optional.ofNullable(parent);
|
return Optional.ofNullable(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,16 +147,15 @@ public final class EventStripe implements EventBundle {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getStartMillis() {
|
public long getStartMillis() {
|
||||||
return spans.span().lowerEndpoint();
|
return clusters.first().getStartMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getEndMillis() {
|
public long getEndMillis() {
|
||||||
return spans.span().upperEndpoint();
|
return clusters.last().getEndMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public SortedSet< EventCluster> getClusters() {
|
||||||
public Iterable<Range<Long>> getRanges() {
|
return Collections.unmodifiableSortedSet(clusters);
|
||||||
return spans.asRanges();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,11 +89,12 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
|
|||||||
* EventTypeMap, and dataSets is all linked directly to the ClusterChart which
|
* EventTypeMap, and dataSets is all linked directly to the ClusterChart which
|
||||||
* must only be manipulated on the JavaFx thread.
|
* must only be manipulated on the JavaFx thread.
|
||||||
*/
|
*/
|
||||||
public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster, EventStripeNode, EventDetailChart> {
|
public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster, EventBundleNodeBase<?, ?, ?>, EventDetailChart> {
|
||||||
|
|
||||||
|
|
||||||
private final static Logger LOGGER = Logger.getLogger(DetailViewPane.class.getName());
|
private final static Logger LOGGER = Logger.getLogger(DetailViewPane.class.getName());
|
||||||
|
|
||||||
private MultipleSelectionModel<TreeItem<EventBundle>> treeSelectionModel;
|
private MultipleSelectionModel<TreeItem<EventBundle<?>>> treeSelectionModel;
|
||||||
|
|
||||||
//these three could be injected from fxml but it was causing npe's
|
//these three could be injected from fxml but it was causing npe's
|
||||||
private final DateAxis dateAxis = new DateAxis();
|
private final DateAxis dateAxis = new DateAxis();
|
||||||
@ -107,9 +108,9 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
|
|
||||||
private final Region region = new Region();
|
private final Region region = new Region();
|
||||||
|
|
||||||
private final ObservableList<EventStripeNode> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
|
private final ObservableList<EventBundleNodeBase<?, ?, ?>> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
|
||||||
|
|
||||||
public ObservableList<EventBundle> getEventBundles() {
|
public ObservableList<EventBundle<?>> getEventBundles() {
|
||||||
return chart.getEventBundles();
|
return chart.getEventBundles();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +137,7 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
vertScrollBar.visibleAmountProperty().bind(chart.heightProperty().multiply(100).divide(chart.maxVScrollProperty()));
|
vertScrollBar.visibleAmountProperty().bind(chart.heightProperty().multiply(100).divide(chart.maxVScrollProperty()));
|
||||||
requestLayout();
|
requestLayout();
|
||||||
|
|
||||||
highlightedNodes.addListener((ListChangeListener.Change<? extends EventStripeNode> change) -> {
|
highlightedNodes.addListener((ListChangeListener.Change<? extends EventBundleNodeBase<?, ?, ?>> change) -> {
|
||||||
|
|
||||||
while (change.next()) {
|
while (change.next()) {
|
||||||
change.getAddedSubList().forEach(node -> {
|
change.getAddedSubList().forEach(node -> {
|
||||||
@ -201,7 +202,7 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
highlightedNodes.clear();
|
highlightedNodes.clear();
|
||||||
selectedNodes.stream().forEach((tn) -> {
|
selectedNodes.stream().forEach((tn) -> {
|
||||||
|
|
||||||
for (EventStripeNode n : chart.getNodes((EventStripeNode t) ->
|
for (EventBundleNodeBase<?, ?, ?> n : chart.getNodes((EventBundleNodeBase<?, ?, ?> t) ->
|
||||||
t.getDescription().equals(tn.getDescription()))) {
|
t.getDescription().equals(tn.getDescription()))) {
|
||||||
highlightedNodes.add(n);
|
highlightedNodes.add(n);
|
||||||
}
|
}
|
||||||
@ -219,14 +220,14 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() + factor * (chart.getHeight() / chart.maxVScrollProperty().get()))));
|
vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() + factor * (chart.getHeight() / chart.maxVScrollProperty().get()))));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSelectionModel(MultipleSelectionModel<TreeItem<EventBundle>> selectionModel) {
|
public void setSelectionModel(MultipleSelectionModel<TreeItem<EventBundle<?>>> selectionModel) {
|
||||||
this.treeSelectionModel = selectionModel;
|
this.treeSelectionModel = selectionModel;
|
||||||
|
|
||||||
treeSelectionModel.getSelectedItems().addListener((Observable observable) -> {
|
treeSelectionModel.getSelectedItems().addListener((Observable observable) -> {
|
||||||
highlightedNodes.clear();
|
highlightedNodes.clear();
|
||||||
for (TreeItem<EventBundle> tn : treeSelectionModel.getSelectedItems()) {
|
for (TreeItem<EventBundle<?>> tn : treeSelectionModel.getSelectedItems()) {
|
||||||
|
|
||||||
for (EventStripeNode n : chart.getNodes((EventStripeNode t) ->
|
for (EventBundleNodeBase<?, ?, ?> n : chart.getNodes((EventBundleNodeBase<?, ?, ?> t) ->
|
||||||
t.getDescription().equals(tn.getValue().getDescription()))) {
|
t.getDescription().equals(tn.getValue().getDescription()))) {
|
||||||
highlightedNodes.add(n);
|
highlightedNodes.add(n);
|
||||||
}
|
}
|
||||||
@ -351,7 +352,7 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void applySelectionEffect(EventStripeNode c1, Boolean selected) {
|
protected void applySelectionEffect(EventBundleNodeBase<?, ?, ?> c1, Boolean selected) {
|
||||||
c1.applySelectionEffect(selected);
|
c1.applySelectionEffect(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,4 +476,6 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
|
|||||||
public Action newHideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
|
public Action newHideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
|
||||||
return chart.new HideDescriptionAction(description, descriptionLoD);
|
return chart.new HideDescriptionAction(description, descriptionLoD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,337 @@
|
|||||||
|
/*
|
||||||
|
* Autopsy Forensic Browser
|
||||||
|
*
|
||||||
|
* Copyright 2015 Basis Technology Corp.
|
||||||
|
* Contact: carrier <at> sleuthkit <dot> 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 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<BundleType extends EventBundle<ParentType>, ParentType extends EventBundle<BundleType>, ParentNodeType extends EventBundleNodeBase<ParentType, BundleType, ?>> 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<EventType, Effect> 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 EventDetailChart chart;
|
||||||
|
final SimpleObjectProperty<DescriptionLoD> descLOD = new SimpleObjectProperty<>();
|
||||||
|
final SimpleObjectProperty<DescriptionVisibility> 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<ParentNodeType> 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 Tooltip tooltip;
|
||||||
|
|
||||||
|
public EventBundleNodeBase(EventDetailChart 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);
|
||||||
|
infoHBox.setPickOnBounds(true);
|
||||||
|
|
||||||
|
//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);
|
||||||
|
|
||||||
|
//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();
|
||||||
|
showHoverControls(true);
|
||||||
|
toFront();
|
||||||
|
});
|
||||||
|
setOnMouseExited((MouseEvent event) -> {
|
||||||
|
showHoverControls(false);
|
||||||
|
if (parentNode != null) {
|
||||||
|
parentNode.showHoverControls(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setDescriptionVisibility(DescriptionVisibility.SHOWN);
|
||||||
|
descVisibility.addListener((ObservableValue<? extends DescriptionVisibility> observable, DescriptionVisibility oldValue, DescriptionVisibility newValue) -> {
|
||||||
|
setDescriptionVisibility(newValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 == null) {
|
||||||
|
final Task<String> tooltTipTask = new Task<String>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String call() throws Exception {
|
||||||
|
HashMap<String, Long> hashSetCounts = new HashMap<>();
|
||||||
|
if (eventBundle.getEventIDsWithHashHits().isEmpty() == false) {
|
||||||
|
try {
|
||||||
|
//TODO:push this to DB
|
||||||
|
for (TimeLineEvent tle : eventsModel.getEventsById(eventBundle.getEventIDsWithHashHits())) {
|
||||||
|
Set<String> 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<String, Long> t) -> t.getKey() + " : " + t.getValue())
|
||||||
|
.collect(Collectors.joining("\n"));
|
||||||
|
|
||||||
|
Map<String, Long> tagCounts = new HashMap<>();
|
||||||
|
if (eventBundle.getEventIDsWithTags().isEmpty() == false) {
|
||||||
|
tagCounts.putAll(eventsModel.getTagCountsByTagName(eventBundle.getEventIDsWithTags()));
|
||||||
|
}
|
||||||
|
String tagCountsString = tagCounts.entrySet().stream()
|
||||||
|
.map((Map.Entry<String, Long> 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 = new Tooltip(get());
|
||||||
|
tooltip.setAutoHide(true);
|
||||||
|
Tooltip.install(EventBundleNodeBase.this, tooltip);
|
||||||
|
} catch (InterruptedException | ExecutionException ex) {
|
||||||
|
LOGGER.log(Level.SEVERE, "Tooltip generation failed.", ex);
|
||||||
|
Tooltip.uninstall(EventBundleNodeBase.this, tooltip);
|
||||||
|
tooltip = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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<ParentNodeType> getSubNodes() {
|
||||||
|
return subNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract void setDescriptionVisibility(DescriptionVisibility get);
|
||||||
|
|
||||||
|
void showHoverControls(final boolean showControls) {
|
||||||
|
Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(),
|
||||||
|
eventType -> new DropShadow(-10, eventType.getColor()));
|
||||||
|
setEffect(showControls ? dropShadow : null);
|
||||||
|
if (parentNode != null) {
|
||||||
|
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<Long> 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 setDescriptionVisibilityLevel(DescriptionVisibility get) {
|
||||||
|
descVisibility.set(get);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,332 @@
|
|||||||
|
/*
|
||||||
|
* Autopsy Forensic Browser
|
||||||
|
*
|
||||||
|
* Copyright 2013-15 Basis Technology Corp.
|
||||||
|
* Contact: carrier <at> sleuthkit <dot> 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<EventCluster, EventStripe, EventStripeNode> {
|
||||||
|
|
||||||
|
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(EventDetailChart 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
|
||||||
|
public void setDescriptionVisibility(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<Collection<EventStripe>> loggedTask = new Task<Collection<EventStripe>>() {
|
||||||
|
|
||||||
|
private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail);
|
||||||
|
|
||||||
|
{
|
||||||
|
updateTitle(Bundle.EventStripeNode_loggedTask_name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Collection<EventStripe> call() throws Exception {
|
||||||
|
Collection<EventStripe> 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<EventStripe> 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<MouseEvent> {
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,13 +19,12 @@
|
|||||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||||
|
|
||||||
import com.google.common.collect.Range;
|
import com.google.common.collect.Range;
|
||||||
import java.util.ArrayList;
|
import com.google.common.collect.TreeRangeMap;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.MissingResourceException;
|
import java.util.MissingResourceException;
|
||||||
@ -38,7 +37,6 @@ import javafx.animation.KeyValue;
|
|||||||
import javafx.animation.Timeline;
|
import javafx.animation.Timeline;
|
||||||
import javafx.beans.InvalidationListener;
|
import javafx.beans.InvalidationListener;
|
||||||
import javafx.beans.Observable;
|
import javafx.beans.Observable;
|
||||||
import javafx.beans.property.Property;
|
|
||||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
@ -48,7 +46,6 @@ import javafx.collections.FXCollections;
|
|||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.event.EventHandler;
|
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.Group;
|
import javafx.scene.Group;
|
||||||
@ -63,7 +60,6 @@ import javafx.scene.input.MouseEvent;
|
|||||||
import javafx.scene.shape.Line;
|
import javafx.scene.shape.Line;
|
||||||
import javafx.scene.shape.StrokeLineCap;
|
import javafx.scene.shape.StrokeLineCap;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import javax.annotation.concurrent.GuardedBy;
|
|
||||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||||
import org.controlsfx.control.action.Action;
|
import org.controlsfx.control.action.Action;
|
||||||
import org.controlsfx.control.action.ActionGroup;
|
import org.controlsfx.control.action.ActionGroup;
|
||||||
@ -100,13 +96,12 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
|
|||||||
*/
|
*/
|
||||||
public final class EventDetailChart extends XYChart<DateTime, EventCluster> implements TimeLineChart<DateTime> {
|
public final class EventDetailChart extends XYChart<DateTime, EventCluster> implements TimeLineChart<DateTime> {
|
||||||
|
|
||||||
static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS
|
private 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 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 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_Y_OFFSET = 5;
|
||||||
private static final int PROJECTED_LINE_STROKE_WIDTH = 5;
|
private static final int PROJECTED_LINE_STROKE_WIDTH = 5;
|
||||||
private static final int DEFAULT_ROW_HEIGHT = 24;
|
private static final int MINIMUM_EVENT_NODE_GAP = 4;
|
||||||
|
|
||||||
private ContextMenu chartContextMenu;
|
private ContextMenu chartContextMenu;
|
||||||
|
|
||||||
private TimeLineController controller;
|
private TimeLineController controller;
|
||||||
@ -114,7 +109,7 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
private FilteredEventsModel filteredEvents;
|
private FilteredEventsModel filteredEvents;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a user position-able vertical line to help the compare events
|
* a user positionable vertical line to help compare events
|
||||||
*/
|
*/
|
||||||
private Line guideLine;
|
private Line guideLine;
|
||||||
|
|
||||||
@ -126,35 +121,27 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
private IntervalSelector<? extends DateTime> intervalSelector;
|
private IntervalSelector<? extends DateTime> intervalSelector;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* listener that triggers layout pass
|
* listener that triggers chart layout pass
|
||||||
*/
|
*/
|
||||||
private final InvalidationListener layoutInvalidationListener = (Observable o) -> {
|
private final InvalidationListener layoutInvalidationListener = (Observable o) -> {
|
||||||
synchronized (EventDetailChart.this) {
|
layoutPlotChildren();
|
||||||
requiresLayout = true;
|
|
||||||
requestChartLayout();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the maximum y value used so far during the most recent layout pass
|
* the maximum y value used so far during the most recent layout pass
|
||||||
*/
|
*/
|
||||||
private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0);
|
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<EventStripeNode> selectedNodes;
|
final ObservableList<EventBundleNodeBase<?, ?, ?>> selectedNodes;
|
||||||
/**
|
/**
|
||||||
* the group that all event nodes are added to. This facilitates scrolling
|
* the group that all event nodes are added to. This facilitates scrolling
|
||||||
* by allowing a single translation of this group.
|
* by allowing a single translation of this group.
|
||||||
*/
|
*/
|
||||||
private final Group nodeGroup = new Group();
|
private final Group nodeGroup = new Group();
|
||||||
private final ObservableList<EventBundle> bundles = FXCollections.observableArrayList();
|
private final ObservableList<EventBundle<?>> bundles = FXCollections.observableArrayList();
|
||||||
private final Map<ImmutablePair<EventType, String>, EventStripe> stripeDescMap = new HashMap<>();
|
private final Map<ImmutablePair<EventType, String>, EventStripe> stripeDescMap = new HashMap<>();
|
||||||
private final Map<EventStripe, EventStripeNode> stripeNodeMap = new HashMap<>();
|
private final Map<EventStripe, EventStripeNode> stripeNodeMap = new HashMap<>();
|
||||||
private final Map<Range<Long>, Line> projectionMap = new HashMap<>();
|
private final Map<EventCluster, Line> projectionMap = new HashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* list of series of data added to this chart
|
* list of series of data added to this chart
|
||||||
@ -164,11 +151,6 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
private final ObservableList<Series<DateTime, EventCluster>> seriesList =
|
private final ObservableList<Series<DateTime, EventCluster>> seriesList =
|
||||||
FXCollections.<Series<DateTime, EventCluster>>observableArrayList();
|
FXCollections.<Series<DateTime, EventCluster>>observableArrayList();
|
||||||
|
|
||||||
private final ObservableList<Series<DateTime, EventCluster>> sortedSeriesList = seriesList
|
|
||||||
.sorted((s1, s2) -> {
|
|
||||||
final List<String> 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
|
* true == layout each event type in its own band, false == mix all the
|
||||||
* events together during layout
|
* events together during layout
|
||||||
@ -193,25 +175,23 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
* the labels, alow them to extend past the timespan indicator and off the
|
* the labels, alow them to extend past the timespan indicator and off the
|
||||||
* edge of the screen
|
* edge of the screen
|
||||||
*/
|
*/
|
||||||
private final SimpleBooleanProperty truncateAll = new SimpleBooleanProperty(false);
|
final SimpleBooleanProperty truncateAll = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the width to truncate all labels to if truncateAll is true. adjustable
|
* the width to truncate all labels to if truncateAll is true. adjustable
|
||||||
* via slider if truncateAll is true
|
* via slider if truncateAll is true
|
||||||
*/
|
*/
|
||||||
private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0);
|
final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0);
|
||||||
private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true);
|
|
||||||
|
|
||||||
EventDetailChart(DateAxis dateAxis, final Axis<EventCluster> verticalAxis, ObservableList<EventStripeNode> selectedNodes) {
|
EventDetailChart(DateAxis dateAxis, final Axis<EventCluster> verticalAxis, ObservableList<EventBundleNodeBase<?, ?, ?>> selectedNodes) {
|
||||||
super(dateAxis, verticalAxis);
|
super(dateAxis, verticalAxis);
|
||||||
dateAxis.setAutoRanging(false);
|
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.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.setTickLabelsVisible(false);
|
||||||
verticalAxis.setTickMarkVisible(false);
|
verticalAxis.setTickMarkVisible(false);
|
||||||
|
|
||||||
setLegendVisible(false);
|
setLegendVisible(false);
|
||||||
|
|
||||||
setPadding(Insets.EMPTY);
|
setPadding(Insets.EMPTY);
|
||||||
setAlternativeColumnFillVisible(true);
|
setAlternativeColumnFillVisible(true);
|
||||||
|
|
||||||
@ -219,8 +199,6 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
getPlotChildren().add(nodeGroup);
|
getPlotChildren().add(nodeGroup);
|
||||||
|
|
||||||
//add listener for events that should trigger layout
|
//add listener for events that should trigger layout
|
||||||
widthProperty().addListener(layoutInvalidationListener);
|
|
||||||
heightProperty().addListener(layoutInvalidationListener);
|
|
||||||
bandByType.addListener(layoutInvalidationListener);
|
bandByType.addListener(layoutInvalidationListener);
|
||||||
oneEventPerRow.addListener(layoutInvalidationListener);
|
oneEventPerRow.addListener(layoutInvalidationListener);
|
||||||
truncateAll.addListener(layoutInvalidationListener);
|
truncateAll.addListener(layoutInvalidationListener);
|
||||||
@ -233,8 +211,8 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
setPrefHeight(boundsInLocalProperty().get().getHeight());
|
setPrefHeight(boundsInLocalProperty().get().getHeight());
|
||||||
});
|
});
|
||||||
|
|
||||||
//set up mouse listeners
|
///////set up mouse listeners
|
||||||
final EventHandler<MouseEvent> clickHandler = (MouseEvent clickEvent) -> {
|
setOnMouseClicked((MouseEvent clickEvent) -> {
|
||||||
if (chartContextMenu != null) {
|
if (chartContextMenu != null) {
|
||||||
chartContextMenu.hide();
|
chartContextMenu.hide();
|
||||||
}
|
}
|
||||||
@ -243,10 +221,7 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
chartContextMenu.show(EventDetailChart.this, clickEvent.getScreenX(), clickEvent.getScreenY());
|
chartContextMenu.show(EventDetailChart.this, clickEvent.getScreenX(), clickEvent.getScreenY());
|
||||||
clickEvent.consume();
|
clickEvent.consume();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
setOnMouseClicked(clickHandler);
|
|
||||||
|
|
||||||
//use one handler with an if chain because it maintains state
|
//use one handler with an if chain because it maintains state
|
||||||
final ChartDragHandler<DateTime, EventDetailChart> dragHandler = new ChartDragHandler<>(this, getXAxis());
|
final ChartDragHandler<DateTime, EventDetailChart> dragHandler = new ChartDragHandler<>(this, getXAxis());
|
||||||
setOnMousePressed(dragHandler);
|
setOnMousePressed(dragHandler);
|
||||||
@ -254,42 +229,10 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
setOnMouseDragged(dragHandler);
|
setOnMouseDragged(dragHandler);
|
||||||
|
|
||||||
this.selectedNodes = selectedNodes;
|
this.selectedNodes = selectedNodes;
|
||||||
this.selectedNodes.addListener((
|
this.selectedNodes.addListener(new SelectionChangeHandler());
|
||||||
ListChangeListener.Change<? extends EventStripeNode> c) -> {
|
|
||||||
while (c.next()) {
|
|
||||||
c.getRemoved().forEach((EventStripeNode t) -> {
|
|
||||||
t.getEventStripe().getRanges().forEach((Range<Long> t1) -> {
|
|
||||||
|
|
||||||
Line removedLine = projectionMap.remove(t1);
|
|
||||||
getChartChildren().removeAll(removedLine);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
c.getAddedSubList().forEach((EventStripeNode t) -> {
|
|
||||||
|
|
||||||
for (Range<Long> 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<EventBundle> getEventBundles() {
|
ObservableList<EventBundle<?>> getEventBundles() {
|
||||||
return bundles;
|
return bundles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,7 +343,7 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
final EventCluster eventCluster = data.getYValue();
|
final EventCluster eventCluster = data.getYValue();
|
||||||
bundles.add(eventCluster);
|
bundles.add(eventCluster);
|
||||||
EventStripe eventStripe = stripeDescMap.merge(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()),
|
EventStripe eventStripe = stripeDescMap.merge(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()),
|
||||||
new EventStripe(eventCluster),
|
new EventStripe(eventCluster, null),
|
||||||
(EventStripe u, EventStripe v) -> {
|
(EventStripe u, EventStripe v) -> {
|
||||||
EventStripeNode remove = stripeNodeMap.remove(u);
|
EventStripeNode remove = stripeNodeMap.remove(u);
|
||||||
nodeGroup.getChildren().remove(remove);
|
nodeGroup.getChildren().remove(remove);
|
||||||
@ -413,7 +356,7 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
stripeNodeMap.put(eventStripe, stripeNode);
|
stripeNodeMap.put(eventStripe, stripeNode);
|
||||||
nodeGroup.getChildren().add(stripeNode);
|
nodeGroup.getChildren().add(stripeNode);
|
||||||
data.setNode(stripeNode);
|
data.setNode(stripeNode);
|
||||||
|
layoutPlotChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -429,103 +372,32 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
EventStripe removedStripe = stripeDescMap.remove(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()));
|
EventStripe removedStripe = stripeDescMap.remove(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()));
|
||||||
EventStripeNode removedNode = stripeNodeMap.remove(removedStripe);
|
EventStripeNode removedNode = stripeNodeMap.remove(removedStripe);
|
||||||
nodeGroup.getChildren().remove(removedNode);
|
nodeGroup.getChildren().remove(removedNode);
|
||||||
|
|
||||||
data.setNode(null);
|
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 void layoutChildren() {
|
|
||||||
super.layoutChildren();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
@Override
|
||||||
protected synchronized void layoutPlotChildren() {
|
protected synchronized void layoutPlotChildren() {
|
||||||
if (requiresLayout) {
|
setCursor(Cursor.WAIT);
|
||||||
setCursor(Cursor.WAIT);
|
maxY.set(0);
|
||||||
|
if (bandByType.get()) {
|
||||||
|
stripeNodeMap.values().stream()
|
||||||
|
.collect(Collectors.groupingBy(EventStripeNode::getEventType)).values()
|
||||||
|
.forEach(inputNodes -> {
|
||||||
|
List<EventStripeNode> stripeNodes = inputNodes.stream()
|
||||||
|
.sorted(Comparator.comparing(EventStripeNode::getStartMillis))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
maxY.set(0.0);
|
maxY.set(layoutEventBundleNodes(stripeNodes, maxY.get()));
|
||||||
|
});
|
||||||
Map<Boolean, List<EventStripeNode>> hiddenPartition;
|
} else {
|
||||||
if (bandByType.get()) {
|
List<EventStripeNode> stripeNodes = stripeNodeMap.values().stream()
|
||||||
double minY = 0;
|
.sorted(Comparator.comparing(EventStripeNode::getStartMillis))
|
||||||
for (Series<DateTime, EventCluster> series : sortedSeriesList) {
|
.collect(Collectors.toList());
|
||||||
hiddenPartition = series.getData().stream().map(Data::getNode).map(EventStripeNode.class::cast)
|
maxY.set(layoutEventBundleNodes(stripeNodes, 0));
|
||||||
.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();
|
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<EventStripeNode> hiddenNodes, List<EventStripeNode> 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
|
@Override
|
||||||
@ -534,7 +406,6 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
dataItemAdded(series, j, series.getData().get(j));
|
dataItemAdded(series, j, series.getData().get(j));
|
||||||
}
|
}
|
||||||
seriesList.add(series);
|
seriesList.add(series);
|
||||||
requiresLayout = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -543,18 +414,21 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
dataItemRemoved(series.getData().get(j), series);
|
dataItemRemoved(series.getData().get(j), series);
|
||||||
}
|
}
|
||||||
seriesList.remove(series);
|
seriesList.remove(series);
|
||||||
requiresLayout = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ReadOnlyDoubleProperty maxVScrollProperty() {
|
ReadOnlyDoubleProperty maxVScrollProperty() {
|
||||||
return maxY.getReadOnlyProperty();
|
return maxY.getReadOnlyProperty();
|
||||||
}
|
}
|
||||||
|
|
||||||
Iterable<EventStripeNode> getNodes(Predicate<EventStripeNode> p) {
|
/**
|
||||||
Function<EventStripeNode, Stream<EventStripeNode>> flattener =
|
* @return all the nodes that pass the given predicate
|
||||||
new Function<EventStripeNode, Stream<EventStripeNode>>() {
|
*/
|
||||||
|
Iterable<EventBundleNodeBase<?, ?, ?>> getNodes(Predicate<EventBundleNodeBase<?, ?, ?>> p) {
|
||||||
|
//use this recursive function to flatten the tree of nodes into an iterable.
|
||||||
|
Function<EventBundleNodeBase<?, ?, ?>, Stream<EventBundleNodeBase<?, ?, ?>>> stripeFlattener =
|
||||||
|
new Function<EventBundleNodeBase<?, ?, ?>, Stream<EventBundleNodeBase<?, ?, ?>>>() {
|
||||||
@Override
|
@Override
|
||||||
public Stream<EventStripeNode> apply(EventStripeNode node) {
|
public Stream<EventBundleNodeBase<?, ?, ?>> apply(EventBundleNodeBase<?, ?, ?> node) {
|
||||||
return Stream.concat(
|
return Stream.concat(
|
||||||
Stream.of(node),
|
Stream.of(node),
|
||||||
node.getSubNodes().stream().flatMap(this::apply));
|
node.getSubNodes().stream().flatMap(this::apply));
|
||||||
@ -562,11 +436,11 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
};
|
};
|
||||||
|
|
||||||
return stripeNodeMap.values().stream()
|
return stripeNodeMap.values().stream()
|
||||||
.flatMap(flattener)
|
.flatMap(stripeFlattener)
|
||||||
.filter(p).collect(Collectors.toList());
|
.filter(p).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
Iterable<EventStripeNode> getAllNodes() {
|
Iterable<EventBundleNodeBase<?, ?, ?>> getAllNodes() {
|
||||||
return getNodes(x -> true);
|
return getNodes(x -> true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -584,131 +458,114 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
* layout the nodes in the given list, starting form the given minimum y
|
* layout the nodes in the given list, starting form the given minimum y
|
||||||
* coordinate.
|
* coordinate.
|
||||||
*
|
*
|
||||||
* @param nodes
|
* Layout the nodes representing events via the following algorithm.
|
||||||
* @param minY
|
*
|
||||||
|
* 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.
|
||||||
*/
|
*/
|
||||||
private synchronized double layoutNodes(final Collection< EventStripeNode> nodes, final double minY, final double xOffset) {
|
synchronized double layoutEventBundleNodes(final Collection<? extends EventBundleNodeBase<?, ?, ?>> nodes, final double minY) {
|
||||||
//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 from y value (ranges) to right most occupied x value.
|
||||||
Map<Integer, Double> maxXatY = new HashMap<>();
|
TreeRangeMap<Double, Double> treeRangeMap = TreeRangeMap.create();
|
||||||
|
// maximum y values occupied by any of the given nodes, updated as nodes are layed out.
|
||||||
double localMax = minY;
|
double localMax = minY;
|
||||||
//for each node lay size it and position it in first available slot
|
|
||||||
|
|
||||||
for (EventStripeNode stripeNode : nodes) {
|
//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.setDescriptionVisibilityLevel(descrVisibility.get());
|
||||||
|
bundleNode.setDescriptionWidth(truncateAll.get() ? truncateWidth.get() : USE_PREF_SIZE);
|
||||||
|
|
||||||
stripeNode.setDescriptionVisibility(descrVisibility.get());
|
//do recursive layout
|
||||||
double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));
|
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;
|
||||||
|
|
||||||
//position of start and end according to range of axis
|
//initial test position
|
||||||
double startX = rawDisplayPosition - xOffset;
|
double yTop = minY;
|
||||||
double layoutNodesResultHeight = 0;
|
double yBottom = yTop + h;
|
||||||
|
|
||||||
double span = 0;
|
if (oneEventPerRow.get()) {
|
||||||
|
// if onePerRow, just put it at end
|
||||||
List<EventStripeNode> subNodes = stripeNode.getSubNodes();
|
yTop = (localMax + MINIMUM_EVENT_NODE_GAP);
|
||||||
if (subNodes.isEmpty() == false) {
|
yBottom = yTop + h;
|
||||||
Map<Boolean, List<EventStripeNode>> hiddenPartition = subNodes.stream()
|
} else {
|
||||||
.collect(Collectors.partitioningBy(testNode -> getController().getQuickHideFilters().stream()
|
//until the node is not overlapping any others try moving it down.
|
||||||
.filter(AbstractFilter::isActive)
|
boolean overlapping = true;
|
||||||
.anyMatch(filter -> filter.getDescription().equals(testNode.getDescription()))));
|
while (overlapping) {
|
||||||
|
overlapping = false;
|
||||||
layoutNodesResultHeight = layoutNodesHelper(hiddenPartition.get(true), hiddenPartition.get(false), minY, rawDisplayPosition);
|
//check each pixel from bottom to top.
|
||||||
}
|
for (double y = yBottom; y >= yTop; y--) {
|
||||||
|
final Double maxX = treeRangeMap.get(y);
|
||||||
List<Double> spanWidths = new ArrayList<>();
|
if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) {
|
||||||
double x = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));;
|
//if that pixel is already used
|
||||||
double x2;
|
//jump top to this y value and repeat until free slot is found.
|
||||||
Iterator<Range<Long>> ranges = stripeNode.getEventStripe().getRanges().iterator();
|
overlapping = true;
|
||||||
Range<Long> range = ranges.next();
|
yTop = y + MINIMUM_EVENT_NODE_GAP;
|
||||||
do {
|
yBottom = yTop + h;
|
||||||
x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint()));
|
break;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} while (ranges.hasNext());
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
treeRangeMap.put(Range.closed(yTop, yBottom), xRight);
|
||||||
}
|
}
|
||||||
//mark used y values
|
|
||||||
for (double y = yPos; y <= yPos2; y++) {
|
localMax = Math.max(yBottom, localMax);
|
||||||
maxXatY.put((int) y, xRight);
|
|
||||||
}
|
//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();
|
||||||
}
|
}
|
||||||
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; //return new max
|
||||||
return localMax - minY;
|
}
|
||||||
|
|
||||||
|
private double getXForEpochMillis(Long millis) {
|
||||||
|
DateTime dateTime = new DateTime(millis, TimeLineController.getJodaTimeZone());
|
||||||
|
return getXAxis().getDisplayPosition(new DateTime(dateTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void layoutProjectionMap() {
|
private void layoutProjectionMap() {
|
||||||
for (final Map.Entry<Range<Long>, Line> entry : projectionMap.entrySet()) {
|
for (final Map.Entry<EventCluster, Line> entry : projectionMap.entrySet()) {
|
||||||
final Range<Long> range = entry.getKey();
|
final EventCluster cluster = entry.getKey();
|
||||||
final Line line = entry.getValue();
|
final Line line = entry.getValue();
|
||||||
|
|
||||||
line.setStartX(getParentXForEpochMillis(range.lowerEndpoint()));
|
line.setStartX(getParentXForEpochMillis(cluster.getStartMillis()));
|
||||||
line.setEndX(getParentXForEpochMillis(range.upperEndpoint()));
|
line.setEndX(getParentXForEpochMillis(cluster.getEndMillis()));
|
||||||
|
|
||||||
line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
|
line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
|
||||||
line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
|
line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
|
||||||
@ -727,10 +584,6 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
return filteredEvents;
|
return filteredEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
Property<Boolean> alternateLayoutProperty() {
|
|
||||||
return alternateLayout;
|
|
||||||
}
|
|
||||||
|
|
||||||
static private class DetailIntervalSelector extends IntervalSelector<DateTime> {
|
static private class DetailIntervalSelector extends IntervalSelector<DateTime> {
|
||||||
|
|
||||||
DetailIntervalSelector(double x, double height, Axis<DateTime> axis, TimeLineController controller) {
|
DetailIntervalSelector(double x, double height, Axis<DateTime> axis, TimeLineController controller) {
|
||||||
@ -778,6 +631,45 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class SelectionChangeHandler implements ListChangeListener<EventBundleNodeBase<?, ?, ?>> {
|
||||||
|
|
||||||
|
private final Axis<DateTime> dateAxis;
|
||||||
|
|
||||||
|
SelectionChangeHandler() {
|
||||||
|
dateAxis = getXAxis();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChanged(ListChangeListener.Change<? extends EventBundleNodeBase<?, ?, ?>> 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
EventDetailChart.this.controller.selectEventIDs(selectedNodes.stream()
|
||||||
|
.flatMap(detailNode -> detailNode.getEventIDs().stream())
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class HideDescriptionAction extends Action {
|
class HideDescriptionAction extends Action {
|
||||||
|
|
||||||
HideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
|
HideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
|
||||||
@ -792,12 +684,13 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
DescriptionFilter descriptionFilter = getController().getQuickHideFilters().stream()
|
DescriptionFilter descriptionFilter = getController().getQuickHideFilters().stream()
|
||||||
.filter(testFilter::equals)
|
.filter(testFilter::equals)
|
||||||
.findFirst().orElseGet(() -> {
|
.findFirst().orElseGet(() -> {
|
||||||
testFilter.selectedProperty().addListener(layoutInvalidationListener);
|
testFilter.selectedProperty().addListener((Observable observable) -> {
|
||||||
|
layoutPlotChildren();
|
||||||
|
});
|
||||||
getController().getQuickHideFilters().add(testFilter);
|
getController().getQuickHideFilters().add(testFilter);
|
||||||
return testFilter;
|
return testFilter;
|
||||||
});
|
});
|
||||||
descriptionFilter.setSelected(true);
|
descriptionFilter.setSelected(true);
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -805,7 +698,6 @@ public final class EventDetailChart extends XYChart<DateTime, EventCluster> impl
|
|||||||
class UnhideDescriptionAction extends Action {
|
class UnhideDescriptionAction extends Action {
|
||||||
|
|
||||||
UnhideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
|
UnhideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
|
||||||
|
|
||||||
super("Unhide");
|
super("Unhide");
|
||||||
setGraphic(new ImageView(SHOW));
|
setGraphic(new ImageView(SHOW));
|
||||||
setEventHandler((ActionEvent t) ->
|
setEventHandler((ActionEvent t) ->
|
||||||
|
@ -19,107 +19,30 @@
|
|||||||
*/
|
*/
|
||||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
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.event.EventHandler;
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Cursor;
|
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ContextMenu;
|
import javafx.scene.control.ContextMenu;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.MenuItem;
|
||||||
import javafx.scene.control.OverrunStyle;
|
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.image.ImageView;
|
||||||
import javafx.scene.input.MouseButton;
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.input.MouseEvent;
|
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.layout.VBox;
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.controlsfx.control.action.Action;
|
|
||||||
import org.controlsfx.control.action.ActionUtils;
|
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.coreutils.Logger;
|
||||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
|
||||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||||
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
|
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
|
||||||
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.configureLoDButton;
|
||||||
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 EventDetailChart} to represent an EventStripe.
|
* Node used in {@link EventDetailChart} to represent an EventStripe.
|
||||||
*/
|
*/
|
||||||
final public class EventStripeNode extends StackPane {
|
final public class EventStripeNode extends EventBundleNodeBase<EventStripe, EventCluster, EventClusterNode> {
|
||||||
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(EventStripeNode.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(EventStripeNode.class.getName());
|
||||||
private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N
|
final Button hideButton;
|
||||||
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<EventType, DropShadow> 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<DescriptionLoD> descLOD = new SimpleObjectProperty<>();
|
|
||||||
private DescriptionVisibility descrVis;
|
|
||||||
private Tooltip tooltip;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pane that contains EventStripeNodes for any 'subevents' if they are
|
* Pane that contains EventStripeNodes for any 'subevents' if they are
|
||||||
* displayed
|
* displayed
|
||||||
@ -127,267 +50,60 @@ final public class EventStripeNode extends StackPane {
|
|||||||
* //TODO: move more of the control of subnodes/events here and out of
|
* //TODO: move more of the control of subnodes/events here and out of
|
||||||
* EventDetail Chart
|
* EventDetail Chart
|
||||||
*/
|
*/
|
||||||
private final Pane subNodePane = new Pane();
|
// private final HBox clustersHBox = new HBox();
|
||||||
private final HBox clustersHBox = new HBox();
|
|
||||||
private final ImageView eventTypeImageView = new ImageView();
|
private final ImageView eventTypeImageView = new ImageView();
|
||||||
private final Label descrLabel = new Label("", eventTypeImageView);
|
|
||||||
private final Label countLabel = new Label();
|
|
||||||
private final Button plusButton = ActionUtils.createButton(new ExpandClusterAction(), ActionUtils.ActionTextBehavior.HIDE);
|
|
||||||
private final Button minusButton = ActionUtils.createButton(new CollapseClusterAction(), ActionUtils.ActionTextBehavior.HIDE);
|
|
||||||
private final ImageView hashIV = new ImageView(HASH_PIN);
|
|
||||||
private final ImageView tagIV = new ImageView(TAG);
|
|
||||||
private final HBox infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV, minusButton, plusButton);
|
|
||||||
|
|
||||||
private final Background highlightedBackground;
|
public EventStripeNode(EventDetailChart chart, EventStripe eventStripe, EventClusterNode parentNode) {
|
||||||
private final Background defaultBackground;
|
super(chart, eventStripe, parentNode);
|
||||||
private final EventDetailChart chart;
|
|
||||||
private final SleuthkitCase sleuthkitCase;
|
|
||||||
private final EventStripe eventStripe;
|
|
||||||
private final EventStripeNode parentNode;
|
|
||||||
private final FilteredEventsModel eventsModel;
|
|
||||||
private final Button hideButton;
|
|
||||||
|
|
||||||
public EventStripeNode(EventDetailChart chart, EventStripe eventStripe, EventStripeNode parentEventNode) {
|
setMinHeight(48);
|
||||||
this.eventStripe = eventStripe;
|
|
||||||
this.parentNode = parentEventNode;
|
|
||||||
this.chart = chart;
|
|
||||||
descLOD.set(eventStripe.getDescriptionLoD());
|
|
||||||
sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase();
|
|
||||||
eventsModel = chart.getController().getEventsModel();
|
|
||||||
final Color 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));
|
|
||||||
|
|
||||||
setBackground(defaultBackground);
|
EventDetailChart.HideDescriptionAction hideClusterAction = chart.new HideDescriptionAction(getDescription(), eventBundle.getDescriptionLoD());
|
||||||
|
|
||||||
setAlignment(Pos.TOP_LEFT);
|
|
||||||
setMinHeight(24);
|
|
||||||
setPrefHeight(USE_COMPUTED_SIZE);
|
|
||||||
setMaxHeight(USE_PREF_SIZE);
|
|
||||||
setMaxWidth(USE_PREF_SIZE);
|
|
||||||
minWidthProperty().bind(clustersHBox.widthProperty());
|
|
||||||
setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(eventStripe.getStartMillis())) - getLayoutXCompensation());
|
|
||||||
|
|
||||||
if (eventStripe.getEventIDsWithHashHits().isEmpty()) {
|
|
||||||
show(hashIV, false);
|
|
||||||
}
|
|
||||||
if (eventStripe.getEventIDsWithTags().isEmpty()) {
|
|
||||||
show(tagIV, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
EventDetailChart.HideDescriptionAction hideClusterAction = chart.new HideDescriptionAction(getDescription(), eventStripe.getDescriptionLoD());
|
|
||||||
hideButton = ActionUtils.createButton(hideClusterAction, ActionUtils.ActionTextBehavior.HIDE);
|
hideButton = ActionUtils.createButton(hideClusterAction, ActionUtils.ActionTextBehavior.HIDE);
|
||||||
configureLoDButton(hideButton);
|
configureLoDButton(hideButton);
|
||||||
configureLoDButton(plusButton);
|
|
||||||
configureLoDButton(minusButton);
|
|
||||||
|
|
||||||
//initialize info hbox
|
infoHBox.getChildren().add(hideButton);
|
||||||
infoHBox.getChildren().add(4, hideButton);
|
|
||||||
|
|
||||||
infoHBox.setMinWidth(USE_PREF_SIZE);
|
|
||||||
infoHBox.setPadding(new Insets(2, 5, 2, 5));
|
|
||||||
infoHBox.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
//setup description label
|
//setup description label
|
||||||
|
|
||||||
eventTypeImageView.setImage(getEventType().getFXImage());
|
eventTypeImageView.setImage(getEventType().getFXImage());
|
||||||
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
|
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
|
||||||
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
|
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
|
||||||
descrLabel.setMouseTransparent(true);
|
descrLabel.setGraphic(eventTypeImageView);
|
||||||
|
|
||||||
//set up subnode pane sizing contraints
|
setAlignment(subNodePane, Pos.BOTTOM_LEFT);
|
||||||
subNodePane.setPrefHeight(USE_COMPUTED_SIZE);
|
for (EventCluster cluster : eventStripe.getClusters()) {
|
||||||
subNodePane.setMinHeight(USE_PREF_SIZE);
|
EventClusterNode clusterNode = new EventClusterNode(chart, cluster, this);
|
||||||
subNodePane.setMinWidth(USE_PREF_SIZE);
|
subNodes.add(clusterNode);
|
||||||
subNodePane.setMaxHeight(USE_PREF_SIZE);
|
subNodePane.getChildren().addAll(clusterNode);
|
||||||
subNodePane.setMaxWidth(USE_PREF_SIZE);
|
|
||||||
subNodePane.setPickOnBounds(false);
|
|
||||||
|
|
||||||
Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS));
|
|
||||||
for (Range<Long> 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);
|
|
||||||
|
|
||||||
final VBox internalVBox = new VBox(infoHBox, subNodePane);
|
getChildren().addAll(new VBox(infoHBox, subNodePane));
|
||||||
internalVBox.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
getChildren().addAll(clustersHBox, internalVBox);
|
|
||||||
|
|
||||||
setCursor(Cursor.HAND);
|
|
||||||
setOnMouseClicked(new MouseClickHandler());
|
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void showDescriptionLoDControls(final boolean showControls) {
|
@Override
|
||||||
DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(),
|
void showHoverControls(final boolean showControls) {
|
||||||
eventType -> new DropShadow(10, eventType.getColor()));
|
super.showHoverControls(showControls);
|
||||||
clustersHBox.setEffect(showControls ? dropShadow : null);
|
|
||||||
show(minusButton, showControls);
|
|
||||||
show(plusButton, showControls);
|
|
||||||
show(hideButton, showControls);
|
show(hideButton, showControls);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSpanWidths(List<Double> 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() {
|
public EventStripe getEventStripe() {
|
||||||
return eventStripe;
|
return getEventBundle();
|
||||||
}
|
|
||||||
|
|
||||||
Collection<EventStripe> makeBundlesFromClusters(List<EventCluster> 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<String> tooltTipTask = new Task<String>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String call() throws Exception {
|
|
||||||
HashMap<String, Long> hashSetCounts = new HashMap<>();
|
|
||||||
if (!eventStripe.getEventIDsWithHashHits().isEmpty()) {
|
|
||||||
hashSetCounts = new HashMap<>();
|
|
||||||
try {
|
|
||||||
for (TimeLineEvent tle : eventsModel.getEventsById(eventStripe.getEventIDsWithHashHits())) {
|
|
||||||
Set<String> 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<String, Long> tagCounts = new HashMap<>();
|
|
||||||
if (getEventStripe().getEventIDsWithTags().isEmpty() == false) {
|
|
||||||
tagCounts.putAll(eventsModel.getTagCountsByTagName(getEventStripe().getEventIDsWithTags()));
|
|
||||||
}
|
|
||||||
|
|
||||||
String hashSetCountsString = hashSetCounts.entrySet().stream()
|
|
||||||
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
|
|
||||||
.collect(Collectors.joining("\n"));
|
|
||||||
String tagCountsString = tagCounts.entrySet().stream()
|
|
||||||
.map((Map.Entry<String, Long> 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<EventStripeNode> 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
|
* @param w the maximum width the description label should have
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public void setDescriptionWidth(double w) {
|
public void setDescriptionWidth(double w) {
|
||||||
descrLabel.setMaxWidth(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
|
* apply the 'effect' to visually indicate highlighted nodes
|
||||||
*
|
*
|
||||||
* @param applied true to apply the highlight 'effect', false to remove it
|
* @param applied true to apply the highlight 'effect', false to remove it
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public synchronized void applyHighlightEffect(boolean applied) {
|
public synchronized void applyHighlightEffect(boolean applied) {
|
||||||
if (applied) {
|
if (applied) {
|
||||||
descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS
|
descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS
|
||||||
@ -398,116 +114,11 @@ final public class EventStripeNode extends StackPane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DescriptionLoD getDescriptionLoD() {
|
@Override
|
||||||
return descLOD.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.getEventBundles().removeIf(bundle ->
|
|
||||||
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<Collection<EventStripe>> loggedTask = new Task<Collection<EventStripe>>() {
|
|
||||||
|
|
||||||
private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail);
|
|
||||||
|
|
||||||
{
|
|
||||||
updateTitle(Bundle.EventStripeNode_loggedTask_name());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Collection<EventStripe> call() throws Exception {
|
|
||||||
Collection<EventStripe> 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<EventStripe> 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) {
|
public void setDescriptionVisibility(DescriptionVisibility descrVis) {
|
||||||
this.descrVis = descrVis;
|
final int size = getEventStripe().getEventIDs().size();
|
||||||
final int size = eventStripe.getEventIDs().size();
|
|
||||||
|
|
||||||
switch (this.descrVis) {
|
switch (descrVis) {
|
||||||
case HIDDEN:
|
case HIDDEN:
|
||||||
countLabel.setText("");
|
countLabel.setText("");
|
||||||
descrLabel.setText("");
|
descrLabel.setText("");
|
||||||
@ -518,7 +129,7 @@ final public class EventStripeNode extends StackPane {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
case SHOWN:
|
case SHOWN:
|
||||||
String description = eventStripe.getDescription();
|
String description = getEventStripe().getDescription();
|
||||||
description = parentNode != null
|
description = parentNode != null
|
||||||
? " ..." + StringUtils.substringAfter(description, parentNode.getDescription())
|
? " ..." + StringUtils.substringAfter(description, parentNode.getDescription())
|
||||||
: description;
|
: description;
|
||||||
@ -528,10 +139,6 @@ final public class EventStripeNode extends StackPane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<Long> getEventsIDs() {
|
|
||||||
return eventStripe.getEventIDs();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* event handler used for mouse events on {@link EventStripeNode}s
|
* event handler used for mouse events on {@link EventStripeNode}s
|
||||||
*/
|
*/
|
||||||
@ -543,19 +150,13 @@ final public class EventStripeNode extends StackPane {
|
|||||||
public void handle(MouseEvent t) {
|
public void handle(MouseEvent t) {
|
||||||
|
|
||||||
if (t.getButton() == MouseButton.PRIMARY) {
|
if (t.getButton() == MouseButton.PRIMARY) {
|
||||||
t.consume();
|
|
||||||
if (t.isShiftDown()) {
|
if (t.isShiftDown()) {
|
||||||
if (chart.selectedNodes.contains(EventStripeNode.this) == false) {
|
if (chart.selectedNodes.contains(EventStripeNode.this) == false) {
|
||||||
chart.selectedNodes.add(EventStripeNode.this);
|
chart.selectedNodes.add(EventStripeNode.this);
|
||||||
}
|
}
|
||||||
} else if (t.isShortcutDown()) {
|
} else if (t.isShortcutDown()) {
|
||||||
chart.selectedNodes.removeAll(EventStripeNode.this);
|
chart.selectedNodes.removeAll(EventStripeNode.this);
|
||||||
} else if (t.getClickCount() > 1) {
|
|
||||||
final DescriptionLoD next = descLOD.get().moreDetailed();
|
|
||||||
if (next != null) {
|
|
||||||
loadSubBundles(DescriptionLoD.RelativeDetail.MORE);
|
|
||||||
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
chart.selectedNodes.setAll(EventStripeNode.this);
|
chart.selectedNodes.setAll(EventStripeNode.this);
|
||||||
}
|
}
|
||||||
@ -566,10 +167,9 @@ final public class EventStripeNode extends StackPane {
|
|||||||
contextMenu = new ContextMenu();
|
contextMenu = new ContextMenu();
|
||||||
contextMenu.setAutoHide(true);
|
contextMenu.setAutoHide(true);
|
||||||
|
|
||||||
contextMenu.getItems().add(ActionUtils.createMenuItem(new ExpandClusterAction()));
|
EventDetailChart.HideDescriptionAction hideClusterAction = chart.new HideDescriptionAction(getDescription(), eventBundle.getDescriptionLoD());
|
||||||
contextMenu.getItems().add(ActionUtils.createMenuItem(new CollapseClusterAction()));
|
MenuItem hideDescriptionMenuItem = ActionUtils.createMenuItem(hideClusterAction);
|
||||||
|
contextMenu.getItems().addAll(hideDescriptionMenuItem);
|
||||||
contextMenu.getItems().add(new SeparatorMenuItem());
|
|
||||||
contextMenu.getItems().addAll(chartContextMenu.getItems());
|
contextMenu.getItems().addAll(chartContextMenu.getItems());
|
||||||
}
|
}
|
||||||
contextMenu.show(EventStripeNode.this, t.getScreenX(), t.getScreenY());
|
contextMenu.show(EventStripeNode.this, t.getScreenX(), t.getScreenY());
|
||||||
@ -578,38 +178,4 @@ final public class EventStripeNode extends StackPane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ExpandClusterAction extends Action {
|
|
||||||
|
|
||||||
@NbBundle.Messages("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("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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -35,13 +35,13 @@ class EventDescriptionTreeItem extends NavTreeItem {
|
|||||||
* maps a description to the child item of this item with that description
|
* maps a description to the child item of this item with that description
|
||||||
*/
|
*/
|
||||||
private final Map<String, EventDescriptionTreeItem> childMap = new ConcurrentHashMap<>();
|
private final Map<String, EventDescriptionTreeItem> childMap = new ConcurrentHashMap<>();
|
||||||
private final EventBundle bundle;
|
private final EventBundle<?> bundle;
|
||||||
|
|
||||||
public EventBundle getEventBundle() {
|
public EventBundle<?> getEventBundle() {
|
||||||
return bundle;
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
EventDescriptionTreeItem(EventBundle g) {
|
EventDescriptionTreeItem(EventBundle<?> g) {
|
||||||
bundle = g;
|
bundle = g;
|
||||||
setValue(g);
|
setValue(g);
|
||||||
}
|
}
|
||||||
@ -51,8 +51,8 @@ class EventDescriptionTreeItem extends NavTreeItem {
|
|||||||
return getValue().getCount();
|
return getValue().getCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void insert(Deque<EventBundle> path) {
|
public void insert(Deque<EventBundle<?>> path) {
|
||||||
EventBundle head = path.removeFirst();
|
EventBundle<?> head = path.removeFirst();
|
||||||
EventDescriptionTreeItem treeItem = childMap.get(head.getDescription());
|
EventDescriptionTreeItem treeItem = childMap.get(head.getDescription());
|
||||||
if (treeItem == null) {
|
if (treeItem == null) {
|
||||||
treeItem = new EventDescriptionTreeItem(head);
|
treeItem = new EventDescriptionTreeItem(head);
|
||||||
@ -68,12 +68,12 @@ class EventDescriptionTreeItem extends NavTreeItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void resort(Comparator<TreeItem<EventBundle>> comp) {
|
public void resort(Comparator<TreeItem<EventBundle<?>>> comp) {
|
||||||
FXCollections.sort(getChildren(), comp);
|
FXCollections.sort(getChildren(), comp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NavTreeItem findTreeItemForEvent(EventBundle t) {
|
public NavTreeItem findTreeItemForEvent(EventBundle<?> t) {
|
||||||
|
|
||||||
if (getValue().getEventType() == t.getEventType()
|
if (getValue().getEventType() == t.getEventType()
|
||||||
&& getValue().getDescription().equals(t.getDescription())) {
|
&& getValue().getDescription().equals(t.getDescription())) {
|
||||||
|
@ -33,9 +33,9 @@ class EventTypeTreeItem extends NavTreeItem {
|
|||||||
*/
|
*/
|
||||||
private final Map<String, EventDescriptionTreeItem> childMap = new ConcurrentHashMap<>();
|
private final Map<String, EventDescriptionTreeItem> childMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private final Comparator<TreeItem<EventBundle>> comparator = TreeComparator.Description;
|
private final Comparator<TreeItem<EventBundle<?>>> comparator = TreeComparator.Description;
|
||||||
|
|
||||||
EventTypeTreeItem(EventBundle g) {
|
EventTypeTreeItem(EventBundle<?> g) {
|
||||||
setValue(g);
|
setValue(g);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,8 +44,8 @@ class EventTypeTreeItem extends NavTreeItem {
|
|||||||
return getValue().getCount();
|
return getValue().getCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void insert(Deque<EventBundle> path) {
|
public void insert(Deque<EventBundle<?>> path) {
|
||||||
EventBundle head = path.removeFirst();
|
EventBundle<?> head = path.removeFirst();
|
||||||
EventDescriptionTreeItem treeItem = childMap.get(head.getDescription());
|
EventDescriptionTreeItem treeItem = childMap.get(head.getDescription());
|
||||||
if (treeItem == null) {
|
if (treeItem == null) {
|
||||||
treeItem = new EventDescriptionTreeItem(head);
|
treeItem = new EventDescriptionTreeItem(head);
|
||||||
@ -61,7 +61,7 @@ class EventTypeTreeItem extends NavTreeItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NavTreeItem findTreeItemForEvent(EventBundle t) {
|
public NavTreeItem findTreeItemForEvent(EventBundle<?> t) {
|
||||||
if (t.getEventType().getBaseType() == getValue().getEventType().getBaseType()) {
|
if (t.getEventType().getBaseType() == getValue().getEventType().getBaseType()) {
|
||||||
|
|
||||||
for (EventDescriptionTreeItem child : childMap.values()) {
|
for (EventDescriptionTreeItem child : childMap.values()) {
|
||||||
@ -75,7 +75,7 @@ class EventTypeTreeItem extends NavTreeItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void resort(Comparator<TreeItem<EventBundle>> comp) {
|
public void resort(Comparator<TreeItem<EventBundle<?>>> comp) {
|
||||||
FXCollections.sort(getChildren(), comp);
|
FXCollections.sort(getChildren(), comp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,13 +68,13 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
|||||||
private DetailViewPane detailViewPane;
|
private DetailViewPane detailViewPane;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private TreeView<EventBundle> eventsTree;
|
private TreeView<EventBundle<?>> eventsTree;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Label eventsTreeLabel;
|
private Label eventsTreeLabel;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private ComboBox<Comparator<TreeItem<EventBundle>>> sortByBox;
|
private ComboBox<Comparator<TreeItem<EventBundle<?>>>> sortByBox;
|
||||||
|
|
||||||
public NavPanel() {
|
public NavPanel() {
|
||||||
FXMLConstructor.construct(this, "NavPanel.fxml"); // NON-NLS
|
FXMLConstructor.construct(this, "NavPanel.fxml"); // NON-NLS
|
||||||
@ -91,8 +91,8 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
|||||||
|
|
||||||
detailViewPane.getSelectedNodes().addListener((Observable observable) -> {
|
detailViewPane.getSelectedNodes().addListener((Observable observable) -> {
|
||||||
eventsTree.getSelectionModel().clearSelection();
|
eventsTree.getSelectionModel().clearSelection();
|
||||||
detailViewPane.getSelectedNodes().forEach(eventStripeNode -> {
|
detailViewPane.getSelectedNodes().forEach(eventBundleNode -> {
|
||||||
eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventStripeNode.getEventStripe()));
|
eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventBundleNode.getEventBundle()));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
|||||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||||
private void setRoot() {
|
private void setRoot() {
|
||||||
RootItem root = new RootItem();
|
RootItem root = new RootItem();
|
||||||
for (EventBundle bundle : detailViewPane.getEventBundles()) {
|
for (EventBundle<?> bundle : detailViewPane.getEventBundles()) {
|
||||||
root.insert(bundle);
|
root.insert(bundle);
|
||||||
}
|
}
|
||||||
eventsTree.setRoot(root);
|
eventsTree.setRoot(root);
|
||||||
@ -134,7 +134,7 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
|||||||
getRoot().resort(sortByBox.getSelectionModel().getSelectedItem());
|
getRoot().resort(sortByBox.getSelectionModel().getSelectedItem());
|
||||||
});
|
});
|
||||||
eventsTree.setShowRoot(false);
|
eventsTree.setShowRoot(false);
|
||||||
eventsTree.setCellFactory((TreeView<EventBundle> p) -> new EventBundleTreeCell());
|
eventsTree.setCellFactory((TreeView<EventBundle<?>> p) -> new EventBundleTreeCell());
|
||||||
eventsTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
eventsTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||||
|
|
||||||
eventsTreeLabel.setText(NbBundle.getMessage(this.getClass(), "NavPanel.eventsTreeLabel.text"));
|
eventsTreeLabel.setText(NbBundle.getMessage(this.getClass(), "NavPanel.eventsTreeLabel.text"));
|
||||||
@ -144,7 +144,7 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
|||||||
* A tree cell to display {@link EventBundle}s. Shows the description, and
|
* A tree cell to display {@link EventBundle}s. Shows the description, and
|
||||||
* count, as well a a "legend icon" for the event type.
|
* count, as well a a "legend icon" for the event type.
|
||||||
*/
|
*/
|
||||||
private class EventBundleTreeCell extends TreeCell<EventBundle> {
|
private class EventBundleTreeCell extends TreeCell<EventBundle<?>> {
|
||||||
|
|
||||||
private static final double HIDDEN_MULTIPLIER = .6;
|
private static final double HIDDEN_MULTIPLIER = .6;
|
||||||
private final Rectangle rect = new Rectangle(24, 24);
|
private final Rectangle rect = new Rectangle(24, 24);
|
||||||
@ -158,7 +158,7 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(EventBundle item, boolean empty) {
|
protected void updateItem(EventBundle<?> item, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(item, empty);
|
||||||
if (item == null || empty) {
|
if (item == null || empty) {
|
||||||
setText(null);
|
setText(null);
|
||||||
@ -177,7 +177,7 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
|||||||
});
|
});
|
||||||
registerListeners(controller.getQuickHideFilters(), item);
|
registerListeners(controller.getQuickHideFilters(), item);
|
||||||
String text = item.getDescription() + " (" + item.getCount() + ")"; // NON-NLS
|
String text = item.getDescription() + " (" + item.getCount() + ")"; // NON-NLS
|
||||||
TreeItem<EventBundle> parent = getTreeItem().getParent();
|
TreeItem<EventBundle<?>> parent = getTreeItem().getParent();
|
||||||
if (parent != null && parent.getValue() != null && (parent instanceof EventDescriptionTreeItem)) {
|
if (parent != null && parent.getValue() != null && (parent instanceof EventDescriptionTreeItem)) {
|
||||||
text = StringUtils.substringAfter(text, parent.getValue().getDescription());
|
text = StringUtils.substringAfter(text, parent.getValue().getDescription());
|
||||||
}
|
}
|
||||||
@ -189,7 +189,7 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerListeners(Collection<? extends DescriptionFilter> filters, EventBundle item) {
|
private void registerListeners(Collection<? extends DescriptionFilter> filters, EventBundle<?> item) {
|
||||||
for (DescriptionFilter filter : filters) {
|
for (DescriptionFilter filter : filters) {
|
||||||
if (filter.getDescription().equals(item.getDescription())) {
|
if (filter.getDescription().equals(item.getDescription())) {
|
||||||
filter.activeProperty().addListener(filterStateChangeListener);
|
filter.activeProperty().addListener(filterStateChangeListener);
|
||||||
@ -205,8 +205,8 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateHiddenState(EventBundle item) {
|
private void updateHiddenState(EventBundle<?> item) {
|
||||||
TreeItem<EventBundle> treeItem = getTreeItem();
|
TreeItem<EventBundle<?>> treeItem = getTreeItem();
|
||||||
ContextMenu newMenu;
|
ContextMenu newMenu;
|
||||||
if (controller.getQuickHideFilters().stream().
|
if (controller.getQuickHideFilters().stream().
|
||||||
filter(AbstractFilter::isActive)
|
filter(AbstractFilter::isActive)
|
||||||
|
@ -28,11 +28,11 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
|||||||
* {@link EventTreeCell}. Each NavTreeItem has a EventBundle which has a type,
|
* {@link EventTreeCell}. Each NavTreeItem has a EventBundle which has a type,
|
||||||
* description , count, etc.
|
* description , count, etc.
|
||||||
*/
|
*/
|
||||||
abstract class NavTreeItem extends TreeItem<EventBundle> {
|
abstract class NavTreeItem extends TreeItem<EventBundle<?>> {
|
||||||
|
|
||||||
abstract long getCount();
|
abstract long getCount();
|
||||||
|
|
||||||
abstract void resort(Comparator<TreeItem<EventBundle>> comp);
|
abstract void resort(Comparator<TreeItem<EventBundle<?>>> comp);
|
||||||
|
|
||||||
abstract NavTreeItem findTreeItemForEvent(EventBundle t);
|
abstract NavTreeItem findTreeItemForEvent(EventBundle<?> t);
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ class RootItem extends NavTreeItem {
|
|||||||
*
|
*
|
||||||
* @param g Group to add
|
* @param g Group to add
|
||||||
*/
|
*/
|
||||||
public void insert(EventBundle g) {
|
public void insert(EventBundle<?> g) {
|
||||||
|
|
||||||
EventTypeTreeItem treeItem = childMap.computeIfAbsent(g.getEventType().getBaseType(),
|
EventTypeTreeItem treeItem = childMap.computeIfAbsent(g.getEventType().getBaseType(),
|
||||||
baseType -> {
|
baseType -> {
|
||||||
@ -69,12 +69,12 @@ class RootItem extends NavTreeItem {
|
|||||||
treeItem.insert(getTreePath(g));
|
treeItem.insert(getTreePath(g));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Deque<EventBundle> getTreePath(EventBundle g) {
|
static Deque< EventBundle<?>> getTreePath(EventBundle<?> g) {
|
||||||
Deque<EventBundle> path = new ArrayDeque<>();
|
Deque<EventBundle<?>> path = new ArrayDeque<>();
|
||||||
Optional<EventBundle> p = Optional.of(g);
|
Optional<? extends EventBundle<?>> p = Optional.of(g);
|
||||||
|
|
||||||
while (p.isPresent()) {
|
while (p.isPresent()) {
|
||||||
EventBundle parent = p.get();
|
EventBundle<?> parent = p.get();
|
||||||
path.addFirst(parent);
|
path.addFirst(parent);
|
||||||
p = parent.getParentBundle();
|
p = parent.getParentBundle();
|
||||||
}
|
}
|
||||||
@ -83,12 +83,12 @@ class RootItem extends NavTreeItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void resort(Comparator<TreeItem<EventBundle>> comp) {
|
public void resort(Comparator<TreeItem<EventBundle<?>>> comp) {
|
||||||
childMap.values().forEach(ti -> ti.resort(comp));
|
childMap.values().forEach(ti -> ti.resort(comp));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NavTreeItem findTreeItemForEvent(EventBundle t) {
|
public NavTreeItem findTreeItemForEvent(EventBundle<?> t) {
|
||||||
for (EventTypeTreeItem child : childMap.values()) {
|
for (EventTypeTreeItem child : childMap.values()) {
|
||||||
final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t);
|
final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t);
|
||||||
if (findTreeItemForEvent != null) {
|
if (findTreeItemForEvent != null) {
|
||||||
|
@ -23,23 +23,23 @@ import javafx.scene.control.TreeItem;
|
|||||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||||
|
|
||||||
enum TreeComparator implements Comparator<TreeItem<EventBundle>> {
|
enum TreeComparator implements Comparator<TreeItem<EventBundle<?>>> {
|
||||||
|
|
||||||
Description {
|
Description {
|
||||||
@Override
|
@Override
|
||||||
public int compare(TreeItem<EventBundle> o1, TreeItem<EventBundle> o2) {
|
public int compare(TreeItem<EventBundle<?>> o1, TreeItem<EventBundle<?>> o2) {
|
||||||
return o1.getValue().getDescription().compareTo(o2.getValue().getDescription());
|
return o1.getValue().getDescription().compareTo(o2.getValue().getDescription());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Count {
|
Count {
|
||||||
@Override
|
@Override
|
||||||
public int compare(TreeItem<EventBundle> o1, TreeItem<EventBundle> o2) {
|
public int compare(TreeItem<EventBundle<?>> o1, TreeItem<EventBundle<?>> o2) {
|
||||||
return Long.compare(o2.getValue().getCount(), o1.getValue().getCount());
|
return Long.compare(o2.getValue().getCount(), o1.getValue().getCount());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Type {
|
Type {
|
||||||
@Override
|
@Override
|
||||||
public int compare(TreeItem<EventBundle> o1, TreeItem<EventBundle> o2) {
|
public int compare(TreeItem<EventBundle<?>> o1, TreeItem<EventBundle<?>> o2) {
|
||||||
return EventType.getComparator().compare(o1.getValue().getEventType(), o2.getValue().getEventType());
|
return EventType.getComparator().compare(o1.getValue().getEventType(), o2.getValue().getEventType());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -43,7 +43,7 @@ public enum DescriptionLoD {
|
|||||||
try {
|
try {
|
||||||
return values()[ordinal() + 1];
|
return values()[ordinal() + 1];
|
||||||
} catch (ArrayIndexOutOfBoundsException e) {
|
} catch (ArrayIndexOutOfBoundsException e) {
|
||||||
return null;
|
return FULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ public enum DescriptionLoD {
|
|||||||
try {
|
try {
|
||||||
return values()[ordinal() - 1];
|
return values()[ordinal() - 1];
|
||||||
} catch (ArrayIndexOutOfBoundsException e) {
|
} catch (ArrayIndexOutOfBoundsException e) {
|
||||||
return null;
|
return SHORT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user