mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-15 09:17:42 +00:00
Merge pull request #1595 from millmanorama/develop
Initial Pull request for new style event clustering.
This commit is contained in:
commit
cbbd6c375c
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.datamodel;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
import java.util.Set;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public interface EventBundle {
|
||||
|
||||
String getDescription();
|
||||
|
||||
DescriptionLOD getDescriptionLOD();
|
||||
|
||||
Set<Long> getEventIDs();
|
||||
|
||||
Set<Long> getEventIDsWithHashHits();
|
||||
|
||||
Set<Long> getEventIDsWithTags();
|
||||
|
||||
EventType getEventType();
|
||||
|
||||
long getEndMillis();
|
||||
|
||||
long getStartMillis();
|
||||
|
||||
Iterable<Range<Long>> getRanges();
|
||||
|
||||
}
|
@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.datamodel;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
import com.google.common.collect.Sets;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
@ -33,7 +34,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
|
||||
* designated 'zoom level'.
|
||||
*/
|
||||
@Immutable
|
||||
public class AggregateEvent {
|
||||
public class EventCluster implements EventBundle {
|
||||
|
||||
/**
|
||||
* the smallest time interval containing all the aggregated events
|
||||
@ -72,7 +73,7 @@ public class AggregateEvent {
|
||||
*/
|
||||
private final Set<Long> hashHits;
|
||||
|
||||
public AggregateEvent(Interval spanningInterval, EventType type, Set<Long> eventIDs, Set<Long> hashHits, Set<Long> tagged, String description, DescriptionLOD lod) {
|
||||
public EventCluster(Interval spanningInterval, EventType type, Set<Long> eventIDs, Set<Long> hashHits, Set<Long> tagged, String description, DescriptionLOD lod) {
|
||||
|
||||
this.span = spanningInterval;
|
||||
this.type = type;
|
||||
@ -90,6 +91,14 @@ public class AggregateEvent {
|
||||
return span;
|
||||
}
|
||||
|
||||
public long getStartMillis() {
|
||||
return span.getStartMillis();
|
||||
}
|
||||
|
||||
public long getEndMillis() {
|
||||
return span.getEndMillis();
|
||||
}
|
||||
|
||||
public Set<Long> getEventIDs() {
|
||||
return Collections.unmodifiableSet(eventIDs);
|
||||
}
|
||||
@ -106,36 +115,51 @@ public class AggregateEvent {
|
||||
return description;
|
||||
}
|
||||
|
||||
public EventType getType() {
|
||||
public EventType getEventType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public DescriptionLOD getLOD() {
|
||||
@Override
|
||||
public DescriptionLOD getDescriptionLOD() {
|
||||
return lod;
|
||||
}
|
||||
|
||||
/**
|
||||
* merge two aggregate events into one new aggregate event.
|
||||
*
|
||||
* @param aggEvent1
|
||||
* @param cluster1
|
||||
* @param aggEVent2
|
||||
*
|
||||
* @return a new aggregate event that is the result of merging the given
|
||||
* events
|
||||
*/
|
||||
public static AggregateEvent merge(AggregateEvent aggEvent1, AggregateEvent ag2) {
|
||||
public static EventCluster merge(EventCluster cluster1, EventCluster cluster2) {
|
||||
|
||||
if (aggEvent1.getType() != ag2.getType()) {
|
||||
if (cluster1.getEventType() != cluster2.getEventType()) {
|
||||
throw new IllegalArgumentException("aggregate events are not compatible they have different types");
|
||||
}
|
||||
|
||||
if (!aggEvent1.getDescription().equals(ag2.getDescription())) {
|
||||
if (!cluster1.getDescription().equals(cluster2.getDescription())) {
|
||||
throw new IllegalArgumentException("aggregate events are not compatible they have different descriptions");
|
||||
}
|
||||
Sets.SetView<Long> idsUnion = Sets.union(aggEvent1.getEventIDs(), ag2.getEventIDs());
|
||||
Sets.SetView<Long> hashHitsUnion = Sets.union(aggEvent1.getEventIDsWithHashHits(), ag2.getEventIDsWithHashHits());
|
||||
Sets.SetView<Long> taggedUnion = Sets.union(aggEvent1.getEventIDsWithTags(), ag2.getEventIDsWithTags());
|
||||
Sets.SetView<Long> idsUnion = Sets.union(cluster1.getEventIDs(), cluster2.getEventIDs());
|
||||
Sets.SetView<Long> hashHitsUnion = Sets.union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits());
|
||||
Sets.SetView<Long> taggedUnion = Sets.union(cluster1.getEventIDsWithTags(), cluster2.getEventIDsWithTags());
|
||||
|
||||
return new AggregateEvent(IntervalUtils.span(aggEvent1.span, ag2.span), aggEvent1.getType(), idsUnion, hashHitsUnion, taggedUnion, aggEvent1.getDescription(), aggEvent1.lod);
|
||||
return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod);
|
||||
}
|
||||
|
||||
Range<Long> getRange() {
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.datamodel;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Range;
|
||||
import com.google.common.collect.RangeMap;
|
||||
import com.google.common.collect.RangeSet;
|
||||
import com.google.common.collect.TreeRangeMap;
|
||||
import com.google.common.collect.TreeRangeSet;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import org.python.google.common.base.Objects;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@Immutable
|
||||
public final class EventStripe implements EventBundle {
|
||||
|
||||
private final RangeSet<Long> spans = TreeRangeSet.create();
|
||||
private final RangeMap<Long, EventCluster> spanMap = TreeRangeMap.create();
|
||||
|
||||
/**
|
||||
* the type of all the aggregted events
|
||||
*/
|
||||
private final EventType type;
|
||||
|
||||
/**
|
||||
* the common description of all the aggregated events
|
||||
*/
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* the description level of detail that the events were aggregated at.
|
||||
*/
|
||||
private final DescriptionLOD lod;
|
||||
|
||||
/**
|
||||
* the set of ids of the aggregated events
|
||||
*/
|
||||
private final Set<Long> eventIDs = new HashSet<>();
|
||||
|
||||
/**
|
||||
* the ids of the subset of aggregated events that have at least one tag
|
||||
* applied to them
|
||||
*/
|
||||
private final Set<Long> tagged = new HashSet<>();
|
||||
|
||||
/**
|
||||
* the ids of the subset of aggregated events that have at least one hash
|
||||
* set hit
|
||||
*/
|
||||
private final Set<Long> hashHits = new HashSet<>();
|
||||
|
||||
public EventStripe(EventCluster cluster) {
|
||||
spans.add(cluster.getRange());
|
||||
spanMap.put(cluster.getRange(), cluster);
|
||||
type = cluster.getEventType();
|
||||
description = cluster.getDescription();
|
||||
lod = cluster.getDescriptionLOD();
|
||||
eventIDs.addAll(cluster.getEventIDs());
|
||||
tagged.addAll(cluster.getEventIDsWithTags());
|
||||
hashHits.addAll(cluster.getEventIDsWithHashHits());
|
||||
}
|
||||
|
||||
private EventStripe(EventStripe u, EventStripe v) {
|
||||
spans.addAll(u.spans);
|
||||
spans.addAll(v.spans);
|
||||
spanMap.putAll(u.spanMap);
|
||||
spanMap.putAll(v.spanMap);
|
||||
type = u.getEventType();
|
||||
description = u.getDescription();
|
||||
lod = u.getDescriptionLOD();
|
||||
eventIDs.addAll(u.getEventIDs());
|
||||
eventIDs.addAll(v.getEventIDs());
|
||||
tagged.addAll(u.getEventIDsWithTags());
|
||||
tagged.addAll(v.getEventIDsWithTags());
|
||||
hashHits.addAll(u.getEventIDsWithHashHits());
|
||||
hashHits.addAll(v.getEventIDsWithHashHits());
|
||||
}
|
||||
|
||||
public static EventStripe merge(EventStripe u, EventStripe v) {
|
||||
Preconditions.checkNotNull(u);
|
||||
Preconditions.checkNotNull(v);
|
||||
Preconditions.checkArgument(Objects.equal(u.description, v.description));
|
||||
Preconditions.checkArgument(Objects.equal(u.lod, v.lod));
|
||||
Preconditions.checkArgument(Objects.equal(u.type, v.type));
|
||||
return new EventStripe(u, v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventType getEventType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DescriptionLOD getDescriptionLOD() {
|
||||
return lod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Long> getEventIDs() {
|
||||
return Collections.unmodifiableSet(eventIDs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Long> getEventIDsWithHashHits() {
|
||||
return Collections.unmodifiableSet(hashHits);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Long> getEventIDsWithTags() {
|
||||
return Collections.unmodifiableSet(tagged);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getStartMillis() {
|
||||
return spans.span().lowerEndpoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEndMillis() {
|
||||
return spans.span().upperEndpoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Range<Long>> getRanges() {
|
||||
return spans.asRanges();
|
||||
}
|
||||
}
|
@ -326,7 +326,7 @@ public final class FilteredEventsModel {
|
||||
* range and pass the requested filter, using the given aggregation
|
||||
* to control the grouping of events
|
||||
*/
|
||||
public List<AggregateEvent> getAggregatedEvents() {
|
||||
public List<EventCluster> getAggregatedEvents() {
|
||||
final Interval range;
|
||||
final RootFilter filter;
|
||||
final EventTypeZoomLevel zoom;
|
||||
@ -337,7 +337,7 @@ public final class FilteredEventsModel {
|
||||
zoom = requestedTypeZoom.get();
|
||||
lod = requestedLOD.get();
|
||||
}
|
||||
return repo.getAggregatedEvents(new ZoomParams(range, zoom, filter, lod));
|
||||
return repo.getEventClusters(new ZoomParams(range, zoom, filter, lod));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -347,8 +347,8 @@ public final class FilteredEventsModel {
|
||||
* range and pass the requested filter, using the given aggregation
|
||||
* to control the grouping of events
|
||||
*/
|
||||
public List<AggregateEvent> getAggregatedEvents(ZoomParams params) {
|
||||
return repo.getAggregatedEvents(params);
|
||||
public List<EventCluster> getEventClusters(ZoomParams params) {
|
||||
return repo.getEventClusters(params);
|
||||
}
|
||||
|
||||
synchronized public boolean handleContentTagAdded(ContentTagAddedEvent evt) {
|
||||
|
@ -54,7 +54,7 @@ import org.joda.time.Period;
|
||||
import org.sleuthkit.autopsy.casemodule.Case;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.BaseTypes;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||
@ -413,10 +413,7 @@ public class EventDB {
|
||||
try (ResultSet rs = getDataSourceIDsStmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
long datasourceID = rs.getLong("datasource_id");
|
||||
//this relies on the fact that no tskObj has ID 0 but 0 is the default value for the datasource_id column in the events table.
|
||||
if (datasourceID != 0) {
|
||||
hashSet.add(datasourceID);
|
||||
}
|
||||
hashSet.add(datasourceID);
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Failed to get MAX time.", ex); // NON-NLS
|
||||
@ -583,6 +580,10 @@ public class EventDB {
|
||||
|
||||
initializeTagsTable();
|
||||
|
||||
createIndex("events", Arrays.asList("datasource_id"));
|
||||
createIndex("events", Arrays.asList("event_id", "hash_hit"));
|
||||
createIndex("events", Arrays.asList("event_id", "tagged"));
|
||||
createIndex("events", Arrays.asList("file_id"));
|
||||
createIndex("events", Arrays.asList("file_id"));
|
||||
createIndex("events", Arrays.asList("artifact_id"));
|
||||
createIndex("events", Arrays.asList("time"));
|
||||
@ -595,7 +596,7 @@ public class EventDB {
|
||||
"INSERT INTO events (datasource_id,file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description, known_state, hash_hit, tagged) " // NON-NLS
|
||||
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"); // NON-NLS
|
||||
getHashSetNamesStmt = prepareStatement("SELECT hash_set_id, hash_set_name FROM hash_sets"); // NON-NLS
|
||||
getDataSourceIDsStmt = prepareStatement("SELECT DISTINCT datasource_id FROM events"); // NON-NLS
|
||||
getDataSourceIDsStmt = prepareStatement("SELECT DISTINCT datasource_id FROM events WHERE datasource_id != 0"); // NON-NLS
|
||||
getMaxTimeStmt = prepareStatement("SELECT Max(time) AS max FROM events"); // NON-NLS
|
||||
getMinTimeStmt = prepareStatement("SELECT Min(time) AS min FROM events"); // NON-NLS
|
||||
getEventByIDStmt = prepareStatement("SELECT * FROM events WHERE event_id = ?"); // NON-NLS
|
||||
@ -1031,7 +1032,7 @@ public class EventDB {
|
||||
}
|
||||
|
||||
/**
|
||||
* get a list of {@link AggregateEvent}s, clustered according to the given
|
||||
* get a list of {@link EventCluster}s, clustered according to the given
|
||||
* zoom paramaters.
|
||||
*
|
||||
* @param params the zoom params that determine the zooming, filtering and
|
||||
@ -1041,7 +1042,7 @@ public class EventDB {
|
||||
* the supplied filter, aggregated according to the given event type
|
||||
* and description zoom levels
|
||||
*/
|
||||
List<AggregateEvent> getAggregatedEvents(ZoomParams params) {
|
||||
List<EventCluster> getClusteredEvents(ZoomParams params) {
|
||||
//unpack params
|
||||
Interval timeRange = params.getTimeRange();
|
||||
RootFilter filter = params.getFilter();
|
||||
@ -1067,8 +1068,8 @@ public class EventDB {
|
||||
//compose query string, new-lines only for nicer formatting if printing the entire query
|
||||
String query = "SELECT strftime('" + strfTimeFormat + "',time , 'unixepoch'" + timeZone + ") AS interval," // NON-NLS
|
||||
+ "\n group_concat(events.event_id) as event_ids,"
|
||||
+ "\n group_concat(CASE WHEN hash_hit = 1 THEN event_id ELSE NULL END) as hash_hits,"
|
||||
+ "\n group_concat(CASE WHEN tagged = 1 THEN event_id ELSE NULL END) as taggeds,"
|
||||
+ "\n group_concat(CASE WHEN hash_hit = 1 THEN events.event_id ELSE NULL END) as hash_hits,"
|
||||
+ "\n group_concat(CASE WHEN tagged = 1 THEN events.event_id ELSE NULL END) as taggeds,"
|
||||
+ "\n min(time), max(time), " + typeColumn + ", " + descriptionColumn // NON-NLS
|
||||
+ "\n FROM events" + useHashHitTablesHelper(filter) + useTagTablesHelper(filter) // NON-NLS
|
||||
+ "\n WHERE time >= " + start + " AND time < " + end + " AND " + SQLHelper.getSQLWhere(filter) // NON-NLS
|
||||
@ -1076,14 +1077,14 @@ public class EventDB {
|
||||
+ "\n ORDER BY min(time)"; // NON-NLS
|
||||
|
||||
// perform query and map results to AggregateEvent objects
|
||||
List<AggregateEvent> events = new ArrayList<>();
|
||||
List<EventCluster> events = new ArrayList<>();
|
||||
|
||||
DBLock.lock();
|
||||
|
||||
try (Statement createStatement = con.createStatement();
|
||||
ResultSet rs = createStatement.executeQuery(query)) {
|
||||
while (rs.next()) {
|
||||
events.add(aggregateEventHelper(rs, useSubTypes, descriptionLOD, filter.getTagsFilter()));
|
||||
events.add(eventClusterHelper(rs, useSubTypes, descriptionLOD, filter.getTagsFilter()));
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Failed to get aggregate events with query: " + query, ex); // NON-NLS
|
||||
@ -1091,11 +1092,11 @@ public class EventDB {
|
||||
DBLock.unlock();
|
||||
}
|
||||
|
||||
return mergeAggregateEvents(rangeInfo.getPeriodSize().getPeriod(), events);
|
||||
return mergeEventClusters(rangeInfo.getPeriodSize().getPeriod(), events);
|
||||
}
|
||||
|
||||
/**
|
||||
* map a single row in a ResultSet to an AggregateEvent
|
||||
* map a single row in a ResultSet to an EventCluster
|
||||
*
|
||||
* @param rs the result set whose current row should be mapped
|
||||
* @param useSubTypes use the sub_type column if true, else use the
|
||||
@ -1107,7 +1108,7 @@ public class EventDB {
|
||||
*
|
||||
* @throws SQLException
|
||||
*/
|
||||
private AggregateEvent aggregateEventHelper(ResultSet rs, boolean useSubTypes, DescriptionLOD descriptionLOD, TagsFilter filter) throws SQLException {
|
||||
private EventCluster eventClusterHelper(ResultSet rs, boolean useSubTypes, DescriptionLOD descriptionLOD, TagsFilter filter) throws SQLException {
|
||||
Interval interval = new Interval(rs.getLong("min(time)") * 1000, rs.getLong("max(time)") * 1000, TimeLineController.getJodaTimeZone());// NON-NLS
|
||||
String eventIDsString = rs.getString("event_ids");// NON-NLS
|
||||
Set<Long> eventIDs = SQLHelper.unGroupConcat(eventIDsString, Long::valueOf);
|
||||
@ -1117,7 +1118,7 @@ public class EventDB {
|
||||
Set<Long> hashHits = SQLHelper.unGroupConcat(rs.getString("hash_hits"), Long::valueOf);
|
||||
Set<Long> tagged = SQLHelper.unGroupConcat(rs.getString("taggeds"), Long::valueOf);
|
||||
|
||||
return new AggregateEvent(interval, type, eventIDs, hashHits, tagged,
|
||||
return new EventCluster(interval, type, eventIDs, hashHits, tagged,
|
||||
description, descriptionLOD);
|
||||
}
|
||||
|
||||
@ -1134,36 +1135,36 @@ public class EventDB {
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
static private List<AggregateEvent> mergeAggregateEvents(Period timeUnitLength, List<AggregateEvent> preMergedEvents) {
|
||||
static private List<EventCluster> mergeEventClusters(Period timeUnitLength, List<EventCluster> preMergedEvents) {
|
||||
|
||||
//effectively map from type to (map from description to events)
|
||||
Map<EventType, SetMultimap< String, AggregateEvent>> typeMap = new HashMap<>();
|
||||
Map<EventType, SetMultimap< String, EventCluster>> typeMap = new HashMap<>();
|
||||
|
||||
for (AggregateEvent aggregateEvent : preMergedEvents) {
|
||||
typeMap.computeIfAbsent(aggregateEvent.getType(), eventType -> HashMultimap.create())
|
||||
for (EventCluster aggregateEvent : preMergedEvents) {
|
||||
typeMap.computeIfAbsent(aggregateEvent.getEventType(), eventType -> HashMultimap.create())
|
||||
.put(aggregateEvent.getDescription(), aggregateEvent);
|
||||
}
|
||||
//result list to return
|
||||
ArrayList<AggregateEvent> aggEvents = new ArrayList<>();
|
||||
ArrayList<EventCluster> aggEvents = new ArrayList<>();
|
||||
|
||||
//For each (type, description) key, merge agg events
|
||||
for (SetMultimap<String, AggregateEvent> descrMap : typeMap.values()) {
|
||||
for (SetMultimap<String, EventCluster> descrMap : typeMap.values()) {
|
||||
//for each description ...
|
||||
for (String descr : descrMap.keySet()) {
|
||||
//run through the sorted events, merging together adjacent events
|
||||
Iterator<AggregateEvent> iterator = descrMap.get(descr).stream()
|
||||
Iterator<EventCluster> iterator = descrMap.get(descr).stream()
|
||||
.sorted(Comparator.comparing(event -> event.getSpan().getStartMillis()))
|
||||
.iterator();
|
||||
AggregateEvent current = iterator.next();
|
||||
EventCluster current = iterator.next();
|
||||
while (iterator.hasNext()) {
|
||||
AggregateEvent next = iterator.next();
|
||||
EventCluster next = iterator.next();
|
||||
Interval gap = current.getSpan().gap(next.getSpan());
|
||||
|
||||
//if they overlap or gap is less one quarter timeUnitLength
|
||||
//TODO: 1/4 factor is arbitrary. review! -jm
|
||||
if (gap == null || gap.toDuration().getMillis() <= timeUnitLength.toDurationFrom(gap.getStart()).getMillis() / 4) {
|
||||
//merge them
|
||||
current = AggregateEvent.merge(current, next);
|
||||
current = EventCluster.merge(current, next);
|
||||
} else {
|
||||
//done merging into current, set next as new current
|
||||
aggEvents.add(current);
|
||||
|
@ -45,7 +45,7 @@ import org.sleuthkit.autopsy.casemodule.Case;
|
||||
import org.sleuthkit.autopsy.casemodule.services.TagsManager;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.timeline.ProgressWindow;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.ArtifactEventType;
|
||||
@ -98,7 +98,7 @@ public class EventsRepository {
|
||||
|
||||
private final LoadingCache<Long, TimeLineEvent> idToEventCache;
|
||||
private final LoadingCache<ZoomParams, Map<EventType, Long>> eventCountsCache;
|
||||
private final LoadingCache<ZoomParams, List<AggregateEvent>> aggregateEventsCache;
|
||||
private final LoadingCache<ZoomParams, List<EventCluster>> eventClusterCache;
|
||||
|
||||
private final ObservableMap<Long, String> datasourcesMap = FXCollections.observableHashMap();
|
||||
private final ObservableMap<Long, String> hashSetMap = FXCollections.observableHashMap();
|
||||
@ -146,10 +146,10 @@ public class EventsRepository {
|
||||
.maximumSize(1000L)
|
||||
.expireAfterAccess(10, TimeUnit.MINUTES)
|
||||
.build(CacheLoader.from(eventDB::countEventsByType));
|
||||
aggregateEventsCache = CacheBuilder.newBuilder()
|
||||
eventClusterCache = CacheBuilder.newBuilder()
|
||||
.maximumSize(1000L)
|
||||
.expireAfterAccess(10, TimeUnit.MINUTES
|
||||
).build(CacheLoader.from(eventDB::getAggregatedEvents));
|
||||
).build(CacheLoader.from(eventDB::getClusteredEvents));
|
||||
maxCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMaxTime));
|
||||
minCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMinTime));
|
||||
this.modelInstance = new FilteredEventsModel(this, currentStateProperty);
|
||||
@ -206,8 +206,8 @@ public class EventsRepository {
|
||||
|
||||
}
|
||||
|
||||
synchronized public List<AggregateEvent> getAggregatedEvents(ZoomParams params) {
|
||||
return aggregateEventsCache.getUnchecked(params);
|
||||
synchronized public List<EventCluster> getEventClusters(ZoomParams params) {
|
||||
return eventClusterCache.getUnchecked(params);
|
||||
}
|
||||
|
||||
synchronized public Map<EventType, Long> countEvents(ZoomParams params) {
|
||||
@ -218,7 +218,7 @@ public class EventsRepository {
|
||||
minCache.invalidateAll();
|
||||
maxCache.invalidateAll();
|
||||
eventCountsCache.invalidateAll();
|
||||
aggregateEventsCache.invalidateAll();
|
||||
eventClusterCache.invalidateAll();
|
||||
idToEventCache.invalidateAll();
|
||||
}
|
||||
|
||||
@ -292,7 +292,7 @@ public class EventsRepository {
|
||||
|
||||
synchronized private void invalidateCaches(Set<Long> updatedEventIDs) {
|
||||
eventCountsCache.invalidateAll();
|
||||
aggregateEventsCache.invalidateAll();
|
||||
eventClusterCache.invalidateAll();
|
||||
idToEventCache.invalidateAll(updatedEventIDs);
|
||||
try {
|
||||
tagNames.setAll(autoCase.getSleuthkitCase().getTagNamesInUse());
|
||||
@ -487,26 +487,29 @@ public class EventsRepository {
|
||||
final String uniquePath = f.getUniquePath();
|
||||
final String parentPath = f.getParentPath();
|
||||
long datasourceID = f.getDataSource().getId();
|
||||
String datasourceName = StringUtils.substringBefore(StringUtils.stripStart(uniquePath, "/"), parentPath);
|
||||
String rootFolder = StringUtils.substringBetween(parentPath, "/", "/");
|
||||
String shortDesc = datasourceName + "/" + StringUtils.defaultIfBlank(rootFolder, "");
|
||||
String medD = datasourceName + parentPath;
|
||||
String datasourceName = StringUtils.substringBeforeLast(uniquePath, parentPath);
|
||||
|
||||
String rootFolder = StringUtils.substringBefore(StringUtils.substringAfter(parentPath, "/"), "/");
|
||||
String shortDesc = datasourceName + "/" + StringUtils.defaultString(rootFolder);
|
||||
shortDesc = shortDesc.endsWith("/") ? shortDesc : shortDesc + "/";
|
||||
String medDesc = datasourceName + parentPath;
|
||||
|
||||
final TskData.FileKnown known = f.getKnown();
|
||||
Set<String> hashSets = f.getHashSetNames();
|
||||
List<ContentTag> tags = tagsManager.getContentTagsByContent(f);
|
||||
|
||||
//insert it into the db if time is > 0 => time is legitimate (drops logical files)
|
||||
if (f.getAtime() > 0) {
|
||||
eventDB.insertEvent(f.getAtime(), FileSystemTypes.FILE_ACCESSED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans);
|
||||
eventDB.insertEvent(f.getAtime(), FileSystemTypes.FILE_ACCESSED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans);
|
||||
}
|
||||
if (f.getMtime() > 0) {
|
||||
eventDB.insertEvent(f.getMtime(), FileSystemTypes.FILE_MODIFIED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans);
|
||||
eventDB.insertEvent(f.getMtime(), FileSystemTypes.FILE_MODIFIED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans);
|
||||
}
|
||||
if (f.getCtime() > 0) {
|
||||
eventDB.insertEvent(f.getCtime(), FileSystemTypes.FILE_CHANGED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans);
|
||||
eventDB.insertEvent(f.getCtime(), FileSystemTypes.FILE_CHANGED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans);
|
||||
}
|
||||
if (f.getCrtime() > 0) {
|
||||
eventDB.insertEvent(f.getCrtime(), FileSystemTypes.FILE_CREATED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans);
|
||||
eventDB.insertEvent(f.getCrtime(), FileSystemTypes.FILE_CREATED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans);
|
||||
}
|
||||
|
||||
publish(new ProgressWindow.ProgressUpdate(i, numFiles,
|
||||
|
@ -30,6 +30,7 @@ import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType;
|
||||
import org.sleuthkit.autopsy.timeline.filters.AbstractFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.DataSourceFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.DataSourcesFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.Filter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.HashHitsFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.HashSetFilter;
|
||||
@ -105,10 +106,20 @@ public class SQLHelper {
|
||||
return getSQLWhere((IntersectionFilter) filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: I don't like this if-else instance of chain, but I can't decide
|
||||
* what to do instead -jm
|
||||
*
|
||||
* @param filter
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private static String getSQLWhere(Filter filter) {
|
||||
String result = "";
|
||||
if (filter == null) {
|
||||
return "1";
|
||||
} else if (filter instanceof DescriptionFilter) {
|
||||
result = getSQLWhere((DescriptionFilter) filter);
|
||||
} else if (filter instanceof TagsFilter) {
|
||||
result = getSQLWhere((TagsFilter) filter);
|
||||
} else if (filter instanceof HashHitsFilter) {
|
||||
@ -130,7 +141,7 @@ public class SQLHelper {
|
||||
} else if (filter instanceof UnionFilter) {
|
||||
result = getSQLWhere((UnionFilter) filter);
|
||||
} else {
|
||||
return "1";
|
||||
throw new IllegalArgumentException("getSQLWhere not defined for " + filter.getClass().getCanonicalName());
|
||||
}
|
||||
result = StringUtils.deleteWhitespace(result).equals("(1and1and1)") ? "1" : result;
|
||||
result = StringUtils.deleteWhitespace(result).equals("()") ? "1" : result;
|
||||
@ -145,6 +156,14 @@ public class SQLHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static String getSQLWhere(DescriptionFilter filter) {
|
||||
if (filter.isSelected()) {
|
||||
return "(" + getDescriptionColumn(filter.getDescriptionLoD()) + " LIKE '" + filter.getDescription() + "')"; // NON-NLS
|
||||
} else {
|
||||
return "1";
|
||||
}
|
||||
}
|
||||
|
||||
private static String getSQLWhere(TagsFilter filter) {
|
||||
if (filter.isSelected()
|
||||
&& (false == filter.isDisabled())
|
||||
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.filters;
|
||||
|
||||
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
|
||||
|
||||
public class DescriptionFilter extends AbstractFilter {
|
||||
|
||||
private final DescriptionLOD descriptionLoD;
|
||||
|
||||
private final String description;
|
||||
|
||||
public DescriptionFilter(DescriptionLOD descriptionLoD, String description) {
|
||||
this.descriptionLoD = descriptionLoD;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DescriptionFilter copyOf() {
|
||||
DescriptionFilter filterCopy = new DescriptionFilter(getDescriptionLoD(), getDescription());
|
||||
filterCopy.setSelected(isSelected());
|
||||
filterCopy.setDisabled(isDisabled());
|
||||
return filterCopy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "description";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHTMLReportString() {
|
||||
return getDescriptionLoD().getDisplayName() + " " + getDisplayName() + " = " + getDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the descriptionLoD
|
||||
*/
|
||||
public DescriptionLOD getDescriptionLoD() {
|
||||
return descriptionLoD;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the description
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
}
|
@ -79,4 +79,6 @@ public interface Filter {
|
||||
SimpleBooleanProperty getDisabledProperty();
|
||||
|
||||
boolean isDisabled();
|
||||
|
||||
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent;
|
||||
* {@link XYChart} doing the rendering. Is this a good idea? -jm TODO: pull up
|
||||
* common history context menu items out of derived classes? -jm
|
||||
*/
|
||||
public abstract class AbstractVisualization<X, Y, N extends Node, C extends XYChart<X, Y> & TimeLineChart<X>> extends BorderPane implements TimeLineView {
|
||||
public abstract class AbstractVisualization<X, Y, N, C extends XYChart<X, Y> & TimeLineChart<X>> extends BorderPane implements TimeLineView {
|
||||
|
||||
protected final SimpleBooleanProperty hasEvents = new SimpleBooleanProperty(true);
|
||||
|
||||
@ -173,8 +173,8 @@ public abstract class AbstractVisualization<X, Y, N extends Node, C extends XYCh
|
||||
protected abstract Axis<Y> getYAxis();
|
||||
|
||||
/**
|
||||
* update this visualization based on current state of zoom /
|
||||
* filters. Primarily this invokes the background {@link Task} returned by
|
||||
* update this visualization based on current state of zoom / filters.
|
||||
* Primarily this invokes the background {@link Task} returned by
|
||||
* {@link #getUpdateTask()} which derived classes must implement.
|
||||
*/
|
||||
synchronized public void update() {
|
||||
|
@ -0,0 +1,554 @@
|
||||
/*
|
||||
* 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.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.concurrent.ExecutionException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import javafx.scene.control.SeparatorMenuItem;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.BorderStroke;
|
||||
import javafx.scene.layout.BorderStrokeStyle;
|
||||
import javafx.scene.layout.BorderWidths;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import static javafx.scene.layout.Region.USE_COMPUTED_SIZE;
|
||||
import static javafx.scene.layout.Region.USE_PREF_SIZE;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.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.LoggedTask;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||
import org.sleuthkit.autopsy.timeline.filters.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;
|
||||
|
||||
public abstract class AbstractDetailViewNode< T extends EventBundle, S extends AbstractDetailViewNode<T, S>> extends StackPane implements DetailViewNode<AbstractDetailViewNode<T, S>> {
|
||||
|
||||
static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png");
|
||||
static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS
|
||||
static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS
|
||||
static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS
|
||||
static final CornerRadii CORNER_RADII = new CornerRadii(3);
|
||||
/**
|
||||
* the border to apply when this node is 'selected'
|
||||
*/
|
||||
static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2)));
|
||||
private static final Logger LOGGER = Logger.getLogger(AbstractDetailViewNode.class
|
||||
.getName());
|
||||
|
||||
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 Map<EventType, DropShadow> dropShadowMap = new HashMap<>();
|
||||
final Color evtColor;
|
||||
|
||||
private final S parentNode;
|
||||
private DescriptionVisibility descrVis;
|
||||
|
||||
/**
|
||||
* Pane that contains AggregateEventNodes of any 'subevents' if they are
|
||||
* displayed
|
||||
*
|
||||
* //TODO: move more of the control of subnodes/events here and out of
|
||||
* EventDetail Chart
|
||||
*/
|
||||
private final Pane subNodePane = new Pane();
|
||||
|
||||
Pane getSubNodePane() {
|
||||
return subNodePane;
|
||||
}
|
||||
|
||||
/**
|
||||
* The ImageView used to show the icon for this node's event's type
|
||||
*/
|
||||
private final ImageView eventTypeImageView = new ImageView();
|
||||
|
||||
/**
|
||||
* The label used to display this node's event's description
|
||||
*/
|
||||
final Label descrLabel = new Label();
|
||||
|
||||
/**
|
||||
* The label used to display this node's event count
|
||||
*/
|
||||
final Label countLabel = new Label();
|
||||
|
||||
private final T eventBundle;
|
||||
private final EventDetailChart chart;
|
||||
private final SleuthkitCase sleuthkitCase;
|
||||
|
||||
SleuthkitCase getSleuthkitCase() {
|
||||
return sleuthkitCase;
|
||||
}
|
||||
|
||||
FilteredEventsModel getEventsModel() {
|
||||
return eventsModel;
|
||||
}
|
||||
private final FilteredEventsModel eventsModel;
|
||||
|
||||
private final Button plusButton;
|
||||
private final Button minusButton;
|
||||
|
||||
private final SimpleObjectProperty<DescriptionLOD> descLOD = new SimpleObjectProperty<>();
|
||||
final HBox header;
|
||||
|
||||
Region getSpacer() {
|
||||
return spacer;
|
||||
}
|
||||
|
||||
private final Region spacer = new Region();
|
||||
|
||||
private final CollapseClusterAction collapseClusterAction;
|
||||
private final ExpandClusterAction expandClusterAction;
|
||||
|
||||
public AbstractDetailViewNode(EventDetailChart chart, T bundle, S parentEventNode) {
|
||||
this.eventBundle = bundle;
|
||||
this.parentNode = parentEventNode;
|
||||
this.chart = chart;
|
||||
descLOD.set(bundle.getDescriptionLOD());
|
||||
sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase();
|
||||
eventsModel = chart.getController().getEventsModel();
|
||||
ImageView hashIV = new ImageView(HASH_PIN);
|
||||
ImageView tagIV = new ImageView(TAG);
|
||||
if (eventBundle.getEventIDsWithHashHits().isEmpty()) {
|
||||
show(hashIV, false);
|
||||
}
|
||||
if (eventBundle.getEventIDsWithTags().isEmpty()) {
|
||||
show(tagIV, false);
|
||||
}
|
||||
|
||||
expandClusterAction = new ExpandClusterAction();
|
||||
plusButton = ActionUtils.createButton(expandClusterAction, ActionUtils.ActionTextBehavior.HIDE);
|
||||
configureLODButton(plusButton);
|
||||
|
||||
collapseClusterAction = new CollapseClusterAction();
|
||||
minusButton = ActionUtils.createButton(collapseClusterAction, ActionUtils.ActionTextBehavior.HIDE);
|
||||
configureLODButton(minusButton);
|
||||
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
header = new HBox(getDescrLabel(), getCountLabel(), hashIV, tagIV, minusButton, plusButton);
|
||||
|
||||
header.setMinWidth(USE_PREF_SIZE);
|
||||
header.setPadding(new Insets(2, 5, 2, 5));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
//setup description label
|
||||
evtColor = getEventType().getColor();
|
||||
|
||||
eventTypeImageView.setImage(getEventType().getFXImage());
|
||||
descrLabel.setGraphic(eventTypeImageView);
|
||||
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
|
||||
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
|
||||
descrLabel.setMouseTransparent(true);
|
||||
|
||||
//set up subnode pane sizing contraints
|
||||
subNodePane.setPrefHeight(USE_COMPUTED_SIZE);
|
||||
subNodePane.setMinHeight(USE_PREF_SIZE);
|
||||
subNodePane.setMinWidth(USE_PREF_SIZE);
|
||||
subNodePane.setMaxHeight(USE_PREF_SIZE);
|
||||
subNodePane.setMaxWidth(USE_PREF_SIZE);
|
||||
subNodePane.setPickOnBounds(false);
|
||||
|
||||
setAlignment(Pos.TOP_LEFT);
|
||||
setMinHeight(24);
|
||||
setPrefHeight(USE_COMPUTED_SIZE);
|
||||
setMaxHeight(USE_PREF_SIZE);
|
||||
setOnMouseClicked(new EventMouseHandler());
|
||||
|
||||
//set up mouse hover effect and tooltip
|
||||
setOnMouseEntered((MouseEvent e) -> {
|
||||
//defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart
|
||||
installTooltip();
|
||||
showDescriptionLoDControls(true);
|
||||
toFront();
|
||||
});
|
||||
|
||||
setOnMouseExited((MouseEvent e) -> {
|
||||
showDescriptionLoDControls(false);
|
||||
});
|
||||
setCursor(Cursor.HAND);
|
||||
|
||||
setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
|
||||
|
||||
setLayoutX(getChart().getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<S> getSubNodes() {
|
||||
return subNodePane.getChildrenUnmodifiable().stream()
|
||||
.map(t -> (S) t)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* apply the 'effect' to visually indicate selection
|
||||
*
|
||||
* @param applied true to apply the selection 'effect', false to remove it
|
||||
*/
|
||||
@Override
|
||||
public void applySelectionEffect(boolean applied) {
|
||||
Platform.runLater(() -> {
|
||||
if (applied) {
|
||||
setBorder(selectionBorder);
|
||||
} else {
|
||||
setBorder(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param showControls the value of par
|
||||
*/
|
||||
void showDescriptionLoDControls(final boolean showControls) {
|
||||
DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(),
|
||||
eventType -> new DropShadow(10, eventType.getColor()));
|
||||
getSpanFillNode().setEffect(showControls ? dropShadow : null);
|
||||
show(minusButton, showControls);
|
||||
show(plusButton, showControls);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()),
|
||||
new TypeFilter(getEventType()));
|
||||
return subClusterFilter;
|
||||
}
|
||||
|
||||
abstract Collection<T> makeBundlesFromClusters(List<EventCluster> eventClusters);
|
||||
|
||||
abstract void showSpans(final boolean showSpans);
|
||||
|
||||
/**
|
||||
* @param w the maximum width the description label should have
|
||||
*/
|
||||
@Override
|
||||
public void setDescriptionWidth(double w) {
|
||||
getDescrLabel().setMaxWidth(w);
|
||||
}
|
||||
|
||||
abstract void installTooltip();
|
||||
|
||||
/**
|
||||
* apply the 'effect' to visually indicate highlighted nodes
|
||||
*
|
||||
* @param applied true to apply the highlight 'effect', false to remove it
|
||||
*/
|
||||
@Override
|
||||
public synchronized void applyHighlightEffect(boolean applied) {
|
||||
if (applied) {
|
||||
getDescrLabel().setStyle("-fx-font-weight: bold;"); // NON-NLS
|
||||
getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY)));
|
||||
setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
|
||||
} else {
|
||||
getDescrLabel().setStyle("-fx-font-weight: normal;"); // NON-NLS
|
||||
getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
|
||||
setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
|
||||
}
|
||||
}
|
||||
|
||||
String getDisplayedDescription() {
|
||||
return getDescrLabel().getText();
|
||||
}
|
||||
|
||||
abstract Region getSpanFillNode();
|
||||
|
||||
Button getPlusButton() {
|
||||
return plusButton;
|
||||
}
|
||||
|
||||
Button getMinusButton() {
|
||||
return minusButton;
|
||||
}
|
||||
|
||||
public final Label getDescrLabel() {
|
||||
return descrLabel;
|
||||
}
|
||||
|
||||
final public Label getCountLabel() {
|
||||
return countLabel;
|
||||
}
|
||||
|
||||
public S getParentNode() {
|
||||
return parentNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final T getEventBundle() {
|
||||
return eventBundle;
|
||||
}
|
||||
|
||||
public final EventDetailChart getChart() {
|
||||
return chart;
|
||||
}
|
||||
|
||||
public DescriptionLOD getDescLOD() {
|
||||
return descLOD.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* loads sub-bundles at the given Description LOD, continues
|
||||
*
|
||||
* @param requestedDescrLoD
|
||||
* @param expand
|
||||
*/
|
||||
private synchronized void loadSubBundles(DescriptionLOD.RelativeDetail relativeDetail) {
|
||||
subNodePane.getChildren().clear();
|
||||
if (descLOD.get().withRelativeDetail(relativeDetail) == getEventBundle().getDescriptionLOD()) {
|
||||
descLOD.set(getEventBundle().getDescriptionLOD());
|
||||
showSpans(true);
|
||||
chart.setRequiresLayout(true);
|
||||
chart.requestChartLayout();
|
||||
} else {
|
||||
showSpans(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(getEventBundle().getStartMillis(), getEventBundle().getEndMillis() + 1000);
|
||||
final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get();
|
||||
final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescLOD());
|
||||
|
||||
LoggedTask<List<S>> loggedTask;
|
||||
loggedTask = new LoggedTask<List<S>>(
|
||||
NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) {
|
||||
private Collection<T> bundles;
|
||||
private volatile DescriptionLOD loadedDescriptionLoD = getDescLOD().withRelativeDetail(relativeDetail);
|
||||
private DescriptionLOD next = loadedDescriptionLoD;
|
||||
|
||||
@Override
|
||||
protected List<S> call() throws Exception {
|
||||
do {
|
||||
loadedDescriptionLoD = next;
|
||||
if (loadedDescriptionLoD == getEventBundle().getDescriptionLOD()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
bundles = loadBundles();
|
||||
next = loadedDescriptionLoD.withRelativeDetail(relativeDetail);
|
||||
} while (bundles.size() == 1 && nonNull(next));
|
||||
|
||||
// return list of AbstractDetailViewNodes representing sub-bundles
|
||||
return bundles.stream()
|
||||
.map(AbstractDetailViewNode.this::getNodeForBundle)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Collection<T> loadBundles() {
|
||||
return makeBundlesFromClusters(eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
chart.setCursor(Cursor.WAIT);
|
||||
try {
|
||||
List<S> subBundleNodes = get();
|
||||
if (subBundleNodes.isEmpty()) {
|
||||
showSpans(true);
|
||||
} else {
|
||||
showSpans(false);
|
||||
}
|
||||
descLOD.set(loadedDescriptionLoD);
|
||||
//assign subNodes and request chart layout
|
||||
subNodePane.getChildren().setAll(subBundleNodes);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
final double getLayoutXCompensation() {
|
||||
return (getParentNode() != null ? getParentNode().getLayoutXCompensation() : 0)
|
||||
+ getBoundsInParent().getMinX();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setDescriptionVisibility(DescriptionVisibility descrVis) {
|
||||
this.descrVis = descrVis;
|
||||
final int size = getEventBundle().getEventIDs().size();
|
||||
|
||||
switch (this.descrVis) {
|
||||
case COUNT_ONLY:
|
||||
descrLabel.setText("");
|
||||
countLabel.setText(String.valueOf(size));
|
||||
break;
|
||||
case HIDDEN:
|
||||
countLabel.setText("");
|
||||
descrLabel.setText("");
|
||||
break;
|
||||
default:
|
||||
case SHOWN:
|
||||
String description = getEventBundle().getDescription();
|
||||
description = getParentNode() != null
|
||||
? " ..." + StringUtils.substringAfter(description, getParentNode().getDescription())
|
||||
: description;
|
||||
descrLabel.setText(description);
|
||||
countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
abstract S getNodeForBundle(T bundle);
|
||||
|
||||
/**
|
||||
* event handler used for mouse events on {@link AggregateEventNode}s
|
||||
*/
|
||||
private class EventMouseHandler implements EventHandler<MouseEvent> {
|
||||
|
||||
private ContextMenu contextMenu;
|
||||
|
||||
@Override
|
||||
public void handle(MouseEvent t) {
|
||||
|
||||
if (t.getButton() == MouseButton.PRIMARY) {
|
||||
t.consume();
|
||||
if (t.isShiftDown()) {
|
||||
if (chart.selectedNodes.contains(AbstractDetailViewNode.this) == false) {
|
||||
chart.selectedNodes.add(AbstractDetailViewNode.this);
|
||||
}
|
||||
} else if (t.isShortcutDown()) {
|
||||
chart.selectedNodes.removeAll(AbstractDetailViewNode.this);
|
||||
} else if (t.getClickCount() > 1) {
|
||||
final DescriptionLOD next = descLOD.get().moreDetailed();
|
||||
if (next != null) {
|
||||
loadSubBundles(DescriptionLOD.RelativeDetail.MORE);
|
||||
|
||||
}
|
||||
} else {
|
||||
chart.selectedNodes.setAll(AbstractDetailViewNode.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(expandClusterAction));
|
||||
contextMenu.getItems().add(ActionUtils.createMenuItem(collapseClusterAction));
|
||||
|
||||
contextMenu.getItems().add(new SeparatorMenuItem());
|
||||
contextMenu.getItems().addAll(chartContextMenu.getItems());
|
||||
}
|
||||
contextMenu.show(AbstractDetailViewNode.this, t.getScreenX(), t.getScreenY());
|
||||
t.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ExpandClusterAction extends Action {
|
||||
|
||||
ExpandClusterAction() {
|
||||
super("Expand");
|
||||
|
||||
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 {
|
||||
|
||||
CollapseClusterAction() {
|
||||
super("Collapse");
|
||||
|
||||
setGraphic(new ImageView(MINUS));
|
||||
setEventHandler((ActionEvent t) -> {
|
||||
final DescriptionLOD previous = descLOD.get().lessDetailed();
|
||||
if (previous != null) {
|
||||
loadSubBundles(DescriptionLOD.RelativeDetail.LESS);
|
||||
}
|
||||
});
|
||||
disabledProperty().bind(descLOD.isEqualTo(getEventBundle().getDescriptionLOD()));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,523 +0,0 @@
|
||||
/*
|
||||
* 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.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.BorderStroke;
|
||||
import javafx.scene.layout.BorderStrokeStyle;
|
||||
import javafx.scene.layout.BorderWidths;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Interval;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.coreutils.ColorUtilities;
|
||||
import org.sleuthkit.autopsy.coreutils.LoggedTask;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
|
||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
|
||||
import org.sleuthkit.autopsy.timeline.filters.RootFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.TextFilter;
|
||||
import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
|
||||
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
|
||||
import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
|
||||
import org.sleuthkit.datamodel.SleuthkitCase;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
|
||||
/**
|
||||
* Represents an {@link AggregateEvent} in a {@link EventDetailChart}.
|
||||
*/
|
||||
public class AggregateEventNode extends StackPane {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(AggregateEventNode.class.getName());
|
||||
|
||||
private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png");
|
||||
private final static Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS
|
||||
private final static Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS
|
||||
private final static Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS
|
||||
|
||||
private static final CornerRadii CORNER_RADII = new CornerRadii(3);
|
||||
|
||||
/**
|
||||
* the border to apply when this node is 'selected'
|
||||
*/
|
||||
private static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2)));
|
||||
|
||||
/**
|
||||
* The event this AggregateEventNode represents visually
|
||||
*/
|
||||
private AggregateEvent aggEvent;
|
||||
|
||||
private final AggregateEventNode parentEventNode;
|
||||
|
||||
/**
|
||||
* the region that represents the time span of this node's event
|
||||
*/
|
||||
private final Region spanRegion = new Region();
|
||||
|
||||
/**
|
||||
* The label used to display this node's event's description
|
||||
*/
|
||||
private final Label descrLabel = new Label();
|
||||
|
||||
/**
|
||||
* The label used to display this node's event count
|
||||
*/
|
||||
private final Label countLabel = new Label();
|
||||
|
||||
/**
|
||||
* The IamgeView used to show the icon for this node's event's type
|
||||
*/
|
||||
private final ImageView eventTypeImageView = new ImageView();
|
||||
|
||||
/**
|
||||
* Pane that contains AggregateEventNodes of any 'subevents' if they are
|
||||
* displayed
|
||||
*
|
||||
* //TODO: move more of the control of subnodes/events here and out of
|
||||
* EventDetail Chart
|
||||
*/
|
||||
private final Pane subNodePane = new Pane();
|
||||
|
||||
/**
|
||||
* the context menu that with the slider that controls subnode/event display
|
||||
*
|
||||
* //TODO: move more of the control of subnodes/events here and out of
|
||||
* EventDetail Chart
|
||||
*/
|
||||
private final SimpleObjectProperty<ContextMenu> contextMenu = new SimpleObjectProperty<>();
|
||||
|
||||
/**
|
||||
* the Background used to fill the spanRegion, this varies epending on the
|
||||
* selected/highlighted state of this node in its parent EventDetailChart
|
||||
*/
|
||||
private Background spanFill;
|
||||
|
||||
private final Button plusButton = new Button(null, new ImageView(PLUS)) {
|
||||
{
|
||||
setMinSize(16, 16);
|
||||
setMaxSize(16, 16);
|
||||
setPrefSize(16, 16);
|
||||
}
|
||||
};
|
||||
private final Button minusButton = new Button(null, new ImageView(MINUS)) {
|
||||
{
|
||||
setMinSize(16, 16);
|
||||
setMaxSize(16, 16);
|
||||
setPrefSize(16, 16);
|
||||
}
|
||||
};
|
||||
private final EventDetailChart chart;
|
||||
|
||||
private SimpleObjectProperty<DescriptionLOD> descLOD = new SimpleObjectProperty<>();
|
||||
private DescriptionVisibility descrVis;
|
||||
private final SleuthkitCase sleuthkitCase;
|
||||
private final FilteredEventsModel eventsModel;
|
||||
|
||||
private Tooltip tooltip;
|
||||
private final ImageView hashIV = new ImageView(HASH_PIN);
|
||||
private final ImageView tagIV = new ImageView(TAG);
|
||||
|
||||
public AggregateEventNode(final AggregateEvent aggEvent, AggregateEventNode parentEventNode, EventDetailChart chart) {
|
||||
this.aggEvent = aggEvent;
|
||||
descLOD.set(aggEvent.getLOD());
|
||||
this.parentEventNode = parentEventNode;
|
||||
this.chart = chart;
|
||||
sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase();
|
||||
eventsModel = chart.getController().getEventsModel();
|
||||
|
||||
final Region region = new Region();
|
||||
HBox.setHgrow(region, Priority.ALWAYS);
|
||||
|
||||
final HBox hBox = new HBox(descrLabel, countLabel, region, hashIV, tagIV, minusButton, plusButton);
|
||||
if (aggEvent.getEventIDsWithHashHits().isEmpty()) {
|
||||
hashIV.setManaged(false);
|
||||
hashIV.setVisible(false);
|
||||
}
|
||||
if (aggEvent.getEventIDsWithTags().isEmpty()) {
|
||||
tagIV.setManaged(false);
|
||||
tagIV.setVisible(false);
|
||||
}
|
||||
hBox.setPrefWidth(USE_COMPUTED_SIZE);
|
||||
hBox.setMinWidth(USE_PREF_SIZE);
|
||||
hBox.setPadding(new Insets(2, 5, 2, 5));
|
||||
hBox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
minusButton.setVisible(false);
|
||||
plusButton.setVisible(false);
|
||||
minusButton.setManaged(false);
|
||||
plusButton.setManaged(false);
|
||||
final BorderPane borderPane = new BorderPane(subNodePane, hBox, null, null, null);
|
||||
BorderPane.setAlignment(subNodePane, Pos.TOP_LEFT);
|
||||
borderPane.setPrefWidth(USE_COMPUTED_SIZE);
|
||||
|
||||
getChildren().addAll(spanRegion, borderPane);
|
||||
|
||||
setAlignment(Pos.TOP_LEFT);
|
||||
setMinHeight(24);
|
||||
minWidthProperty().bind(spanRegion.widthProperty());
|
||||
setPrefHeight(USE_COMPUTED_SIZE);
|
||||
setMaxHeight(USE_PREF_SIZE);
|
||||
|
||||
//set up subnode pane sizing contraints
|
||||
subNodePane.setPrefHeight(USE_COMPUTED_SIZE);
|
||||
subNodePane.setMinHeight(USE_PREF_SIZE);
|
||||
subNodePane.setMinWidth(USE_PREF_SIZE);
|
||||
subNodePane.setMaxHeight(USE_PREF_SIZE);
|
||||
subNodePane.setMaxWidth(USE_PREF_SIZE);
|
||||
subNodePane.setPickOnBounds(false);
|
||||
|
||||
//setup description label
|
||||
eventTypeImageView.setImage(aggEvent.getType().getFXImage());
|
||||
descrLabel.setGraphic(eventTypeImageView);
|
||||
descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
|
||||
descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
|
||||
|
||||
descrLabel.setMouseTransparent(true);
|
||||
setDescriptionVisibility(chart.getDescrVisibility().get());
|
||||
|
||||
//setup backgrounds
|
||||
final Color evtColor = aggEvent.getType().getColor();
|
||||
spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY));
|
||||
setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
|
||||
setCursor(Cursor.HAND);
|
||||
spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor) + ";"); // NON-NLS
|
||||
spanRegion.setBackground(spanFill);
|
||||
|
||||
//set up mouse hover effect and tooltip
|
||||
setOnMouseEntered((MouseEvent e) -> {
|
||||
//defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart
|
||||
installTooltip();
|
||||
spanRegion.setEffect(new DropShadow(10, evtColor));
|
||||
minusButton.setVisible(true);
|
||||
plusButton.setVisible(true);
|
||||
minusButton.setManaged(true);
|
||||
plusButton.setManaged(true);
|
||||
toFront();
|
||||
});
|
||||
|
||||
setOnMouseExited((MouseEvent e) -> {
|
||||
spanRegion.setEffect(null);
|
||||
minusButton.setVisible(false);
|
||||
plusButton.setVisible(false);
|
||||
minusButton.setManaged(false);
|
||||
plusButton.setManaged(false);
|
||||
});
|
||||
|
||||
setOnMouseClicked(new EventMouseHandler());
|
||||
|
||||
plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL));
|
||||
minusButton.disableProperty().bind(descLOD.isEqualTo(aggEvent.getLOD()));
|
||||
|
||||
plusButton.setOnMouseClicked(e -> {
|
||||
final DescriptionLOD next = descLOD.get().next();
|
||||
if (next != null) {
|
||||
loadSubClusters(next);
|
||||
descLOD.set(next);
|
||||
}
|
||||
});
|
||||
minusButton.setOnMouseClicked(e -> {
|
||||
final DescriptionLOD previous = descLOD.get().previous();
|
||||
if (previous != null) {
|
||||
loadSubClusters(previous);
|
||||
descLOD.set(previous);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
synchronized private void installTooltip() {
|
||||
//TODO: all this work should probably go on a background thread...
|
||||
if (tooltip == null) {
|
||||
HashMap<String, Long> hashSetCounts = new HashMap<>();
|
||||
if (!aggEvent.getEventIDsWithHashHits().isEmpty()) {
|
||||
hashSetCounts = new HashMap<>();
|
||||
try {
|
||||
for (TimeLineEvent tle : eventsModel.getEventsById(aggEvent.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 (!aggEvent.getEventIDsWithTags().isEmpty()) {
|
||||
tagCounts.putAll( eventsModel.getTagCountsByTagName(aggEvent.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"));
|
||||
|
||||
tooltip = new Tooltip(
|
||||
NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text",
|
||||
getEvent().getEventIDs().size(), getEvent().getType(), getEvent().getDescription(),
|
||||
getEvent().getSpan().getStart().toString(TimeLineController.getZonedFormatter()),
|
||||
getEvent().getSpan().getEnd().toString(TimeLineController.getZonedFormatter()))
|
||||
+ (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString)
|
||||
+ (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString)
|
||||
);
|
||||
Tooltip.install(AggregateEventNode.this, tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
public Pane getSubNodePane() {
|
||||
return subNodePane;
|
||||
}
|
||||
|
||||
synchronized public AggregateEvent getEvent() {
|
||||
return aggEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the width of the {@link Region} with border and background used to
|
||||
* indicate the temporal span of this aggregate event
|
||||
*
|
||||
* @param w
|
||||
*/
|
||||
public void setSpanWidth(double w) {
|
||||
spanRegion.setPrefWidth(w);
|
||||
spanRegion.setMaxWidth(w);
|
||||
spanRegion.setMinWidth(Math.max(2, w));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param w the maximum width the description label should have
|
||||
*/
|
||||
public void setDescriptionWidth(double w) {
|
||||
descrLabel.setMaxWidth(w);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param descrVis the level of description that should be displayed
|
||||
*/
|
||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||
synchronized final void setDescriptionVisibility(DescriptionVisibility descrVis) {
|
||||
this.descrVis = descrVis;
|
||||
final int size = aggEvent.getEventIDs().size();
|
||||
|
||||
switch (descrVis) {
|
||||
case COUNT_ONLY:
|
||||
descrLabel.setText("");
|
||||
countLabel.setText(String.valueOf(size));
|
||||
break;
|
||||
case HIDDEN:
|
||||
countLabel.setText("");
|
||||
descrLabel.setText("");
|
||||
break;
|
||||
default:
|
||||
case SHOWN:
|
||||
String description = aggEvent.getDescription();
|
||||
description = parentEventNode != null
|
||||
? " ..." + StringUtils.substringAfter(description, parentEventNode.getEvent().getDescription())
|
||||
: description;
|
||||
descrLabel.setText(description);
|
||||
countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* apply the 'effect' to visually indicate selection
|
||||
*
|
||||
* @param applied true to apply the selection 'effect', false to remove it
|
||||
*/
|
||||
void applySelectionEffect(final boolean applied) {
|
||||
Platform.runLater(() -> {
|
||||
if (applied) {
|
||||
setBorder(selectionBorder);
|
||||
} else {
|
||||
setBorder(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* apply the 'effect' to visually indicate highlighted nodes
|
||||
*
|
||||
* @param applied true to apply the highlight 'effect', false to remove it
|
||||
*/
|
||||
synchronized void applyHighlightEffect(boolean applied) {
|
||||
|
||||
if (applied) {
|
||||
descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS
|
||||
spanFill = new Background(new BackgroundFill(aggEvent.getType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY));
|
||||
spanRegion.setBackground(spanFill);
|
||||
setBackground(new Background(new BackgroundFill(aggEvent.getType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
|
||||
} else {
|
||||
descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS
|
||||
spanFill = new Background(new BackgroundFill(aggEvent.getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY));
|
||||
spanRegion.setBackground(spanFill);
|
||||
setBackground(new Background(new BackgroundFill(aggEvent.getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
|
||||
}
|
||||
}
|
||||
|
||||
String getDisplayedDescription() {
|
||||
return descrLabel.getText();
|
||||
}
|
||||
|
||||
double getLayoutXCompensation() {
|
||||
return (parentEventNode != null ? parentEventNode.getLayoutXCompensation() : 0)
|
||||
+ getBoundsInParent().getMinX();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the contextMenu
|
||||
*/
|
||||
public ContextMenu getContextMenu() {
|
||||
return contextMenu.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param contextMenu the contextMenu to set
|
||||
*/
|
||||
public void setContextMenu(ContextMenu contextMenu) {
|
||||
this.contextMenu.set(contextMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
* loads sub-clusters at the given Description LOD
|
||||
*
|
||||
* @param newDescriptionLOD
|
||||
*/
|
||||
synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) {
|
||||
getSubNodePane().getChildren().clear();
|
||||
if (newDescriptionLOD == aggEvent.getLOD()) {
|
||||
chart.setRequiresLayout(true);
|
||||
chart.requestChartLayout();
|
||||
} else {
|
||||
RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf();
|
||||
//make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters
|
||||
combinedFilter.getSubFilters().addAll(new TextFilter(aggEvent.getDescription()),
|
||||
new TypeFilter(aggEvent.getType()));
|
||||
|
||||
//make a new end inclusive span (to 'filter' with)
|
||||
final Interval span = aggEvent.getSpan().withEndMillis(aggEvent.getSpan().getEndMillis() + 1000);
|
||||
|
||||
//make a task to load the subnodes
|
||||
LoggedTask<List<AggregateEventNode>> loggedTask = new LoggedTask<List<AggregateEventNode>>(
|
||||
NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) {
|
||||
|
||||
@Override
|
||||
protected List<AggregateEventNode> call() throws Exception {
|
||||
//query for the sub-clusters
|
||||
List<AggregateEvent> aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span,
|
||||
eventsModel.eventTypeZoomProperty().get(),
|
||||
combinedFilter,
|
||||
newDescriptionLOD));
|
||||
//for each sub cluster make an AggregateEventNode to visually represent it, and set x-position
|
||||
return aggregatedEvents.stream().map(aggEvent -> {
|
||||
AggregateEventNode subNode = new AggregateEventNode(aggEvent, AggregateEventNode.this, chart);
|
||||
subNode.setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis())) - getLayoutXCompensation());
|
||||
return subNode;
|
||||
}).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
try {
|
||||
chart.setCursor(Cursor.WAIT);
|
||||
//assign subNodes and request chart layout
|
||||
getSubNodePane().getChildren().setAll(get());
|
||||
setDescriptionVisibility(descrVis);
|
||||
chart.setRequiresLayout(true);
|
||||
chart.requestChartLayout();
|
||||
chart.setCursor(null);
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//start task
|
||||
chart.getController().monitorTask(loggedTask);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* event handler used for mouse events on {@link AggregateEventNode}s
|
||||
*/
|
||||
private class EventMouseHandler implements EventHandler<MouseEvent> {
|
||||
|
||||
@Override
|
||||
public void handle(MouseEvent t) {
|
||||
if (t.getButton() == MouseButton.PRIMARY) {
|
||||
t.consume();
|
||||
if (t.isShiftDown()) {
|
||||
if (chart.selectedNodes.contains(AggregateEventNode.this) == false) {
|
||||
chart.selectedNodes.add(AggregateEventNode.this);
|
||||
}
|
||||
} else if (t.isShortcutDown()) {
|
||||
chart.selectedNodes.removeAll(AggregateEventNode.this);
|
||||
} else if (t.getClickCount() > 1) {
|
||||
final DescriptionLOD next = descLOD.get().next();
|
||||
if (next != null) {
|
||||
loadSubClusters(next);
|
||||
descLOD.set(next);
|
||||
}
|
||||
} else {
|
||||
chart.selectedNodes.setAll(AggregateEventNode.this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -22,8 +22,9 @@ package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||
* Level of description shown in UI NOTE: this is a separate concept form
|
||||
* {@link DescriptionLOD}
|
||||
*/
|
||||
enum DescriptionVisibility {
|
||||
|
||||
HIDDEN, COUNT_ONLY, SHOWN;
|
||||
public enum DescriptionVisibility {
|
||||
|
||||
HIDDEN,
|
||||
COUNT_ONLY,
|
||||
SHOWN;
|
||||
}
|
||||
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public interface DetailViewNode<S extends DetailViewNode<S>> {
|
||||
|
||||
public void setDescriptionVisibility(DescriptionVisibility get);
|
||||
|
||||
public List<? extends S> getSubNodes();
|
||||
|
||||
public void setSpanWidths(List<Double> spanWidths);
|
||||
|
||||
public void setDescriptionWidth(double max);
|
||||
|
||||
public EventBundle getEventBundle();
|
||||
|
||||
/**
|
||||
* apply the 'effect' to visually indicate highlighted nodes
|
||||
*
|
||||
* @param applied true to apply the highlight 'effect', false to remove it
|
||||
*/
|
||||
void applyHighlightEffect(boolean applied);
|
||||
|
||||
public void applySelectionEffect(boolean applied);
|
||||
|
||||
default String getDescription() {
|
||||
return getEventBundle().getDescription();
|
||||
}
|
||||
|
||||
default EventType getEventType() {
|
||||
return getEventBundle().getEventType();
|
||||
}
|
||||
|
||||
default Set<Long> getEventIDs() {
|
||||
return getEventBundle().getEventIDs();
|
||||
}
|
||||
|
||||
default public long getStartMillis() {
|
||||
return getEventBundle().getStartMillis();
|
||||
}
|
||||
|
||||
default long getEndMillis() {
|
||||
return getEventBundle().getEndMillis();
|
||||
}
|
||||
|
||||
|
||||
|
||||
static class StartTimeComparator implements Comparator<DetailViewNode<?>> {
|
||||
|
||||
@Override
|
||||
public int compare(DetailViewNode<?> o1, DetailViewNode<?> o2) {
|
||||
return Long.compare(o1.getStartMillis(), o2.getStartMillis());
|
||||
}
|
||||
}
|
||||
}
|
@ -30,7 +30,6 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.Cursor;
|
||||
@ -70,7 +69,7 @@ import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
|
||||
import org.sleuthkit.autopsy.timeline.FXMLConstructor;
|
||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||
import org.sleuthkit.autopsy.timeline.ui.AbstractVisualization;
|
||||
@ -100,7 +99,7 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;
|
||||
* TODO: refactor common code out of this class and CountsChartPane into
|
||||
* {@link AbstractVisualization}
|
||||
*/
|
||||
public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEvent, AggregateEventNode, EventDetailChart> {
|
||||
public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster, DetailViewNode<?>, EventDetailChart> {
|
||||
|
||||
private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName());
|
||||
|
||||
@ -109,20 +108,20 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
|
||||
//these three could be injected from fxml but it was causing npe's
|
||||
private final DateAxis dateAxis = new DateAxis();
|
||||
|
||||
private final Axis<AggregateEvent> verticalAxis = new EventAxis();
|
||||
private final Axis<EventCluster> verticalAxis = new EventAxis();
|
||||
|
||||
//private access to barchart data
|
||||
private final Map<EventType, XYChart.Series<DateTime, AggregateEvent>> eventTypeToSeriesMap = new ConcurrentHashMap<>();
|
||||
private final Map<EventType, XYChart.Series<DateTime, EventCluster>> eventTypeToSeriesMap = new ConcurrentHashMap<>();
|
||||
|
||||
private final ScrollBar vertScrollBar = new ScrollBar();
|
||||
|
||||
private final Region region = new Region();
|
||||
|
||||
private final ObservableList<AggregateEvent> aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
|
||||
private final ObservableList<EventCluster> aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
|
||||
|
||||
private final ObservableList<AggregateEventNode> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
|
||||
private final ObservableList<DetailViewNode<?>> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
|
||||
|
||||
public ObservableList<AggregateEvent> getAggregatedEvents() {
|
||||
public ObservableList<EventCluster> getAggregatedEvents() {
|
||||
return aggregatedEvents;
|
||||
}
|
||||
|
||||
@ -149,7 +148,7 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
|
||||
vertScrollBar.visibleAmountProperty().bind(chart.heightProperty().multiply(100).divide(chart.getMaxVScroll()));
|
||||
requestLayout();
|
||||
|
||||
highlightedNodes.addListener((ListChangeListener.Change<? extends AggregateEventNode> change) -> {
|
||||
highlightedNodes.addListener((ListChangeListener.Change<? extends DetailViewNode<?>> change) -> {
|
||||
while (change.next()) {
|
||||
change.getAddedSubList().forEach(aeNode -> {
|
||||
aeNode.applyHighlightEffect(true);
|
||||
@ -166,7 +165,7 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
|
||||
|
||||
//These scroll related handlers don't affect any other view or the model, so they are handled internally
|
||||
//mouse wheel scroll handler
|
||||
this.onScrollProperty().set((EventHandler<ScrollEvent>) (ScrollEvent t) -> {
|
||||
this.onScrollProperty().set((ScrollEvent t) -> {
|
||||
vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() - t.getDeltaY() / 200.0)));
|
||||
});
|
||||
|
||||
@ -212,8 +211,8 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
|
||||
selectedNodes.addListener((Observable observable) -> {
|
||||
highlightedNodes.clear();
|
||||
selectedNodes.stream().forEach((tn) -> {
|
||||
for (AggregateEventNode n : chart.getNodes((AggregateEventNode t)
|
||||
-> t.getEvent().getDescription().equals(tn.getEvent().getDescription()))) {
|
||||
for (DetailViewNode<?> n : chart.getNodes((DetailViewNode<?> t) ->
|
||||
t.getDescription().equals(tn.getDescription()))) {
|
||||
highlightedNodes.add(n);
|
||||
}
|
||||
});
|
||||
@ -236,8 +235,8 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
|
||||
treeSelectionModel.getSelectedItems().addListener((Observable observable) -> {
|
||||
highlightedNodes.clear();
|
||||
for (TreeItem<NavTreeNode> tn : treeSelectionModel.getSelectedItems()) {
|
||||
for (AggregateEventNode n : chart.getNodes((AggregateEventNode t)
|
||||
-> t.getEvent().getDescription().equals(tn.getValue().getDescription()))) {
|
||||
for (DetailViewNode<?> n : chart.getNodes((DetailViewNode<?> t) ->
|
||||
t.getDescription().equals(tn.getValue().getDescription()))) {
|
||||
highlightedNodes.add(n);
|
||||
}
|
||||
}
|
||||
@ -250,7 +249,7 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Axis<AggregateEvent> getYAxis() {
|
||||
protected Axis<EventCluster> getYAxis() {
|
||||
return verticalAxis;
|
||||
}
|
||||
|
||||
@ -279,15 +278,13 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
|
||||
* EventType
|
||||
*/
|
||||
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
|
||||
private XYChart.Series<DateTime, AggregateEvent> getSeries(final EventType et) {
|
||||
XYChart.Series<DateTime, AggregateEvent> series = eventTypeToSeriesMap.get(et);
|
||||
if (series == null) {
|
||||
series = new XYChart.Series<>();
|
||||
private XYChart.Series<DateTime, EventCluster> getSeries(final EventType et) {
|
||||
return eventTypeToSeriesMap.computeIfAbsent(et, (EventType t) -> {
|
||||
XYChart.Series<DateTime, EventCluster> series = new XYChart.Series<>();
|
||||
series.setName(et.getDisplayName());
|
||||
eventTypeToSeriesMap.put(et, series);
|
||||
dataSets.add(series);
|
||||
}
|
||||
return series;
|
||||
return series;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -328,17 +325,17 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
|
||||
});
|
||||
final int size = aggregatedEvents.size();
|
||||
int i = 0;
|
||||
for (final AggregateEvent e : aggregatedEvents) {
|
||||
for (final EventCluster e : aggregatedEvents) {
|
||||
if (isCancelled()) {
|
||||
break;
|
||||
}
|
||||
updateProgress(i++, size);
|
||||
updateMessage(NbBundle.getMessage(this.getClass(), "DetailViewPane.loggedTask.updateUI"));
|
||||
final XYChart.Data<DateTime, AggregateEvent> xyData = new BarChart.Data<>(new DateTime(e.getSpan().getStartMillis()), e);
|
||||
final XYChart.Data<DateTime, EventCluster> xyData = new BarChart.Data<>(new DateTime(e.getSpan().getStartMillis()), e);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if (isCancelled() == false) {
|
||||
getSeries(e.getType()).getData().add(xyData);
|
||||
getSeries(e.getEventType()).getData().add(xyData);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -359,8 +356,8 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applySelectionEffect(AggregateEventNode c1, Boolean applied) {
|
||||
c1.applySelectionEffect(applied);
|
||||
protected void applySelectionEffect(DetailViewNode<?> c1, Boolean selected) {
|
||||
chart.applySelectionEffect(c1, selected);
|
||||
}
|
||||
|
||||
private class DetailViewSettingsPane extends HBox {
|
||||
@ -435,9 +432,10 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
|
||||
assert oneEventPerRowBox != null : "fx:id=\"oneEventPerRowBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; // NON-NLS
|
||||
assert truncateAllBox != null : "fx:id=\"truncateAllBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; // NON-NLS
|
||||
assert truncateWidthSlider != null : "fx:id=\"truncateAllSlider\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; // NON-NLS
|
||||
bandByTypeBox.selectedProperty().bindBidirectional(chart.getBandByType());
|
||||
truncateAllBox.selectedProperty().bindBidirectional(chart.getTruncateAll());
|
||||
oneEventPerRowBox.selectedProperty().bindBidirectional(chart.getOneEventPerRow());
|
||||
|
||||
bandByTypeBox.selectedProperty().bindBidirectional(chart.bandByTypeProperty());
|
||||
truncateAllBox.selectedProperty().bindBidirectional(chart.truncateAllProperty());
|
||||
oneEventPerRowBox.selectedProperty().bindBidirectional(chart.oneEventPerRowProperty());
|
||||
truncateSliderLabel.disableProperty().bind(truncateAllBox.selectedProperty().not());
|
||||
truncateSliderLabel.setText(NbBundle.getMessage(this.getClass(), "DetailViewPane.truncateSliderLabel.text"));
|
||||
final InvalidationListener sliderListener = o -> {
|
||||
|
@ -22,21 +22,21 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javafx.scene.chart.Axis;
|
||||
import javafx.scene.chart.XYChart;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
|
||||
/**
|
||||
* No-Op axis that doesn't do anything usefull but is necessary to pass
|
||||
* AggregateEvent as the second member of {@link XYChart.Data} objects
|
||||
*/
|
||||
class EventAxis extends Axis<AggregateEvent> {
|
||||
class EventAxis extends Axis<EventCluster> {
|
||||
|
||||
@Override
|
||||
public double getDisplayPosition(AggregateEvent value) {
|
||||
public double getDisplayPosition(EventCluster value) {
|
||||
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public AggregateEvent getValueForDisplay(double displayPosition) {
|
||||
public EventCluster getValueForDisplay(double displayPosition) {
|
||||
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@ -46,17 +46,17 @@ class EventAxis extends Axis<AggregateEvent> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValueOnAxis(AggregateEvent value) {
|
||||
public boolean isValueOnAxis(EventCluster value) {
|
||||
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public double toNumericValue(AggregateEvent value) {
|
||||
public double toNumericValue(EventCluster value) {
|
||||
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public AggregateEvent toRealValue(double value) {
|
||||
public EventCluster toRealValue(double value) {
|
||||
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ class EventAxis extends Axis<AggregateEvent> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<AggregateEvent> calculateTickValues(double length, Object range) {
|
||||
protected List<EventCluster> calculateTickValues(double length, Object range) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ class EventAxis extends Axis<AggregateEvent> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTickMarkLabel(AggregateEvent value) {
|
||||
protected String getTickMarkLabel(EventCluster value) {
|
||||
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.coreutils.ColorUtilities;
|
||||
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.TimeLineEvent;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
|
||||
/**
|
||||
* Represents an {@link EventCluster} in a {@link EventDetailChart}.
|
||||
*/
|
||||
public class EventClusterNode extends AbstractDetailViewNode<EventCluster, EventClusterNode> {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName());
|
||||
|
||||
/**
|
||||
* the region that represents the time span of this node's event
|
||||
*/
|
||||
private final Region spanRegion = new Region();
|
||||
|
||||
/**
|
||||
* the context menu that with the slider that controls subnode/event display
|
||||
*
|
||||
* //TODO: move more of the control of subnodes/events here and out of
|
||||
* EventDetail Chart
|
||||
*/
|
||||
private final SimpleObjectProperty<ContextMenu> contextMenu = new SimpleObjectProperty<>();
|
||||
|
||||
private Tooltip tooltip;
|
||||
|
||||
public EventClusterNode(final EventCluster eventCluster, EventClusterNode parentEventNode, EventDetailChart chart) {
|
||||
super(chart, eventCluster, parentEventNode);
|
||||
minWidthProperty().bind(spanRegion.widthProperty());
|
||||
header.setPrefWidth(USE_COMPUTED_SIZE);
|
||||
|
||||
final BorderPane borderPane = new BorderPane(getSubNodePane(), header, null, null, null);
|
||||
BorderPane.setAlignment(getSubNodePane(), Pos.TOP_LEFT);
|
||||
borderPane.setPrefWidth(USE_COMPUTED_SIZE);
|
||||
|
||||
getChildren().addAll(spanRegion, borderPane);
|
||||
|
||||
//setup backgrounds
|
||||
spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor) + ";"); // NON-NLS
|
||||
spanRegion.setBackground(getBackground());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
synchronized void installTooltip() {
|
||||
//TODO: all this work should probably go on a background thread...
|
||||
if (tooltip == null) {
|
||||
HashMap<String, Long> hashSetCounts = new HashMap<>();
|
||||
if (!getEventCluster().getEventIDsWithHashHits().isEmpty()) {
|
||||
hashSetCounts = new HashMap<>();
|
||||
try {
|
||||
for (TimeLineEvent tle : getEventsModel().getEventsById(getEventCluster().getEventIDsWithHashHits())) {
|
||||
Set<String> hashSetNames = getSleuthkitCase().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 (!getEventCluster().getEventIDsWithTags().isEmpty()) {
|
||||
tagCounts.putAll(getEventsModel().getTagCountsByTagName(getEventCluster().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"));
|
||||
|
||||
tooltip = new Tooltip(
|
||||
NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text",
|
||||
getEventCluster().getEventIDs().size(), getEventCluster().getEventType(), getEventCluster().getDescription(),
|
||||
getEventCluster().getSpan().getStart().toString(TimeLineController.getZonedFormatter()),
|
||||
getEventCluster().getSpan().getEnd().toString(TimeLineController.getZonedFormatter()))
|
||||
+ (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString)
|
||||
+ (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString)
|
||||
);
|
||||
Tooltip.install(EventClusterNode.this, tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized public EventCluster getEventCluster() {
|
||||
return getEventBundle();
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the width of the {@link Region} with border and background used to
|
||||
* indicate the temporal span of this aggregate event
|
||||
*
|
||||
* @param w
|
||||
*/
|
||||
private void setSpanWidth(double w) {
|
||||
spanRegion.setPrefWidth(w);
|
||||
spanRegion.setMaxWidth(w);
|
||||
spanRegion.setMinWidth(Math.max(2, w));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSpanWidths(List<Double> spanWidths) {
|
||||
setSpanWidth(spanWidths.get(0));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
Region getSpanFillNode() {
|
||||
return spanRegion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the contextMenu
|
||||
*/
|
||||
public ContextMenu getContextMenu() {
|
||||
return contextMenu.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param contextMenu the contextMenu to set
|
||||
*/
|
||||
public void setContextMenu(ContextMenu contextMenu) {
|
||||
this.contextMenu.set(contextMenu);
|
||||
}
|
||||
|
||||
@Override
|
||||
void showSpans(boolean showSpans) {
|
||||
//no-op for now
|
||||
}
|
||||
|
||||
@Override
|
||||
Collection<EventCluster> makeBundlesFromClusters(List<EventCluster> eventClusters) {
|
||||
return eventClusters;
|
||||
}
|
||||
|
||||
@Override
|
||||
EventClusterNode getNodeForBundle(EventCluster cluster) {
|
||||
return new EventClusterNode(cluster, this, getChart());
|
||||
}
|
||||
|
||||
}
|
@ -18,22 +18,27 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.collect.Range;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@ -41,9 +46,7 @@ import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.MapChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
@ -62,6 +65,7 @@ import javafx.scene.shape.Line;
|
||||
import javafx.scene.shape.StrokeLineCap;
|
||||
import javafx.util.Duration;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.controlsfx.control.action.Action;
|
||||
import org.controlsfx.control.action.ActionGroup;
|
||||
import org.controlsfx.control.action.ActionUtils;
|
||||
@ -71,7 +75,8 @@ import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||
import org.sleuthkit.autopsy.timeline.actions.Back;
|
||||
import org.sleuthkit.autopsy.timeline.actions.Forward;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||
import org.sleuthkit.autopsy.timeline.ui.TimeLineChart;
|
||||
@ -90,7 +95,7 @@ import org.sleuthkit.autopsy.timeline.ui.TimeLineChart;
|
||||
*
|
||||
* //TODO: refactor the projected lines to a separate class. -jm
|
||||
*/
|
||||
public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> implements TimeLineChart<DateTime> {
|
||||
public final class EventDetailChart extends XYChart<DateTime, EventCluster> implements TimeLineChart<DateTime> {
|
||||
|
||||
private static final int PROJECTED_LINE_Y_OFFSET = 5;
|
||||
|
||||
@ -102,7 +107,6 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
*/
|
||||
private final SimpleBooleanProperty bandByType = new SimpleBooleanProperty(false);
|
||||
|
||||
// I don't like having these package visible, but it was the easiest way to
|
||||
private ContextMenu chartContextMenu;
|
||||
|
||||
private TimeLineController controller;
|
||||
@ -151,17 +155,9 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
/**
|
||||
* map from event to node
|
||||
*/
|
||||
private final Map<AggregateEvent, AggregateEventNode> nodeMap = new TreeMap<>((
|
||||
AggregateEvent o1,
|
||||
AggregateEvent o2) -> {
|
||||
int comp = Long.compare(o1.getSpan().getStartMillis(), o2.getSpan().getStartMillis());
|
||||
if (comp != 0) {
|
||||
return comp;
|
||||
} else {
|
||||
return Comparator.comparing(AggregateEvent::hashCode).compare(o1, o2);
|
||||
}
|
||||
});
|
||||
|
||||
private final Map<EventCluster, EventClusterNode> clusterNodeMap = new HashMap<>();
|
||||
private final Map<ImmutablePair<EventType, String>, EventStripe> stripeDescMap = new HashMap<>();
|
||||
private final Map<EventStripe, EventStripeNode> stripeNodeMap = new HashMap<>();
|
||||
/**
|
||||
* true == enforce that no two events can share the same 'row', leading to
|
||||
* sparser but possibly clearer layout. false == put unrelated events in the
|
||||
@ -169,7 +165,7 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
*/
|
||||
private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false);
|
||||
|
||||
private final ObservableMap<AggregateEventNode, Line> projectionMap = FXCollections.observableHashMap();
|
||||
private final Map<Range<Long>, Line> projectionMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* flag indicating whether this chart actually needs a layout pass
|
||||
@ -177,16 +173,16 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
@GuardedBy(value = "this")
|
||||
private boolean requiresLayout = true;
|
||||
|
||||
final ObservableList<AggregateEventNode> selectedNodes;
|
||||
final ObservableList<DetailViewNode<?>> selectedNodes;
|
||||
|
||||
/**
|
||||
* list of series of data added to this chart TODO: replace this with a map
|
||||
* from name to series? -jm
|
||||
*/
|
||||
private final ObservableList<Series<DateTime, AggregateEvent>> seriesList
|
||||
= FXCollections.<Series<DateTime, AggregateEvent>>observableArrayList();
|
||||
private final ObservableList<Series<DateTime, EventCluster>> seriesList
|
||||
= FXCollections.<Series<DateTime, EventCluster>>observableArrayList();
|
||||
|
||||
private final ObservableList<Series<DateTime, AggregateEvent>> sortedSeriesList = seriesList
|
||||
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()));
|
||||
@ -205,12 +201,13 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
* via slider if truncateAll is true
|
||||
*/
|
||||
private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0);
|
||||
private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true);
|
||||
|
||||
EventDetailChart(DateAxis dateAxis, final Axis<AggregateEvent> verticalAxis, ObservableList<AggregateEventNode> selectedNodes) {
|
||||
EventDetailChart(DateAxis dateAxis, final Axis<EventCluster> verticalAxis, ObservableList<DetailViewNode<?>> selectedNodes) {
|
||||
super(dateAxis, verticalAxis);
|
||||
dateAxis.setAutoRanging(false);
|
||||
|
||||
//yAxis.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.setTickMarkVisible(false);
|
||||
|
||||
@ -224,7 +221,7 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
//bind listener to events that should trigger layout
|
||||
widthProperty().addListener(layoutInvalidationListener);
|
||||
heightProperty().addListener(layoutInvalidationListener);
|
||||
// boundsInLocalProperty().addListener(layoutInvalidationListener);
|
||||
|
||||
bandByType.addListener(layoutInvalidationListener);
|
||||
oneEventPerRow.addListener(layoutInvalidationListener);
|
||||
truncateAll.addListener(layoutInvalidationListener);
|
||||
@ -242,36 +239,7 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
chartContextMenu.hide();
|
||||
}
|
||||
if (clickEvent.getButton() == MouseButton.SECONDARY && clickEvent.isStillSincePress()) {
|
||||
|
||||
chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new Action(
|
||||
NbBundle.getMessage(this.getClass(), "EventDetailChart.chartContextMenu.placeMarker.name")) {
|
||||
{
|
||||
setGraphic(new ImageView(new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true))); // NON-NLS
|
||||
setEventHandler((ActionEvent t) -> {
|
||||
if (guideLine == null) {
|
||||
guideLine = new GuideLine(0, 0, 0, getHeight(), dateAxis);
|
||||
guideLine.relocate(clickEvent.getX(), 0);
|
||||
guideLine.endYProperty().bind(heightProperty().subtract(dateAxis.heightProperty().subtract(dateAxis.tickLengthProperty())));
|
||||
|
||||
getChartChildren().add(guideLine);
|
||||
|
||||
guideLine.setOnMouseClicked((MouseEvent event) -> {
|
||||
if (event.getButton() == MouseButton.SECONDARY) {
|
||||
clearGuideLine();
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
guideLine.relocate(clickEvent.getX(), 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}, new ActionGroup(
|
||||
NbBundle.getMessage(this.getClass(), "EventDetailChart.contextMenu.zoomHistory.name"),
|
||||
new Back(controller),
|
||||
new Forward(controller))));
|
||||
chartContextMenu.setAutoHide(true);
|
||||
getChartContextMenu(clickEvent);
|
||||
chartContextMenu.show(EventDetailChart.this, clickEvent.getScreenX(), clickEvent.getScreenY());
|
||||
clickEvent.consume();
|
||||
}
|
||||
@ -285,51 +253,85 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
setOnMouseReleased(dragHandler);
|
||||
setOnMouseDragged(dragHandler);
|
||||
|
||||
projectionMap.addListener((MapChangeListener.Change<? extends AggregateEventNode, ? extends Line> change) -> {
|
||||
final Line valueRemoved = change.getValueRemoved();
|
||||
if (valueRemoved != null) {
|
||||
getChartChildren().removeAll(valueRemoved);
|
||||
}
|
||||
final Line valueAdded = change.getValueAdded();
|
||||
if (valueAdded != null) {
|
||||
getChartChildren().add(valueAdded);
|
||||
}
|
||||
});
|
||||
|
||||
this.selectedNodes = selectedNodes;
|
||||
this.selectedNodes.addListener((
|
||||
ListChangeListener.Change<? extends AggregateEventNode> c) -> {
|
||||
ListChangeListener.Change<? extends DetailViewNode<?>> c) -> {
|
||||
while (c.next()) {
|
||||
c.getRemoved().forEach((AggregateEventNode t) -> {
|
||||
projectionMap.remove(t);
|
||||
});
|
||||
c.getAddedSubList().forEach((AggregateEventNode t) -> {
|
||||
Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET,
|
||||
dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET
|
||||
);
|
||||
line.setStroke(t.getEvent().getType().getColor().deriveColor(0, 1, 1, .5));
|
||||
line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH);
|
||||
line.setStrokeLineCap(StrokeLineCap.ROUND);
|
||||
projectionMap.put(t, line);
|
||||
});
|
||||
c.getRemoved().forEach((DetailViewNode<?> t) -> {
|
||||
t.getEventBundle().getRanges().forEach((Range<Long> t1) -> {
|
||||
Line removedLine = projectionMap.remove(t1);
|
||||
getChartChildren().removeAll(removedLine);
|
||||
});
|
||||
|
||||
});
|
||||
c.getAddedSubList().forEach((DetailViewNode<?> t) -> {
|
||||
|
||||
for (Range<Long> range : t.getEventBundle().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((AggregateEventNode aggNode) -> aggNode.getEvent().getEventIDs().stream())
|
||||
.flatMap(detailNode -> detailNode.getEventIDs().stream())
|
||||
.collect(Collectors.toList()));
|
||||
});
|
||||
|
||||
requestChartLayout();
|
||||
}
|
||||
|
||||
ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException {
|
||||
if (chartContextMenu != null) {
|
||||
chartContextMenu.hide();
|
||||
}
|
||||
chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new Action(
|
||||
NbBundle.getMessage(this.getClass(), "EventDetailChart.chartContextMenu.placeMarker.name")) {
|
||||
{
|
||||
setGraphic(new ImageView(new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true))); // NON-NLS
|
||||
setEventHandler((ActionEvent t) -> {
|
||||
if (guideLine == null) {
|
||||
guideLine = new GuideLine(0, 0, 0, getHeight(), getXAxis());
|
||||
|
||||
guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0);
|
||||
guideLine.endYProperty().bind(heightProperty().subtract(getXAxis().heightProperty().subtract(getXAxis().tickLengthProperty())));
|
||||
|
||||
getChartChildren().add(guideLine);
|
||||
|
||||
guideLine.setOnMouseClicked((MouseEvent event) -> {
|
||||
if (event.getButton() == MouseButton.SECONDARY) {
|
||||
clearGuideLine();
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}, new ActionGroup(
|
||||
NbBundle.getMessage(this.getClass(), "EventDetailChart.contextMenu.zoomHistory.name"),
|
||||
new Back(controller),
|
||||
new Forward(controller))));
|
||||
chartContextMenu.setAutoHide(true);
|
||||
return chartContextMenu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearIntervalSelector() {
|
||||
getChartChildren().remove(intervalSelector);
|
||||
intervalSelector = null;
|
||||
}
|
||||
|
||||
public synchronized SimpleBooleanProperty getBandByType() {
|
||||
public synchronized SimpleBooleanProperty bandByTypeProperty() {
|
||||
return bandByType;
|
||||
}
|
||||
|
||||
@ -389,11 +391,11 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
getChartChildren().add(getIntervalSelector());
|
||||
}
|
||||
|
||||
public synchronized SimpleBooleanProperty getOneEventPerRow() {
|
||||
public synchronized SimpleBooleanProperty oneEventPerRowProperty() {
|
||||
return oneEventPerRow;
|
||||
}
|
||||
|
||||
public synchronized SimpleBooleanProperty getTruncateAll() {
|
||||
public synchronized SimpleBooleanProperty truncateAllProperty() {
|
||||
return truncateAll;
|
||||
}
|
||||
|
||||
@ -407,30 +409,49 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void dataItemAdded(Series<DateTime, AggregateEvent> series, int i, Data<DateTime, AggregateEvent> data) {
|
||||
final AggregateEvent aggEvent = data.getYValue();
|
||||
AggregateEventNode eventNode = nodeMap.get(aggEvent);
|
||||
if (eventNode == null) {
|
||||
eventNode = new AggregateEventNode(aggEvent, null, this);
|
||||
|
||||
eventNode.setLayoutX(getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis())));
|
||||
data.setNode(eventNode);
|
||||
nodeMap.put(aggEvent, eventNode);
|
||||
nodeGroup.getChildren().add(eventNode);
|
||||
requiresLayout = true;
|
||||
protected synchronized void dataItemAdded(Series<DateTime, EventCluster> series, int i, Data<DateTime, EventCluster> data) {
|
||||
final EventCluster aggEvent = data.getYValue();
|
||||
if (alternateLayout.get()) {
|
||||
EventStripe eventCluster = stripeDescMap.merge(ImmutablePair.of(aggEvent.getEventType(), aggEvent.getDescription()),
|
||||
new EventStripe(aggEvent),
|
||||
(EventStripe u, EventStripe v) -> {
|
||||
EventStripeNode remove = stripeNodeMap.remove(u);
|
||||
nodeGroup.getChildren().remove(remove);
|
||||
remove = stripeNodeMap.remove(v);
|
||||
nodeGroup.getChildren().remove(remove);
|
||||
return EventStripe.merge(u, v);
|
||||
}
|
||||
);
|
||||
EventStripeNode clusterNode = new EventStripeNode(eventCluster, null, EventDetailChart.this);
|
||||
stripeNodeMap.put(eventCluster, clusterNode);
|
||||
nodeGroup.getChildren().add(clusterNode);
|
||||
} else {
|
||||
clusterNodeMap.computeIfAbsent(aggEvent, (EventCluster t) -> {
|
||||
EventClusterNode eventNode = new EventClusterNode(aggEvent, null, EventDetailChart.this);
|
||||
eventNode.setLayoutX(getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis())));
|
||||
clusterNodeMap.put(aggEvent, eventNode);
|
||||
nodeGroup.getChildren().add(eventNode);
|
||||
return eventNode;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void dataItemChanged(Data<DateTime, AggregateEvent> data) {
|
||||
protected synchronized void dataItemChanged(Data<DateTime, EventCluster> data) {
|
||||
//TODO: can we use this to help with local detail level adjustment -jm
|
||||
throw new UnsupportedOperationException("Not supported yet."); // NON-NLS //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void dataItemRemoved(Data<DateTime, AggregateEvent> data, Series<DateTime, AggregateEvent> series) {
|
||||
nodeMap.remove(data.getYValue());
|
||||
nodeGroup.getChildren().remove(data.getNode());
|
||||
protected synchronized void dataItemRemoved(Data<DateTime, EventCluster> data, Series<DateTime, EventCluster> series) {
|
||||
EventCluster aggEvent = data.getYValue();
|
||||
Node removedNode = clusterNodeMap.remove(aggEvent);
|
||||
nodeGroup.getChildren().remove(removedNode);
|
||||
|
||||
EventStripe removedCluster = stripeDescMap.remove(ImmutablePair.of(aggEvent.getEventType(), aggEvent.getDescription()));
|
||||
removedNode = stripeNodeMap.remove(removedCluster);
|
||||
nodeGroup.getChildren().remove(removedNode);
|
||||
|
||||
data.setNode(null);
|
||||
}
|
||||
|
||||
@ -465,17 +486,36 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
maxY.set(0.0);
|
||||
|
||||
if (bandByType.get() == false) {
|
||||
if (alternateLayout.get() == true) {
|
||||
List<EventStripeNode> nodes = new ArrayList<>(stripeNodeMap.values());
|
||||
nodes.sort(Comparator.comparing(DetailViewNode<?>::getStartMillis));
|
||||
layoutNodes(nodes, minY, 0);
|
||||
} else {
|
||||
List<EventClusterNode> nodes = new ArrayList<>(clusterNodeMap.values());
|
||||
nodes.sort(Comparator.comparing(DetailViewNode<?>::getStartMillis));
|
||||
layoutNodes(nodes, minY, 0);
|
||||
}
|
||||
|
||||
ObservableList<Node> nodes = FXCollections.observableArrayList(nodeMap.values());
|
||||
FXCollections.sort(nodes, new StartTimeComparator());
|
||||
layoutNodes(nodes, minY, 0);
|
||||
// layoutNodes(new ArrayList<>(nodeMap.values()), minY, 0);
|
||||
} else {
|
||||
for (Series<DateTime, AggregateEvent> s : sortedSeriesList) {
|
||||
ObservableList<Node> nodes = FXCollections.observableArrayList(Collections2.transform(s.getData(), Data::getNode));
|
||||
|
||||
FXCollections.sort(nodes, new StartTimeComparator());
|
||||
layoutNodes(nodes.filtered((Node n) -> n != null), minY, 0);
|
||||
for (Series<DateTime, EventCluster> s : sortedSeriesList) {
|
||||
if (alternateLayout.get() == true) {
|
||||
List<EventStripeNode> nodes = s.getData().stream()
|
||||
.map(Data::getYValue)
|
||||
.map(cluster -> stripeDescMap.get(ImmutablePair.of(cluster.getEventType(), cluster.getDescription())))
|
||||
.distinct()
|
||||
.sorted(Comparator.comparing(EventStripe::getStartMillis))
|
||||
.map(stripeNodeMap::get)
|
||||
.collect(Collectors.toList());
|
||||
layoutNodes(nodes, minY, 0);
|
||||
} else {
|
||||
List<EventClusterNode> nodes = s.getData().stream()
|
||||
.map(Data::getYValue)
|
||||
.map(clusterNodeMap::get)
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator.comparing(EventClusterNode::getStartMillis))
|
||||
.collect(Collectors.toList());
|
||||
layoutNodes(nodes, minY, 0);
|
||||
}
|
||||
minY = maxY.get();
|
||||
}
|
||||
}
|
||||
@ -486,7 +526,7 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void seriesAdded(Series<DateTime, AggregateEvent> series, int i) {
|
||||
protected synchronized void seriesAdded(Series<DateTime, EventCluster> series, int i) {
|
||||
for (int j = 0; j < series.getData().size(); j++) {
|
||||
dataItemAdded(series, j, series.getData().get(j));
|
||||
}
|
||||
@ -495,7 +535,7 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void seriesRemoved(Series<DateTime, AggregateEvent> series) {
|
||||
protected synchronized void seriesRemoved(Series<DateTime, EventCluster> series) {
|
||||
for (int j = 0; j < series.getData().size(); j++) {
|
||||
dataItemRemoved(series.getData().get(j), series);
|
||||
}
|
||||
@ -503,7 +543,7 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
requiresLayout = true;
|
||||
}
|
||||
|
||||
synchronized SimpleObjectProperty<DescriptionVisibility> getDescrVisibility() {
|
||||
synchronized SimpleObjectProperty< DescriptionVisibility> getDescrVisibility() {
|
||||
return descrVisibility;
|
||||
}
|
||||
|
||||
@ -511,40 +551,39 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
return maxY.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
Iterable<AggregateEventNode> getNodes(Predicate<AggregateEventNode> p) {
|
||||
List<AggregateEventNode> nodes = new ArrayList<>();
|
||||
Iterable<DetailViewNode<?>> getNodes(Predicate<DetailViewNode<?>> p) {
|
||||
Collection<? extends DetailViewNode<?>> values = alternateLayout.get()
|
||||
? stripeNodeMap.values()
|
||||
: clusterNodeMap.values();
|
||||
|
||||
for (AggregateEventNode node : nodeMap.values()) {
|
||||
checkNode(node, p, nodes);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
//collapse tree of DetailViewNoeds to list and then filter on given predicate
|
||||
return values.stream()
|
||||
.flatMap(EventDetailChart::flatten)
|
||||
.filter(p).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
Iterable<AggregateEventNode> getAllNodes() {
|
||||
public static Stream<? extends DetailViewNode<?>> flatten(DetailViewNode<?> node) {
|
||||
return Stream.concat(
|
||||
Stream.of(node),
|
||||
node.getSubNodes().stream().flatMap(EventDetailChart::flatten));
|
||||
}
|
||||
|
||||
Iterable<DetailViewNode<?>> getAllNodes() {
|
||||
return getNodes(x -> true);
|
||||
}
|
||||
|
||||
synchronized SimpleDoubleProperty getTruncateWidth() {
|
||||
synchronized SimpleDoubleProperty
|
||||
getTruncateWidth() {
|
||||
return truncateWidth;
|
||||
}
|
||||
|
||||
synchronized void setVScroll(double d) {
|
||||
synchronized void
|
||||
setVScroll(double d
|
||||
) {
|
||||
final double h = maxY.get() - (getHeight() * .9);
|
||||
nodeGroup.setTranslateY(-d * h);
|
||||
}
|
||||
|
||||
private static void checkNode(AggregateEventNode node, Predicate<AggregateEventNode> p, List<AggregateEventNode> nodes) {
|
||||
if (node != null) {
|
||||
if (p.test(node)) {
|
||||
nodes.add(node);
|
||||
}
|
||||
for (Node n : node.getSubNodePane().getChildrenUnmodifiable()) {
|
||||
checkNode((AggregateEventNode) n, p, nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearGuideLine() {
|
||||
getChartChildren().remove(guideLine);
|
||||
guideLine = null;
|
||||
@ -557,41 +596,75 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
* @param nodes
|
||||
* @param minY
|
||||
*/
|
||||
private synchronized double layoutNodes(final List<Node> nodes, final double minY, final double xOffset) {
|
||||
private synchronized double layoutNodes(final Collection<? extends AbstractDetailViewNode<?, ?>> nodes, final double minY, final double xOffset) {
|
||||
//hash map from y value to right most occupied x value. This tells you for a given 'row' what is the first avaialable slot
|
||||
Map<Integer, Double> maxXatY = new HashMap<>();
|
||||
double localMax = minY;
|
||||
//for each node lay size it and position it in first available slot
|
||||
for (Node n : nodes) {
|
||||
final AggregateEventNode tlNode = (AggregateEventNode) n;
|
||||
tlNode.setDescriptionVisibility(descrVisibility.get());
|
||||
for (AbstractDetailViewNode<?, ?> node : nodes) {
|
||||
node.setDescriptionVisibility(descrVisibility.get());
|
||||
double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(node.getStartMillis()));
|
||||
|
||||
AggregateEvent ie = tlNode.getEvent();
|
||||
final double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(ie.getSpan().getStartMillis()));
|
||||
//position of start and end according to range of axis
|
||||
double xPos = rawDisplayPosition - xOffset;
|
||||
double startX = rawDisplayPosition - xOffset;
|
||||
double layoutNodesResultHeight = 0;
|
||||
if (tlNode.getSubNodePane().getChildren().isEmpty() == false) {
|
||||
FXCollections.sort(tlNode.getSubNodePane().getChildren(), new StartTimeComparator());
|
||||
layoutNodesResultHeight = layoutNodes(tlNode.getSubNodePane().getChildren(), 0, rawDisplayPosition);
|
||||
}
|
||||
double xPos2 = getXAxis().getDisplayPosition(new DateTime(ie.getSpan().getEndMillis())) - xOffset;
|
||||
double span = xPos2 - xPos;
|
||||
|
||||
//size timespan border
|
||||
tlNode.setSpanWidth(span);
|
||||
if (truncateAll.get()) { //if truncate option is selected limit width of description label
|
||||
tlNode.setDescriptionWidth(Math.max(span, truncateWidth.get()));
|
||||
} else { //else set it unbounded
|
||||
tlNode.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth());
|
||||
double span = 0;
|
||||
List<? extends AbstractDetailViewNode<?, ?>> subNodes = node.getSubNodes();
|
||||
if (subNodes.isEmpty() == false) {
|
||||
subNodes.sort(new DetailViewNode.StartTimeComparator());
|
||||
layoutNodesResultHeight = layoutNodes(subNodes, 0, rawDisplayPosition);
|
||||
}
|
||||
tlNode.autosize(); //compute size of tlNode based on constraints and event data
|
||||
|
||||
if (alternateLayout.get() == false) {
|
||||
double endX = getXAxis().getDisplayPosition(new DateTime(node.getEndMillis())) - xOffset;
|
||||
span = endX - startX;
|
||||
//size timespan border
|
||||
node.setSpanWidths(Arrays.asList(span));
|
||||
} else {
|
||||
|
||||
EventStripeNode stripeNode = (EventStripeNode) node;
|
||||
List<Double> spanWidths = new ArrayList<>();
|
||||
double x = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));;
|
||||
double x2;
|
||||
Iterator<Range<Long>> ranges = stripeNode.getStripe().getRanges().iterator();
|
||||
Range<Long> range = ranges.next();
|
||||
do {
|
||||
x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint()));
|
||||
double clusterSpan = x2 - x;
|
||||
span += clusterSpan;
|
||||
spanWidths.add(clusterSpan);
|
||||
if (ranges.hasNext()) {
|
||||
range = ranges.next();
|
||||
x = getXAxis().getDisplayPosition(new DateTime(range.lowerEndpoint()));
|
||||
double gapSpan = x - x2;
|
||||
span += gapSpan;
|
||||
spanWidths.add(gapSpan);
|
||||
if (ranges.hasNext() == false) {
|
||||
x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint()));
|
||||
clusterSpan = x2 - x;
|
||||
span += clusterSpan;
|
||||
spanWidths.add(clusterSpan);
|
||||
}
|
||||
}
|
||||
|
||||
} while (ranges.hasNext());
|
||||
|
||||
stripeNode.setSpanWidths(spanWidths);
|
||||
}
|
||||
if (truncateAll.get()) { //if truncate option is selected limit width of description label
|
||||
node.setDescriptionWidth(Math.max(span, truncateWidth.get()));
|
||||
} else { //else set it unbounded
|
||||
node.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth());
|
||||
}
|
||||
|
||||
node.autosize(); //compute size of tlNode based on constraints and event data
|
||||
|
||||
//get position of right edge of node ( influenced by description label)
|
||||
double xRight = xPos + tlNode.getWidth();
|
||||
double xRight = startX + node.getWidth();
|
||||
|
||||
//get the height of the node
|
||||
final double h = layoutNodesResultHeight == 0 ? tlNode.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT;
|
||||
final double h = layoutNodesResultHeight == 0 ? node.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT;
|
||||
//initial test position
|
||||
double yPos = minY;
|
||||
|
||||
@ -612,7 +685,7 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
//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 >= xPos - 4) {
|
||||
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;
|
||||
@ -630,24 +703,24 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
localMax = Math.max(yPos2, localMax);
|
||||
|
||||
Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0),
|
||||
new KeyValue(tlNode.layoutXProperty(), xPos),
|
||||
new KeyValue(tlNode.layoutYProperty(), yPos)));
|
||||
new KeyValue(node.layoutXProperty(), startX),
|
||||
new KeyValue(node.layoutYProperty(), yPos)));
|
||||
|
||||
tm.play();
|
||||
// tlNode.relocate(xPos, yPos);
|
||||
}
|
||||
maxY.set(Math.max(maxY.get(), localMax));
|
||||
return localMax - minY;
|
||||
}
|
||||
|
||||
private static final int DEFAULT_ROW_HEIGHT = 24;
|
||||
|
||||
private void layoutProjectionMap() {
|
||||
for (final Map.Entry<AggregateEventNode, Line> entry : projectionMap.entrySet()) {
|
||||
final AggregateEventNode aggNode = entry.getKey();
|
||||
for (final Map.Entry<Range<Long>, Line> entry : projectionMap.entrySet()) {
|
||||
final Range<Long> eventBundle = entry.getKey();
|
||||
final Line line = entry.getValue();
|
||||
|
||||
line.setStartX(getParentXForValue(new DateTime(aggNode.getEvent().getSpan().getStartMillis(), TimeLineController.getJodaTimeZone())));
|
||||
line.setEndX(getParentXForValue(new DateTime(aggNode.getEvent().getSpan().getEndMillis(), TimeLineController.getJodaTimeZone())));
|
||||
line.setStartX(getParentXForValue(new DateTime(eventBundle.lowerEndpoint(), TimeLineController.getJodaTimeZone())));
|
||||
line.setEndX(getParentXForValue(new DateTime(eventBundle.upperEndpoint(), TimeLineController.getJodaTimeZone())));
|
||||
line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
|
||||
line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
|
||||
}
|
||||
@ -671,29 +744,8 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
return filteredEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the chartContextMenu
|
||||
*/
|
||||
public ContextMenu getChartContextMenu() {
|
||||
return chartContextMenu;
|
||||
}
|
||||
|
||||
private static class StartTimeComparator implements Comparator<Node> {
|
||||
|
||||
@Override
|
||||
public int compare(Node n1, Node n2) {
|
||||
|
||||
if (n1 == null) {
|
||||
return 1;
|
||||
} else if (n2 == null) {
|
||||
return -1;
|
||||
} else {
|
||||
|
||||
return Long.compare(((AggregateEventNode) n1).getEvent().getSpan().getStartMillis(),
|
||||
(((AggregateEventNode) n2).getEvent().getSpan().getStartMillis()));
|
||||
}
|
||||
}
|
||||
|
||||
Property<Boolean> alternateLayoutProperty() {
|
||||
return alternateLayout;
|
||||
}
|
||||
|
||||
private class DetailIntervalSelector extends IntervalSelector<DateTime> {
|
||||
@ -730,4 +782,8 @@ public final class EventDetailChart extends XYChart<DateTime, AggregateEvent> im
|
||||
protected void requestChartLayout() {
|
||||
super.requestChartLayout();
|
||||
}
|
||||
|
||||
void applySelectionEffect(DetailViewNode<?> c1, Boolean selected) {
|
||||
c1.applySelectionEffect(selected);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import static javafx.scene.layout.Region.USE_PREF_SIZE;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.sleuthkit.autopsy.coreutils.ColorUtilities;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
|
||||
import static org.sleuthkit.autopsy.timeline.ui.detailview.AbstractDetailViewNode.show;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class EventStripeNode extends AbstractDetailViewNode<EventStripe, EventStripeNode> {
|
||||
|
||||
private final HBox rangesHBox = new HBox();
|
||||
|
||||
EventStripeNode(EventStripe eventStripe, EventStripeNode parentNode, EventDetailChart chart) {
|
||||
super(chart, eventStripe, parentNode);
|
||||
minWidthProperty().bind(rangesHBox.widthProperty());
|
||||
final VBox internalVBox = new VBox(header, getSubNodePane());
|
||||
internalVBox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
for (Range<Long> range : eventStripe.getRanges()) {
|
||||
Region rangeRegion = new Region();
|
||||
rangeRegion.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS
|
||||
rangeRegion.setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
|
||||
rangesHBox.getChildren().addAll(rangeRegion, new Region());
|
||||
}
|
||||
rangesHBox.getChildren().remove(rangesHBox.getChildren().size() - 1);
|
||||
rangesHBox.setMaxWidth(USE_PREF_SIZE);
|
||||
setMaxWidth(USE_PREF_SIZE);
|
||||
getChildren().addAll(rangesHBox, internalVBox);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param showControls the value of par
|
||||
*/
|
||||
@Override
|
||||
void showDescriptionLoDControls(final boolean showControls) {
|
||||
super.showDescriptionLoDControls(showControls);
|
||||
show(getSpacer(), showControls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSpanWidths(List<Double> spanWidths) {
|
||||
for (int i = 0; i < spanWidths.size(); i++) {
|
||||
Region spanRegion = (Region) rangesHBox.getChildren().get(i);
|
||||
Double w = spanWidths.get(i);
|
||||
spanRegion.setPrefWidth(w);
|
||||
spanRegion.setMaxWidth(w);
|
||||
spanRegion.setMinWidth(Math.max(2, w));
|
||||
}
|
||||
}
|
||||
|
||||
EventStripe getStripe() {
|
||||
return getEventBundle();
|
||||
}
|
||||
|
||||
@Override
|
||||
HBox getSpanFillNode() {
|
||||
return rangesHBox;
|
||||
}
|
||||
|
||||
@Override
|
||||
Collection<EventStripe> makeBundlesFromClusters(List<EventCluster> eventClusters) {
|
||||
return eventClusters.stream().collect(
|
||||
Collectors.toMap(
|
||||
EventCluster::getDescription, //key
|
||||
EventStripe::new, //value
|
||||
EventStripe::merge)//merge method
|
||||
).values();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param showSpans the value of showSpans
|
||||
*/
|
||||
@Override
|
||||
void showSpans(final boolean showSpans) {
|
||||
rangesHBox.setVisible(showSpans);
|
||||
}
|
||||
|
||||
@Override
|
||||
void installTooltip() {
|
||||
// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
EventStripeNode getNodeForBundle(EventStripe cluster) {
|
||||
return new EventStripeNode(cluster, this, getChart());
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@
|
||||
package org.sleuthkit.autopsy.timeline.ui.detailview;
|
||||
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.chart.Axis;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.paint.Color;
|
||||
@ -32,7 +33,7 @@ import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||
*/
|
||||
class GuideLine extends Line {
|
||||
|
||||
private final DateAxis dateAxis;
|
||||
private final Axis<DateTime> dateAxis;
|
||||
|
||||
private double startLayoutX;
|
||||
|
||||
@ -40,7 +41,7 @@ class GuideLine extends Line {
|
||||
|
||||
private double dragStartX = 0;
|
||||
|
||||
GuideLine(double startX, double startY, double endX, double endY, DateAxis axis) {
|
||||
GuideLine(double startX, double startY, double endX, double endY, Axis<DateTime> axis) {
|
||||
super(startX, startY, endX, endY);
|
||||
dateAxis = axis;
|
||||
setCursor(Cursor.E_RESIZE);
|
||||
|
@ -20,15 +20,16 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
|
||||
|
||||
import java.util.Comparator;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class EventDescriptionTreeItem extends NavTreeItem {
|
||||
|
||||
public EventDescriptionTreeItem(AggregateEvent g) {
|
||||
setValue(new NavTreeNode(g.getType().getBaseType(), g.getDescription(), g.getEventIDs().size()));
|
||||
public EventDescriptionTreeItem(EventCluster g) {
|
||||
setValue(new NavTreeNode(g.getEventType().getBaseType(), g.getDescription(), g.getEventIDs().size()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -37,9 +38,9 @@ class EventDescriptionTreeItem extends NavTreeItem {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insert(AggregateEvent g) {
|
||||
public void insert(EventCluster g) {
|
||||
NavTreeNode value = getValue();
|
||||
if ((value.getType().getBaseType().equals(g.getType().getBaseType()) == false) || ((value.getDescription().equals(g.getDescription()) == false))) {
|
||||
if ((value.getType().getBaseType().equals(g.getEventType().getBaseType()) == false) || ((value.getDescription().equals(g.getDescription()) == false))) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
@ -52,8 +53,8 @@ class EventDescriptionTreeItem extends NavTreeItem {
|
||||
}
|
||||
|
||||
@Override
|
||||
public TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t) {
|
||||
if (getValue().getType().getBaseType() == t.getType().getBaseType() && getValue().getDescription().equals(t.getDescription())) {
|
||||
public TreeItem<NavTreeNode> findTreeItemForEvent(EventBundle t) {
|
||||
if (getValue().getType().getBaseType() == t.getEventType().getBaseType() && getValue().getDescription().equals(t.getDescription())) {
|
||||
return this;
|
||||
}
|
||||
return null;
|
||||
|
@ -24,7 +24,8 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
|
||||
class EventTypeTreeItem extends NavTreeItem {
|
||||
|
||||
@ -35,8 +36,8 @@ class EventTypeTreeItem extends NavTreeItem {
|
||||
|
||||
private final Comparator<TreeItem<NavTreeNode>> comparator = TreeComparator.Description;
|
||||
|
||||
EventTypeTreeItem(AggregateEvent g) {
|
||||
setValue(new NavTreeNode(g.getType().getBaseType(), g.getType().getBaseType().getDisplayName(), 0));
|
||||
EventTypeTreeItem(EventCluster g) {
|
||||
setValue(new NavTreeNode(g.getEventType().getBaseType(), g.getEventType().getBaseType().getDisplayName(), 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -52,7 +53,7 @@ class EventTypeTreeItem extends NavTreeItem {
|
||||
* @param tree True if it is part of a tree (versus a list)
|
||||
*/
|
||||
@Override
|
||||
public void insert(AggregateEvent g) {
|
||||
public void insert(EventCluster g) {
|
||||
|
||||
EventDescriptionTreeItem treeItem = childMap.get(g.getDescription());
|
||||
if (treeItem == null) {
|
||||
@ -77,8 +78,8 @@ class EventTypeTreeItem extends NavTreeItem {
|
||||
}
|
||||
|
||||
@Override
|
||||
public TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t) {
|
||||
if (t.getType().getBaseType() == getValue().getType().getBaseType()) {
|
||||
public TreeItem<NavTreeNode> findTreeItemForEvent(EventBundle t) {
|
||||
if (t.getEventType().getBaseType() == getValue().getType().getBaseType()) {
|
||||
|
||||
for (TreeItem<NavTreeNode> child : getChildren()) {
|
||||
final TreeItem<NavTreeNode> findTreeItemForEvent = ((NavTreeItem) child).findTreeItemForEvent(t);
|
||||
|
@ -41,9 +41,9 @@ import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.timeline.FXMLConstructor;
|
||||
import org.sleuthkit.autopsy.timeline.TimeLineController;
|
||||
import org.sleuthkit.autopsy.timeline.TimeLineView;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
|
||||
import org.sleuthkit.autopsy.timeline.ui.detailview.AggregateEventNode;
|
||||
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewNode;
|
||||
import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane;
|
||||
|
||||
/**
|
||||
@ -91,8 +91,8 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
||||
});
|
||||
detailViewPane.getSelectedNodes().addListener((Observable observable) -> {
|
||||
eventsTree.getSelectionModel().clearSelection();
|
||||
detailViewPane.getSelectedNodes().forEach((AggregateEventNode t) -> {
|
||||
eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getEvent()));
|
||||
detailViewPane.getSelectedNodes().forEach((DetailViewNode<?> t) -> {
|
||||
eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getEventBundle()));
|
||||
});
|
||||
});
|
||||
|
||||
@ -100,10 +100,10 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
||||
|
||||
private void setRoot() {
|
||||
RootItem root = new RootItem();
|
||||
final ObservableList<AggregateEvent> aggregatedEvents = detailViewPane.getAggregatedEvents();
|
||||
final ObservableList<EventCluster> aggregatedEvents = detailViewPane.getAggregatedEvents();
|
||||
|
||||
synchronized (aggregatedEvents) {
|
||||
for (AggregateEvent agg : aggregatedEvents) {
|
||||
for (EventCluster agg : aggregatedEvents) {
|
||||
root.insert(agg);
|
||||
}
|
||||
}
|
||||
@ -131,7 +131,7 @@ public class NavPanel extends BorderPane implements TimeLineView {
|
||||
sortByBox.getItems().setAll(Arrays.asList(TreeComparator.Description, TreeComparator.Count));
|
||||
sortByBox.getSelectionModel().select(TreeComparator.Description);
|
||||
sortByBox.getSelectionModel().selectedItemProperty().addListener((Observable o) -> {
|
||||
((RootItem) eventsTree.getRoot()).resort(sortByBox.getSelectionModel().getSelectedItem());
|
||||
((NavTreeItem) eventsTree.getRoot()).resort(sortByBox.getSelectionModel().getSelectedItem());
|
||||
});
|
||||
eventsTree.setShowRoot(false);
|
||||
eventsTree.setCellFactory((TreeView<NavTreeNode> p) -> new EventTreeCell());
|
||||
|
@ -20,7 +20,8 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
|
||||
|
||||
import java.util.Comparator;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
|
||||
/**
|
||||
* A node in the nav tree. Manages inserts and resorts. Has parents and
|
||||
@ -30,12 +31,12 @@ import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
|
||||
*/
|
||||
abstract class NavTreeItem extends TreeItem<NavTreeNode> {
|
||||
|
||||
abstract void insert(AggregateEvent g);
|
||||
abstract void insert(EventCluster g);
|
||||
|
||||
abstract int getCount();
|
||||
|
||||
abstract void resort(Comparator<TreeItem<NavTreeNode>> comp);
|
||||
|
||||
abstract TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t);
|
||||
abstract TreeItem<NavTreeNode> findTreeItemForEvent(EventBundle t);
|
||||
|
||||
}
|
||||
|
@ -24,8 +24,9 @@ import java.util.Map;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
|
||||
import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -56,13 +57,13 @@ class RootItem extends NavTreeItem {
|
||||
* @param g Group to add
|
||||
*/
|
||||
@Override
|
||||
public void insert(AggregateEvent g) {
|
||||
public void insert(EventCluster g) {
|
||||
|
||||
EventTypeTreeItem treeItem = childMap.get(g.getType().getBaseType());
|
||||
EventTypeTreeItem treeItem = childMap.get(g.getEventType().getBaseType());
|
||||
if (treeItem == null) {
|
||||
final EventTypeTreeItem newTreeItem = new EventTypeTreeItem(g);
|
||||
newTreeItem.setExpanded(true);
|
||||
childMap.put(g.getType().getBaseType(), newTreeItem);
|
||||
childMap.put(g.getEventType().getBaseType(), newTreeItem);
|
||||
newTreeItem.insert(g);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
@ -85,7 +86,7 @@ class RootItem extends NavTreeItem {
|
||||
}
|
||||
|
||||
@Override
|
||||
public TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t) {
|
||||
public TreeItem<NavTreeNode> findTreeItemForEvent(EventBundle t) {
|
||||
for (TreeItem<NavTreeNode> child : getChildren()) {
|
||||
final TreeItem<NavTreeNode> findTreeItemForEvent = ((NavTreeItem) child).findTreeItemForEvent(t);
|
||||
if (findTreeItemForEvent != null) {
|
||||
|
@ -21,7 +21,7 @@ package org.sleuthkit.autopsy.timeline.zooming;
|
||||
import org.openide.util.NbBundle;
|
||||
|
||||
/**
|
||||
*
|
||||
* Enumeration of all description levels of detail.
|
||||
*/
|
||||
public enum DescriptionLOD {
|
||||
|
||||
@ -39,7 +39,7 @@ public enum DescriptionLOD {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public DescriptionLOD next() {
|
||||
public DescriptionLOD moreDetailed() {
|
||||
try {
|
||||
return values()[ordinal() + 1];
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
@ -47,11 +47,31 @@ public enum DescriptionLOD {
|
||||
}
|
||||
}
|
||||
|
||||
public DescriptionLOD previous() {
|
||||
public DescriptionLOD lessDetailed() {
|
||||
try {
|
||||
return values()[ordinal() - 1];
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public DescriptionLOD withRelativeDetail(RelativeDetail relativeDetail) {
|
||||
switch (relativeDetail) {
|
||||
case EQUAL:
|
||||
return this;
|
||||
case MORE:
|
||||
return moreDetailed();
|
||||
case LESS:
|
||||
return lessDetailed();
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown RelativeDetail value " + relativeDetail);
|
||||
}
|
||||
}
|
||||
|
||||
public enum RelativeDetail {
|
||||
|
||||
EQUAL,
|
||||
MORE,
|
||||
LESS;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
<ivy-module version="2.0">
|
||||
<info organisation="org.sleuthkit.autopsy" module="corelibs"/>
|
||||
<configurations >
|
||||
@ -18,6 +16,7 @@
|
||||
<dependency conf="autopsy_core->*" org="org.jbundle.thin.base.screen" name="jcalendarbutton" rev="1.4.6"/>
|
||||
|
||||
<!-- commmon -->
|
||||
<dependency org="com.google.guava" name="guava" rev="18.0"/>
|
||||
<dependency conf="autopsy_core->*" org="org.apache.commons" name="commons-lang3" rev="3.0"/>
|
||||
<!-- keep old commons-lang because some deps may need it at runtime.
|
||||
Note there is no namespace collision with ver 3 -->
|
||||
|
@ -20,7 +20,7 @@ file.reference.dom4j-1.6.1.jar=release/modules/ext/dom4j-1.6.1.jar
|
||||
file.reference.geronimo-jms_1.1_spec-1.0.jar=release/modules/ext/geronimo-jms_1.1_spec-1.0.jar
|
||||
file.reference.gson-1.4.jar=release/modules/ext/gson-1.4.jar
|
||||
file.reference.gstreamer-java-1.5.jar=release/modules/ext/gstreamer-java-1.5.jar
|
||||
file.reference.guava-11.0.2.jar=release/modules/ext/guava-11.0.2.jar
|
||||
file.reference.guava-18.0.jar=release/modules/ext/guava-18.0.jar
|
||||
file.reference.imageio-bmp-3.1.1.jar=release/modules/ext/imageio-bmp-3.1.1.jar
|
||||
file.reference.imageio-core-3.1.1.jar=release/modules/ext/imageio-core-3.1.1.jar
|
||||
file.reference.imageio-icns-3.1.1.jar=release/modules/ext/imageio-icns-3.1.1.jar
|
||||
@ -71,13 +71,13 @@ file.reference.xmlbeans-2.3.0.jar=release/modules/ext/xmlbeans-2.3.0.jar
|
||||
javac.source=1.7
|
||||
javac.compilerargs=-Xlint -Xlint:-serial
|
||||
javadoc.reference.controlsfx-8.40.9.jar=release/modules/ext/controlsfx-8.40.9-javadoc.jar
|
||||
javadoc.reference.guava-11.0.2.jar=release/modules/ext/guava-11.0.2-javadoc.jar
|
||||
javadoc.reference.guava-18.0.jar=release/modules/ext/guava-18.0-javadoc.jar
|
||||
javadoc.reference.jfxtras-common-8.0-r4.jar=release/modules/ext/jfxtras-common-8.0-r4-javadoc.jar
|
||||
javadoc.reference.jfxtras-controls-8.0-r4.jar=release/modules/ext/jfxtras-controls-8.0-r4-javadoc.jar
|
||||
javadoc.reference.jfxtras-fxml-8.0-r4.jar=release/modules/ext/jfxtras-fxml-8.0-r4-javadoc.jar
|
||||
nbm.needs.restart=true
|
||||
source.reference.controlsfx-8.40.9.jar=release/modules/ext/controlsfx-8.40.9-sources.jar
|
||||
source.reference.guava-11.0.2.jar=release/modules/ext/guava-11.0.2-sources.jar
|
||||
source.reference.guava-18.0.jar=release/modules/ext/guava-18.0-sources.jar
|
||||
source.reference.jfxtras-common-8.0-r4.jar=release/modules/ext/jfxtras-common-8.0-r4-sources.jar
|
||||
source.reference.jfxtras-controls-8.0-r4.jar=release/modules/ext/jfxtras-controls-8.0-r4-sources.jar
|
||||
source.reference.jfxtras-fxml-8.0-r4.jar=release/modules/ext/jfxtras-fxml-8.0-r4-sources.jar
|
||||
|
@ -34,13 +34,17 @@
|
||||
<package>com.google.common.base.internal</package>
|
||||
<package>com.google.common.cache</package>
|
||||
<package>com.google.common.collect</package>
|
||||
<package>com.google.common.escape</package>
|
||||
<package>com.google.common.eventbus</package>
|
||||
<package>com.google.common.hash</package>
|
||||
<package>com.google.common.html</package>
|
||||
<package>com.google.common.io</package>
|
||||
<package>com.google.common.math</package>
|
||||
<package>com.google.common.net</package>
|
||||
<package>com.google.common.primitives</package>
|
||||
<package>com.google.common.reflect</package>
|
||||
<package>com.google.common.util.concurrent</package>
|
||||
<package>com.google.common.xml</package>
|
||||
<package>com.google.gson</package>
|
||||
<package>com.google.gson.annotations</package>
|
||||
<package>com.google.gson.reflect</package>
|
||||
@ -708,10 +712,6 @@
|
||||
<runtime-relative-path>ext/commons-lang3-3.0-sources.jar</runtime-relative-path>
|
||||
<binary-origin>release/modules/ext/commons-lang3-3.0-sources.jar</binary-origin>
|
||||
</class-path-extension>
|
||||
<class-path-extension>
|
||||
<runtime-relative-path>ext/guava-11.0.2.jar</runtime-relative-path>
|
||||
<binary-origin>release/modules/ext/guava-11.0.2.jar</binary-origin>
|
||||
</class-path-extension>
|
||||
<class-path-extension>
|
||||
<runtime-relative-path>ext/mail-1.4.3.jar</runtime-relative-path>
|
||||
<binary-origin>release/modules/ext/mail-1.4.3.jar</binary-origin>
|
||||
@ -728,6 +728,10 @@
|
||||
<runtime-relative-path>ext/common-lang-3.1.1.jar</runtime-relative-path>
|
||||
<binary-origin>release/modules/ext/common-lang-3.1.1.jar</binary-origin>
|
||||
</class-path-extension>
|
||||
<class-path-extension>
|
||||
<runtime-relative-path>ext/guava-18.0.jar</runtime-relative-path>
|
||||
<binary-origin>release/modules/ext/guava-18.0.jar</binary-origin>
|
||||
</class-path-extension>
|
||||
<class-path-extension>
|
||||
<runtime-relative-path>ext/slf4j-api-1.6.1.jar</runtime-relative-path>
|
||||
<binary-origin>release/modules/ext/slf4j-api-1.6.1.jar</binary-origin>
|
||||
|
Loading…
x
Reference in New Issue
Block a user