diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java
index deae355fc6a06bc80df86f61119fac42a008add9..ecf44a0268ae2abd8976e2c5861dd1d0cb765a18 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java
@@ -66,6 +66,10 @@
 import org.sleuthkit.autopsy.coreutils.History;
 import org.sleuthkit.autopsy.coreutils.LoggedTask;
 import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.events.BlackBoardArtifactTagAddedEvent;
+import org.sleuthkit.autopsy.events.BlackBoardArtifactTagDeletedEvent;
+import org.sleuthkit.autopsy.events.ContentTagAddedEvent;
+import org.sleuthkit.autopsy.events.ContentTagDeletedEvent;
 import org.sleuthkit.autopsy.ingest.IngestManager;
 import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
 import org.sleuthkit.autopsy.timeline.events.db.EventsRepository;
@@ -162,10 +166,8 @@ synchronized public ReadOnlyStringProperty getTaskTitle() {
     @GuardedBy("this")
     private boolean listeningToAutopsy = false;
 
-    private final PropertyChangeListener caseListener;
-
+    private final PropertyChangeListener caseListener = new AutopsyCaseListener();
     private final PropertyChangeListener ingestJobListener = new AutopsyIngestJobListener();
-
     private final PropertyChangeListener ingestModuleListener = new AutopsyIngestModuleListener();
 
     @GuardedBy("this")
@@ -239,8 +241,6 @@ public TimeLineController(Case autoCase) {
                 DescriptionLOD.SHORT);
         historyManager.advance(InitialZoomState);
 
-        //persistent listener instances
-        caseListener = new AutopsyCaseListener();
     }
 
     /**
@@ -792,6 +792,18 @@ private class AutopsyCaseListener implements PropertyChangeListener {
         @Override
         public void propertyChange(PropertyChangeEvent evt) {
             switch (Case.Events.valueOf(evt.getPropertyName())) {
+                case BLACKBOARD_ARTIFACT_TAG_ADDED:
+                    filteredEvents.handleTagAdded((BlackBoardArtifactTagAddedEvent) evt);
+                    break;
+                case BLACKBOARD_ARTIFACT_TAG_DELETED:
+                    filteredEvents.handleTagDeleted((BlackBoardArtifactTagDeletedEvent) evt);
+                    break;
+                case CONTENT_TAG_ADDED:
+                    filteredEvents.handleTagAdded((ContentTagAddedEvent) evt);
+                    break;
+                case CONTENT_TAG_DELETED:
+                    filteredEvents.handleTagDeleted((ContentTagDeletedEvent) evt);
+                    break;
                 case DATA_SOURCE_ADDED:
 //                    Content content = (Content) evt.getNewValue();
                     //if we are doing incremental updates, drop this
@@ -804,7 +816,7 @@ public void propertyChange(PropertyChangeEvent evt) {
                     });
                     break;
                 case CURRENT_CASE:
-                    OpenTimelineAction.invalidateController();
+                   OpenTimelineAction.invalidateController();
                     SwingUtilities.invokeLater(TimeLineController.this::closeTimeLine);
                     break;
             }
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/AggregateEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/events/AggregateEvent.java
index b22b8a47b0f27fc839d5dd877b0a4bd5e64643d8..91cfcb5aaa639e7ff1688af35d8fcd27b183d737 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/events/AggregateEvent.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/events/AggregateEvent.java
@@ -28,32 +28,57 @@
 import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
 
 /**
- * An event that represent a set of other events aggregated together. All the
+ * Represents a set of other (TimeLineEvent) events aggregated together. All the
  * sub events should have the same type and matching descriptions at the
  * designated 'zoom level'.
  */
 @Immutable
 public class AggregateEvent {
 
+    /**
+     * the smallest time interval containing all the aggregated events
+     */
     final private Interval span;
 
+    /**
+     * the type of all the aggregted events
+     */
     final private EventType type;
 
-    final private Set<Long> eventIDs;
-
+    /**
+     * the common description of all the aggregated events
+     */
     final private 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
+     */
+    final private Set<Long> eventIDs;
+
+    /**
+     * the ids of the subset of aggregated events that have at least one tag
+     * applied to them
+     */
+    private final Set<Long> tagged;
+
+    /**
+     * the ids of the subset of aggregated events that have at least one hash
+     * set hit
+     */
     private final Set<Long> hashHits;
 
-    public AggregateEvent(Interval spanningInterval, EventType type, Set<Long> eventIDs, Set<Long> hashHits, String description, DescriptionLOD lod) {
+    public AggregateEvent(Interval spanningInterval, EventType type, Set<Long> eventIDs, Set<Long> hashHits, Set<Long> tagged, String description, DescriptionLOD lod) {
 
         this.span = spanningInterval;
         this.type = type;
         this.hashHits = hashHits;
+        this.tagged = tagged;
         this.description = description;
-
         this.eventIDs = eventIDs;
         this.lod = lod;
     }
@@ -73,6 +98,10 @@ public Set<Long> getEventIDsWithHashHits() {
         return Collections.unmodifiableSet(hashHits);
     }
 
+    public Set<Long> getEventIDsWithTags() {
+        return Collections.unmodifiableSet(tagged);
+    }
+
     public String getDescription() {
         return description;
     }
@@ -81,30 +110,72 @@ public EventType getType() {
         return type;
     }
 
+    public DescriptionLOD getLOD() {
+        return lod;
+    }
+
     /**
      * merge two aggregate events into one new aggregate event.
      *
-     * @param ag1
-     * @param ag2
+     * @param aggEvent1
+     * @param aggEVent2
      *
-     * @return
+     * @return a new aggregate event that is the result of merging the given
+     *         events
      */
-    public static AggregateEvent merge(AggregateEvent ag1, AggregateEvent ag2) {
+    public static AggregateEvent merge(AggregateEvent aggEvent1, AggregateEvent ag2) {
 
-        if (ag1.getType() != ag2.getType()) {
+        if (aggEvent1.getType() != ag2.getType()) {
             throw new IllegalArgumentException("aggregate events are not compatible they have different types");
         }
 
-        if (!ag1.getDescription().equals(ag2.getDescription())) {
+        if (!aggEvent1.getDescription().equals(ag2.getDescription())) {
             throw new IllegalArgumentException("aggregate events are not compatible they have different descriptions");
         }
-        Sets.SetView<Long> idsUnion = Sets.union(ag1.getEventIDs(), ag2.getEventIDs());
-        Sets.SetView<Long> hashHitsUnion = Sets.union(ag1.getEventIDsWithHashHits(), ag2.getEventIDsWithHashHits());
+        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());
 
-        return new AggregateEvent(IntervalUtils.span(ag1.span, ag2.span), ag1.getType(), idsUnion, hashHitsUnion, ag1.getDescription(), ag1.lod);
+        return new AggregateEvent(IntervalUtils.span(aggEvent1.span, ag2.span), aggEvent1.getType(), idsUnion, hashHitsUnion, taggedUnion, aggEvent1.getDescription(), aggEvent1.lod);
     }
 
-    public DescriptionLOD getLOD() {
-        return lod;
+    /**
+     * get an AggregateEvent the same as this one but with the given eventIDs
+     * removed from the list of tagged events
+     *
+     * @param unTaggedIDs
+     *
+     * @return a new Aggregate event that is the same as this one but with the
+     *         given event Ids removed from the list of tagged ids, or, this
+     *         AggregateEvent if no event ids would be removed
+     */
+    public AggregateEvent withTagsRemoved(Set<Long> unTaggedIDs) {
+        Sets.SetView<Long> stillTagged = Sets.difference(tagged, unTaggedIDs);
+        if (stillTagged.size() < tagged.size()) {
+            return new AggregateEvent(span, type, eventIDs, hashHits, stillTagged.immutableCopy(), description, lod);
+        }
+        return this; //no change
+    }
+
+    /**
+     * get an AggregateEvent the same as this one but with the given eventIDs
+     * added to the list of tagged events if there are part of this Aggregate
+     *
+     * @param taggedIDs
+     *
+     * @return a new Aggregate event that is the same as this one but with the
+     *         given event Ids added to the list of tagged ids, or, this
+     *         AggregateEvent if no event ids would be added
+     */
+    public AggregateEvent withTagsAdded(Set<Long> taggedIDs) {
+        Sets.SetView<Long> taggedIdsInAgg = Sets.intersection(eventIDs, taggedIDs);//events that are in this aggregate and (newly) marked as tagged
+        if (taggedIdsInAgg.size() > 0) {
+            Sets.SetView<Long> notYetIncludedTagged = Sets.difference(taggedIdsInAgg, tagged); // events that are tagged, but not already marked as tagged in this Agg
+            if (notYetIncludedTagged.size() > 0) {
+                return new AggregateEvent(span, type, eventIDs, hashHits, Sets.union(tagged, taggedIdsInAgg).immutableCopy(), description, lod);
+            }
+        }
+
+        return this;    //no change
     }
 }
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/EventsTaggedEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/events/EventsTaggedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..114fe053a8d096662889625b9d26506223741ef4
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/timeline/events/EventsTaggedEvent.java
@@ -0,0 +1,39 @@
+/*
+ * 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.events;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Posted to eventbus when a tag as been added to a file artifact that
+ * corresponds to an event
+ */
+public class EventsTaggedEvent {
+
+    private final Set<Long> eventIDs;
+
+    public EventsTaggedEvent(Set<Long> eventIDs) {
+        this.eventIDs = eventIDs;
+    }
+
+    public Set<Long> getEventIDs() {
+        return Collections.unmodifiableSet(eventIDs);
+    }
+}
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/EventsUnTaggedEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/events/EventsUnTaggedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..474676b65aab20196b2ca3adf20c02dde5aba64f
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/timeline/events/EventsUnTaggedEvent.java
@@ -0,0 +1,40 @@
+/*
+ * 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.events;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Posted to eventbus when a tag as been removed from a file artifact that
+ * corresponds to an event
+ */
+public class EventsUnTaggedEvent {
+
+    private final Set<Long> eventIDs;
+
+    public Set<Long> getEventIDs() {
+        return Collections.unmodifiableSet(eventIDs);
+    }
+
+    public EventsUnTaggedEvent(Set<Long> eventIDs) {
+        this.eventIDs = eventIDs;
+    }
+
+}
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/FilteredEventsModel.java b/Core/src/org/sleuthkit/autopsy/timeline/events/FilteredEventsModel.java
index 8a8f5b511df1549a46cff52e7fed184047cf3324..07915a15fa74c6c4d47e675c4dbdd043145e1730 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/events/FilteredEventsModel.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/events/FilteredEventsModel.java
@@ -18,10 +18,12 @@
  */
 package org.sleuthkit.autopsy.timeline.events;
 
+import com.google.common.eventbus.EventBus;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.logging.Level;
 import javafx.beans.Observable;
 import javafx.beans.property.ReadOnlyObjectProperty;
 import javafx.beans.property.ReadOnlyObjectWrapper;
@@ -29,6 +31,12 @@
 import javax.annotation.concurrent.GuardedBy;
 import org.joda.time.DateTimeZone;
 import org.joda.time.Interval;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.events.BlackBoardArtifactTagAddedEvent;
+import org.sleuthkit.autopsy.events.BlackBoardArtifactTagDeletedEvent;
+import org.sleuthkit.autopsy.events.ContentTagAddedEvent;
+import org.sleuthkit.autopsy.events.ContentTagDeletedEvent;
 import org.sleuthkit.autopsy.timeline.TimeLineView;
 import org.sleuthkit.autopsy.timeline.events.db.EventsRepository;
 import org.sleuthkit.autopsy.timeline.events.type.EventType;
@@ -45,6 +53,9 @@
 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.BlackboardArtifact;
+import org.sleuthkit.datamodel.Content;
+import org.sleuthkit.datamodel.TskCoreException;
 
 /**
  * This class acts as the model for a {@link TimeLineView}
@@ -70,11 +81,9 @@
  */
 public final class FilteredEventsModel {
 
-    /*
-     * requested time range, filter, event_type zoom, and description level of
-     * detail. if specifics are not passed to methods, the values of these
-     * members are used to query repository.
-     */
+    private static final Logger LOGGER = Logger.getLogger(FilteredEventsModel.class.getName());
+
+ 
     /**
      * time range that spans the filtered events
      */
@@ -93,6 +102,8 @@ public final class FilteredEventsModel {
     @GuardedBy("this")
     private final ReadOnlyObjectWrapper<ZoomParams> requestedZoomParamters = new ReadOnlyObjectWrapper<>();
 
+    private final EventBus eventbus = new EventBus("Event_Repository_EventBus");
+
     /**
      * The underlying repo for events. Atomic access to repo is synchronized
      * internally, but compound access should be done with the intrinsic lock of
@@ -100,12 +111,14 @@ public final class FilteredEventsModel {
      */
     @GuardedBy("this")
     private final EventsRepository repo;
+    private final Case autoCase;
 
     /**
      * @return the default filter used at startup
      */
     public RootFilter getDefaultFilter() {
         DataSourcesFilter dataSourcesFilter = new DataSourcesFilter();
+
         repo.getDatasourcesMap().entrySet().stream().forEach((Map.Entry<Long, String> t) -> {
             DataSourceFilter dataSourceFilter = new DataSourceFilter(t.getValue(), t.getKey());
             dataSourceFilter.setSelected(Boolean.TRUE);
@@ -123,7 +136,7 @@ public RootFilter getDefaultFilter() {
 
     public FilteredEventsModel(EventsRepository repo, ReadOnlyObjectProperty<ZoomParams> currentStateProperty) {
         this.repo = repo;
-
+        this.autoCase = repo.getAutoCase();
         repo.getDatasourcesMap().addListener((MapChangeListener.Change<? extends Long, ? extends String> change) -> {
             DataSourceFilter dataSourceFilter = new DataSourceFilter(change.getValueAdded(), change.getKey());
             RootFilter rootFilter = filter().get();
@@ -302,4 +315,53 @@ synchronized public DescriptionLOD getDescriptionLOD() {
         return requestedLOD.get();
     }
 
+    synchronized public void handleTagAdded(BlackBoardArtifactTagAddedEvent e) {
+        BlackboardArtifact artifact = e.getTag().getArtifact();
+        Set<Long> updatedEventIDs = repo.markEventsTagged(artifact.getObjectID(), artifact.getArtifactID(), true);
+        if (!updatedEventIDs.isEmpty()) {
+            eventbus.post(new EventsTaggedEvent(updatedEventIDs));
+        }
+    }
+
+    synchronized public void handleTagDeleted(BlackBoardArtifactTagDeletedEvent e) {
+        BlackboardArtifact artifact = e.getTag().getArtifact();
+        try {
+            boolean tagged = autoCase.getServices().getTagsManager().getBlackboardArtifactTagsByArtifact(artifact).isEmpty() == false;
+            Set<Long> updatedEventIDs = repo.markEventsTagged(artifact.getObjectID(), artifact.getArtifactID(), tagged);
+            if (!updatedEventIDs.isEmpty()) {
+                eventbus.post(new EventsUnTaggedEvent(updatedEventIDs));
+            }
+        } catch (TskCoreException ex) {
+            LOGGER.log(Level.SEVERE, "unable to determine tagged status of attribute.", ex);
+        }
+    }
+
+    synchronized public void handleTagAdded(ContentTagAddedEvent e) {
+        Content content = e.getTag().getContent();
+        Set<Long> updatedEventIDs = repo.markEventsTagged(content.getId(), null, true);
+        if (!updatedEventIDs.isEmpty()) {
+            eventbus.post(new EventsTaggedEvent(updatedEventIDs));
+        }
+    }
+
+    synchronized public void handleTagDeleted(ContentTagDeletedEvent e) {
+        Content content = e.getTag().getContent();
+        try {
+            boolean tagged = autoCase.getServices().getTagsManager().getContentTagsByContent(content).isEmpty() == false;
+            Set<Long> updatedEventIDs = repo.markEventsTagged(content.getId(), null, tagged);
+            if (!updatedEventIDs.isEmpty()) {
+                eventbus.post(new EventsUnTaggedEvent(updatedEventIDs));
+            }
+        } catch (TskCoreException ex) {
+            LOGGER.log(Level.SEVERE, "unable to determine tagged status of content.", ex);
+        }
+    }
+
+    synchronized public void registerForEvents(Object o) {
+        eventbus.register(o);
+    }
+
+    synchronized public void unRegisterForEvents(Object o) {
+        eventbus.unregister(0);
+    }
 }
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/TimeLineEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/events/TimeLineEvent.java
index b8087adb83c1ce80c85cbb81c30f7b4300e33385..53ed2904ebec04186895dafe068c8d45c0aedca6 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/events/TimeLineEvent.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/events/TimeLineEvent.java
@@ -18,6 +18,7 @@
  */
 package org.sleuthkit.autopsy.timeline.events;
 
+import javax.annotation.Nullable;
 import org.sleuthkit.autopsy.timeline.events.type.EventType;
 import org.sleuthkit.datamodel.TskData;
 
@@ -41,8 +42,9 @@ public class TimeLineEvent {
     private final TskData.FileKnown known;
 
     private final boolean hashHit;
+    private final boolean tagged;
 
-    public TimeLineEvent(Long eventID, Long objID, Long artifactID, Long time, EventType type, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known, boolean hashHit) {
+    public TimeLineEvent(Long eventID, Long objID, @Nullable Long artifactID, Long time, EventType type, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known, boolean hashHit, boolean tagged) {
         this.eventID = eventID;
         this.fileID = objID;
         this.artifactID = artifactID;
@@ -54,12 +56,18 @@ public TimeLineEvent(Long eventID, Long objID, Long artifactID, Long time, Event
         this.shortDescription = shortDescription;
         this.known = known;
         this.hashHit = hashHit;
+        this.tagged = tagged;
+    }
+
+    public boolean isTagged() {
+        return tagged;
     }
 
     public boolean isHashHit() {
         return hashHit;
     }
 
+    @Nullable
     public Long getArtifactID() {
         return artifactID;
     }
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/db/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/events/db/Bundle.properties
deleted file mode 100644
index b09aa42a5b9e04fdd91dbe9f3f9b577db2f2e577..0000000000000000000000000000000000000000
--- a/Core/src/org/sleuthkit/autopsy/timeline/events/db/Bundle.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-EventsRepository.progressWindow.msg.reinit_db=(re)initializing events database
-EventsRepository.progressWindow.msg.populateMacEventsFiles=populating mac events for files\:
-EventsRepository.progressWindow.msg.populateMacEventsFiles2=populating mac events for files\:
-EventsRepository.progressWindow.msg.commitingDb=committing events db
-EventsRepository.msgdlg.problem.text=There was a problem populating the timeline.  Not all events may be present or accurate. See the log for details.
-EventsRepository.progressWindow.populatingXevents=populating {0} events
\ No newline at end of file
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/db/EventDB.java b/Core/src/org/sleuthkit/autopsy/timeline/events/db/EventDB.java
index 12f56bf51d03f0208b20d1cbee8dc666ead7d6e5..7e682e7255c985d2afa5e483ed5af6e6270b612d 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/events/db/EventDB.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/events/db/EventDB.java
@@ -18,7 +18,6 @@
  */
 package org.sleuthkit.autopsy.timeline.events.db;
 
-import com.google.common.base.Stopwatch;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.SetMultimap;
 import java.nio.file.Paths;
@@ -46,6 +45,7 @@
 import java.util.logging.Level;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import javax.annotation.Nonnull;
 import org.apache.commons.lang3.StringUtils;
 import org.joda.time.DateTimeZone;
 import org.joda.time.Interval;
@@ -87,42 +87,8 @@
  */
 public class EventDB {
 
-    private PreparedStatement insertHashSetStmt;
-    private PreparedStatement insertHashHitStmt;
-    private PreparedStatement selectHashSetStmt;
-
-    /**
-     * enum to represent columns in the events table
-     */
-    enum EventTableColumn {
-
-        EVENT_ID("event_id"), // NON-NLS
-        FILE_ID("file_id"), // NON-NLS
-        ARTIFACT_ID("artifact_id"), // NON-NLS
-        BASE_TYPE("base_type"), // NON-NLS
-        SUB_TYPE("sub_type"), // NON-NLS
-        KNOWN("known_state"), // NON-NLS
-        DATA_SOURCE_ID("datasource_id"), // NON-NLS
-        FULL_DESCRIPTION("full_description"), // NON-NLS
-        MED_DESCRIPTION("med_description"), // NON-NLS
-        SHORT_DESCRIPTION("short_description"), // NON-NLS
-        TIME("time"),
-        HASH_HIT("hash_hit"); // NON-NLS
-
-        private final String columnName;
-
-        private EventTableColumn(String columnName) {
-            this.columnName = columnName;
-        }
-
-        @Override
-        public String toString() {
-            return columnName;
-        }
-
-    }
-
     /**
+    
      * enum to represent keys stored in db_info table
      */
     private enum DBInfoKey {
@@ -186,12 +152,19 @@ public static EventDB getEventDB(Case autoCase) {
     private PreparedStatement getDataSourceIDsStmt;
     private PreparedStatement insertRowStmt;
     private PreparedStatement recordDBInfoStmt;
+    private PreparedStatement insertHashSetStmt;
+    private PreparedStatement insertHashHitStmt;
+    private PreparedStatement selectHashSetStmt;
+    private PreparedStatement countAllEventsStmt;
+    private PreparedStatement dropEventsTableStmt;
+    private PreparedStatement dropHashSetHitsTableStmt;
+    private PreparedStatement dropHashSetsTableStmt;
+    private PreparedStatement dropDBInfoTableStmt;
+    private PreparedStatement selectEventsFromOBjectAndArtifactStmt;
 
     private final Set<PreparedStatement> preparedStatements = new HashSet<>();
 
-    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true); //use fairness policy
-
-    private final Lock DBLock = rwLock.writeLock(); //using exclusive lock for all db ops for now
+    private final Lock DBLock = new ReentrantReadWriteLock(true).writeLock(); //using exclusive lock for all db ops for now
 
     private EventDB(Case autoCase) throws SQLException, Exception {
         //should this go into module output (or even cache, we should be able to rebuild it)?
@@ -208,42 +181,37 @@ public void finalize() throws Throwable {
         }
     }
 
-    public Interval getSpanningInterval(Collection<Long> eventIDs) {
+    void closeDBCon() {
+        if (con != null) {
+            try {
+                closeStatements();
+                con.close();
+            } catch (SQLException ex) {
+                LOGGER.log(Level.WARNING, "Failed to close connection to evetns.db", ex); // NON-NLS
+            }
+        }
+        con = null;
+    }
 
-        Interval span = null;
+    public Interval getSpanningInterval(Collection<Long> eventIDs) {
         DBLock.lock();
         try (Statement stmt = con.createStatement();
-                //You can't inject multiple values into one ? paramater in prepared statement,
-                //so we make new statement each time...
                 ResultSet rs = stmt.executeQuery("select Min(time), Max(time) from events where event_id in (" + StringUtils.join(eventIDs, ", ") + ")");) { // NON-NLS
             while (rs.next()) {
-                span = new Interval(rs.getLong("Min(time)"), rs.getLong("Max(time)") + 1, DateTimeZone.UTC); // NON-NLS
-
+                return new Interval(rs.getLong("Min(time)"), rs.getLong("Max(time)") + 1, DateTimeZone.UTC); // NON-NLS
             }
         } catch (SQLException ex) {
             LOGGER.log(Level.SEVERE, "Error executing get spanning interval query.", ex); // NON-NLS
         } finally {
             DBLock.unlock();
         }
-        return span;
+        return null;
     }
 
     EventTransaction beginTransaction() {
         return new EventTransaction();
     }
 
-    void closeDBCon() {
-        if (con != null) {
-            try {
-                closeStatements();
-                con.close();
-            } catch (SQLException ex) {
-                LOGGER.log(Level.WARNING, "Failed to close connection to evetns.db", ex); // NON-NLS
-            }
-        }
-        con = null;
-    }
-
     void commitTransaction(EventTransaction tr, Boolean notify) {
         if (tr.isClosed()) {
             throw new IllegalArgumentException("can't close already closed transaction"); // NON-NLS
@@ -251,24 +219,34 @@ void commitTransaction(EventTransaction tr, Boolean notify) {
         tr.commit(notify);
     }
 
+    /**
+     * @return the total number of events in the database or, -1 if there is an
+     *         error.
+     */
     int countAllEvents() {
-        int result = -1;
         DBLock.lock();
-        //TODO convert this to prepared statement -jm
-        try (ResultSet rs = con.createStatement().executeQuery("select count(*) as count from events")) { // NON-NLS
+        try (ResultSet rs = countAllEventsStmt.executeQuery()) { // NON-NLS
             while (rs.next()) {
-                result = rs.getInt("count"); // NON-NLS
-                break;
+                return rs.getInt("count"); // NON-NLS
             }
         } catch (SQLException ex) {
-            Exceptions.printStackTrace(ex);
+            LOGGER.log(Level.SEVERE, "Error counting all events", ex);
         } finally {
             DBLock.unlock();
         }
-        return result;
+        return -1;
     }
 
-    Map<EventType, Long> countEvents(ZoomParams params) {
+    /**
+     * get the count of all events that fit the given zoom params organized by
+     * the EvenType of the level spcified in the ZoomParams
+     *
+     * @param params the params that control what events to count and how to
+     *               organize the returned map
+     *
+     * @return a map from event type( of the requested level) to event counts
+     */
+    Map<EventType, Long> countEventsByType(ZoomParams params) {
         if (params.getTimeRange() != null) {
             return countEvents(params.getTimeRange().getStartMillis() / 1000,
                     params.getTimeRange().getEndMillis() / 1000,
@@ -278,22 +256,25 @@ Map<EventType, Long> countEvents(ZoomParams params) {
         }
     }
 
-    void dropEventsTable() {
-        //TODO: use prepared statement - jm
+    /**
+     * drop the tables from this database and recreate them in order to start
+     * over.
+     */
+    void reInitializeDB() {
         DBLock.lock();
-        try (Statement createStatement = con.createStatement()) {
-            createStatement.execute("drop table if exists events"); // NON-NLS
+        try {
+            dropEventsTableStmt.executeUpdate();
+            dropHashSetHitsTableStmt.executeUpdate();
+            dropHashSetsTableStmt.executeUpdate();
+            dropDBInfoTableStmt.executeUpdate();
+            initializeDB();;
         } catch (SQLException ex) {
-            LOGGER.log(Level.SEVERE, "could not drop old events table", ex); // NON-NLS
+            LOGGER.log(Level.SEVERE, "could not drop old tables table", ex); // NON-NLS
         } finally {
             DBLock.unlock();
         }
     }
 
-    List<AggregateEvent> getAggregatedEvents(ZoomParams params) {
-        return getAggregatedEvents(params.getTimeRange(), params.getFilter(), params.getTypeZoomLevel(), params.getDescrLOD());
-    }
-
     Interval getBoundingEventsInterval(Interval timeRange, RootFilter filter) {
         long start = timeRange.getStartMillis() / 1000;
         long end = timeRange.getEndMillis() / 1000;
@@ -310,7 +291,6 @@ Interval getBoundingEventsInterval(Interval timeRange, RootFilter filter) {
                 if (end2 == 0) {
                     end2 = getMaxTime();
                 }
-                //System.out.println(start2 + " " + start + " " + end + " " + end2);
                 return new Interval(start2 * 1000, (end2 + 1) * 1000, TimeLineController.getJodaTimeZone());
             }
         } catch (SQLException ex) {
@@ -353,12 +333,11 @@ Set<Long> getEventIDs(Long startTime, Long endTime, RootFilter filter) {
 
         DBLock.lock();
         final String query = "select event_id from  from events" + useHashHitTablesHelper(filter) + " where time >=  " + startTime + " and time <" + endTime + " and " + SQLHelper.getSQLWhere(filter); // NON-NLS
-        //System.out.println(query);
         try (Statement stmt = con.createStatement();
                 ResultSet rs = stmt.executeQuery(query)) {
 
             while (rs.next()) {
-                resultIDs.add(rs.getLong(EventTableColumn.EVENT_ID.toString()));
+                resultIDs.add(rs.getLong("event_id"));
             }
 
         } catch (SQLException sqlEx) {
@@ -383,7 +362,7 @@ boolean hasNewColumns() {
          * 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.
          */
-        return hasHashHitColumn() && hasDataSourceIDColumn()
+        return hasHashHitColumn() && hasDataSourceIDColumn() && hasTaggedColumn()
                 && (getDataSourceIDs().isEmpty() == false);
     }
 
@@ -392,7 +371,7 @@ Set<Long> getDataSourceIDs() {
         DBLock.lock();
         try (ResultSet rs = getDataSourceIDsStmt.executeQuery()) {
             while (rs.next()) {
-                long datasourceID = rs.getLong(EventTableColumn.DATA_SOURCE_ID.toString());
+                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);
@@ -494,7 +473,7 @@ final synchronized void initializeDB() {
                         + "PRIMARY KEY (key))"; // NON-NLS
                 stmt.execute(sql);
             } catch (SQLException ex) {
-                LOGGER.log(Level.SEVERE, "problem creating  db_info table", ex); // NON-NLS
+                LOGGER.log(Level.SEVERE, "problem creating db_info table", ex); // NON-NLS
             }
 
             try (Statement stmt = con.createStatement()) {
@@ -525,6 +504,15 @@ final synchronized void initializeDB() {
                     LOGGER.log(Level.SEVERE, "problem upgrading events table", ex); // NON-NLS
                 }
             }
+            if (hasTaggedColumn() == false) {
+                try (Statement stmt = con.createStatement()) {
+                    String sql = "ALTER TABLE events ADD COLUMN tagged INTEGER"; // NON-NLS
+                    stmt.execute(sql);
+                } catch (SQLException ex) {
+
+                    LOGGER.log(Level.SEVERE, "problem upgrading events table", ex); // NON-NLS
+                }
+            }
 
             if (hasHashHitColumn() == false) {
                 try (Statement stmt = con.createStatement()) {
@@ -554,26 +542,32 @@ final synchronized void initializeDB() {
                 LOGGER.log(Level.SEVERE, "problem creating hash_set_hits table", ex);
             }
 
-            createEventsIndex(Arrays.asList(EventTableColumn.FILE_ID));
-            createEventsIndex(Arrays.asList(EventTableColumn.ARTIFACT_ID));
-            createEventsIndex(Arrays.asList(EventTableColumn.SUB_TYPE, EventTableColumn.TIME));
-            createEventsIndex(Arrays.asList(EventTableColumn.BASE_TYPE, EventTableColumn.TIME));
-            createEventsIndex(Arrays.asList(EventTableColumn.KNOWN));
+            createIndex("events", Arrays.asList("file_id"));
+            createIndex("events", Arrays.asList("artifact_id"));
+            createIndex("events", Arrays.asList("sub_type", "time"));
+            createIndex("events", Arrays.asList("base_type", "time"));
+            createIndex("events", Arrays.asList("known_state"));
 
             try {
                 insertRowStmt = prepareStatement(
-                        "INSERT INTO events (datasource_id,file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description, known_state, hash_hit) " // NON-NLS
-                        + "VALUES (?,?,?,?,?,?,?,?,?,?,?)"); // NON-NLS
-
-                getDataSourceIDsStmt = prepareStatement("select distinct datasource_id from events"); // 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
-                recordDBInfoStmt = prepareStatement("insert or replace into db_info (key, value) values (?, ?)"); // NON-NLS
-                getDBInfoStmt = prepareStatement("select value from db_info where key = ?"); // NON-NLS
-                insertHashSetStmt = prepareStatement("insert or ignore into hash_sets (hash_set_name)  values (?)");
-                selectHashSetStmt = prepareStatement("select hash_set_id from hash_sets where hash_set_name = ?");
-                insertHashHitStmt = prepareStatement("insert or ignore into hash_set_hits (hash_set_id, event_id) values (?,?)");
+                        "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
+
+                getDataSourceIDsStmt = prepareStatement("SELECT DISTINCT datasource_id FROM events"); // 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
+                recordDBInfoStmt = prepareStatement("INSERT OR REPLACE INTO db_info (key, value) values (?, ?)"); // NON-NLS
+                getDBInfoStmt = prepareStatement("SELECT value FROM db_info WHERE key = ?"); // NON-NLS
+                insertHashSetStmt = prepareStatement("INSERT OR IGNORE INTO hash_sets (hash_set_name)  values (?)");
+                selectHashSetStmt = prepareStatement("SELECT hash_set_id FROM hash_sets WHERE hash_set_name = ?");
+                insertHashHitStmt = prepareStatement("INSERT OR IGNORE INTO hash_set_hits (hash_set_id, event_id) values (?,?)");
+                countAllEventsStmt = prepareStatement("SELECT count(*) AS count FROM events");
+                dropEventsTableStmt = prepareStatement("DROP TABLE IF EXISTS events");
+                dropHashSetHitsTableStmt = prepareStatement("DROP TABLE IF EXISTS hash_set_hits");
+                dropHashSetsTableStmt = prepareStatement("DROP TABLE IF EXISTS hash_sets");
+                dropDBInfoTableStmt = prepareStatement("DROP TABLE IF EXISTS db_ino");
+                selectEventsFromOBjectAndArtifactStmt = prepareStatement("SELECT event_id FROM events WHERE file_id == ? AND artifact_id IS ?");
             } catch (SQLException sQLException) {
                 LOGGER.log(Level.SEVERE, "failed to prepareStatment", sQLException); // NON-NLS
             }
@@ -583,15 +577,6 @@ final synchronized void initializeDB() {
         }
     }
 
-    /**
-     * @param tableName  the value of tableName
-     * @param columnList the value of columnList
-     */
-    private void createEventsIndex(final List<EventTableColumn> columnList) {
-        createIndex("events",
-                columnList.stream().map(EventTableColumn::toString).collect(Collectors.toList()));
-    }
-
     /**
      *
      * @param tableName  the value of tableName
@@ -614,12 +599,12 @@ private void createIndex(final String tableName, final List<String> columnList)
      *
      * @return the boolean
      */
-    private boolean hasDBColumn(final EventTableColumn dbColumn) {
+    private boolean hasDBColumn(@Nonnull final String dbColumn) {
         try (Statement stmt = con.createStatement()) {
 
             ResultSet executeQuery = stmt.executeQuery("PRAGMA table_info(events)");
             while (executeQuery.next()) {
-                if (dbColumn.toString().equals(executeQuery.getString("name"))) {
+                if (dbColumn.equals(executeQuery.getString("name"))) {
                     return true;
                 }
             }
@@ -630,20 +615,24 @@ private boolean hasDBColumn(final EventTableColumn dbColumn) {
     }
 
     private boolean hasDataSourceIDColumn() {
-        return hasDBColumn(EventTableColumn.DATA_SOURCE_ID);
+        return hasDBColumn("datasource_id");
+    }
+
+    private boolean hasTaggedColumn() {
+        return hasDBColumn("tagged");
     }
 
     private boolean hasHashHitColumn() {
-        return hasDBColumn(EventTableColumn.HASH_HIT);
+        return hasDBColumn("hash_hit");
     }
 
-    void insertEvent(long time, EventType type, long datasourceID, Long objID,
+    void insertEvent(long time, EventType type, long datasourceID, long objID,
             Long artifactID, String fullDescription, String medDescription,
-            String shortDescription, TskData.FileKnown known, Set<String> hashSets) {
+            String shortDescription, TskData.FileKnown known, Set<String> hashSets, boolean tagged) {
 
-        EventTransaction trans = beginTransaction();
-        insertEvent(time, type, datasourceID, objID, artifactID, fullDescription, medDescription, shortDescription, known, hashSets, trans);
-        commitTransaction(trans, true);
+        EventTransaction transaction = beginTransaction();
+        insertEvent(time, type, datasourceID, objID, artifactID, fullDescription, medDescription, shortDescription, known, hashSets, tagged, transaction);
+        commitTransaction(transaction, true);
     }
 
     /**
@@ -652,9 +641,10 @@ void insertEvent(long time, EventType type, long datasourceID, Long objID,
      * @param f
      * @param transaction
      */
-    void insertEvent(long time, EventType type, long datasourceID, Long objID,
+    void insertEvent(long time, EventType type, long datasourceID, long objID,
             Long artifactID, String fullDescription, String medDescription,
             String shortDescription, TskData.FileKnown known, Set<String> hashSetNames,
+            boolean tagged,
             EventTransaction transaction) {
 
         if (transaction.isClosed()) {
@@ -669,18 +659,14 @@ void insertEvent(long time, EventType type, long datasourceID, Long objID,
         DBLock.lock();
         try {
 
-            //"INSERT INTO events (datasource_id,file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description, known_state, hashHit) " 
+            //"INSERT INTO events (datasource_id,file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description, known_state, hashHit, tagged) " 
             insertRowStmt.clearParameters();
             insertRowStmt.setLong(1, datasourceID);
-            if (objID != null) {
-                insertRowStmt.setLong(2, objID);
-            } else {
-                insertRowStmt.setNull(2, Types.INTEGER);
-            }
+            insertRowStmt.setLong(2, objID);
             if (artifactID != null) {
                 insertRowStmt.setLong(3, artifactID);
             } else {
-                insertRowStmt.setNull(3, Types.INTEGER);
+                insertRowStmt.setNull(3, Types.NULL);
             }
             insertRowStmt.setLong(4, time);
 
@@ -698,6 +684,7 @@ void insertEvent(long time, EventType type, long datasourceID, Long objID,
             insertRowStmt.setByte(10, known == null ? TskData.FileKnown.UNKNOWN.getFileKnownValue() : known.getFileKnownValue());
 
             insertRowStmt.setInt(11, hashSetNames.isEmpty() ? 0 : 1);
+            insertRowStmt.setInt(12, tagged ? 1 : 0);
 
             insertRowStmt.executeUpdate();
 
@@ -735,6 +722,36 @@ void insertEvent(long time, EventType type, long datasourceID, Long objID,
         }
     }
 
+    Set<Long> markEventsTagged(long objectID, Long artifactID, boolean tagged) {
+        HashSet<Long> eventIDs = new HashSet<>();
+
+        DBLock.lock();
+
+        try {
+            selectEventsFromOBjectAndArtifactStmt.clearParameters();
+            selectEventsFromOBjectAndArtifactStmt.setLong(1, objectID);
+            if (Objects.isNull(artifactID)) {
+                selectEventsFromOBjectAndArtifactStmt.setNull(2, Types.NULL);
+            } else {
+                selectEventsFromOBjectAndArtifactStmt.setLong(2, artifactID);
+            }
+            try (ResultSet executeQuery = selectEventsFromOBjectAndArtifactStmt.executeQuery();) {
+                while (executeQuery.next()) {
+                    eventIDs.add(executeQuery.getLong("event_id"));
+                }
+                try (Statement updateStatement = con.createStatement();) {
+                    updateStatement.executeUpdate("UPDATE events SET tagged = " + (tagged ? 1 : 0)
+                            + " WHERE event_id IN (" + StringUtils.join(eventIDs, ",") + ")");
+                }
+            }
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "failed to mark events as " + (tagged ? "" : "(un)") + tagged, ex); // NON-NLS
+        } finally {
+            DBLock.unlock();
+        }
+        return eventIDs;
+    }
+
     void recordLastArtifactID(long lastArtfID) {
         recordDBInfo(DBInfoKey.LAST_ARTIFACT_ID, lastArtfID);
     }
@@ -800,15 +817,16 @@ private void configureDB() throws SQLException {
     }
 
     private TimeLineEvent constructTimeLineEvent(ResultSet rs) throws SQLException {
-        return new TimeLineEvent(rs.getLong(EventTableColumn.EVENT_ID.toString()),
-                rs.getLong(EventTableColumn.FILE_ID.toString()),
-                rs.getLong(EventTableColumn.ARTIFACT_ID.toString()),
-                rs.getLong(EventTableColumn.TIME.toString()), RootEventType.allTypes.get(rs.getInt(EventTableColumn.SUB_TYPE.toString())),
-                rs.getString(EventTableColumn.FULL_DESCRIPTION.toString()),
-                rs.getString(EventTableColumn.MED_DESCRIPTION.toString()),
-                rs.getString(EventTableColumn.SHORT_DESCRIPTION.toString()),
-                TskData.FileKnown.valueOf(rs.getByte(EventTableColumn.KNOWN.toString())),
-                rs.getInt(EventTableColumn.HASH_HIT.toString()) != 0);
+        return new TimeLineEvent(rs.getLong("event_id"),
+                rs.getLong("file_id"),
+                rs.getLong("artifact_id"),
+                rs.getLong("time"), RootEventType.allTypes.get(rs.getInt("sub_type")),
+                rs.getString("full_description"),
+                rs.getString("med_description"),
+                rs.getString("short_description"),
+                TskData.FileKnown.valueOf(rs.getByte("known_state")),
+                rs.getInt("hash_hit") != 0,
+                rs.getInt("tagged") != 0);
     }
 
     /**
@@ -843,38 +861,29 @@ private Map<EventType, Long> countEvents(Long startTime, Long endTime, RootFilte
                 + " from events" + useHashHitTablesHelper(filter) + " where time >= " + startTime + " and time < " + endTime + " and " + SQLHelper.getSQLWhere(filter) // NON-NLS
                 + " GROUP BY " + useSubTypeHelper(useSubTypes); // NON-NLS
 
-        ResultSet rs = null;
         DBLock.lock();
-        //System.out.println(queryString);
-        try (Statement stmt = con.createStatement();) {
-            Stopwatch stopwatch = new Stopwatch();
-            stopwatch.start();
-            System.out.println(queryString);
-            rs = stmt.executeQuery(queryString);
-            stopwatch.stop();
-            // System.out.println(stopwatch.elapsedMillis() / 1000.0 + " seconds");
+        try (Statement stmt = con.createStatement();
+                ResultSet rs = stmt.executeQuery(queryString);) {
             while (rs.next()) {
-
                 EventType type = useSubTypes
-                        ? RootEventType.allTypes.get(rs.getInt(EventTableColumn.SUB_TYPE.toString()))
-                        : BaseTypes.values()[rs.getInt(EventTableColumn.BASE_TYPE.toString())];
+                        ? RootEventType.allTypes.get(rs.getInt("sub_type"))
+                        : BaseTypes.values()[rs.getInt("base_type")];
 
                 typeMap.put(type, rs.getLong("count(*)")); // NON-NLS
             }
 
         } catch (Exception ex) {
-            LOGGER.log(Level.SEVERE, "error getting count of events from db.", ex); // NON-NLS
+            LOGGER.log(Level.SEVERE, "Error getting count of events from db.", ex); // NON-NLS
         } finally {
-            try {
-                rs.close();
-            } catch (SQLException ex) {
-                Exceptions.printStackTrace(ex);
-            }
             DBLock.unlock();
         }
         return typeMap;
     }
 
+    List<AggregateEvent> getAggregatedEvents(ZoomParams params) {
+        return getAggregatedEvents(params.getTimeRange(), params.getFilter(), params.getTypeZoomLevel(), params.getDescrLOD());
+    }
+
     /**
      * //TODO: update javadoc //TODO: split this into helper methods
      *
@@ -882,9 +891,9 @@ private Map<EventType, Long> countEvents(Long startTime, Long endTime, RootFilte
      *
      * General algorithm is as follows:
      *
-     * - get all aggregate events, via one db query. - sort them into a map from
-     * (type, description)-> aggevent - for each key in map, merge the events
-     * and accumulate them in a list to return
+     * 1)get all aggregate events, via one db query. 2) sort them into a map
+     * from (type, description)-> aggevent 3) for each key in map, merge the
+     * events and accumulate them in a list to return
      *
      *
      * @param timeRange the Interval within in which all returned aggregate
@@ -925,36 +934,36 @@ private List<AggregateEvent> getAggregatedEvents(Interval timeRange, RootFilter
                 + " from events" + useHashHitTablesHelper(filter) + " where " + "time >= " + start + " and time < " + end + " and " + SQLHelper.getSQLWhere(filter) // NON-NLS
                 + " group by interval, " + useSubTypeHelper(useSubTypes) + " , " + descriptionColumn // NON-NLS
                 + " order by Min(time)"; // NON-NLS
-        System.out.println(query);
-        ResultSet rs = null;
-        try (Statement stmt = con.createStatement(); // scoop up requested events in groups organized by interval, type, and desription
-                ) {
-
-            Stopwatch stopwatch = new Stopwatch();
-            stopwatch.start();
-
-            rs = stmt.executeQuery(query);
-            stopwatch.stop();
-            System.out.println(stopwatch.elapsedMillis() / 1000.0 + " seconds");
+        // scoop up requested events in groups organized by interval, type, and desription
+        try (ResultSet rs = con.createStatement().executeQuery(query);) {
             while (rs.next()) {
+                Interval interval = new Interval(rs.getLong("Min(time)") * 1000, rs.getLong("Max(time)") * 1000, TimeLineController.getJodaTimeZone());
                 String eventIDS = rs.getString("event_ids");
-                HashSet<Long> hashHits = new HashSet<>();
-                try (Statement st2 = con.createStatement();) {
+                EventType type = useSubTypes ? RootEventType.allTypes.get(rs.getInt("sub_type")) : BaseTypes.values()[rs.getInt("base_type")];
 
-                    ResultSet executeQuery = st2.executeQuery("select event_id from events where event_id in (" + eventIDS + ") and hash_hit = 1");
-                    while (executeQuery.next()) {
-                        hashHits.add(executeQuery.getLong(EventTableColumn.EVENT_ID.toString()));
+                HashSet<Long> hashHits = new HashSet<>();
+                HashSet<Long> tagged = new HashSet<>();
+                try (Statement st2 = con.createStatement();
+                        ResultSet hashQueryResults = st2.executeQuery("select event_id , tagged, hash_hit from events where event_id in (" + eventIDS + ")");) {
+                    while (hashQueryResults.next()) {
+                        long eventID = hashQueryResults.getLong("event_id");
+                        if (hashQueryResults.getInt("tagged") != 0) {
+                            tagged.add(eventID);
+                        }
+                        if (hashQueryResults.getInt("hash_hit") != 0) {
+                            hashHits.add(eventID);
+                        }
                     }
                 }
 
-                EventType type = useSubTypes ? RootEventType.allTypes.get(rs.getInt(EventTableColumn.SUB_TYPE.toString())) : BaseTypes.values()[rs.getInt(EventTableColumn.BASE_TYPE.toString())];
-
                 AggregateEvent aggregateEvent = new AggregateEvent(
-                        new Interval(rs.getLong("Min(time)") * 1000, rs.getLong("Max(time)") * 1000, TimeLineController.getJodaTimeZone()), // NON-NLS
+                        interval, // NON-NLS
                         type,
                         Stream.of(eventIDS.split(",")).map(Long::valueOf).collect(Collectors.toSet()), // NON-NLS
                         hashHits,
-                        rs.getString(descriptionColumn), lod);
+                        tagged,
+                        rs.getString(descriptionColumn),
+                        lod);
 
                 //put events in map from type/descrition -> event
                 SetMultimap<String, AggregateEvent> descrMap = typeMap.get(type);
@@ -968,11 +977,6 @@ private List<AggregateEvent> getAggregatedEvents(Interval timeRange, RootFilter
         } catch (SQLException ex) {
             Exceptions.printStackTrace(ex);
         } finally {
-            try {
-                rs.close();
-            } catch (SQLException ex) {
-                Exceptions.printStackTrace(ex);
-            }
             DBLock.unlock();
         }
 
@@ -1020,7 +1024,7 @@ private String useHashHitTablesHelper(RootFilter filter) {
     }
 
     private static String useSubTypeHelper(final boolean useSubTypes) {
-        return useSubTypes ? EventTableColumn.SUB_TYPE.toString() : EventTableColumn.BASE_TYPE.toString();
+        return useSubTypes ? "sub_type" : "base_type";
     }
 
     private long getDBInfo(DBInfoKey key, long defaultValue) {
@@ -1049,12 +1053,12 @@ private long getDBInfo(DBInfoKey key, long defaultValue) {
     private String getDescriptionColumn(DescriptionLOD lod) {
         switch (lod) {
             case FULL:
-                return EventTableColumn.FULL_DESCRIPTION.toString();
+                return "full_description";
             case MEDIUM:
-                return EventTableColumn.MED_DESCRIPTION.toString();
+                return "med_description";
             case SHORT:
             default:
-                return EventTableColumn.SHORT_DESCRIPTION.toString();
+                return "short_description";
         }
     }
 
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/events/db/EventsRepository.java
index 835c486c64c96972ca54f7edcc7b8025d130ce0e..ecc202473d3979669036da49462fdbb3d5971a32 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/events/db/EventsRepository.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/events/db/EventsRepository.java
@@ -21,11 +21,9 @@
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
-import com.google.common.cache.RemovalNotification;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -44,6 +42,7 @@
 import org.joda.time.Interval;
 import org.openide.util.NbBundle;
 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.events.AggregateEvent;
@@ -91,15 +90,17 @@ public class EventsRepository {
     private final FilteredEventsModel modelInstance;
 
     private final LoadingCache<Long, TimeLineEvent> idToEventCache;
-
     private final LoadingCache<ZoomParams, Map<EventType, Long>> eventCountsCache;
-
     private final LoadingCache<ZoomParams, List<AggregateEvent>> aggregateEventsCache;
 
     private final ObservableMap<Long, String> datasourcesMap = FXCollections.observableHashMap();
     private final ObservableMap<Long, String> hashSetMap = FXCollections.observableHashMap();
     private final Case autoCase;
 
+    public Case getAutoCase() {
+        return autoCase;
+    }
+
     synchronized public ObservableMap<Long, String> getDatasourcesMap() {
         return datasourcesMap;
     }
@@ -125,19 +126,21 @@ public EventsRepository(Case autoCase, ReadOnlyObjectProperty<ZoomParams> curren
         //TODO: we should check that case is open, or get passed a case object/directory -jm
         this.eventDB = EventDB.getEventDB(autoCase);
         populateFilterMaps(autoCase.getSleuthkitCase());
-        idToEventCache = CacheBuilder.newBuilder().maximumSize(5000L).expireAfterAccess(10, TimeUnit.MINUTES).removalListener((RemovalNotification<Long, TimeLineEvent> rn) -> {
-            //LOGGER.log(Level.INFO, "evicting event: {0}", rn.toString());
-        }).build(CacheLoader.from(eventDB::getEventById));
-        eventCountsCache = CacheBuilder.newBuilder().maximumSize(1000L).expireAfterAccess(10, TimeUnit.MINUTES).removalListener((RemovalNotification<ZoomParams, Map<EventType, Long>> rn) -> {
-            //LOGGER.log(Level.INFO, "evicting counts: {0}", rn.toString());
-        }).build(CacheLoader.from(eventDB::countEvents));
-        aggregateEventsCache = CacheBuilder.newBuilder().maximumSize(1000L).expireAfterAccess(10, TimeUnit.MINUTES).removalListener((RemovalNotification<ZoomParams, List<AggregateEvent>> rn) -> {
-            //LOGGER.log(Level.INFO, "evicting aggregated events: {0}", rn.toString());
-        }).build(CacheLoader.from(eventDB::getAggregatedEvents));
+        idToEventCache = CacheBuilder.newBuilder()
+                .maximumSize(5000L)
+                .expireAfterAccess(10, TimeUnit.MINUTES)
+                .build(CacheLoader.from(eventDB::getEventById));
+        eventCountsCache = CacheBuilder.newBuilder()
+                .maximumSize(1000L)
+                .expireAfterAccess(10, TimeUnit.MINUTES)
+                .build(CacheLoader.from(eventDB::countEventsByType));
+        aggregateEventsCache = CacheBuilder.newBuilder()
+                .maximumSize(1000L)
+                .expireAfterAccess(10, TimeUnit.MINUTES
+                ).build(CacheLoader.from(eventDB::getAggregatedEvents));
         maxCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMaxTime));
         minCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMinTime));
         this.modelInstance = new FilteredEventsModel(this, currentStateProperty);
-
     }
 
     /**
@@ -184,19 +187,18 @@ public TimeLineEvent getEventById(Long eventID) {
         return idToEventCache.getUnchecked(eventID);
     }
 
-    public Set<TimeLineEvent> getEventsById(Collection<Long> eventIDs) {
+    synchronized public Set<TimeLineEvent> getEventsById(Collection<Long> eventIDs) {
         return eventIDs.stream()
                 .map(idToEventCache::getUnchecked)
                 .collect(Collectors.toSet());
 
     }
 
-    public List<AggregateEvent> getAggregatedEvents(ZoomParams params) {
-
+    synchronized public List<AggregateEvent> getAggregatedEvents(ZoomParams params) {
         return aggregateEventsCache.getUnchecked(params);
     }
 
-    public Map<EventType, Long> countEvents(ZoomParams params) {
+    synchronized public Map<EventType, Long> countEvents(ZoomParams params) {
         return eventCountsCache.getUnchecked(params);
     }
 
@@ -205,6 +207,7 @@ private void invalidateCaches() {
         maxCache.invalidateAll();
         eventCountsCache.invalidateAll();
         aggregateEventsCache.invalidateAll();
+        idToEventCache.invalidateAll();
     }
 
     public Set<Long> getEventIDs(Interval timeRange, RootFilter filter) {
@@ -234,30 +237,35 @@ private class DBPopulationWorker extends SwingWorker<Void, ProgressWindow.Progre
 
         //TODO: can we avoid this with a state listener?  does it amount to the same thing?
         //post population operation to execute
-        private final Runnable r;
+        private final Runnable postPopulationOperation;
+        private final SleuthkitCase skCase;
+        private final TagsManager tagsManager;
 
-        public DBPopulationWorker(Runnable r) {
+        public DBPopulationWorker(Runnable postPopulationOperation) {
             progressDialog = new ProgressWindow(null, true, this);
             progressDialog.setVisible(true);
-            this.r = r;
+
+            skCase = autoCase.getSleuthkitCase();
+            tagsManager = autoCase.getServices().getTagsManager();
+
+            this.postPopulationOperation = postPopulationOperation;
         }
 
         @Override
+        @NbBundle.Messages({"progressWindow.msg.populateMacEventsFiles=populating mac events for files:",
+            "progressWindow.msg.reinit_db=(re)initializing events database",
+            "progressWindow.msg.commitingDb=committing events db"})
         protected Void doInBackground() throws Exception {
-            process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, -1, NbBundle.getMessage(this.getClass(),
-                    "EventsRepository.progressWindow.msg.reinit_db"), "")));
+            process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, -1, Bundle.progressWindow_msg_reinit_db(), "")));
             //reset database 
             //TODO: can we do more incremental updates? -jm
-            eventDB.dropEventsTable();
-            eventDB.initializeDB();
+            eventDB.reInitializeDB();
 
             //grab ids of all files
-            SleuthkitCase skCase = autoCase.getSleuthkitCase();
             List<Long> files = skCase.findAllFileIdsWhere("name != '.' AND name != '..'");
 
             final int numFiles = files.size();
-            process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, numFiles, NbBundle.getMessage(this.getClass(),
-                    "EventsRepository.progressWindow.msg.populateMacEventsFiles"), "")));
+            process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, numFiles, Bundle.progressWindow_msg_populateMacEventsFiles(), "")));
 
             //insert file events into db
             int i = 1;
@@ -269,7 +277,9 @@ protected Void doInBackground() throws Exception {
                     try {
                         AbstractFile f = skCase.getAbstractFileById(fID);
 
-                        if (f != null) {
+                        if (f == null) {
+                            LOGGER.log(Level.WARNING, "Failed to get data for file : {0}", fID); // NON-NLS
+                        } else {
                             //TODO: This is broken for logical files? fix -jm
                             //TODO: logical files don't necessarily have valid timestamps, so ... -jm
                             final String uniquePath = f.getUniquePath();
@@ -279,29 +289,26 @@ protected Void doInBackground() throws Exception {
                             String rootFolder = StringUtils.substringBetween(parentPath, "/", "/");
                             String shortDesc = datasourceName + "/" + StringUtils.defaultIfBlank(rootFolder, "");
                             String medD = datasourceName + parentPath;
-                            final TskData.FileKnown known = f.getKnown();
-                            boolean hashHit = f.getArtifactsCount(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT) > 0;
-                            Set<String> hashSets = hashHit ? f.getHashSetNames() : Collections.emptySet();
+                            final TskData.FileKnown known = f.getKnown();                   
+                            Set<String> hashSets =  f.getHashSetNames() ;
+                            boolean tagged = !tagsManager.getContentTagsByContent(f).isEmpty();
 
                             //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, trans);
+                                eventDB.insertEvent(f.getAtime(), FileSystemTypes.FILE_ACCESSED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tagged, trans);
                             }
                             if (f.getMtime() > 0) {
-                                eventDB.insertEvent(f.getMtime(), FileSystemTypes.FILE_MODIFIED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, trans);
+                                eventDB.insertEvent(f.getMtime(), FileSystemTypes.FILE_MODIFIED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tagged, trans);
                             }
                             if (f.getCtime() > 0) {
-                                eventDB.insertEvent(f.getCtime(), FileSystemTypes.FILE_CHANGED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, trans);
+                                eventDB.insertEvent(f.getCtime(), FileSystemTypes.FILE_CHANGED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tagged, trans);
                             }
                             if (f.getCrtime() > 0) {
-                                eventDB.insertEvent(f.getCrtime(), FileSystemTypes.FILE_CREATED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, trans);
+                                eventDB.insertEvent(f.getCrtime(), FileSystemTypes.FILE_CREATED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tagged, trans);
                             }
 
                             process(Arrays.asList(new ProgressWindow.ProgressUpdate(i, numFiles,
-                                    NbBundle.getMessage(this.getClass(),
-                                            "EventsRepository.progressWindow.msg.populateMacEventsFiles2"), f.getName())));
-                        } else {
-                            LOGGER.log(Level.WARNING, "failed to look up data for file : {0}", fID); // NON-NLS
+                                    Bundle.progressWindow_msg_populateMacEventsFiles(), f.getName())));
                         }
                     } catch (TskCoreException tskCoreException) {
                         LOGGER.log(Level.WARNING, "failed to insert mac event for file : " + fID, tskCoreException); // NON-NLS
@@ -318,12 +325,11 @@ protected Void doInBackground() throws Exception {
                 }
                 //skip file_system events, they are already handled above.
                 if (type instanceof ArtifactEventType) {
-                    populateEventType((ArtifactEventType) type, trans, skCase);
+                    populateEventType((ArtifactEventType) type, trans);
                 }
             }
 
-            process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, -1, NbBundle.getMessage(this.getClass(),
-                    "EventsRepository.progressWindow.msg.commitingDb"), "")));
+            process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, -1, Bundle.progressWindow_msg_commitingDb(), "")));
             if (isCancelled()) {
                 eventDB.rollBackTransaction(trans);
             } else {
@@ -349,24 +355,23 @@ protected void process(List<ProgressWindow.ProgressUpdate> chunks) {
         }
 
         @Override
+        @NbBundle.Messages("msgdlg.problem.text=There was a problem populating the timeline."
+                + "  Not all events may be present or accurate. See the log for details.")
         protected void done() {
             super.done();
             try {
                 progressDialog.close();
                 get();
-
             } catch (CancellationException ex) {
                 LOGGER.log(Level.INFO, "Database population was cancelled by the user.  Not all events may be present or accurate. See the log for details.", ex); // NON-NLS
             } catch (InterruptedException | ExecutionException ex) {
                 LOGGER.log(Level.WARNING, "Exception while populating database.", ex); // NON-NLS
-                JOptionPane.showMessageDialog(null, NbBundle.getMessage(this.getClass(),
-                        "EventsRepository.msgdlg.problem.text"));
+                JOptionPane.showMessageDialog(null, Bundle.msgdlg_problem_text());
             } catch (Exception ex) {
                 LOGGER.log(Level.WARNING, "Unexpected exception while populating database.", ex); // NON-NLS
-                JOptionPane.showMessageDialog(null, NbBundle.getMessage(this.getClass(),
-                        "EventsRepository.msgdlg.problem.text"));
+                JOptionPane.showMessageDialog(null, Bundle.msgdlg_problem_text());
             }
-            r.run();  //execute post db population operation
+            postPopulationOperation.run();  //execute post db population operation
         }
 
         /**
@@ -376,16 +381,15 @@ protected void done() {
          * @param trans   the db transaction to use
          * @param skCase  a reference to the sleuthkit case
          */
-        private void populateEventType(final ArtifactEventType type, EventDB.EventTransaction trans, SleuthkitCase skCase) {
+        @NbBundle.Messages({"# {0} - event type ", "progressWindow.populatingXevents=populating {0} events"})
+        private void populateEventType(final ArtifactEventType type, EventDB.EventTransaction trans) {
             try {
                 //get all the blackboard artifacts corresponding to the given event sub_type
                 final ArrayList<BlackboardArtifact> blackboardArtifacts = skCase.getBlackboardArtifacts(type.getArtifactType());
                 final int numArtifacts = blackboardArtifacts.size();
 
                 process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, numArtifacts,
-                        NbBundle.getMessage(this.getClass(),
-                                "EventsRepository.progressWindow.populatingXevents",
-                                type.toString()), "")));
+                        Bundle.progressWindow_populatingXevents(type.toString()), "")));
 
                 int i = 0;
                 for (final BlackboardArtifact bbart : blackboardArtifacts) {
@@ -396,16 +400,15 @@ private void populateEventType(final ArtifactEventType type, EventDB.EventTransa
                         long datasourceID = skCase.getContentById(bbart.getObjectID()).getDataSource().getId();
 
                         AbstractFile f = skCase.getAbstractFileById(bbart.getObjectID());
-                        boolean hashHit = f.getArtifactsCount(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT) > 0;
-                        Set<String> hashSets = hashHit ? f.getHashSetNames() : Collections.emptySet();
-                        eventDB.insertEvent(eventDescription.getTime(), type, datasourceID, bbart.getObjectID(), bbart.getArtifactID(), eventDescription.getFullDescription(), eventDescription.getMedDescription(), eventDescription.getShortDescription(), null, hashSets, trans);
+                        Set<String> hashSets =  f.getHashSetNames();
+                        boolean tagged = tagsManager.getBlackboardArtifactTagsByArtifact(bbart).isEmpty() == false;
+
+                        eventDB.insertEvent(eventDescription.getTime(), type, datasourceID, bbart.getObjectID(), bbart.getArtifactID(), eventDescription.getFullDescription(), eventDescription.getMedDescription(), eventDescription.getShortDescription(), null, hashSets, tagged, trans);
                     }
 
                     i++;
                     process(Arrays.asList(new ProgressWindow.ProgressUpdate(i, numArtifacts,
-                            NbBundle.getMessage(this.getClass(),
-                                    "EventsRepository.progressWindow.populatingXevents",
-                                    type.toString()), "")));
+                            Bundle.progressWindow_populatingXevents(type), "")));
                 }
             } catch (TskCoreException ex) {
                 LOGGER.log(Level.SEVERE, "There was a problem getting events with sub type = " + type.toString() + ".", ex); // NON-NLS
@@ -436,4 +439,13 @@ synchronized private void populateFilterMaps(SleuthkitCase skCase) {
             }
         }
     }
+
+  synchronized public Set<Long> markEventsTagged(long objID, Long artifactID, boolean tagged) {
+        Set<Long> updatedEventIDs = eventDB.markEventsTagged(objID, artifactID, tagged);
+        if (!updatedEventIDs.isEmpty()) {
+            aggregateEventsCache.invalidateAll();
+            idToEventCache.invalidateAll(updatedEventIDs);
+        }
+        return updatedEventIDs;
+    }
 }
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/db/SQLHelper.java b/Core/src/org/sleuthkit/autopsy/timeline/events/db/SQLHelper.java
index 58c45aba8c88c93311d7185b0946e98ebb8f2623..1c63f9f2d96c401680ec7015e58cfd34d8f232de 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/events/db/SQLHelper.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/events/db/SQLHelper.java
@@ -89,9 +89,7 @@ static String getSQLWhere(Filter filter) {
 
     static String getSQLWhere(HideKnownFilter filter) {
         if (filter.isSelected()) {
-            return "(" + EventDB.EventTableColumn.KNOWN.toString()
-                    + " is not '" + TskData.FileKnown.KNOWN.getFileKnownValue()
-                    + "')"; // NON-NLS
+            return "(known_state  IS NOT '" + TskData.FileKnown.KNOWN.getFileKnownValue() + "')"; // NON-NLS
         } else {
             return "1";
         }
@@ -111,11 +109,11 @@ static String getSQLWhere(HashHitsFilter filter) {
     }
 
     static String getSQLWhere(DataSourceFilter filter) {
-        return (filter.isSelected()) ? "(" + EventDB.EventTableColumn.DATA_SOURCE_ID.toString() + " = '" + filter.getDataSourceID() + "')" : "1";
+        return (filter.isSelected()) ? "(datasource_id = '" + filter.getDataSourceID() + "')" : "1";
     }
 
     static String getSQLWhere(DataSourcesFilter filter) {
-        return (filter.isSelected()) ? "(" + EventDB.EventTableColumn.DATA_SOURCE_ID.toString() + " in ("
+        return (filter.isSelected()) ? "(datasource_id in ("
                 + filter.getSubFilters().stream()
                 .filter(AbstractFilter::isSelected)
                 .map((dataSourceFilter) -> String.valueOf(dataSourceFilter.getDataSourceID()))
@@ -127,10 +125,10 @@ static String getSQLWhere(TextFilter filter) {
             if (StringUtils.isBlank(filter.getText())) {
                 return "1";
             }
-            String strip = StringUtils.strip(filter.getText());
-            return "((" + EventDB.EventTableColumn.MED_DESCRIPTION.toString() + " like '%" + strip + "%') or (" // NON-NLS
-                    + EventDB.EventTableColumn.FULL_DESCRIPTION.toString() + " like '%" + strip + "%') or (" // NON-NLS
-                    + EventDB.EventTableColumn.SHORT_DESCRIPTION.toString() + " like '%" + strip + "%'))";
+            String strippedFilterText = StringUtils.strip(filter.getText());
+            return "((med_description like '%" + strippedFilterText + "%')"
+                    + " or (full_description like '%" + strippedFilterText + "%')"
+                    + " or (short_description like '%" + strippedFilterText + "%'))";
         } else {
             return "1";
         }
@@ -140,19 +138,20 @@ static String getSQLWhere(TextFilter filter) {
      * generate a sql where clause for the given type filter, while trying to be
      * as simple as possible to improve performance.
      *
-     * @param filter
+     * @param typeFilter
      *
      * @return
      */
-    static String getSQLWhere(TypeFilter filter) {
-        if (filter.isSelected() == false) {
+    static String getSQLWhere(TypeFilter typeFilter) {
+        if (typeFilter.isSelected() == false) {
             return "0";
-        } else if (filter.getEventType() instanceof RootEventType) {
-            if (filter.getSubFilters().stream().allMatch((Filter f) -> f.isSelected() && ((TypeFilter) f).getSubFilters().stream().allMatch(Filter::isSelected))) {
+        } else if (typeFilter.getEventType() instanceof RootEventType) {
+            if (typeFilter.getSubFilters().stream()
+                    .allMatch(subFilter -> subFilter.isSelected() && subFilter.getSubFilters().stream().allMatch(Filter::isSelected))) {
                 return "1"; //then collapse clause to true
             }
         }
-        return "(" + EventDB.EventTableColumn.SUB_TYPE.toString() + " in (" + StringUtils.join(getActiveSubTypes(filter), ",") + "))";
+        return "(sub_type IN (" + StringUtils.join(getActiveSubTypes(typeFilter), ",") + "))";
     }
 
 }
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java
index c08c305fa5fef99a7efeb2f21b3d22e7d081299d..81ddc585ab19a84fbc468a29d3289ffb7cdc2ed4 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java
@@ -18,10 +18,10 @@
  */
 package org.sleuthkit.autopsy.timeline.ui.detailview;
 
+import com.google.common.eventbus.Subscribe;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.logging.Level;
@@ -59,13 +59,14 @@
 import org.apache.commons.lang3.StringUtils;
 import org.joda.time.DateTime;
 import org.joda.time.Interval;
-import org.openide.util.Exceptions;
 import org.openide.util.NbBundle;
 import org.sleuthkit.autopsy.coreutils.ColorUtilities;
 import org.sleuthkit.autopsy.coreutils.LoggedTask;
 import org.sleuthkit.autopsy.coreutils.Logger;
 import org.sleuthkit.autopsy.timeline.TimeLineController;
 import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
+import org.sleuthkit.autopsy.timeline.events.EventsTaggedEvent;
+import org.sleuthkit.autopsy.timeline.events.EventsUnTaggedEvent;
 import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
 import org.sleuthkit.autopsy.timeline.events.TimeLineEvent;
 import org.sleuthkit.autopsy.timeline.filters.RootFilter;
@@ -73,6 +74,10 @@
 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.AbstractFile;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.BlackboardArtifactTag;
+import org.sleuthkit.datamodel.ContentTag;
 import org.sleuthkit.datamodel.SleuthkitCase;
 import org.sleuthkit.datamodel.TskCoreException;
 
@@ -83,9 +88,10 @@ public class AggregateEventNode extends StackPane {
 
     private static final Logger LOGGER = Logger.getLogger(AggregateEventNode.class.getName());
 
-    private static final Image HASH_PIN = new Image(AggregateEventNode.class.getResourceAsStream("/org/sleuthkit/autopsy/images/hashset_hits.png"));
+    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);
 
@@ -97,7 +103,7 @@ public class AggregateEventNode extends StackPane {
     /**
      * The event this AggregateEventNode represents visually
      */
-    private final AggregateEvent event;
+    private AggregateEvent aggEvent;
 
     private final AggregateEventNode parentEventNode;
 
@@ -164,22 +170,30 @@ public class AggregateEventNode extends StackPane {
     private DescriptionVisibility descrVis;
     private final SleuthkitCase sleuthkitCase;
     private final FilteredEventsModel eventsModel;
-    private Map<String, Long> hashSetCounts = null;
+
     private Tooltip tooltip;
+    private final ImageView hashIV = new ImageView(HASH_PIN);
+    private final ImageView tagIV = new ImageView(TAG);
 
-    public AggregateEventNode(final AggregateEvent event, AggregateEventNode parentEventNode, EventDetailChart chart) {
-        this.event = event;
-        descLOD.set(event.getLOD());
+    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);
-        ImageView imageView = new ImageView(HASH_PIN);
-        final HBox hBox = new HBox(descrLabel, countLabel, region, imageView, minusButton, plusButton);
-        if (event.getEventIDsWithHashHits().isEmpty()) {
-            hBox.getChildren().remove(imageView);
+
+        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);
@@ -211,7 +225,7 @@ public AggregateEventNode(final AggregateEvent event, AggregateEventNode parentE
         subNodePane.setPickOnBounds(false);
 
         //setup description label
-        eventTypeImageView.setImage(event.getType().getFXImage());
+        eventTypeImageView.setImage(aggEvent.getType().getFXImage());
         descrLabel.setGraphic(eventTypeImageView);
         descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
         descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
@@ -220,7 +234,7 @@ public AggregateEventNode(final AggregateEvent event, AggregateEventNode parentE
         setDescriptionVisibility(chart.getDescrVisibility().get());
 
         //setup backgrounds
-        final Color evtColor = event.getType().getColor();
+        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);
@@ -237,7 +251,6 @@ public AggregateEventNode(final AggregateEvent event, AggregateEventNode parentE
             minusButton.setManaged(true);
             plusButton.setManaged(true);
             toFront();
-
         });
 
         setOnMouseExited((MouseEvent e) -> {
@@ -246,13 +259,12 @@ public AggregateEventNode(final AggregateEvent event, AggregateEventNode parentE
             plusButton.setVisible(false);
             minusButton.setManaged(false);
             plusButton.setManaged(false);
-
         });
 
         setOnMouseClicked(new EventMouseHandler());
 
         plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL));
-        minusButton.disableProperty().bind(descLOD.isEqualTo(event.getLOD()));
+        minusButton.disableProperty().bind(descLOD.isEqualTo(aggEvent.getLOD()));
 
         plusButton.setOnMouseClicked(e -> {
             final DescriptionLOD next = descLOD.get().next();
@@ -270,35 +282,64 @@ public AggregateEventNode(final AggregateEvent event, AggregateEventNode parentE
         });
     }
 
-    private void installTooltip() {
+    synchronized private void installTooltip() {
+        //TODO: all this work should probably go on a background thread...
         if (tooltip == null) {
-            String collect = "";
-            if (!event.getEventIDsWithHashHits().isEmpty()) {
-                if (Objects.isNull(hashSetCounts)) {
-                    hashSetCounts = new HashMap<>();
-                    try {
-                        for (TimeLineEvent tle : eventsModel.getEventsById(event.getEventIDsWithHashHits())) {
-                            Set<String> hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames();
-                            for (String hashSetName : hashSetNames) {
-                                hashSetCounts.merge(hashSetName, 1L, Long::sum);
-                            }
+            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);
                     }
+                } 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()) {
+                try {
+                    for (TimeLineEvent tle : eventsModel.getEventsById(aggEvent.getEventIDsWithTags())) {
 
-                collect = hashSetCounts.entrySet().stream()
-                        .map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
-                        .collect(Collectors.joining("\n"));
+                        AbstractFile abstractFileById = sleuthkitCase.getAbstractFileById(tle.getFileID());
+                        List<ContentTag> contentTags = sleuthkitCase.getContentTagsByContent(abstractFileById);
+                        for (ContentTag tag : contentTags) {
+                            tagCounts.merge(tag.getName().getDisplayName(), 1l, Long::sum);
+                        }
 
+                        Long artifactID = tle.getArtifactID();
+                        if (artifactID != 0) {
+                            BlackboardArtifact blackboardArtifact = sleuthkitCase.getBlackboardArtifact(artifactID);
+                            List<BlackboardArtifactTag> artifactTags = sleuthkitCase.getBlackboardArtifactTagsByArtifact(blackboardArtifact);
+                            for (BlackboardArtifactTag tag : artifactTags) {
+                                tagCounts.merge(tag.getName().getDisplayName(), 1l, Long::sum);
+                            }
+                        }
+                    }
+                } catch (TskCoreException ex) {
+                    LOGGER.log(Level.SEVERE, "Error getting tag info for event.", ex);
+                }
             }
+
+            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()))
-                    + (collect.isEmpty() ? "" : "\n\nHash Set Hits\n" + collect));
+                    + (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString)
+                    + (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString)
+            );
             Tooltip.install(AggregateEventNode.this, tooltip);
         }
     }
@@ -307,8 +348,8 @@ public Pane getSubNodePane() {
         return subNodePane;
     }
 
-    public AggregateEvent getEvent() {
-        return event;
+    synchronized public AggregateEvent getEvent() {
+        return aggEvent;
     }
 
     /**
@@ -334,12 +375,11 @@ public void setDescriptionWidth(double w) {
     /**
      * @param descrVis the level of description that should be displayed
      */
-    final void setDescriptionVisibility(DescriptionVisibility descrVis) {
+    synchronized final void setDescriptionVisibility(DescriptionVisibility descrVis) {
         this.descrVis = descrVis;
-        final int size = event.getEventIDs().size();
+        final int size = aggEvent.getEventIDs().size();
 
         switch (descrVis) {
-
             case COUNT_ONLY:
                 descrLabel.setText("");
                 countLabel.setText(String.valueOf(size));
@@ -350,7 +390,7 @@ final void setDescriptionVisibility(DescriptionVisibility descrVis) {
                 break;
             default:
             case SHOWN:
-                String description = event.getDescription();
+                String description = aggEvent.getDescription();
                 description = parentEventNode != null
                         ? "    ..." + StringUtils.substringAfter(description, parentEventNode.getEvent().getDescription())
                         : description;
@@ -380,18 +420,18 @@ void applySelectionEffect(final boolean applied) {
      *
      * @param applied true to apply the highlight 'effect', false to remove it
      */
-    void applyHighlightEffect(boolean applied) {
+    synchronized void applyHighlightEffect(boolean applied) {
 
         if (applied) {
             descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS
-            spanFill = new Background(new BackgroundFill(getEvent().getType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY));
+            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(getEvent().getType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)));
+            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(getEvent().getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY));
+            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(getEvent().getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
+            setBackground(new Background(new BackgroundFill(aggEvent.getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
         }
     }
 
@@ -421,22 +461,21 @@ public void setContextMenu(ContextMenu contextMenu) {
     /**
      * loads sub-clusters at the given Description LOD
      *
-     * @param newLOD
+     * @param newDescriptionLOD
      */
-    private void loadSubClusters(DescriptionLOD newLOD) {
+    synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) {
         getSubNodePane().getChildren().clear();
-        if (newLOD == event.getLOD()) {
-            getSubNodePane().getChildren().clear();
+        if (newDescriptionLOD == aggEvent.getLOD()) {
             chart.setRequiresLayout(true);
             chart.requestChartLayout();
         } else {
-            RootFilter combinedFilter = chart.getFilteredEvents().filter().get().copyOf();
+            RootFilter combinedFilter = eventsModel.filter().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(event.getDescription()),
-                    new TypeFilter(event.getType()));
+            combinedFilter.getSubFilters().addAll(new TextFilter(aggEvent.getDescription()),
+                    new TypeFilter(aggEvent.getType()));
 
             //make a new end inclusive span (to 'filter' with)
-            final Interval span = event.getSpan().withEndMillis(event.getSpan().getEndMillis() + 1000);
+            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>>(
@@ -445,14 +484,14 @@ private void loadSubClusters(DescriptionLOD newLOD) {
                         @Override
                         protected List<AggregateEventNode> call() throws Exception {
                             //query for the sub-clusters
-                            List<AggregateEvent> aggregatedEvents = chart.getFilteredEvents().getAggregatedEvents(new ZoomParams(span,
-                                            chart.getFilteredEvents().eventTypeZoom().get(),
+                            List<AggregateEvent> aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span,
+                                            eventsModel.eventTypeZoom().get(),
                                             combinedFilter,
-                                            newLOD));
+                                            newDescriptionLOD));
                             //for each sub cluster make an AggregateEventNode to visually represent it, and set x-position
-                            return aggregatedEvents.stream().map((AggregateEvent t) -> {
-                                AggregateEventNode subNode = new AggregateEventNode(t, AggregateEventNode.this, chart);
-                                subNode.setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(t.getSpan().getStartMillis())) - getLayoutXCompensation());
+                            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
                         }
@@ -468,7 +507,7 @@ protected void succeeded() {
                                 chart.requestChartLayout();
                                 chart.setCursor(null);
                             } catch (InterruptedException | ExecutionException ex) {
-                                Exceptions.printStackTrace(ex);
+                                LOGGER.log(Level.SEVERE, "Error loading subnodes", ex);
                             }
                         }
                     };
@@ -505,4 +544,30 @@ public void handle(MouseEvent t) {
             }
         }
     }
+
+    synchronized void handleEventsUnTagged(EventsUnTaggedEvent tagEvent) {
+        AggregateEvent withTagsRemoved = aggEvent.withTagsRemoved(tagEvent.getEventIDs());
+        if (withTagsRemoved != aggEvent) {
+            aggEvent = withTagsRemoved;
+            tooltip = null;
+            boolean hasTags = aggEvent.getEventIDsWithTags().isEmpty() == false;
+            Platform.runLater(() -> {
+                tagIV.setManaged(hasTags);
+                tagIV.setVisible(hasTags);
+            });
+        }
+    }
+
+    @Subscribe
+    synchronized void handleEventsTagged(EventsTaggedEvent tagEvent) {
+        AggregateEvent withTagsAdded = aggEvent.withTagsAdded(tagEvent.getEventIDs());
+        if (withTagsAdded != aggEvent) {
+            aggEvent = withTagsAdded;
+            tooltip = null;
+            Platform.runLater(() -> {
+                tagIV.setManaged(true);
+                tagIV.setVisible(true);
+            });
+        }
+    }
 }
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java
index 3f8fcc5c7639b1852e80ecd2f35f03b99799db17..5f0136a35264c5a6d6a0abb5ee7cc9308379f9b4 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java
@@ -37,7 +37,17 @@
 import javafx.scene.chart.Axis;
 import javafx.scene.chart.BarChart;
 import javafx.scene.chart.XYChart;
-import javafx.scene.control.*;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.CustomMenuItem;
+import javafx.scene.control.Label;
+import javafx.scene.control.MenuButton;
+import javafx.scene.control.MultipleSelectionModel;
+import javafx.scene.control.RadioButton;
+import javafx.scene.control.ScrollBar;
+import javafx.scene.control.SeparatorMenuItem;
+import javafx.scene.control.Slider;
+import javafx.scene.control.ToggleGroup;
+import javafx.scene.control.TreeItem;
 import javafx.scene.effect.Effect;
 import static javafx.scene.input.KeyCode.DOWN;
 import static javafx.scene.input.KeyCode.KP_DOWN;
@@ -95,12 +105,6 @@ public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEve
 
     private MultipleSelectionModel<TreeItem<NavTreeNode>> treeSelectionModel;
 
-    @FXML
-    protected ResourceBundle resources;
-
-    @FXML
-    protected URL location;
-
     //these three could be injected from fxml but it was causing npe's
     private final DateAxis dateAxis = new DateAxis();
 
@@ -207,8 +211,8 @@ public DetailViewPane(Pane partPane, Pane contextPane, Region spacer) {
         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 (AggregateEventNode n : chart.getNodes((AggregateEventNode t)
+                        -> t.getEvent().getDescription().equals(tn.getEvent().getDescription()))) {
                     highlightedNodes.add(n);
                 }
             });
@@ -226,8 +230,7 @@ public void setSelectionModel(MultipleSelectionModel<TreeItem<NavTreeNode>> sele
         treeSelectionModel.getSelectedItems().addListener((Observable observable) -> {
             highlightedNodes.clear();
             for (TreeItem<NavTreeNode> tn : treeSelectionModel.getSelectedItems()) {
-                for (AggregateEventNode n : chart.getNodes((
-                        AggregateEventNode t)
+                for (AggregateEventNode n : chart.getNodes((AggregateEventNode t)
                         -> t.getEvent().getDescription().equals(tn.getValue().getDescription()))) {
                     highlightedNodes.add(n);
                 }
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java
index 4f1b9a8a75f404542e7ea7550e85f410028f8658..8c1305a38de182e94b351ac513da6a4da8da950b 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java
@@ -1,7 +1,7 @@
 /*
  * Autopsy Forensic Browser
  *
- * Copyright 2013-14 Basis Technology Corp.
+ * Copyright 2013-15 Basis Technology Corp.
  * Contact: carrier <at> sleuthkit <dot> org
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,6 +19,7 @@
 package org.sleuthkit.autopsy.timeline.ui.detailview;
 
 import com.google.common.collect.Collections2;
+import com.google.common.eventbus.Subscribe;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -72,6 +73,8 @@
 import org.sleuthkit.autopsy.timeline.actions.Back;
 import org.sleuthkit.autopsy.timeline.actions.Forward;
 import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
+import org.sleuthkit.autopsy.timeline.events.EventsTaggedEvent;
+import org.sleuthkit.autopsy.timeline.events.EventsUnTaggedEvent;
 import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
 import org.sleuthkit.autopsy.timeline.events.type.EventType;
 import org.sleuthkit.autopsy.timeline.ui.TimeLineChart;
@@ -341,15 +344,22 @@ public synchronized void setController(TimeLineController controller) {
 
     @Override
     public void setModel(FilteredEventsModel filteredEvents) {
+        if (this.filteredEvents != null) {
+            this.filteredEvents.unRegisterForEvents(this);
+        }
+        if (this.filteredEvents != filteredEvents) {
+            filteredEvents.registerForEvents(this);
+            filteredEvents.getRequestedZoomParamters().addListener(o -> {
+                clearGuideLine();
+                clearIntervalSelector();
+
+                selectedNodes.clear();
+                projectionMap.clear();
+                controller.selectEventIDs(Collections.emptyList());
+            });
+        }
         this.filteredEvents = filteredEvents;
-        filteredEvents.getRequestedZoomParamters().addListener(o -> {
-            clearGuideLine();
-            clearIntervalSelector();
 
-            selectedNodes.clear();
-            projectionMap.clear();
-            controller.selectEventIDs(Collections.emptyList());
-        });
     }
 
     @Override
@@ -517,6 +527,10 @@ Iterable<AggregateEventNode> getNodes(Predicate<AggregateEventNode> p) {
         return nodes;
     }
 
+    private Iterable<AggregateEventNode> getAllNodes() {
+        return getNodes(x -> true);
+    }
+
     synchronized SimpleDoubleProperty getTruncateWidth() {
         return truncateWidth;
     }
@@ -526,9 +540,8 @@ synchronized void setVScroll(double d) {
         nodeGroup.setTranslateY(-d * h);
     }
 
-    private void checkNode(AggregateEventNode node, Predicate<AggregateEventNode> p, List<AggregateEventNode> nodes) {
+    private static void checkNode(AggregateEventNode node, Predicate<AggregateEventNode> p, List<AggregateEventNode> nodes) {
         if (node != null) {
-            AggregateEvent event = node.getEvent();
             if (p.test(node)) {
                 nodes.add(node);
             }
@@ -716,8 +729,25 @@ synchronized void setRequiresLayout(boolean b) {
         requiresLayout = true;
     }
 
+    /**
+     * make this accessible to AggregateEventNode
+     */
     @Override
     protected void requestChartLayout() {
-        super.requestChartLayout(); //To change body of generated methods, choose Tools | Templates.
+        super.requestChartLayout();
+    }
+
+    @Subscribe
+    synchronized public void handleEventsUnTagged(EventsUnTaggedEvent tagEvent) {
+        for (AggregateEventNode t : getAllNodes()) {
+            t.handleEventsUnTagged(tagEvent);
+        }
+    }
+
+    @Subscribe
+    synchronized public void handleEventsTagged(EventsTaggedEvent tagEvent) {
+        for (AggregateEventNode t : getAllNodes()) {
+            t.handleEventsTagged(tagEvent);
+        }
     }
 }
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java
index a04227b7e6e341b8c758f3ea900b356f3c15696d..d1bdb4e0672c5aee11b3e4fccbcdaf7dd0eb55a0 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java
@@ -25,9 +25,9 @@
  */
 public enum DescriptionLOD {
 
-    SHORT(NbBundle.getMessage(DescriptionLOD.class, "DescriptionLOD.short")), MEDIUM(
-            NbBundle.getMessage(DescriptionLOD.class, "DescriptionLOD.medium")), FULL(
-                    NbBundle.getMessage(DescriptionLOD.class, "DescriptionLOD.full"));
+    SHORT(NbBundle.getMessage(DescriptionLOD.class, "DescriptionLOD.short")),
+    MEDIUM(NbBundle.getMessage(DescriptionLOD.class, "DescriptionLOD.medium")),
+    FULL(NbBundle.getMessage(DescriptionLOD.class, "DescriptionLOD.full"));
 
     private final String displayName;
 
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java
index dc917d416c55f46a78f5caf34c5005d7fef41e51..c6de9f5d1ef4eaf714ba406b9595633df051b4b4 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java
@@ -18,10 +18,7 @@
  */
 package org.sleuthkit.autopsy.timeline.zooming;
 
-import java.util.Collections;
-import java.util.EnumSet;
 import java.util.Objects;
-import java.util.Set;
 import org.joda.time.Interval;
 import org.openide.util.NbBundle;
 import org.sleuthkit.autopsy.timeline.filters.Filter;
@@ -41,20 +38,6 @@ public class ZoomParams {
 
     private final DescriptionLOD descrLOD;
 
-    private final Set<Field> changedFields;
-
-    public Set<Field> getChangedFields() {
-        return Collections.unmodifiableSet(changedFields);
-    }
-
-    public enum Field {
-
-        TIME,
-        EVENT_TYPE_ZOOM,
-        FILTER,
-        DESCRIPTION_LOD;
-    }
-
     public Interval getTimeRange() {
         return timeRange;
     }
@@ -76,35 +59,27 @@ public ZoomParams(Interval timeRange, EventTypeZoomLevel zoomLevel, RootFilter f
         this.typeZoomLevel = zoomLevel;
         this.filter = filter;
         this.descrLOD = descrLOD;
-        changedFields = EnumSet.allOf(Field.class);
-    }
 
-    public ZoomParams(Interval timeRange, EventTypeZoomLevel zoomLevel, RootFilter filter, DescriptionLOD descrLOD, EnumSet<Field> changed) {
-        this.timeRange = timeRange;
-        this.typeZoomLevel = zoomLevel;
-        this.filter = filter;
-        this.descrLOD = descrLOD;
-        changedFields = changed;
     }
 
     public ZoomParams withTimeAndType(Interval timeRange, EventTypeZoomLevel zoomLevel) {
-        return new ZoomParams(timeRange, zoomLevel, filter, descrLOD, EnumSet.of(Field.TIME, Field.EVENT_TYPE_ZOOM));
+        return new ZoomParams(timeRange, zoomLevel, filter, descrLOD);
     }
 
     public ZoomParams withTypeZoomLevel(EventTypeZoomLevel zoomLevel) {
-        return new ZoomParams(timeRange, zoomLevel, filter, descrLOD, EnumSet.of(Field.EVENT_TYPE_ZOOM));
+        return new ZoomParams(timeRange, zoomLevel, filter, descrLOD);
     }
 
     public ZoomParams withTimeRange(Interval timeRange) {
-        return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD, EnumSet.of(Field.TIME));
+        return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD);
     }
 
     public ZoomParams withDescrLOD(DescriptionLOD descrLOD) {
-        return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD, EnumSet.of(Field.DESCRIPTION_LOD));
+        return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD);
     }
 
     public ZoomParams withFilter(RootFilter filter) {
-        return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD, EnumSet.of(Field.FILTER));
+        return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD);
     }
 
     public boolean hasFilter(Filter filterSet) {
@@ -153,11 +128,7 @@ public boolean equals(Object obj) {
         if (this.filter.equals(other.filter) == false) {
             return false;
         }
-        if (this.descrLOD != other.descrLOD) {
-            return false;
-        }
-
-        return true;
+        return this.descrLOD == other.descrLOD;
     }
 
     @Override