diff --git a/Core/src/org/sleuthkit/autopsy/timeline/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/Bundle.properties
deleted file mode 100644
index b320647823fdf858ad9b13268643d9535525c1f7..0000000000000000000000000000000000000000
--- a/Core/src/org/sleuthkit/autopsy/timeline/Bundle.properties
+++ /dev/null
@@ -1,33 +0,0 @@
-OpenIDE-Module-Display-Category=External Viewers
-OpenIDE-Module-Long-Description=\
-    Displays user activity as an interactive timeline chart with year, month and day granularity. \n\
-    Events for a selected day are viewable in the built-in result and content viewers.
-OpenIDE-Module-Name=Timeline
-CTL_MakeTimeline="Make Timeline (Beta)"
-CTL_TimelineView=Generate Timeline
-OpenIDE-Module-Short-Description=Displays user activity timeline
-TimelineProgressDialog.jLabel1.text=Creating timeline . . .
-TimelineFrame.title=Timeline
-Timeline.frameName.text={0} - Autopsy Timeline (Beta)
-Timeline.resultsPanel.title=Timeline Results
-Timeline.getName=Make Timeline (Beta)
-Timeline.runJavaFxThread.progress.creating=Creating timeline . . .
-Timeline.runJavaFxThread.progress.genBodyFile=Generating Bodyfile
-Timeline.runJavaFxThread.progress.genMacTime=Generating Mactime
-Timeline.runJavaFxThread.progress.parseMacTime=Parsing Mactime
-Timeline.zoomOutButton.text=Zoom Out
-Timeline.goToButton.text=Go To\:
-Timeline.yearBarChart.x.years=Years
-Timeline.yearBarChart.y.numEvents=Number of Events
-Timeline.MonthsBarChart.x.monthYY=Month ({0})
-Timeline.MonthsBarChart.y.numEvents=Number of Events
-Timeline.eventsByMoBarChart.x.dayOfMo=Day of Month
-Timeline.eventsByMoBarChart.y.numEvents=Number of Events
-Timeline.node.emptyRoot=Empty Root
-Timeline.resultPanel.loading=Loading...
-Timeline.node.root=Root
-Timeline.propChg.confDlg.timelineOOD.msg=Timeline is out of date. Would you like to regenerate it?
-Timeline.propChg.confDlg.timelineOOD.details=Select an option
-Timeline.initTimeline.confDlg.genBeforeIngest.msg=You are trying to generate a timeline before ingest has been completed. The timeline may be incomplete. Do you want to continue?
-Timeline.initTimeline.confDlg.genBeforeIngest.deails=Timeline
-TimelineProgressDialog.setName.text=Make Timeline (Beta)
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/timeline/Bundle_ja.properties
deleted file mode 100644
index b905bd12053d786f21c70d3ed2c1a08983688293..0000000000000000000000000000000000000000
--- a/Core/src/org/sleuthkit/autopsy/timeline/Bundle_ja.properties
+++ /dev/null
@@ -1,33 +0,0 @@
-OpenIDE-Module-Display-Category=\u5916\u90E8\u30D3\u30E5\u30FC\u30A2
-OpenIDE-Module-Long-Description=\
-    \u30E6\u30FC\u30B6\u30A2\u30AF\u30C6\u30A3\u30D3\u30C6\u30A3\u3092\u5E74\u3001\u6708\u3001\u65E5\u306E\u5358\u4F4D\u3067\u3001\u30A4\u30F3\u30BF\u30E9\u30AF\u30C6\u30A3\u30D6\u306A\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u30C1\u30E3\u30FC\u30C8\u3068\u3057\u3066\u8868\u793A\u3057\u307E\u3059\u3002\n\
-    \u9078\u629E\u3057\u305F\u65E5\u306E\u30A4\u30D9\u30F3\u30C8\u306F\u5185\u8535\u306E\u7D50\u679C\u304A\u3088\u3073\u30B3\u30F3\u30C6\u30F3\u30C4\u30D3\u30E5\u30FC\u30A2\u3067\u78BA\u8A8D\u3067\u304D\u307E\u3059\u3002
-OpenIDE-Module-Name=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3
-CTL_MakeTimeline="\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u4F5C\u6210\uFF08\u30D9\u30FC\u30BF\uFF09"
-CTL_TimelineView=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u4F5C\u6210
-OpenIDE-Module-Short-Description=\u30E6\u30FC\u30B6\u30A2\u30AF\u30C6\u30A3\u30D3\u30C6\u30A3\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u3092\u8868\u793A
-TimelineProgressDialog.jLabel1.text=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u3092\u4F5C\u6210\u4E2D\u2026
-TimelineFrame.title=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3
-Timeline.frameName.text={0} - Autopsy\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\uFF08\u30D9\u30FC\u30BF\uFF09
-Timeline.resultsPanel.title=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u7D50\u679C
-Timeline.getName=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u4F5C\u6210\uFF08\u30D9\u30FC\u30BF\uFF09
-Timeline.runJavaFxThread.progress.creating=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u3092\u4F5C\u6210\u4E2D\u2026
-Timeline.runJavaFxThread.progress.genBodyFile=Bodyfile\u3092\u4F5C\u6210\u4E2D
-Timeline.runJavaFxThread.progress.genMacTime=MAC\u30BF\u30A4\u30E0\u3092\u4F5C\u6210\u4E2D
-Timeline.runJavaFxThread.progress.parseMacTime=MAC\u30BF\u30A4\u30E0\u3092\u30D1\u30FC\u30B9\u4E2D
-Timeline.zoomOutButton.text=\u7E2E\u5C0F
-Timeline.goToButton.text=\u4E0B\u8A18\u3078\u79FB\u52D5\uFF1A
-Timeline.yearBarChart.x.years=\u5E74
-Timeline.yearBarChart.y.numEvents=\u30A4\u30D9\u30F3\u30C8\u6570
-Timeline.MonthsBarChart.x.monthYY=\u6708 ({0})
-Timeline.MonthsBarChart.y.numEvents=\u30A4\u30D9\u30F3\u30C8\u6570
-Timeline.eventsByMoBarChart.x.dayOfMo=\u65E5
-Timeline.eventsByMoBarChart.y.numEvents=\u30A4\u30D9\u30F3\u30C8\u6570
-Timeline.node.emptyRoot=\u7A7A\u306E\u30EB\u30FC\u30C8
-Timeline.resultPanel.loading=\u30ED\u30FC\u30C9\u4E2D\u2026
-Timeline.node.root=\u30EB\u30FC\u30C8
-Timeline.propChg.confDlg.timelineOOD.msg=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u304C\u6700\u65B0\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002\u518D\u5EA6\u4F5C\u6210\u3057\u307E\u3059\u304B\uFF1F
-Timeline.propChg.confDlg.timelineOOD.details=\u30AA\u30D7\u30B7\u30E7\u30F3\u3092\u9078\u629E\u3057\u3066\u4E0B\u3055\u3044
-Timeline.initTimeline.confDlg.genBeforeIngest.msg=\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u304C\u5B8C\u4E86\u3059\u308B\u524D\u306B\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u3092\u4F5C\u6210\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307E\u3059\u3002\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u304C\u4E0D\u5B8C\u5168\u306B\u306A\u308B\u304B\u3082\u3057\u308C\u307E\u305B\u3093\u3002\u7D9A\u884C\u3057\u307E\u3059\u304B\uFF1F
-Timeline.initTimeline.confDlg.genBeforeIngest.deails=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3
-TimelineProgressDialog.setName.text=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u3092\u4F5C\u6210\uFF08\u30D9\u30FC\u30BF\uFF09
\ No newline at end of file
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/Timeline.java b/Core/src/org/sleuthkit/autopsy/timeline/Timeline.java
deleted file mode 100644
index 0e8cb2111060b7fa3663a541fb6cd0c5df30b587..0000000000000000000000000000000000000000
--- a/Core/src/org/sleuthkit/autopsy/timeline/Timeline.java
+++ /dev/null
@@ -1,1237 +0,0 @@
-/*
- * Autopsy Forensic Browser
- *
- * Copyright 2013 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;
-
-import java.awt.Component;
-import java.awt.Cursor;
-import java.awt.Dimension;
-import java.awt.EventQueue;
-import java.beans.PropertyChangeEvent;
-import java.beans.PropertyChangeListener;
-import java.io.BufferedWriter;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.Writer;
-import java.text.DateFormat;
-import java.text.DateFormatSymbols;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-import java.util.Scanner;
-import java.util.Stack;
-import java.util.TimeZone;
-import java.util.logging.Level;
-import java.util.prefs.Preferences;
-import javafx.application.Platform;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableValue;
-import javafx.collections.FXCollections;
-import javafx.collections.ObservableList;
-import javafx.embed.swing.JFXPanel;
-import javafx.event.ActionEvent;
-import javafx.event.EventHandler;
-import javafx.geometry.Pos;
-import javafx.scene.Group;
-import javafx.scene.Scene;
-import javafx.scene.chart.BarChart;
-import javafx.scene.chart.CategoryAxis;
-import javafx.scene.chart.NumberAxis;
-import javafx.scene.control.Button;
-import javafx.scene.control.ComboBox;
-import javafx.scene.control.Label;
-import javafx.scene.control.ScrollPane;
-import javafx.scene.input.MouseButton;
-import javafx.scene.input.MouseEvent;
-import javafx.scene.layout.HBox;
-import javafx.scene.layout.VBox;
-import javafx.scene.paint.Color;
-import javax.swing.JFrame;
-import javax.swing.JOptionPane;
-import javax.swing.SwingUtilities;
-import org.netbeans.api.progress.ProgressHandle;
-import org.netbeans.api.progress.ProgressHandleFactory;
-import org.openide.awt.ActionID;
-import org.openide.awt.ActionReference;
-import org.openide.awt.ActionReferences;
-import org.openide.awt.ActionRegistration;
-import org.openide.modules.InstalledFileLocator;
-import org.openide.nodes.Children;
-import org.openide.nodes.Node;
-import org.openide.util.HelpCtx;
-import org.openide.util.NbBundle;
-import org.openide.util.NbPreferences;
-import org.openide.util.actions.CallableSystemAction;
-import org.openide.util.actions.Presenter;
-import org.openide.util.lookup.Lookups;
-import org.openide.windows.WindowManager;
-import org.sleuthkit.autopsy.casemodule.Case;
-import org.sleuthkit.autopsy.core.Installer;
-import org.sleuthkit.autopsy.corecomponents.DataContentPanel;
-import org.sleuthkit.autopsy.corecomponents.DataResultPanel;
-import org.sleuthkit.autopsy.coreutils.Logger;
-import org.sleuthkit.autopsy.coreutils.PlatformUtil;
-import org.sleuthkit.autopsy.datamodel.FilterNodeLeaf;
-import org.sleuthkit.autopsy.datamodel.DirectoryNode;
-import org.sleuthkit.autopsy.datamodel.DisplayableItemNode;
-import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor;
-import org.sleuthkit.autopsy.datamodel.FileNode;
-import org.sleuthkit.autopsy.ingest.IngestManager;
-import org.sleuthkit.autopsy.coreutils.ExecUtil;
-import org.sleuthkit.autopsy.datamodel.ContentUtils;
-import org.sleuthkit.datamodel.AbstractFile;
-import org.sleuthkit.datamodel.SleuthkitCase;
-import org.sleuthkit.datamodel.TskCoreException;
-
-@ActionID(category = "Tools", id = "org.sleuthkit.autopsy.timeline.Timeline")
-@ActionRegistration(displayName = "#CTL_MakeTimeline", lazy = false)
-@ActionReferences(value = {
-    @ActionReference(path = "Menu/Tools", position = 100)})
-//@NbBundle.Messages(value = "CTL_TimelineView=Generate Timeline")
-/**
- * The Timeline Action entry point. Collects data and pushes data to javafx
- * widgets
- *
- */
-public class Timeline extends CallableSystemAction implements Presenter.Toolbar, PropertyChangeListener {
- 
-    private static final Logger logger = Logger.getLogger(Timeline.class.getName());
-    private final java.io.File macRoot = InstalledFileLocator.getDefault().locate("mactime", Timeline.class.getPackage().getName(), false); //NON-NLS
-    private TimelineFrame mainFrame;          //frame for holding all the elements
-    private Group fxGroupCharts; //Orders the charts
-    private Scene fxSceneCharts; //Displays the charts
-    private HBox fxHBoxCharts;      //Holds the navigation buttons in horiztonal fashion. 
-    private VBox fxVBox;        //Holds the JavaFX Elements in vertical fashion. 
-    private JFXPanel fxPanelCharts;  //FX panel to hold the group
-    private BarChart<String, Number> fxChartEvents;      //Yearly/Monthly events - Bar chart
-    private ScrollPane fxScrollEvents;  //Scroll Panes for dealing with oversized an oversized chart
-    private static final int FRAME_HEIGHT = 700; //Sizing constants
-    private static final int FRAME_WIDTH = 1200;
-    private Button fxZoomOutButton;  //Navigation buttons
-    private ComboBox<String> fxDropdownSelectYears; //Dropdown box for selecting years. Useful when the charts' scale means some years are unclickable, despite having events.
-    private final Stack<BarChart<String, Number>> fxStackPrevCharts = new Stack<BarChart<String, Number>>();  //Stack for storing drill-up information.
-    private BarChart<String, Number> fxChartTopLevel; //the topmost chart, used for resetting to default view.
-    private BarChart<String, Number> fxMonthView; //the month chart
-    private DataResultPanel dataResultPanel;
-    private DataContentPanel dataContentPanel;
-    private ProgressHandle progress;
-    private java.io.File moduleDir;
-    private String mactimeFileName;
-    private List<YearEpoch> data;
-    private boolean listeningToAddImage = false;
-    private long lastObjectId = -1;
-    private TimelineProgressDialog progressDialog;
-    private EventHandler<MouseEvent> fxMouseEnteredListener;
-    private EventHandler<MouseEvent> fxMouseExitedListener;
-    private SleuthkitCase skCase;
-    private boolean fxInited = false;
-    private int monthCounter = 0;
-
-    public Timeline() {
-        super();
-
-        fxInited = Installer.isJavaFxInited();
-       // TimeZone.setDefault(TimeZone.getTimeZone("UTC")); //sets the default timezone to UTC unless otherwise stated
-    }
-
-    //Swing components and JavafX components don't play super well together
-    //Swing components need to be initialized first, in the swing specific thread
-    //Next, the javafx components may be initialized.
-    private void customize() {
-
-        //listeners
-        fxMouseEnteredListener = new EventHandler<MouseEvent>() {
-            @Override
-            public void handle(MouseEvent e) {
-                fxPanelCharts.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
-            }
-        };
-        fxMouseExitedListener = new EventHandler<MouseEvent>() {
-            @Override
-            public void handle(MouseEvent e) {
-                fxPanelCharts.setCursor(null);
-            }
-        };
-
-        SwingUtilities.invokeLater(new Runnable() {
-            @Override
-            public void run() {
-                //Making the main frame *
-
-                mainFrame = new TimelineFrame();
-                mainFrame.setFrameName(
-                        NbBundle.getMessage(this.getClass(), "Timeline.frameName.text", Case.getCurrentCase().getName()));
-
-                //use the same icon on jframe as main application
-                mainFrame.setIconImage(WindowManager.getDefault().getMainWindow().getIconImage());
-                mainFrame.setFrameSize(new Dimension(FRAME_WIDTH, FRAME_HEIGHT)); //(Width, Height)
-
-
-                dataContentPanel = DataContentPanel.createInstance();
-                //dataContentPanel.setAlignmentX(Component.RIGHT_ALIGNMENT);
-                //dataContentPanel.setPreferredSize(new Dimension(FRAME_WIDTH, (int) (FRAME_HEIGHT * 0.4)));
-
-                dataResultPanel = DataResultPanel.createInstance(
-                        NbBundle.getMessage(this.getClass(), "Timeline.resultsPanel.title"),
-                        "", Node.EMPTY, 0, dataContentPanel);
-                dataResultPanel.setContentViewer(dataContentPanel);
-                //dataResultPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
-                //dataResultPanel.setPreferredSize(new Dimension((int)(FRAME_WIDTH * 0.5), (int) (FRAME_HEIGHT * 0.5)));
-                logger.log(Level.INFO, "Successfully created viewers"); //NON-NLS
-
-                mainFrame.setBottomLeftPanel(dataResultPanel);
-                mainFrame.setBottomRightPanel(dataContentPanel);
-
-                runJavaFxThread();
-            }
-        });
-
-
-    }
-
-    private void runJavaFxThread() {
-        //JavaFX thread
-        //JavaFX components MUST be run in the JavaFX thread, otherwise massive amounts of exceptions will be thrown and caught. Liable to freeze up and crash.
-        //Components can be declared whenever, but initialization and manipulation must take place here.
-        Platform.runLater(new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    // start the progress bar
-                    progress = ProgressHandleFactory.createHandle(
-                            NbBundle.getMessage(this.getClass(), "Timeline.runJavaFxThread.progress.creating"));
-                    progress.start();
-
-                    fxChartEvents = null; //important to reset old data
-                    fxPanelCharts = new JFXPanel();
-                    fxGroupCharts = new Group();
-                    fxSceneCharts = new Scene(fxGroupCharts, FRAME_WIDTH, FRAME_HEIGHT * 0.6); //Width, Height
-                    fxVBox = new VBox(5);
-                    fxVBox.setAlignment(Pos.BOTTOM_CENTER);
-                    fxHBoxCharts = new HBox(10);
-                    fxHBoxCharts.setAlignment(Pos.BOTTOM_CENTER);
-
-                    //Initializing default values for the scroll pane
-                    fxScrollEvents = new ScrollPane();
-                    fxScrollEvents.setPrefSize(FRAME_WIDTH, FRAME_HEIGHT * 0.6); //Width, Height
-                    fxScrollEvents.setContent(null); //Needs some content, otherwise it crashes
-
-                    // set up moduleDir
-                    moduleDir = new java.io.File(Case.getCurrentCase().getModulesOutputDirAbsPath() + java.io.File.separator + "timeline");
-                    if (!moduleDir.exists()) {
-                        moduleDir.mkdir();
-                    }
-
-                    int currentProgress = 0;
-                    java.io.File mactimeFile = new java.io.File(moduleDir, mactimeFileName);
-                    if (!mactimeFile.exists()) {
-                        progressDialog.setProgressTotal(3); //total 3 units
-                        logger.log(Level.INFO, "Creating body file"); //NON-NLS
-                        progressDialog.updateProgressBar(
-                                NbBundle.getMessage(this.getClass(), "Timeline.runJavaFxThread.progress.genBodyFile"));
-                        String bodyFilePath = makeBodyFile();
-                        progressDialog.updateProgressBar(++currentProgress);
-                        logger.log(Level.INFO, "Creating mactime file: " + mactimeFile.getAbsolutePath()); //NON-NLS
-                        progressDialog.updateProgressBar(
-                                NbBundle.getMessage(this.getClass(), "Timeline.runJavaFxThread.progress.genMacTime"));
-                        makeMacTime(bodyFilePath);
-                        progressDialog.updateProgressBar(++currentProgress);
-                        data = null;
-                    } else {
-                        progressDialog.setProgressTotal(1); //total 1 units
-                        logger.log(Level.INFO, "Mactime file already exists; parsing that: " + mactimeFile.getAbsolutePath()); //NON-NLS
-                    }
-
-
-                    progressDialog.updateProgressBar(
-                            NbBundle.getMessage(this.getClass(), "Timeline.runJavaFxThread.progress.parseMacTime"));
-                    if (data == null) {
-                        logger.log(Level.INFO, "Parsing mactime file: " + mactimeFile.getAbsolutePath()); //NON-NLS
-                        data = parseMacTime(mactimeFile); //The sum total of the mactime parsing.  YearEpochs contain everything you need to make a timeline.
-                    }
-                    progressDialog.updateProgressBar(++currentProgress);
-
-                    //Making a dropdown box to select years.
-                    List<String> lsi = new ArrayList<String>();  //List is in the format of {Year : Number of Events}, used for selecting from the dropdown.
-                    for (YearEpoch ye : data) {
-                        lsi.add(ye.year + " : " + ye.getNumFiles());
-                    }
-                    ObservableList<String> listSelect = FXCollections.observableArrayList(lsi);
-                    fxDropdownSelectYears = new ComboBox<String>(listSelect);
-
-                    //Buttons for navigating up and down the timeline
-                    fxZoomOutButton = new Button(NbBundle.getMessage(this.getClass(), "Timeline.zoomOutButton.text"));
-                    fxZoomOutButton.setOnAction(new EventHandler<ActionEvent>() {
-                        @Override
-                        public void handle(ActionEvent e) {
-                            BarChart<String, Number> bc;
-                            if (fxStackPrevCharts.size() == 0) {
-                                bc = fxChartTopLevel;
-                            } else {
-                                bc = fxStackPrevCharts.pop();                             
-                            }
-                            fxChartEvents = bc;
-                            fxScrollEvents.setContent(fxChartEvents);
-                        }
-                    });
-
-                    fxDropdownSelectYears.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<String>() {
-                        @Override
-                        public void changed(ObservableValue<? extends String> ov, String t, String t1) {
-                            if (fxDropdownSelectYears.getValue() != null) {
-                                mainFrame.setTopComponentCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
-                                try {
-                                    fxChartEvents = createMonthsWithDrill(findYear(data, Integer.valueOf(fxDropdownSelectYears.getValue().split(" ")[0])));
-                                    fxScrollEvents.setContent(fxChartEvents);
-                                } finally {
-                                    mainFrame.setTopComponentCursor(null);
-                                }
-                            }
-                        }
-                    });
-
-                    //Adding things to the V and H boxes. 
-                    //hBox_Charts stores the pseudo menu bar at the top of the timeline. |Zoom Out|View Year: [Select Year]|►|
-                    fxHBoxCharts.getChildren().addAll(fxZoomOutButton, new Label(
-                            NbBundle.getMessage(this.getClass(), "Timeline.goToButton.text")), fxDropdownSelectYears);
-                    fxVBox.getChildren().addAll(fxHBoxCharts, fxScrollEvents); //FxBox_V holds things in a visual stack. 
-                    fxGroupCharts.getChildren().add(fxVBox); //Adding the FxBox to the group. Groups make things easier to manipulate without having to update a hundred things every change.
-                    fxPanelCharts.setScene(fxSceneCharts);
-
-
-                    fxPanelCharts.setAlignmentX(Component.LEFT_ALIGNMENT);
-
-                    fxChartTopLevel = createYearChartWithDrill(data);
-                    fxChartEvents = fxChartTopLevel;
-                    fxScrollEvents.setContent(fxChartEvents);
-
-                    EventQueue.invokeLater(new Runnable() {
-                        @Override
-                        public void run() {
-                            mainFrame.setTopPanel(fxPanelCharts);
-                            dataResultPanel.open();
-                            //mainFrame.pack();
-                            mainFrame.setVisible(true);
-                        }
-                    });
-                } finally {
-                    // stop the progress bar
-                    progress.finish();
-
-                    // close the progressDialog
-                    progressDialog.doClose(0);
-                }
-            }
-        });
-    }
-
-    /**
-     * Creates a BarChart with datapoints for all the years from the parsed
-     * mactime file.
-     *
-     * @param allYears The list of years that have barData from the mactime file
-     * @return BarChart scaled to the year level
-     */
-    private BarChart<String, Number> createYearChartWithDrill(final List<YearEpoch> allYears) {
-        final CategoryAxis xAxis = new CategoryAxis(); //Axes are very specific types. Categorys are strings.
-        final NumberAxis yAxis = new NumberAxis();
-        final Label l = new Label("");
-        l.setStyle("-fx-font: 24 arial;"); //NON-NLS
-        l.setTextFill(Color.AZURE);
-        xAxis.setLabel(NbBundle.getMessage(this.getClass(), "Timeline.yearBarChart.x.years"));
-        yAxis.setLabel(NbBundle.getMessage(this.getClass(), "Timeline.yearBarChart.y.numEvents"));
-        //Charts are made up of individual pieces of Chart.Data. In this case, a piece of barData is a single bar on the graph.
-        //Data is packaged into a series, which can be assigned custom colors or styling
-        //After the series are created, 1 or more series are packaged into a single chart.
-        ObservableList<BarChart.Series<String, Number>> bcData = FXCollections.observableArrayList();
-        BarChart.Series<String, Number> se = new BarChart.Series<String, Number>();
-        if (allYears != null) {
-            for (final YearEpoch ye : allYears) {
-                se.getData().add(new BarChart.Data<String, Number>(String.valueOf(ye.year), ye.getNumFiles()));
-            }
-        }
-        bcData.add(se);
-
-
-        //Note: 
-        // BarChart.Data wraps the Java Nodes class. BUT, until a BarChart.Data gets added to an actual series, it's node is null, and you can perform no operations on it.
-        // When the Data is added to a series(or a chart? I am unclear on where), a node is automaticaly generated for it, after which you can perform any of the operations it offers. 
-        // In addtion, you are free to set the node to whatever you want. It wraps the most generic Node class.
-        // But it is for this reason that the chart generating functions have two forloops. I do not believe they can be condensed into a single loop due to the nodes being null until 
-        // an undetermined point in time. 
-        BarChart<String, Number> bc = new BarChart<String, Number>(xAxis, yAxis, bcData);
-        for (final BarChart.Data<String, Number> barData : bc.getData().get(0).getData()) { //.get(0) refers to the BarChart.Series class to work on. There is only one series in this graph, so get(0) is safe.
-            barData.getNode().setScaleX(.5);
-
-            final javafx.scene.Node barNode = barData.getNode();
-            //hover listener
-            barNode.addEventHandler(MouseEvent.MOUSE_ENTERED_TARGET, fxMouseEnteredListener);
-            barNode.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, fxMouseExitedListener);
-
-            //click listener
-            barNode.addEventHandler(MouseEvent.MOUSE_CLICKED,
-                    new EventHandler<MouseEvent>() {
-                @Override
-                public void handle(MouseEvent e) {
-                    if (e.getButton().equals(MouseButton.PRIMARY)) {
-                        if (e.getClickCount() == 1) {
-                            Platform.runLater(new Runnable() {
-                                @Override
-                                public void run() {
-                                    BarChart<String, Number> b =
-                                            createMonthsWithDrill(findYear(allYears, Integer.valueOf(barData.getXValue())));
-                                    fxChartEvents = b;
-                                    fxScrollEvents.setContent(fxChartEvents);
-                                }
-                            });
-
-                        }
-                    }
-                }
-            });
-        }
-
-        bc.autosize(); //Get an auto height
-        bc.setPrefWidth(FRAME_WIDTH);  //but override the width
-        bc.setLegendVisible(false); //The legend adds too much extra chart space, it's not necessary.
-        return bc;
-    }
-
-    /*
-     * Displays a chart with events from one year only, separated into 1-month chunks.
-     * Always 12 per year, empty months are represented by no bar.
-     */
-    private BarChart<String, Number> createMonthsWithDrill(final YearEpoch ye) {
-        final CategoryAxis xAxis = new CategoryAxis();
-        final NumberAxis yAxis = new NumberAxis();
-        xAxis.setLabel(NbBundle.getMessage(this.getClass(), "Timeline.MonthsBarChart.x.monthYY", ye.year));
-        yAxis.setLabel(NbBundle.getMessage(this.getClass(), "Timeline.MonthsBarChart.y.numEvents"));
-        ObservableList<BarChart.Series<String, Number>> bcData = FXCollections.observableArrayList();
-
-        BarChart.Series<String, Number> se = new BarChart.Series<String, Number>();
-        for (int monthNum = 0; monthNum < 12; ++monthNum) {
-            String monthName = new DateFormatSymbols().getMonths()[monthNum];
-            MonthEpoch month = ye.getMonth(monthNum);
-            int numEvents = month == null ? 0 : month.getNumFiles();
-            se.getData().add(new BarChart.Data<String, Number>(monthName, numEvents)); //Adding new barData at {X-pos, Y-Pos}
-        }
-        bcData.add(se);
-        final BarChart<String, Number> bc = new BarChart<String, Number>(xAxis, yAxis, bcData);
-
-        for (int i = 0; i < 12; i++) {
-            for (final BarChart.Data<String, Number> barData : bc.getData().get(0).getData()) {
-                //Note: 
-                // All the charts of this package have a problem where when the chart gets below a certain pixel ratio, the barData stops drawing. The axes and the labels remain, 
-                // But the actual chart barData is invisible, unclickable, and unrendered. To partially compensate for that, barData.getNode() can be manually scaled up to increase visibility.
-                // Sometimes I've had it jacked up to as much as x2400 just to see a sliver of information.
-                // But that doesn't work all the time. Adding it to a scrollpane and letting the user scroll up and down to view the chart is the other workaround. Both of these fixes suck.
-                final javafx.scene.Node barNode = barData.getNode();
-                barNode.setScaleX(.5);
-
-                //hover listener
-                barNode.addEventHandler(MouseEvent.MOUSE_ENTERED_TARGET, fxMouseEnteredListener);
-                barNode.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, fxMouseExitedListener);
-
-                //clicks
-                barNode.addEventHandler(MouseEvent.MOUSE_PRESSED,
-                        new EventHandler<MouseEvent>() {
-                    @Override
-                    public void handle(MouseEvent e) {
-                        if (e.getButton().equals(MouseButton.PRIMARY)) {
-                            if (e.getClickCount() == 1) {
-                                Platform.runLater(new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        fxChartEvents = createEventsByMonth(findMonth(ye.months, monthStringToInt(barData.getXValue())), ye);
-                                        fxScrollEvents.setContent(fxChartEvents);
-                                    }
-                                });
-                            }
-                        }
-                    }
-                });
-            }
-        }
-
-        bc.autosize();
-        bc.setPrefWidth(FRAME_WIDTH);
-        bc.setLegendVisible(false);
-        fxMonthView= bc;
-        return bc;
-    }
-
-
-    /*
-     * Displays a chart with events from one month only.
-     * Up to 31 days per month, as low as 28 as determined by the specific MonthEpoch
-     */
-    @SuppressWarnings("unchecked")
-    private BarChart<String, Number> createEventsByMonth(final MonthEpoch me, final YearEpoch ye) {
-        final CategoryAxis xAxis = new CategoryAxis();
-        final NumberAxis yAxis = new NumberAxis();
-        xAxis.setLabel(NbBundle.getMessage(this.getClass(), "Timeline.eventsByMoBarChart.x.dayOfMo"));
-        yAxis.setLabel(NbBundle.getMessage(this.getClass(), "Timeline.eventsByMoBarChart.y.numEvents"));
-        ObservableList<BarChart.Data<String, Number>> bcData = makeObservableListByMonthAllDays(me, ye.getYear());
-        BarChart.Series<String, Number> series = new BarChart.Series<String, Number>(bcData);
-        series.setName(me.getMonthName() + " " + ye.getYear());
-
-
-        ObservableList<BarChart.Series<String, Number>> ol =
-                FXCollections.<BarChart.Series<String, Number>>observableArrayList(series);
-
-        final BarChart<String, Number> bc = new BarChart<String, Number>(xAxis, yAxis, ol);
-        for (final BarChart.Data<String, Number> barData : bc.getData().get(0).getData()) {
-            //data.getNode().setScaleX(2);
-
-            final javafx.scene.Node barNode = barData.getNode();
-
-            //hover listener
-            barNode.addEventHandler(MouseEvent.MOUSE_ENTERED_TARGET, fxMouseEnteredListener);
-            barNode.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, fxMouseExitedListener);
-
-            barNode.addEventHandler(MouseEvent.MOUSE_PRESSED,
-                    new EventHandler<MouseEvent>() {
-                MonthEpoch myme = me;
-
-                @Override
-                public void handle(MouseEvent e) {
-                     SwingUtilities.invokeLater(new Runnable() {
-                        @Override
-                        public void run() {
-                            //reset the view and free the current nodes before loading new ones
-                            final FileRootNode d = new FileRootNode(
-                                    NbBundle.getMessage(this.getClass(), "Timeline.node.emptyRoot"), new ArrayList<Long>());
-                            dataResultPanel.setNode(d);
-                            dataResultPanel.setPath(NbBundle.getMessage(this.getClass(), "Timeline.resultPanel.loading"));
-                        }
-                    });
-                    final int day = (Integer.valueOf((barData.getXValue()).split("-")[1]));
-                    final DayEpoch de = myme.getDay(day);
-                    final List<Long> afs;
-                    if (de != null) {
-                        afs = de.getEvents();
-                    } else {
-                        logger.log(Level.SEVERE, "There were no events for the clicked-on day: " + day); //NON-NLS
-                        return;
-                    }
-
-                    SwingUtilities.invokeLater(new Runnable() {
-                        @Override
-                        public void run() {
-                            final FileRootNode d = new FileRootNode(
-                                    NbBundle.getMessage(this.getClass(), "Timeline.node.root"), afs);
-                            dataResultPanel.setNode(d);
-                            //set result viewer title path with the current date
-                            String dateString = ye.getYear() + "-" + (1 + me.getMonthInt()) + "-" + +de.dayNum;
-                            dataResultPanel.setPath(dateString);
-                        }
-                    });
-
-
-                }
-            });
-        }
-        bc.autosize();
-        bc.setPrefWidth(FRAME_WIDTH); 
-        monthCounter++;
-        if (monthCounter==12)
-        {
-        fxStackPrevCharts.push(fxMonthView);
-        monthCounter=0;
-        }
-        return bc;
-    }
-
-    private static ObservableList<BarChart.Data<String, Number>> makeObservableListByMonthAllDays(final MonthEpoch me, int year) {
-        ObservableList<BarChart.Data<String, Number>> bcData = FXCollections.observableArrayList();
-        int totalDays = me.getTotalNumDays(year);
-        for (int i = 1; i <= totalDays; ++i) {
-            DayEpoch day = me.getDay(i);
-            int numFiles = day == null ? 0 : day.getNumFiles();
-            BarChart.Data<String, Number> d = new BarChart.Data<String, Number>(me.month + 1 + "-" + i, numFiles);
-            d.setExtraValue(me);
-            bcData.add(d);
-        }
-        return bcData;
-    }
-
-    /*
-     * Section for Utility functions
-     */
-    /**
-     *
-     * @param mon The month to convert. Must be minimum 4 characters long
-     * "February" and "Febr" are acceptable.
-     * @return The integer value of the month. February = 1, July = 6
-     */
-    private static int monthStringToInt(String mon) {
-        try {
-            Date date = new SimpleDateFormat("MMMM", Locale.ENGLISH).parse(mon);
-            Calendar cal = Calendar.getInstance();
-            cal.setTime(date);
-            return cal.get(Calendar.MONTH);
-        } catch (ParseException ex) {
-            logger.log(Level.WARNING, "Unable to convert string " + mon + " to integer", ex); //NON-NLS
-            return -1;
-        }
-    }
-
-    /**
-     * Used for finding the proper month in a list of available months
-     *
-     * @param lst The list of months to search through. It is assumed that the
-     * desired match is in this list.
-     * @param match The month, in integer format, to retrieve.
-     * @return The month epoch as specified by match.
-     */
-    private static MonthEpoch findMonth(List<MonthEpoch> lst, int match) {
-        for (MonthEpoch e : lst) {
-            if (e.month == match) {
-                return e;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Used for finding the proper year in a list of available years
-     *
-     * @param lst The list of years to search through. It is assumed that the
-     * desired match is in this list.
-     * @param match The year to retrieve.
-     * @return The year epoch as specified by match.
-     */
-    private static YearEpoch findYear(List<YearEpoch> lst, int match) {
-        for (YearEpoch e : lst) {
-            if (e.year == match) {
-                return e;
-            }
-        }
-        return null;
-    }
-
-    @Override
-    public void propertyChange(PropertyChangeEvent evt) {
-        String prop = evt.getPropertyName();
-        if (prop.equals(Case.Events.DATA_SOURCE_ADDED.toString())) {
-            if (mainFrame != null && !mainFrame.isVisible()) {
-                // change the lastObjectId to trigger a reparse of mactime barData
-                ++lastObjectId;
-                return;
-            }
-
-            int answer = JOptionPane.showConfirmDialog(mainFrame,
-                                                       NbBundle.getMessage(this.getClass(),
-                                                                           "Timeline.propChg.confDlg.timelineOOD.msg"),
-                                                       NbBundle.getMessage(this.getClass(),
-                                                                           "Timeline.propChg.confDlg.timelineOOD.details"),
-                                                       JOptionPane.YES_NO_OPTION);
-            if (answer != JOptionPane.YES_OPTION) {
-                return;
-            }
-
-            clearMactimeData();
-
-            // call performAction as if the user selected 'Make Timeline' from the menu
-            performAction();
-        } else if (prop.equals(Case.Events.CURRENT_CASE.toString())) {
-            if (mainFrame != null && mainFrame.isVisible()) {
-                mainFrame.dispose();
-                mainFrame = null;
-            }
-
-            data = null;
-        }
-    }
-
-    private void clearMactimeData() {
-        // get rid of the old barData
-        data = null;
-
-        // get rid of the mactime file
-        java.io.File mactimeFile = new java.io.File(moduleDir, mactimeFileName);
-        mactimeFile.delete();
-
-        // close the jframe
-        if (mainFrame != null) {
-            mainFrame.setVisible(false);
-            mainFrame.dispose();
-            mainFrame = null;
-        }
-
-        // remove ourself as change listener on Case
-        Case.removePropertyChangeListener(this);
-        listeningToAddImage = false;
-
-    }
-
-    /*
-     * The backbone of the timeline functionality, years are split into months, months into days, and days contain the events of that given day.
-     * All of those are Epochs.
-     */
-    abstract class Epoch {
-
-        abstract public int getNumFiles();
-    }
-
-    private class YearEpoch extends Epoch {
-
-        private int year;
-        private List<MonthEpoch> months = new ArrayList<>();
-
-        YearEpoch(int year) {
-            this.year = year;
-        }
-
-        public int getYear() {
-            return year;
-        }
-
-        @Override
-        public int getNumFiles() {
-            int size = 0;
-            for (MonthEpoch me : months) {
-                size += me.getNumFiles();
-            }
-            return size;
-        }
-
-        public MonthEpoch getMonth(int monthNum) {
-            MonthEpoch month = null;
-            for (MonthEpoch me : months) {
-                if (me.getMonthInt() == monthNum) {
-                    month = me;
-                    break;
-                }
-            }
-            return month;
-        }
-
-        public void add(long fileId, int month, int day) {
-            // see if this month is in the list
-            MonthEpoch monthEpoch = null;
-            for (MonthEpoch me : months) {
-                if (me.getMonthInt() == month) {
-                    monthEpoch = me;
-                    break;
-                }
-            }
-
-            if (monthEpoch == null) {
-                monthEpoch = new MonthEpoch(month);
-                months.add(monthEpoch);
-            }
-
-            // add the file the the MonthEpoch object
-            monthEpoch.add(fileId, day);
-        }
-    }
-
-    private class MonthEpoch extends Epoch {
-
-        private int month; //Zero-indexed: June = 5, August = 7, etc
-        private List<DayEpoch> days = new ArrayList<>(); //List of DayEpochs in this month, max 31
-
-        MonthEpoch(int month) {
-            this.month = month;
-        }
-
-        public int getMonthInt() {
-            return month;
-        }
-
-        public int getTotalNumDays(int year) {
-            Calendar cal = Calendar.getInstance();
-            cal.set(year, month, 1);
-            return cal.getActualMaximum(Calendar.DAY_OF_MONTH);
-        }
-
-        @Override
-        public int getNumFiles() {
-            int numFiles = 0;
-            for (DayEpoch de : days) {
-                numFiles += de.getNumFiles();
-            }
-            return numFiles;
-        }
-
-        public DayEpoch getDay(int dayNum) {
-            DayEpoch de = null;
-            for (DayEpoch d : days) {
-                if (d.dayNum == dayNum) {
-                    de = d;
-                    break;
-                }
-            }
-            return de;
-        }
-
-        public void add(long fileId, int day) {
-            DayEpoch dayEpoch = null;
-            for (DayEpoch de : days) {
-                if (de.getDayInt() == day) {
-                    dayEpoch = de;
-                    break;
-                }
-            }
-
-            if (dayEpoch == null) {
-                dayEpoch = new DayEpoch(day);
-                days.add(dayEpoch);
-            }
-
-            dayEpoch.add(fileId);
-        }
-
-        /**
-         * Returns the month's name in String format, e.g., September, July,
-         */
-        String getMonthName() {
-            return new DateFormatSymbols().getMonths()[month];
-        }
-
-        /**
-         * @return the list of days in this month
-         */
-        List<DayEpoch> getDays() {
-            return this.days;
-        }
-    }
-
-    private class DayEpoch extends Epoch {
-
-        private final List<Long> fileIds = new ArrayList<>();
-        int dayNum = 0; //Day of the month this Epoch represents, 1 indexed: 28=28.
-
-        DayEpoch(int dayOfMonth) {
-            this.dayNum = dayOfMonth;
-        }
-
-        public int getDayInt() {
-            return dayNum;
-        }
-
-        @Override
-        public int getNumFiles() {
-            return fileIds.size();
-        }
-
-        public void add(long fileId) {
-            fileIds.add(fileId);
-        }
-
-        List<Long> getEvents() {
-            return this.fileIds;
-        }
-    }
-
-    // The node factories used to make lists of files to send to the result viewer
-    // using the lazy loading (rather than background) loading option to facilitate
-    // loading a huge number of nodes for the given day
-    private class FileNodeChildFactory extends Children.Keys<Long> {
-
-        private List<Long> fileIds;
-
-        FileNodeChildFactory(List<Long> fileIds) {
-            super(true);
-            this.fileIds = fileIds;
-        }
-
-        @Override
-        protected void addNotify() {
-            super.addNotify();
-            setKeys(fileIds);
-        }
-
-        @Override
-        protected void removeNotify() {
-            super.removeNotify();
-            setKeys(new ArrayList<Long>());
-        }
-
-        @Override
-        protected Node[] createNodes(Long t) {
-            return new Node[]{createNodeForKey(t)};
-        }
-
-        //  @Override
-        //  protected boolean createKeys(List<Long> list) {
-        //     list.addAll(fileIds);
-        //     return true;
-        //  }
-        //@Override
-        protected Node createNodeForKey(Long fileId) {
-            AbstractFile af = null;
-            try {
-                af = skCase.getAbstractFileById(fileId);
-            } catch (TskCoreException ex) {
-                logger.log(Level.SEVERE, "Error getting file by id and creating a node in Timeline: " + fileId, ex); //NON-NLS
-                //no node will be shown for this object
-                return null;
-            }
-
-            Node wrapped;
-            if (af.isDir()) {
-                wrapped = new DirectoryNode(af, false);
-            } else {
-                wrapped = new FileNode(af, false);
-            }
-            return new FilterNodeLeaf(wrapped);
-        }
-    }
-
-    private class FileRootNode extends DisplayableItemNode {
-
-        FileRootNode(String NAME, List<Long> fileIds) {
-            //super(Children.create(new FileNodeChildFactory(fileIds), true));
-            super(new FileNodeChildFactory(fileIds), Lookups.singleton(fileIds));
-            super.setName(NAME);
-            super.setDisplayName(NAME);
-        }
-
-        @Override
-        public boolean isLeafTypeNode() {
-            return false;
-        }
-        
-        @Override
-        public <T> T accept(DisplayableItemNodeVisitor<T> v) {
-            return null;
-        }
-    }
-
-    /**
-     * Parse the output of mactime to break the results in to day-sized chunks (in GMT)
-     * @param f handle to mactime csv output
-     * @return 
-     */
-    private List<YearEpoch> parseMacTime(java.io.File f) {
-        List<YearEpoch> years = new ArrayList<>();
-        Scanner scan;
-        try {
-            scan = new Scanner(new FileInputStream(f));
-        } catch (FileNotFoundException ex) {
-            logger.log(Level.SEVERE, "Error: could not find mactime file.", ex); //NON-NLS
-            return years;
-        }
-        scan.useDelimiter(",");
-        scan.nextLine();   // skip the header line
-
-        int prevYear = -1;
-        YearEpoch ye = null;
-
-        while (scan.hasNextLine()) {
-            String[] s = scan.nextLine().split(","); //1999-02-08T11:08:08Z, 78706, m..b, rrwxrwxrwx, 0, 0, 8355, /img...
-            
-            // break the date into year,month,day,hour,minute, and second: Note that the ISO times are in GMT
-            String delims = "[T:Z\\-]+"; //split by the delimiters NON-NLS
-            String[] date = s[0].split(delims); //{1999,02,08,11,08,08,...}
-   
-            int year = Integer.valueOf(date[0]);
-            int month = Integer.valueOf(date[1]) - 1; //Months are zero indexed: 1 = February, 6 = July, 11 = December
-            int day = Integer.valueOf(date[2]); //Days are 1 indexed
-            int hour=Integer.valueOf(date[3]);
-            int minute=Integer.valueOf(date[4]);
-            int second=Integer.valueOf(date[5]);
-
-            Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")); //set calendar to GMT due to ISO format
-            calendar.set(year, month, day, hour, minute, second); 
-            day=calendar.get(Calendar.DAY_OF_MONTH); // this is needed or else timezone change wont work. probably incorrect optimization by compiler
-           
-            //conversion to GMT
-                
-            if (!ContentUtils.shouldDisplayTimesInLocalTime()) {
-               calendar.setTimeZone(TimeZone.getTimeZone("GMT"));
-            }
-            else{
-                calendar.setTimeZone(TimeZone.getDefault());// local timezone OF the user. should be what the user SETS at startup
-            }
-            
-            day=calendar.get(Calendar.DAY_OF_MONTH);//get the day which may be affected by timezone change
-            long ObjId = Long.valueOf(s[4]);
-
-            // when the year changes, create and add a new YearEpoch object to the list
-            if (year != prevYear) {
-                ye = new YearEpoch(year);
-                years.add(ye);
-                prevYear = year;
-            }
-
-            // save the object id along with the day
-            if (ye != null) {
-                ye.add(ObjId, month, day);
-            }
-        }
-
-        scan.close();
-
-        return years;
-    }
-
-    /**
-     * Crate a body file and return its path or null if error
-     *
-     * @return absolute path string or null if error
-     */
-    private String makeBodyFile() {
-        // Setup timestamp
-        DateFormat dateFormat = new SimpleDateFormat("MM-dd-yyyy-HH-mm-ss");
-        Date date = new Date();
-        String datenotime = dateFormat.format(date);
-
-        final Case currentCase = Case.getCurrentCase();
-
-        // Get report path
-        String bodyFilePath = moduleDir.getAbsolutePath()
-                + java.io.File.separator + currentCase.getName() + "-" + datenotime + ".txt"; //NON-NLS
-        // Run query to get all files
-        final String filesAndDirs = "name != '.' " //NON-NLS
-                + "AND name != '..'"; //NON-NLS
-        List<Long> fileIds = null;
-        try {
-            fileIds = skCase.findAllFileIdsWhere(filesAndDirs);
-        } catch (TskCoreException ex) {
-            logger.log(Level.SEVERE, "Error querying image files to make a body file: " + bodyFilePath, ex); //NON-NLS
-            return null;
-        }
-
-        // Loop files and write info to report
-        FileWriter fileWriter = null;
-        try {
-            fileWriter = new FileWriter(bodyFilePath, true);
-        } catch (IOException ex) {
-            logger.log(Level.SEVERE, "Error creating output stream to write body file to: " + bodyFilePath, ex); //NON-NLS
-            return null;
-        }
-
-        BufferedWriter out = null;
-        try {
-            out = new BufferedWriter(fileWriter);
-            for (long fileId : fileIds) {
-                AbstractFile file = skCase.getAbstractFileById(fileId);
-                // try {
-                // MD5|name|inode|mode_as_string|ObjId|GID|size|atime|mtime|ctime|crtime
-                if (file.getMd5Hash() != null) {
-                    out.write(file.getMd5Hash());
-                }
-                out.write("|");
-                String path = null;
-                try {
-                    path = file.getUniquePath();
-                } catch (TskCoreException e) {
-                    logger.log(Level.SEVERE, "Failed to get the unique path of: " + file + " and writing body file.", e); //NON-NLS
-                    return null;
-                }
-
-                out.write(path);
-
-                out.write("|");
-                out.write(Long.toString(file.getMetaAddr()));
-                out.write("|");
-                String modeString = file.getModesAsString();
-                if (modeString != null) {
-                    out.write(modeString);
-                }
-                out.write("|");
-                out.write(Long.toString(file.getId()));
-                out.write("|");
-                out.write(Long.toString(file.getGid()));
-                out.write("|");
-                out.write(Long.toString(file.getSize()));
-                out.write("|");
-                out.write(Long.toString(file.getAtime()));
-                out.write("|");
-                out.write(Long.toString(file.getMtime()));
-                out.write("|");
-                out.write(Long.toString(file.getCtime()));
-                out.write("|");
-                out.write(Long.toString(file.getCrtime()));
-                out.write("\n");
-            }
-        } catch (TskCoreException ex) {
-            logger.log(Level.SEVERE, "Error querying file by id", ex); //NON-NLS
-            return null;
-
-        } catch (IOException ex) {
-            logger.log(Level.WARNING, "Error while trying to write data to the body file.", ex); //NON-NLS
-            return null;
-        } finally {
-            if (out != null) {
-                try {
-                    out.flush();
-                    out.close();
-                } catch (IOException ex1) {
-                    logger.log(Level.WARNING, "Could not flush and/or close body file.", ex1); //NON-NLS
-                }
-            }
-        }
-
-
-        return bodyFilePath;
-    }
-
-    /**
-     * Run mactime on the given body file.  Generates CSV file with ISO dates (in GMT) 
-     * @param pathToBodyFile
-     * @return Path to output file. 
-     */
-    private String makeMacTime(String pathToBodyFile) {
-        String cmdpath = "";
-        String macpath = "";
-        String[] mactimeArgs;
-        final String machome = macRoot.getAbsolutePath();
-        pathToBodyFile = PlatformUtil.getOSFilePath(pathToBodyFile);
-        if (PlatformUtil.isWindowsOS()) {
-            macpath = machome + java.io.File.separator + "mactime.exe"; //NON-NLS
-            cmdpath = PlatformUtil.getOSFilePath(macpath);
-            mactimeArgs = new String[]{"-b", pathToBodyFile, "-d", "-y"}; //NON-NLS
-        } else {
-            cmdpath = "perl"; //NON-NLS
-            macpath = machome + java.io.File.separator + "mactime.pl"; //NON-NLS
-            mactimeArgs = new String[]{macpath, "-b", pathToBodyFile, "-d", "-y"}; //NON-NLS
-        }
-
-        String macfile = moduleDir.getAbsolutePath() + java.io.File.separator + mactimeFileName;
-        
-
-        String output = "";
-        ExecUtil execUtil = new ExecUtil();
-        Writer writer = null;
-        try {
-            //JavaSystemCaller.Exec.execute("\"" + command + "\"");
-            writer = new FileWriter(macfile);
-            execUtil.execute(writer, cmdpath, mactimeArgs);
-        } catch (InterruptedException ie) {
-            logger.log(Level.WARNING, "Mactime process was interrupted by user", ie); //NON-NLS
-            return null;
-        } catch (IOException ioe) {
-            logger.log(Level.SEVERE, "Could not create mactime file, encountered error ", ioe); //NON-NLS
-            return null;
-        } finally {
-            if (writer != null) {
-                try {
-                    writer.close();
-                } catch (IOException ex) {
-                    logger.log(Level.SEVERE, "Could not clsoe writer after creating mactime file, encountered error ", ex); //NON-NLS
-                }
-            }
-            execUtil.stop();
-        }
-
-        return macfile;
-    }
-
-    @Override
-    public boolean isEnabled() {
-        return Case.isCaseOpen() && this.fxInited;
-    }
-
-    @Override
-    public void performAction() {
-        initTimeline();
-    }
-    
-    private void initTimeline() {
-        if (!Case.existsCurrentCase()) {
-            return;
-        }
-
-        final Case currentCase = Case.getCurrentCase();
-        skCase = currentCase.getSleuthkitCase();
-
-        try {
-            if (currentCase.hasData() == false) {
-                logger.log(Level.INFO, "Error creating timeline, there are no data sources. "); //NON-NLS
-            } else {
-
-                if (IngestManager.getInstance().isIngestRunning()) {
-                    int answer = JOptionPane.showConfirmDialog(new JFrame(),
-                                                               NbBundle.getMessage(this.getClass(),
-                                                                                   "Timeline.initTimeline.confDlg.genBeforeIngest.msg"),
-                                                               NbBundle.getMessage(this.getClass(),
-                                                                                   "Timeline.initTimeline.confDlg.genBeforeIngest.deails"),
-                                                               JOptionPane.YES_NO_OPTION);
-                    if (answer != JOptionPane.YES_OPTION) {
-                        return;
-                    }
-                }
-
-                logger.log(Level.INFO, "Beginning generation of timeline"); //NON-NLS
-
-                // if the timeline window is already open, bring to front and do nothing
-                if (mainFrame != null && mainFrame.isVisible()) {
-                    mainFrame.toFront();
-                    return;
-                }
-
-                // listen for case changes (specifically images being added).
-                if (Case.isCaseOpen() && !listeningToAddImage) {
-                    Case.addPropertyChangeListener(this);
-                    listeningToAddImage = true;
-                }
-
-                // create the modal progressDialog
-                SwingUtilities.invokeLater(new Runnable() {
-                    @Override
-                    public void run() {
-                        progressDialog = new TimelineProgressDialog(WindowManager.getDefault().getMainWindow(), true);
-                        progressDialog.setVisible(true);
-                    }
-                });
-
-                // initialize mactimeFileName
-                mactimeFileName = currentCase.getName() + "-MACTIME.txt"; //NON-NLS
-
-                // see if barData has been added to the database since the last
-                // time timeline ran
-                long objId = skCase.getLastObjectId();
-                if (objId != lastObjectId && lastObjectId != -1) {
-                    clearMactimeData();
-                }
-                lastObjectId = objId;
-
-                customize();
-            }
-        } catch (TskCoreException ex) {
-            logger.log(Level.SEVERE, "Error when generating timeline, ", ex); //NON-NLS
-        } catch (Exception ex) {
-            logger.log(Level.SEVERE, "Unexpected error when generating timeline, ", ex); //NON-NLS
-        }
-    }
-
-    @Override
-    public String getName() {
-        return NbBundle.getMessage(this.getClass(), "Timeline.getName");
-    }
-
-    @Override
-    public HelpCtx getHelpCtx() {
-        return HelpCtx.DEFAULT_HELP;
-    }
-
-    @Override
-    public boolean asynchronous() {
-        return false;
-    }
-}
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimelineFrame.java b/Core/src/org/sleuthkit/autopsy/timeline/TimelineFrame.java
deleted file mode 100644
index 76256770d498a12a59e6220744337e3b3e50ef78..0000000000000000000000000000000000000000
--- a/Core/src/org/sleuthkit/autopsy/timeline/TimelineFrame.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Autopsy Forensic Browser
- *
- * Copyright 2013 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;
-
-import java.awt.BorderLayout;
-import java.awt.Component;
-import java.awt.Cursor;
-import java.awt.Dimension;
-import java.awt.Image;
-
-/**
- *
- * Ready timeline frame with layout to hold dynamic components
- */
- class TimelineFrame extends javax.swing.JFrame {
-
-    /**
-     * Creates new form TimelineFrame
-     */
-    public TimelineFrame() {
-        initComponents();
-    }
-
-    /**
-     * This method is called from within the constructor to initialize the form.
-     * WARNING: Do NOT modify this code. The content of this method is always
-     * regenerated by the Form Editor.
-     */
-    @SuppressWarnings("unchecked")
-    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
-    private void initComponents() {
-
-        splitYPane = new javax.swing.JSplitPane();
-        topPane = new javax.swing.JPanel();
-        splitXPane = new javax.swing.JSplitPane();
-        leftPane = new javax.swing.JPanel();
-        rightPane = new javax.swing.JPanel();
-
-        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
-        setTitle(org.openide.util.NbBundle.getMessage(TimelineFrame.class, "TimelineFrame.title")); // NOI18N
-        setPreferredSize(new java.awt.Dimension(1200, 700));
-
-        splitYPane.setDividerLocation(420);
-        splitYPane.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT);
-        splitYPane.setResizeWeight(0.5);
-        splitYPane.setPreferredSize(new java.awt.Dimension(1024, 400));
-
-        topPane.setPreferredSize(new java.awt.Dimension(1200, 400));
-        topPane.setLayout(new java.awt.BorderLayout());
-        splitYPane.setLeftComponent(topPane);
-
-        splitXPane.setDividerLocation(600);
-        splitXPane.setResizeWeight(0.5);
-        splitXPane.setPreferredSize(new java.awt.Dimension(1200, 300));
-        splitXPane.setRequestFocusEnabled(false);
-
-        leftPane.setPreferredSize(new java.awt.Dimension(700, 300));
-        leftPane.setLayout(new java.awt.BorderLayout());
-        splitXPane.setLeftComponent(leftPane);
-
-        rightPane.setPreferredSize(new java.awt.Dimension(500, 300));
-        rightPane.setLayout(new java.awt.BorderLayout());
-        splitXPane.setRightComponent(rightPane);
-
-        splitYPane.setRightComponent(splitXPane);
-
-        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
-        getContentPane().setLayout(layout);
-        layout.setHorizontalGroup(
-            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
-            .addComponent(splitYPane, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
-        );
-        layout.setVerticalGroup(
-            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
-            .addComponent(splitYPane, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
-        );
-
-        pack();
-    }// </editor-fold>//GEN-END:initComponents
-
-    void setTopPanel(Component component) {
-        this.topPane.add(component, BorderLayout.CENTER);
-    }
-    
-    void setBottomLeftPanel(Component component) {
-        this.leftPane.add(component, BorderLayout.CENTER);
-    }
-    
-    
-    void setBottomRightPanel(Component component) {
-        this.rightPane.add(component, BorderLayout.CENTER);
-    }
-    
-    void setFrameName(String name) {
-        this.setTitle(name);
-    }
-    
-    void setFrameIcon(Image iconImage) {
-        this.setIconImage(iconImage);
-    }
-    
-    void setFrameSize(Dimension size) {
-        this.setSize(size);
-    }
-    
-    void setTopComponentCursor(Cursor cursor) {
-        this.topPane.setCursor(cursor);
-    }
-    
-    
-    /**
-     * @param args the command line arguments
-     */
-    public static void main(String args[]) {
-        /* Set the Nimbus look and feel */
-        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
-        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
-         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
-         */
-        try {
-            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
-                if ("Nimbus".equals(info.getName())) { //NON-NLS
-                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
-                    break;
-                }
-            }
-        } catch (ClassNotFoundException ex) {
-            java.util.logging.Logger.getLogger(TimelineFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
-        } catch (InstantiationException ex) {
-            java.util.logging.Logger.getLogger(TimelineFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
-        } catch (IllegalAccessException ex) {
-            java.util.logging.Logger.getLogger(TimelineFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
-        } catch (javax.swing.UnsupportedLookAndFeelException ex) {
-            java.util.logging.Logger.getLogger(TimelineFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
-        }
-        //</editor-fold>
-
-        /* Create and display the form */
-        java.awt.EventQueue.invokeLater(new Runnable() {
-            public void run() {
-                new TimelineFrame().setVisible(true);
-            }
-        });
-    }
-    // Variables declaration - do not modify//GEN-BEGIN:variables
-    private javax.swing.JPanel leftPane;
-    private javax.swing.JPanel rightPane;
-    private javax.swing.JSplitPane splitXPane;
-    private javax.swing.JSplitPane splitYPane;
-    private javax.swing.JPanel topPane;
-    // End of variables declaration//GEN-END:variables
-}
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimelineProgressDialog.java b/Core/src/org/sleuthkit/autopsy/timeline/TimelineProgressDialog.java
deleted file mode 100644
index 3b4844d415c54872c5712b260bf1491607847df8..0000000000000000000000000000000000000000
--- a/Core/src/org/sleuthkit/autopsy/timeline/TimelineProgressDialog.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Autopsy Forensic Browser
- *
- * Copyright 2013 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;
-
-import java.awt.EventQueue;
-import java.awt.event.ActionEvent;
-import java.awt.event.KeyEvent;
-import javax.swing.AbstractAction;
-import javax.swing.ActionMap;
-import javax.swing.InputMap;
-import javax.swing.JComponent;
-import javax.swing.KeyStroke;
-
-import org.openide.util.NbBundle;
-import org.openide.windows.WindowManager;
-
-/**
- * Dialog with progress bar that pops up when timeline is being generated
- */
- class TimelineProgressDialog extends javax.swing.JDialog {
-
-    /**
-     * A return status code - returned if Cancel button has been pressed
-     */
-    public static final int RET_CANCEL = 0;
-    /**
-     * A return status code - returned if OK button has been pressed
-     */
-    public static final int RET_OK = 1;
-
-    /**
-     * Creates new form TimelineProgressDialog
-     */
-    public TimelineProgressDialog(java.awt.Frame parent, boolean modal) {
-        super(parent, modal);
-        initComponents();
-
-        setLocationRelativeTo(null);
-        //set icon the same as main app
-        setIconImage(WindowManager.getDefault().getMainWindow().getIconImage());
-
-        //progressBar.setIndeterminate(true);
-
-        setName(NbBundle.getMessage(this.getClass(), "TimelineProgressDialog.setName.text"));
-
-        // Close the dialog when Esc is pressed
-        String cancelName = "cancel"; //NON-NLS
-        InputMap inputMap = getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
-        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), cancelName);
-        ActionMap actionMap = getRootPane().getActionMap();
-        actionMap.put(cancelName, new AbstractAction() {
-            public void actionPerformed(ActionEvent e) {
-                doClose(RET_CANCEL);
-            }
-        });
-    }
-
-    /**
-     * @return the return status of this dialog - one of RET_OK or RET_CANCEL
-     */
-    public int getReturnStatus() {
-        return returnStatus;
-    }
-
-    void updateProgressBar(final int progress) {
-        EventQueue.invokeLater(new Runnable() {
-            @Override
-            public void run() {
-                progressBar.setValue(progress);
-            }
-        });
-
-    }
-
-    void updateProgressBar(final String message) {
-        EventQueue.invokeLater(new Runnable() {
-            @Override
-            public void run() {
-                progressBar.setString(message);
-            }
-        });
-
-    }
-
-    void setProgressTotal(final int total) {
-        EventQueue.invokeLater(new Runnable() {
-            @Override
-            public void run() {
-                //progressBar.setIndeterminate(false);
-                progressBar.setMaximum(total);
-                //progressBar.setValue(0);
-                progressBar.setStringPainted(true);
-                progressBar.setVisible(true);
-            }
-        });
-
-    }
-
-    /**
-     * This method is called from within the constructor to initialize the form.
-     * WARNING: Do NOT modify this code. The content of this method is always
-     * regenerated by the Form Editor.
-     */
-    @SuppressWarnings("unchecked")
-    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
-    private void initComponents() {
-
-        jLabel1 = new javax.swing.JLabel();
-        progressBar = new javax.swing.JProgressBar();
-
-        addWindowListener(new java.awt.event.WindowAdapter() {
-            public void windowClosing(java.awt.event.WindowEvent evt) {
-                closeDialog(evt);
-            }
-        });
-
-        org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(TimelineProgressDialog.class, "TimelineProgressDialog.jLabel1.text")); // NOI18N
-
-        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
-        getContentPane().setLayout(layout);
-        layout.setHorizontalGroup(
-            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
-            .addGroup(layout.createSequentialGroup()
-                .addContainerGap()
-                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
-                    .addComponent(progressBar, javax.swing.GroupLayout.DEFAULT_SIZE, 504, Short.MAX_VALUE)
-                    .addGroup(layout.createSequentialGroup()
-                        .addComponent(jLabel1)
-                        .addGap(0, 0, Short.MAX_VALUE)))
-                .addContainerGap())
-        );
-        layout.setVerticalGroup(
-            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
-            .addGroup(layout.createSequentialGroup()
-                .addContainerGap()
-                .addComponent(jLabel1)
-                .addGap(7, 7, 7)
-                .addComponent(progressBar, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
-                .addContainerGap(16, Short.MAX_VALUE))
-        );
-
-        pack();
-    }// </editor-fold>//GEN-END:initComponents
-
-    /**
-     * Closes the dialog
-     */
-    private void closeDialog(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_closeDialog
-        doClose(RET_CANCEL);
-    }//GEN-LAST:event_closeDialog
-
-    void doClose(final int retStatus) {
-        EventQueue.invokeLater(new Runnable() {
-            @Override
-            public void run() {
-                returnStatus = retStatus;
-                setVisible(false);
-                dispose();
-            }
-        });
-
-    }
-    // Variables declaration - do not modify//GEN-BEGIN:variables
-    private javax.swing.JLabel jLabel1;
-    private javax.swing.JProgressBar progressBar;
-    // End of variables declaration//GEN-END:variables
-    private int returnStatus = RET_CANCEL;
-}
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/layer.xml b/Core/src/org/sleuthkit/autopsy/timeline/layer.xml
deleted file mode 100644
index 1a06f5740a7e08b991ea4abc0b91a64af5ec7319..0000000000000000000000000000000000000000
--- a/Core/src/org/sleuthkit/autopsy/timeline/layer.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.2//EN" "http://www.netbeans.org/dtds/filesystem-1_2.dtd">
-<filesystem>
-
- 
-    <!-- ======================================================
-    Windows2
-    ====================================================== -->
-    <folder name="Actions">
-        <folder name="Tools">
-            <file name="org-sleuthkit-autopsy-timeline-Simile2.instance_hidden"/>
-            <file name="org-sleuthkit-autopsy-timeline-Timeline.instance"/>
-        </folder>
-        <folder name="Window">
-            <file name="org-sleuthkit-autopsy-timeline-Timeline2TopComponent.instance_hidden"/>
-            <file name="org-sleuthkit-autopsy-timeline-TimelineTopComponent.instance_hidden"/>
-        </folder>
-    </folder>
-    <folder name="Windows2">
-        <folder name="Components">
-          
-        </folder>
-        <folder name="Modes">
-            <file name="timeline.wsmode" url="timelineWsmode.xml"/>
-        </folder>
-    </folder>
-    
-</filesystem>
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/timelineWsmode.xml b/Core/src/org/sleuthkit/autopsy/timeline/timelineWsmode.xml
deleted file mode 100644
index cde9642f4a3f62393862eda5fc569fa88eb0ae0f..0000000000000000000000000000000000000000
--- a/Core/src/org/sleuthkit/autopsy/timeline/timelineWsmode.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE mode PUBLIC
-          "-//NetBeans//DTD Mode Properties 2.0//EN"
-          "http://www.netbeans.org/dtds/mode-properties2_0.dtd">
-
-<mode version="2.3">
-    <name unique="timeline" />
-    <!--<kind type="view" /> --> <!-- modal -->
-    <kind type="editor" />  <!-- non-modal -->
-  <state type="separated" />
-   <constraints>
-    <path orientation="horizontal" number="100" weight="0.5"/>
-  </constraints>
-  <bounds x="7" y="909" width="400" height="250" />
-  <frame state="0"/>
-    <empty-behavior permanent="true"/>
-</mode>
\ No newline at end of file
diff --git a/Timeline/build.xml b/Timeline/build.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b99c79655da4a275300f09e759b499e420aa6cd4
--- /dev/null
+++ b/Timeline/build.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- You may freely edit this file. See harness/README in the NetBeans platform -->
+<!-- for some information on what you could do (e.g. targets to override). -->
+<!-- If you delete this file and reopen the project it will be recreated. -->
+<project name="org.sleuthkit.autopsy.timeline" default="netbeans" basedir=".">
+    <description>Builds, tests, and runs the project org.sleuthkit.autopsy.timeline.</description>
+    <import file="nbproject/build-impl.xml"/>
+   
+</project>
diff --git a/Timeline/manifest.mf b/Timeline/manifest.mf
new file mode 100644
index 0000000000000000000000000000000000000000..89925c271ec5176a7bda9cd4a0c0438a7eccc317
--- /dev/null
+++ b/Timeline/manifest.mf
@@ -0,0 +1,6 @@
+Manifest-Version: 1.0
+OpenIDE-Module: org.sleuthkit.autopsy.timeline
+OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/timeline/Bundle.properties
+OpenIDE-Module-Requires: org.openide.windows.WindowManager
+OpenIDE-Module-Specification-Version: 1.0
+
diff --git a/Timeline/nbproject/build-impl.xml b/Timeline/nbproject/build-impl.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4babba24f71fcaac06c8eb32def4c87a1ebe5956
--- /dev/null
+++ b/Timeline/nbproject/build-impl.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+*** GENERATED FROM project.xml - DO NOT EDIT  ***
+***         EDIT ../build.xml INSTEAD         ***
+-->
+<project name="org.sleuthkit.autopsy.timeline-impl" basedir="..">
+    <fail message="Please build using Ant 1.7.1 or higher.">
+        <condition>
+            <not>
+                <antversion atleast="1.7.1"/>
+            </not>
+        </condition>
+    </fail>
+    <property file="nbproject/private/suite-private.properties"/>
+    <property file="nbproject/suite.properties"/>
+    <fail unless="suite.dir">You must set 'suite.dir' to point to your containing module suite</fail>
+    <property file="${suite.dir}/nbproject/private/platform-private.properties"/>
+    <property file="${suite.dir}/nbproject/platform.properties"/>
+    <macrodef name="property" uri="http://www.netbeans.org/ns/nb-module-project/2">
+        <attribute name="name"/>
+        <attribute name="value"/>
+        <sequential>
+            <property name="@{name}" value="${@{value}}"/>
+        </sequential>
+    </macrodef>
+    <macrodef name="evalprops" uri="http://www.netbeans.org/ns/nb-module-project/2">
+        <attribute name="property"/>
+        <attribute name="value"/>
+        <sequential>
+            <property name="@{property}" value="@{value}"/>
+        </sequential>
+    </macrodef>
+    <property file="${user.properties.file}"/>
+    <nbmproject2:property name="harness.dir" value="nbplatform.${nbplatform.active}.harness.dir" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/>
+    <nbmproject2:property name="nbplatform.active.dir" value="nbplatform.${nbplatform.active}.netbeans.dest.dir" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/>
+    <nbmproject2:evalprops property="cluster.path.evaluated" value="${cluster.path}" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/>
+    <fail message="Path to 'platform' cluster missing in $${cluster.path} property or using corrupt Netbeans Platform (missing harness).">
+        <condition>
+            <not>
+                <contains string="${cluster.path.evaluated}" substring="platform"/>
+            </not>
+        </condition>
+    </fail>
+    <import file="${harness.dir}/build.xml"/>
+</project>
diff --git a/Timeline/nbproject/platform.properties b/Timeline/nbproject/platform.properties
new file mode 100644
index 0000000000000000000000000000000000000000..867137ed43359526671e623e503fb0e1eb2a8b73
--- /dev/null
+++ b/Timeline/nbproject/platform.properties
@@ -0,0 +1,7 @@
+cluster.path=\
+    ${nbplatform.active.dir}/autopsy:\
+    ${nbplatform.active.dir}/harness:\
+    ${nbplatform.active.dir}/java:\
+    ${nbplatform.active.dir}/platform
+nbjdk.active=default
+nbplatform.active=Autopsy_Dev
diff --git a/Timeline/nbproject/project.properties b/Timeline/nbproject/project.properties
new file mode 100644
index 0000000000000000000000000000000000000000..949be310a18406a37f930b6ab662468b441f8cba
--- /dev/null
+++ b/Timeline/nbproject/project.properties
@@ -0,0 +1,11 @@
+file.reference.controlsfx-8.0.6_20.jar=release/modules/ext/controlsfx-8.0.6_20.jar
+file.reference.jfxtras-common-8.0-r2-20140616.080113-115.jar=release\\modules\\ext\\jfxtras-common-8.0-r2-20140616.080113-115.jar
+file.reference.jfxtras-controls-8.0-r2-20140616.080125-115.jar=release\\modules\\ext\\jfxtras-controls-8.0-r2-20140616.080125-115.jar
+file.reference.jfxtras-fxml-8.0-r2-20140616.080139-115.jar=release\\modules\\ext\\jfxtras-fxml-8.0-r2-20140616.080139-115.jar
+file.reference.joda-time-2.3.jar=release/modules/ext/joda-time-2.3.jar
+file.reference.sqlite-jdbc-3.7.8-SNAPSHOT.jar=release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar
+javac.source=1.8
+javac.compilerargs=-Xlint -Xlint:-serial 
+license.file=LICENSE-2.0.txt
+project.license=advanced_timeline
+
diff --git a/Timeline/nbproject/project.xml b/Timeline/nbproject/project.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e7df2f5673d64306022504c0c4f94920e215d242
--- /dev/null
+++ b/Timeline/nbproject/project.xml
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://www.netbeans.org/ns/project/1">
+    <type>org.netbeans.modules.apisupport.project</type>
+    <configuration>
+        <data xmlns="http://www.netbeans.org/ns/nb-module-project/3">
+            <code-name-base>org.sleuthkit.autopsy.timeline</code-name-base>
+            <suite-component/>
+            <module-dependencies>
+                <dependency>
+                    <code-name-base>org.netbeans.api.progress</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.32.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.modules.settings</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.38.2</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.awt</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.55.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.explorer</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>6.50.3</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.filesystems</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>8.5.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.modules</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.35.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.nodes</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.33.2</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.util</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>8.29.3</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.util.lookup</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>8.19.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.windows</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>6.60.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.sleuthkit.autopsy.core</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>10</release-version>
+                        <specification-version>10.0.11</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.sleuthkit.autopsy.corelibs</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>3</release-version>
+                        <specification-version>1.1</specification-version>
+                    </run-dependency>
+                </dependency>
+            </module-dependencies>
+            <public-packages/>
+            <class-path-extension>
+                <runtime-relative-path>ext/joda-time-2.3.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/joda-time-2.3.jar</binary-origin>
+            </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/controlsfx-8.0.6_20.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/controlsfx-8.0.6_20.jar</binary-origin>
+            </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/jfxtras-common-8.0-r2-20140616.080113-115.jar</runtime-relative-path>
+                <binary-origin>release\modules\ext\jfxtras-common-8.0-r2-20140616.080113-115.jar</binary-origin>
+            </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/jfxtras-controls-8.0-r2-20140616.080125-115.jar</runtime-relative-path>
+                <binary-origin>release\modules\ext\jfxtras-controls-8.0-r2-20140616.080125-115.jar</binary-origin>
+            </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar</binary-origin>
+            </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/jfxtras-fxml-8.0-r2-20140616.080139-115.jar</runtime-relative-path>
+                <binary-origin>release\modules\ext\jfxtras-fxml-8.0-r2-20140616.080139-115.jar</binary-origin>
+            </class-path-extension>
+        </data>
+    </configuration>
+</project>
diff --git a/Timeline/nbproject/suite.properties b/Timeline/nbproject/suite.properties
new file mode 100644
index 0000000000000000000000000000000000000000..29d7cc9bd6fdd81453543cdf1bcf1dab301e3a92
--- /dev/null
+++ b/Timeline/nbproject/suite.properties
@@ -0,0 +1 @@
+suite.dir=${basedir}/..
diff --git a/Timeline/release/modules/ext/controlsfx-8.0.6.jar b/Timeline/release/modules/ext/controlsfx-8.0.6.jar
new file mode 100644
index 0000000000000000000000000000000000000000..2f2d1badd0facb374d5c2519ce3525ff8693c222
Binary files /dev/null and b/Timeline/release/modules/ext/controlsfx-8.0.6.jar differ
diff --git a/Timeline/release/modules/ext/controlsfx-8.0.6_20.jar b/Timeline/release/modules/ext/controlsfx-8.0.6_20.jar
new file mode 100644
index 0000000000000000000000000000000000000000..39a73c0b9acb47fb4ab23fc5922313885d0e8e91
Binary files /dev/null and b/Timeline/release/modules/ext/controlsfx-8.0.6_20.jar differ
diff --git a/Timeline/release/modules/ext/jfxtras-all-8.0-r2-20140616.080024-115.jar b/Timeline/release/modules/ext/jfxtras-all-8.0-r2-20140616.080024-115.jar
new file mode 100644
index 0000000000000000000000000000000000000000..05825d15b8b7b69fd07c2b475127f86e67861b05
Binary files /dev/null and b/Timeline/release/modules/ext/jfxtras-all-8.0-r2-20140616.080024-115.jar differ
diff --git a/Timeline/release/modules/ext/jfxtras-common-8.0-r2-20140616.080113-115.jar b/Timeline/release/modules/ext/jfxtras-common-8.0-r2-20140616.080113-115.jar
new file mode 100644
index 0000000000000000000000000000000000000000..5330e96922ef275d4e2d7288c546671b0a721f3b
Binary files /dev/null and b/Timeline/release/modules/ext/jfxtras-common-8.0-r2-20140616.080113-115.jar differ
diff --git a/Timeline/release/modules/ext/jfxtras-controls-8.0-r2-20140616.080125-115.jar b/Timeline/release/modules/ext/jfxtras-controls-8.0-r2-20140616.080125-115.jar
new file mode 100644
index 0000000000000000000000000000000000000000..50e52486fcd051e73a92a5881b586326cb76f668
Binary files /dev/null and b/Timeline/release/modules/ext/jfxtras-controls-8.0-r2-20140616.080125-115.jar differ
diff --git a/Timeline/release/modules/ext/jfxtras-fxml-8.0-r2-20140616.080139-115.jar b/Timeline/release/modules/ext/jfxtras-fxml-8.0-r2-20140616.080139-115.jar
new file mode 100644
index 0000000000000000000000000000000000000000..8925347141da65a7032584adde0e9ba44289b175
Binary files /dev/null and b/Timeline/release/modules/ext/jfxtras-fxml-8.0-r2-20140616.080139-115.jar differ
diff --git a/Timeline/release/modules/ext/joda-time-2.3.jar b/Timeline/release/modules/ext/joda-time-2.3.jar
new file mode 100644
index 0000000000000000000000000000000000000000..9dce4f92a61489984d74ab393daa111a24f47535
Binary files /dev/null and b/Timeline/release/modules/ext/joda-time-2.3.jar differ
diff --git a/Timeline/release/modules/ext/joda-time-2.3/LICENSE.txt b/Timeline/release/modules/ext/joda-time-2.3/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d645695673349e3947e8e5ae42332d0ac3164cd7
--- /dev/null
+++ b/Timeline/release/modules/ext/joda-time-2.3/LICENSE.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/Timeline/release/modules/ext/joda-time-2.3/NOTICE.txt b/Timeline/release/modules/ext/joda-time-2.3/NOTICE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..dffbcf31cacf6e01cd738cfd6a6ffea1905bace2
--- /dev/null
+++ b/Timeline/release/modules/ext/joda-time-2.3/NOTICE.txt
@@ -0,0 +1,5 @@
+=============================================================================
+= NOTICE file corresponding to section 4d of the Apache License Version 2.0 =
+=============================================================================
+This product includes software developed by
+Joda.org (http://www.joda.org/).
diff --git a/Timeline/release/modules/ext/joda-time-2.3/RELEASE-NOTES.txt b/Timeline/release/modules/ext/joda-time-2.3/RELEASE-NOTES.txt
new file mode 100644
index 0000000000000000000000000000000000000000..fc2249f11aa6161fb601082e8f5556bc2f28acdf
--- /dev/null
+++ b/Timeline/release/modules/ext/joda-time-2.3/RELEASE-NOTES.txt
@@ -0,0 +1,130 @@
+Joda-Time version 2.3
+---------------------
+
+Joda-Time is a date and time handling library that seeks to replace the JDK
+Date and Calendar classes.
+
+This release contains enhancements, bug fixes and a time zone update.
+The release runs on JDK 5 or later.
+
+Joda-Time is licensed under the business-friendly Apache License Version 2.
+This is the same license as all of Apache, plus other open source projects such as Spring.
+The intent is to make the code available to the Java community with the minimum
+of restrictions. If the license causes you problems please contact the mailing list.
+
+**  Please also check out our related projects   **
+** http://www.joda.org/joda-time/related.html **
+
+
+Enhancements since 2.2
+----------------------
+- Interval/MutableInterval .isEqual() [#20]
+  Add method to compare intervals ignoring the chronology
+  https://github.com/JodaOrg/joda-time/issues/20
+
+- Chronology classes now define equals methods [#36]
+  Previously, the Chronology classes relied on caching in factory methods
+  to guarantee instances were singletons
+  Now, there are dedicated, normal, equals methods
+  This will aid weird cases where deserialization or similar avoids the caches
+  It will make no difference to most users
+
+- Maximum size for pattern cache [#49]
+  Sets a maximum size for the cache to avoid memory issues
+
+- Add LocalDateTime.toDate(TimeZone) [#48]
+  Provides an alternate way to create a java.util.Date that avoids some synchronization
+
+- Home page moved
+  http://www.joda.org/joda-time
+
+
+Compatibility with 2.2
+----------------------
+Build system - Yes
+
+Binary compatible - Yes
+
+Source compatible - Yes
+
+Serialization compatible - Yes
+
+Data compatible - Yes, except
+ - DateTimeZone data updated to version 2013d
+
+Semantic compatible - Yes, except
+ - DateTimeZone is now limited to offsets from -23:59:59.999 to +23:59:59.999
+
+ - BasicChronology now defines an equals method
+   This which would affect you if you subclassed it (unlikely)
+
+ - GJChronology now has a minimum cutover instant of 0001-01-01 (Gregorian)
+   Its unlikely you have it set earlier than this
+   If you did your code was broken anyway
+
+
+Deprecations since 2.2
+----------------------
+- DateMidnight [#41]
+  This class is flawed in concept
+  The time of midnight occasionally does not occur in some time-zones
+  This is a result of a daylight savings time from 00:00 to 01:00
+  DateMidnight is essentially a DateTime with a time locked to midnight
+  Such a concept is more generally a poor one to use, given LocalDate
+  Replace DateMidnight with LocalDate
+  Or replace it with DateTime, perhaps using the withTimeAtStartOfDay() method
+
+
+Bug fixes since 2.2
+-------------------
+- ZoneInfoCompiler and DateTimeZoneBuilder multi-threading [#18]
+  A thread local variable was previously only initialised in one thread causing NPE
+  https://github.com/JodaOrg/joda-time/issues/18
+
+- Short time-zone name parsing failed to match the longest name
+  This affected two short names where one is a short form of the second such as "UT" and "UTC"
+
+- Days.daysBetween fails for MonthDay [#22]
+  Incorrect calculation around leap years
+
+- DateTimeZone failed to validate offsets [#43]
+  Previously, there was little validation, resulting in the ability to create large offsets
+  Those offsets could fail in other parts of the library
+  Now, it is limited to -23:59:59.999 to +23:59:59.999
+
+- DateTimeZone.forOffsetHoursMinutes failed to allow offsets from -00:01 to -00:59 [#42]
+  The forOffsetHoursMinutes() method could not create an offset  from -00:01 to -00:59
+  This was due to an inappropriate design
+  A backwards compatible change to the input handling has been made
+  forOffsetHoursMinutes(0, -15) now creates -00:15
+
+- DateTimeFormatter.parseInto [#21]
+  Fix parseInto() where it obtains the default year for parsing from the ReadWritableInstant
+  Previously, the wrong year could be obtained at the start or end of the year in non UTC zones
+  Now obtains the year from the ReadWritableInstant using the chronology of the ReadWritableInstant
+
+- Better thread-safety in ISODateTimeFormat [#45]
+
+- Fix GJChronology.plus/minus across cutover and year zero [#28]
+  When subtracting a number of years from a date in the GJChronology there are two considerations
+  The cutover date might be crossed, and year zero might be crossed (there is no year zero in GJ)
+  Previously, each were handled separately, but not together. Now it is fixed
+  As part of this change, the minimum cutover instant was set to 0001-01-01 (Gregorian)
+
+
+Scala
+--------
+Joda-Time uses annotations from Joda-Convert.
+In the Java programming language, this dependency is optional, however in Scala it is not.
+Scala users must manually add the Joda-Convert v1.2 dependency.
+
+
+Feedback
+--------
+Feedback is best received using GitHub issues and Pull Requests.
+https://github.com/JodaOrg/joda-time/
+
+Feedback is also welcomed via the joda-interest mailing list.
+
+The Joda team
+http://joda-time.sourceforge.net
diff --git a/Timeline/release/modules/ext/joda-time-2.3/pom.xml b/Timeline/release/modules/ext/joda-time-2.3/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..eb2e4c095bd471679eb31b919e0cc95a6b6d999d
--- /dev/null
+++ b/Timeline/release/modules/ext/joda-time-2.3/pom.xml
@@ -0,0 +1,699 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project
+    xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>joda-time</groupId>
+  <artifactId>joda-time</artifactId>
+  <packaging>jar</packaging>
+  <name>Joda-Time</name>
+  <version>2.3</version>
+  <description>Date and time library to replace JDK date handling</description>
+  <url>http://www.joda.org/joda-time/</url>
+
+  <!-- ==================================================================== -->
+  <issueManagement>
+    <system>GitHub</system>
+    <url>https://github.com/JodaOrg/joda-time/issues</url>
+  </issueManagement>
+  <inceptionYear>2002</inceptionYear>
+  <mailingLists>
+    <mailingList>
+      <name>Joda Interest list</name>
+      <subscribe>https://lists.sourceforge.net/lists/listinfo/joda-interest</subscribe>
+      <unsubscribe>https://lists.sourceforge.net/lists/listinfo/joda-interest</unsubscribe>
+      <archive>http://sourceforge.net/mailarchive/forum.php?forum_name=joda-interest</archive>
+    </mailingList>
+  </mailingLists>
+
+  <!-- ==================================================================== -->
+  <developers>
+    <developer>
+      <id>scolebourne</id>
+      <name>Stephen Colebourne</name>
+      <email></email>
+      <roles>
+        <role>Project Lead</role>
+      </roles>
+      <timezone>0</timezone>
+      <url>https://github.com/jodastephen</url>
+    </developer>
+    <developer>
+      <id>broneill</id>
+      <name>Brian S O'Neill</name>
+      <email></email>
+      <roles>
+        <role>Senior Developer</role>
+      </roles>
+      <url>https://github.com/broneill</url>
+    </developer>
+  </developers>
+  <contributors>
+    <contributor>
+      <name>Guy Allard</name>
+    </contributor>
+    <contributor>
+      <name>Oren Benjamin</name>
+      <url>https://github.com/oby1</url>
+    </contributor>
+    <contributor>
+      <name>Fredrik Borgh</name>
+    </contributor>
+    <contributor>
+      <name>Dave Brosius</name>
+      <url>https://github.com/mebigfatguy</url>
+    </contributor>
+    <contributor>
+      <name>Luc Claes</name>
+      <url>https://github.com/lucclaes</url>
+    </contributor>
+    <contributor>
+      <name>Dan Cojocar</name>
+      <url>https://github.com/dancojocar</url>
+    </contributor>
+    <contributor>
+      <name>Christopher Elkins</name>
+      <url>https://github.com/celkins</url>
+    </contributor>
+    <contributor>
+      <name>Jeroen van Erp</name>
+    </contributor>
+    <contributor>
+      <name>Gwyn Evans</name>
+    </contributor>
+    <contributor>
+      <name>John Fletcher</name>
+    </contributor>
+    <contributor>
+      <name>Sean Geoghegan</name>
+    </contributor>
+    <contributor>
+      <name>mjunginger</name>
+      <url>https://github.com/mjunginger</url>
+    </contributor>
+    <contributor>
+      <name>Ashish Katyal</name>
+    </contributor>
+    <contributor>
+      <name>Martin Kneissl</name>
+      <url>https://github.com/mkneissl</url>
+    </contributor>
+    <contributor>
+      <name>Vidar Larsen</name>
+      <url>https://github.com/vlarsen</url>
+    </contributor>
+    <contributor>
+      <name>Kasper Laudrup</name>
+    </contributor>
+    <contributor>
+      <name>Jeff Lavallee</name>
+      <url>https://github.com/jlavallee</url>
+    </contributor>
+    <contributor>
+      <name>Antonio Leitao</name>
+    </contributor>
+    <contributor>
+      <name>Kostas Maistrelis</name>
+    </contributor>
+    <contributor>
+      <name>Al Major</name>
+    </contributor>
+    <contributor>
+      <name>Blair Martin</name>
+    </contributor>
+    <contributor>
+      <name>Julen Parra</name>
+    </contributor>
+    <contributor>
+      <name>Michael Plump</name>
+    </contributor>
+    <contributor>
+      <name>Ryan Propper</name>
+    </contributor>
+    <contributor>
+      <name>Mike Schrag</name>
+    </contributor>
+    <contributor>
+      <name>Hajime Senuma</name>
+      <url>https://github.com/hajimes</url>
+    </contributor>
+    <contributor>
+      <name>Kandarp Shah</name>
+    </contributor>
+    <contributor>
+      <name>Francois Staes</name>
+    </contributor>
+    <contributor>
+      <name>Ricardo Trindade</name>
+    </contributor>
+    <contributor>
+      <name>Bram Van Dam</name>
+      <url>https://github.com/codematters</url>
+    </contributor>
+    <contributor>
+      <name>Maxim Zhao</name>
+    </contributor>
+  </contributors>
+
+  <!-- ==================================================================== -->
+  <licenses>
+    <license>
+      <name>Apache 2</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+  <scm>
+    <connection>scm:git:https://github.com/JodaOrg/joda-time.git</connection>
+    <developerConnection>scm:git:git@github.com:JodaOrg/joda-time.git</developerConnection>
+    <url>https://github.com/JodaOrg/joda-time</url>
+  </scm>
+  <organization>
+    <name>Joda.org</name>
+    <url>http://www.joda.org</url>
+  </organization>
+
+  <!-- ==================================================================== -->
+  <build>
+    <resources>
+      <resource>
+        <targetPath>META-INF</targetPath>
+        <directory>${project.basedir}</directory>
+        <includes>
+          <include>LICENSE.txt</include>
+          <include>NOTICE.txt</include>
+        </includes>
+      </resource>
+      <resource>
+        <directory>${project.basedir}/src/main/java</directory>
+        <includes>
+          <include>**/*.properties</include>
+        </includes>
+      </resource>
+    </resources>
+    <!-- define build -->
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>      
+        <artifactId>exec-maven-plugin</artifactId>
+        <version>1.2.1</version>
+        <executions>
+          <execution>
+            <phase>compile</phase>
+            <goals>
+              <goal>java</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <mainClass>org.joda.time.tz.ZoneInfoCompiler</mainClass>
+          <classpathScope>compile</classpathScope>
+          <verbose>true</verbose>
+          <systemProperties>
+            <systemProperty>
+              <key>org.joda.time.DateTimeZone.Provider</key>
+              <value>org.joda.time.tz.UTCProvider</value>
+            </systemProperty>
+          </systemProperties>
+          <arguments>
+            <argument>-src</argument>
+            <argument>${project.build.sourceDirectory}/org/joda/time/tz/src</argument>
+            <argument>-dst</argument>
+            <argument>${project.build.outputDirectory}/org/joda/time/tz/data</argument>
+            <argument>africa</argument>
+            <argument>antarctica</argument>
+            <argument>asia</argument>
+            <argument>australasia</argument>
+            <argument>europe</argument>
+            <argument>northamerica</argument>
+            <argument>southamerica</argument>
+            <argument>pacificnew</argument>
+            <argument>etcetera</argument>
+            <argument>backward</argument>
+            <argument>systemv</argument>
+          </arguments>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <includes>
+            <include>**/TestAllPackages.java</include>
+          </includes>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+          <archive>
+            <manifestFile>src/conf/MANIFEST.MF</manifestFile>
+            <manifest>
+              <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+              <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
+            </manifest>
+          </archive>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <configuration>
+          <groups>
+            <group>
+              <title>User packages</title>
+              <packages>org.joda.time:org.joda.time.format:org.joda.time.chrono</packages>
+            </group>
+            <group>
+              <title>Implementation packages</title>
+              <packages>org.joda.time.base:org.joda.time.convert:org.joda.time.field:org.joda.time.tz</packages>
+            </group>
+          </groups>
+        </configuration>
+        <executions>
+          <execution>
+            <id>attach-javadocs</id>
+            <phase>package</phase>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>attach-sources</id>
+            <phase>package</phase>
+            <goals>
+              <goal>jar-no-fork</goal>
+            </goals>
+          </execution>
+        </executions>
+        <!-- work around maven bug where properties files added twice -->
+        <configuration>
+          <excludes>
+            <exclude>**/*.properties</exclude>
+          </excludes>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-assembly-plugin</artifactId>
+        <configuration>
+          <attach>false</attach>
+          <descriptors>
+            <descriptor>src/main/assembly/dist.xml</descriptor>
+          </descriptors>
+          <tarLongFileMode>gnu</tarLongFileMode>
+        </configuration>
+        <executions>
+          <execution>
+            <id>make-assembly</id>
+            <phase>deploy</phase>
+            <goals>
+              <goal>single</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-site-plugin</artifactId>
+        <configuration>
+          <skipDeploy>true</skipDeploy>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>com.github.github</groupId>
+        <artifactId>site-maven-plugin</artifactId>
+        <version>0.8</version>
+        <executions>
+          <execution>
+            <id>github-site</id>
+            <goals>
+              <goal>site</goal>
+            </goals>
+            <phase>site-deploy</phase>
+          </execution>
+        </executions>
+        <configuration>
+          <message>Create website for ${project.artifactId} v${project.version}</message>
+          <path>${project.artifactId}</path>
+          <merge>true</merge>
+          <server>github</server>
+          <repositoryOwner>JodaOrg</repositoryOwner>
+          <repositoryName>jodaorg.github.io</repositoryName>
+          <branch>refs/heads/master</branch>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>clirr-maven-plugin</artifactId>
+        <version>2.3</version>
+        <configuration>
+          <comparisonVersion>2.2</comparisonVersion>
+          <minSeverity>info</minSeverity>
+          <logResults>true</logResults>
+        </configuration>
+      </plugin>
+    </plugins>
+    <!-- Manage plugin versions -->
+    <pluginManagement>
+      <plugins>
+        <!-- Maven build and reporting plugins (alphabetical) -->
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-assembly-plugin</artifactId>
+          <version>${maven-assembly-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-checkstyle-plugin</artifactId>
+          <version>${maven-checkstyle-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-changes-plugin</artifactId>
+          <version>${maven-changes-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-clean-plugin</artifactId>
+          <version>${maven-clean-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>${maven-compiler-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-deploy-plugin</artifactId>
+          <version>${maven-deploy-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-dependency-plugin</artifactId>
+          <version>${maven-dependency-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-gpg-plugin</artifactId>
+          <version>${maven-gpg-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-install-plugin</artifactId>
+          <version>${maven-install-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-jar-plugin</artifactId>
+          <version>${maven-jar-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-javadoc-plugin</artifactId>
+          <version>${maven-javadoc-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-jxr-plugin</artifactId>
+          <version>${maven-jxr-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-plugin-plugin</artifactId>
+          <version>${maven-plugin-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-pmd-plugin</artifactId>
+          <version>${maven-pmd-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-project-info-reports-plugin</artifactId>
+          <version>${maven-project-info-reports-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-repository-plugin</artifactId>
+          <version>${maven-repository-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-resources-plugin</artifactId>
+          <version>${maven-resources-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-site-plugin</artifactId>
+          <version>${maven-site-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-source-plugin</artifactId>
+          <version>${maven-source-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <version>${maven-surefire-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-report-plugin</artifactId>
+          <version>${maven-surefire-report-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-toolchains-plugin</artifactId>
+          <version>${maven-toolchains-plugin.version}</version>
+        </plugin>
+        <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
+        <plugin>
+        	<groupId>org.eclipse.m2e</groupId>
+        	<artifactId>lifecycle-mapping</artifactId>
+        	<version>1.0.0</version>
+        	<configuration>
+        		<lifecycleMappingMetadata>
+        			<pluginExecutions>
+        				<pluginExecution>
+        					<pluginExecutionFilter>
+        						<groupId>org.codehaus.mojo</groupId>
+        						<artifactId>
+        							exec-maven-plugin
+        						</artifactId>
+        						<versionRange>[1.2.1,)</versionRange>
+        						<goals>
+        							<goal>java</goal>
+        						</goals>
+        					</pluginExecutionFilter>
+        					<action>
+        						<ignore></ignore>
+        					</action>
+        				</pluginExecution>
+        			</pluginExecutions>
+        		</lifecycleMappingMetadata>
+        	</configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+  </build>
+
+  <!-- ==================================================================== -->
+  <prerequisites>
+    <maven>3.0.4</maven>
+  </prerequisites>
+  <dependencies>
+    <dependency>
+      <groupId>org.joda</groupId>
+      <artifactId>joda-convert</artifactId>
+      <version>1.2</version>
+      <scope>compile</scope>
+      <optional>true</optional><!-- mandatory in Scala -->
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>3.8.2</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <!-- ==================================================================== -->
+  <reporting>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-project-info-reports-plugin</artifactId>
+        <version>${maven-project-info-plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <reports>
+              <report>dependencies</report>
+              <report>dependency-info</report>
+              <report>issue-tracking</report>
+              <report>license</report>
+              <report>mailing-list</report>
+              <report>project-team</report>
+              <report>scm</report>
+              <report>summary</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <version>${maven-javadoc-plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <reports>
+              <report>javadoc</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-report-plugin</artifactId>
+        <version>${maven-surefire-report-plugin.version}</version>
+        <configuration>
+           <showSuccess>true</showSuccess>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jxr-plugin</artifactId>
+        <version>${maven-jxr-plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <reports>
+              <report>jxr</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+      </plugin>
+    </plugins>
+  </reporting>
+
+  <!-- ==================================================================== -->
+  <distributionManagement>
+    <repository>
+      <id>sonatype-joda-staging</id>
+      <name>Sonatype OSS staging repository</name>
+      <url>http://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
+      <layout>default</layout>
+    </repository>
+    <snapshotRepository>
+      <uniqueVersion>false</uniqueVersion>
+      <id>sonatype-joda-snapshot</id>
+      <name>Sonatype OSS snapshot repository</name>
+      <url>http://oss.sonatype.org/content/repositories/joda-snapshots</url>
+      <layout>default</layout>
+    </snapshotRepository>
+    <downloadUrl>http://oss.sonatype.org/content/repositories/joda-releases</downloadUrl>
+  </distributionManagement>
+
+  <!-- ==================================================================== -->
+  <profiles>
+    <profile>
+      <id>repo-sign-artifacts</id>
+      <activation>
+        <property>
+          <name>oss.repo</name>
+          <value>true</value>
+        </property>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-toolchains-plugin</artifactId>
+            <executions>
+              <execution>
+                <phase>validate</phase>
+                <goals>
+                  <goal>toolchain</goal>
+                </goals>
+              </execution>
+            </executions>
+            <configuration>
+              <toolchains>
+                <jdk>
+                  <version>1.5</version>
+                  <vendor>sun</vendor>
+                </jdk>
+              </toolchains>
+            </configuration>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-gpg-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>sign-artifacts</id>
+                <phase>verify</phase>
+                <goals>
+                  <goal>sign</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+
+  <!-- ==================================================================== -->
+  <properties>
+    <!-- Plugin version numbers -->
+    <maven-assembly-plugin.version>2.4</maven-assembly-plugin.version>
+    <maven-changes-plugin.version>2.9</maven-changes-plugin.version>
+    <maven-checkstyle-plugin.version>2.10</maven-checkstyle-plugin.version>
+    <maven-clean-plugin.version>2.5</maven-clean-plugin.version>
+    <maven-compiler-plugin.version>3.1</maven-compiler-plugin.version>
+    <maven-deploy-plugin.version>2.7</maven-deploy-plugin.version>
+    <maven-dependency-plugin.version>2.8</maven-dependency-plugin.version>
+    <maven-gpg-plugin.version>1.4</maven-gpg-plugin.version>
+    <maven-install-plugin.version>2.4</maven-install-plugin.version>
+    <maven-jar-plugin.version>2.4</maven-jar-plugin.version>
+    <maven-javadoc-plugin.version>2.9.1</maven-javadoc-plugin.version>
+    <maven-jxr-plugin.version>2.3</maven-jxr-plugin.version>
+    <maven-plugin-plugin.version>3.2</maven-plugin-plugin.version>
+    <maven-pmd-plugin.version>3.0.1</maven-pmd-plugin.version>
+    <maven-project-info-reports-plugin.version>2.7</maven-project-info-reports-plugin.version>
+    <maven-repository-plugin.version>2.3.1</maven-repository-plugin.version>
+    <maven-resources-plugin.version>2.6</maven-resources-plugin.version>
+    <maven-site-plugin.version>3.3</maven-site-plugin.version>
+    <maven-source-plugin.version>2.2.1</maven-source-plugin.version>
+    <maven-surefire-plugin.version>2.15</maven-surefire-plugin.version>
+    <maven-surefire-report-plugin.version>2.15</maven-surefire-report-plugin.version>
+    <maven-toolchains-plugin.version>1.0</maven-toolchains-plugin.version>
+    <!-- Properties for maven-compiler-plugin -->
+    <maven.compiler.compilerVersion>1.5</maven.compiler.compilerVersion>
+    <maven.compiler.source>1.5</maven.compiler.source>
+    <maven.compiler.target>1.5</maven.compiler.target>
+    <maven.compiler.fork>true</maven.compiler.fork>
+    <maven.compiler.verbose>true</maven.compiler.verbose>
+    <maven.compiler.optimize>true</maven.compiler.optimize>
+    <maven.compiler.debug>true</maven.compiler.debug>
+    <maven.compiler.debuglevel>lines,source</maven.compiler.debuglevel>
+    <!-- Properties for maven-javadoc-plugin -->
+    <author>false</author>
+    <notimestamp>true</notimestamp>
+    <!-- Properties for maven-checkstyle-plugin -->
+    <checkstyle.config.location>${project.basedir}/src/main/checkstyle/checkstyle.xml</checkstyle.config.location>
+    <!-- Other properties -->
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+  </properties>
+</project>
diff --git a/Timeline/release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar b/Timeline/release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar
new file mode 100644
index 0000000000000000000000000000000000000000..bcea83745ab32246e315dd0c53209dc6a0cd39b9
Binary files /dev/null and b/Timeline/release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar differ
diff --git a/Timeline/release/modules/ext/sqlite-jdbc-3.8.0-SNAPSHOT.jar b/Timeline/release/modules/ext/sqlite-jdbc-3.8.0-SNAPSHOT.jar
new file mode 100644
index 0000000000000000000000000000000000000000..0ec70e33be9c8175ec567f86b54a8a13d6f297f8
Binary files /dev/null and b/Timeline/release/modules/ext/sqlite-jdbc-3.8.0-SNAPSHOT.jar differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/Bundle.properties b/Timeline/src/org/sleuthkit/autopsy/timeline/Bundle.properties
new file mode 100644
index 0000000000000000000000000000000000000000..a2a287e3220501c96467e63b8803cacb164f7b9a
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/Bundle.properties
@@ -0,0 +1,37 @@
+OpenIDE-Module-Name=Timeline
+CTL_MakeTimeline="Timeline"
+Timeline.frameName.text={0} - Autopsy Timeline
+Timeline.resultsPanel.title=Timeline Results
+Timeline.runJavaFxThread.progress.creating=Creating timeline . . .
+Timeline.zoomOutButton.text=Zoom Out
+Timeline.goToButton.text=Go To\:
+Timeline.yearBarChart.x.years=Years
+Timeline.yearBarChart.y.numEvents=Number of Events
+Timeline.MonthsBarChart.x.monthYY=Month ({0})
+Timeline.MonthsBarChart.y.numEvents=Number of Events
+Timeline.eventsByMoBarChart.x.dayOfMo=Day of Month
+Timeline.eventsByMoBarChart.y.numEvents=Number of Events
+Timeline.node.emptyRoot=Empty Root
+Timeline.resultPanel.loading=Loading...
+Timeline.node.root=Root
+Timeline.propChg.confDlg.timelineOOD.msg=The event data is out of date. Would you like to regenerate it?
+Timeline.propChg.confDlg.timelineOOD.details=Select an option
+Timeline.initTimeline.confDlg.genBeforeIngest.msg=You are trying to generate a timeline before ingest has been completed. The timeline may be incomplete. Do you want to continue?
+Timeline.initTimeline.confDlg.genBeforeIngest.details=Timeline
+TimelineFrame.title=Timeline
+TimelinePanel.jButton1.text=6m
+TimelinePanel.jButton13.text=all
+TimelinePanel.jButton10.text=1h
+TimelinePanel.jButton9.text=12h
+TimelinePanel.jButton11.text=5y
+TimelinePanel.jButton12.text=10y
+TimelinePanel.jButton6.text=1w
+TimelinePanel.jButton5.text=1y
+TimelinePanel.jButton8.text=1d
+TimelinePanel.jButton7.text=3d
+TimelinePanel.jButton2.text=1m
+TimelinePanel.jButton3.text=3m
+TimelinePanel.jButton4.text=2w
+ProgressWindow.progressHeader.text=\ 
+AggregateEvent.differentTypes="aggregate events are not compatible they have different types"
+AggregateEvent.differentDescriptions="aggregate events are not compatible they have different descriptions"
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/FXMLConstructor.java b/Timeline/src/org/sleuthkit/autopsy/timeline/FXMLConstructor.java
new file mode 100644
index 0000000000000000000000000000000000000000..685d50d3d56b8231946f85f3a4fbb7feb226c97f
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/FXMLConstructor.java
@@ -0,0 +1,145 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Node;
+import org.openide.util.Exceptions;
+
+/**
+ * This class support both programmer productivity by abstracting frequently
+ * used code to load FXML-defined GUI components, and code performance by
+ * implementing a caching FXMLLoader as described at
+ * http://stackoverflow.com/questions/11734885/javafx2-very-poor-performance-when-adding-custom-made-fxmlpanels-to-gridpane.
+ *
+ * TODO: this code is duplicated in the Image Analyze module, we should move it
+ * into a centralized places like a JavaFX utils class/package/module in
+ * Autopsy- jm
+ */
+public class FXMLConstructor {
+
+    private static final CachingClassLoader CACHING_CLASS_LOADER = new CachingClassLoader((FXMLLoader.getDefaultClassLoader()));
+
+    static public void construct(Node n, String fxmlFileName) {
+        FXMLLoader fxmlLoader = new FXMLLoader(n.getClass().getResource(fxmlFileName));
+        fxmlLoader.setRoot(n);
+        fxmlLoader.setController(n);
+        fxmlLoader.setClassLoader(CACHING_CLASS_LOADER);
+
+        try {
+            fxmlLoader.load();
+        } catch (Exception exception) {
+            try {
+                fxmlLoader.setClassLoader(FXMLLoader.getDefaultClassLoader());
+                fxmlLoader.load();
+            } catch (Exception ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+    }
+
+    /**
+     * The default FXMLLoader does not cache information about previously loaded
+     * FXML files. See
+     * http://stackoverflow.com/questions/11734885/javafx2-very-poor-performance-when-adding-custom-made-fxmlpanels-to-gridpane.
+     * for more details. As a partial workaround, we cache information on
+     * previously loaded classes. This does not solve all performance issues,
+     * but is a big improvement.
+     */
+    static public class CachingClassLoader extends ClassLoader {
+
+        private final Map<String, Class<?>> classes = new HashMap<>();
+
+        private final ClassLoader parent;
+
+        public CachingClassLoader(ClassLoader parent) {
+            this.parent = parent;
+        }
+
+        @Override
+        public Class<?> loadClass(String name) throws ClassNotFoundException {
+            Class<?> c = findClass(name);
+            if (c == null) {
+                throw new ClassNotFoundException(name);
+            }
+            return c;
+        }
+
+        @Override
+        protected Class<?> findClass(String className) throws ClassNotFoundException {
+// System.out.print("try to load " + className); 
+            if (classes.containsKey(className)) {
+                Class<?> result = classes.get(className);
+                return result;
+            } else {
+                try {
+                    Class<?> result = parent.loadClass(className);
+// System.out.println(" -> success!"); 
+                    classes.put(className, result);
+                    return result;
+                } catch (ClassNotFoundException ignore) {
+// System.out.println(); 
+                    classes.put(className, null);
+                    return null;
+                }
+            }
+        }
+
+        // ========= delegating methods ============= 
+        @Override
+        public URL getResource(String name) {
+            return parent.getResource(name);
+        }
+
+        @Override
+        public Enumeration<URL> getResources(String name) throws IOException {
+            return parent.getResources(name);
+        }
+
+        @Override
+        public String toString() {
+            return parent.toString();
+        }
+
+        @Override
+        public void setDefaultAssertionStatus(boolean enabled) {
+            parent.setDefaultAssertionStatus(enabled);
+        }
+
+        @Override
+        public void setPackageAssertionStatus(String packageName, boolean enabled) {
+            parent.setPackageAssertionStatus(packageName, enabled);
+        }
+
+        @Override
+        public void setClassAssertionStatus(String className, boolean enabled) {
+            parent.setClassAssertionStatus(className, enabled);
+        }
+
+        @Override
+        public void clearAssertionStatus() {
+            parent.clearAssertionStatus();
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/LoggedTask.java b/Timeline/src/org/sleuthkit/autopsy/timeline/LoggedTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..5a0a55ba4f03eaa8d935b485c674d1c4eeef69b5
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/LoggedTask.java
@@ -0,0 +1,79 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 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;
+
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Level;
+import javafx.concurrent.Task;
+import org.openide.util.Cancellable;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.coreutils.Logger;
+
+/**
+ *
+ */
+public abstract class LoggedTask<T> extends Task<T> implements Cancellable {
+
+    private static final Logger LOGGER = Logger.getLogger(LoggedTask.class.getName());
+
+    private final Boolean logStateChanges;
+
+    public LoggedTask(String taskName, Boolean logStateChanges) {
+        updateTitle(taskName);
+        this.logStateChanges = logStateChanges;
+    }
+
+    @Override
+    protected void cancelled() {
+        super.cancelled();
+        if (logStateChanges) {
+            LOGGER.log(Level.WARNING, "{0} cancelled!", getTitle());
+        }
+    }
+
+    @Override
+    protected void failed() {
+        super.failed();
+        LOGGER.log(Level.SEVERE, getTitle() + " failed!", getException());
+
+    }
+
+    @Override
+    protected void scheduled() {
+        super.scheduled();
+        if (logStateChanges) {
+            LOGGER.log(Level.INFO, "{0} scheduled", getTitle());
+        }
+    }
+
+    @Override
+    protected void succeeded() {
+        super.succeeded();
+        try {
+            get();
+        } catch (InterruptedException ex) {
+            Exceptions.printStackTrace(ex);
+        } catch (ExecutionException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        if (logStateChanges) {
+            LOGGER.log(Level.INFO, "{0} succeeded", getTitle());
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/OpenTimelineAction.java b/Timeline/src/org/sleuthkit/autopsy/timeline/OpenTimelineAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..fd1f450370a1f2064d0d8734c12bc62b5b270abd
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/OpenTimelineAction.java
@@ -0,0 +1,96 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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;
+
+import java.util.logging.Level;
+import javax.swing.JOptionPane;
+import org.openide.awt.ActionID;
+import org.openide.awt.ActionReference;
+import org.openide.awt.ActionReferences;
+import org.openide.awt.ActionRegistration;
+import org.openide.util.HelpCtx;
+import org.openide.util.actions.CallableSystemAction;
+import org.openide.windows.WindowManager;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.core.Installer;
+import org.sleuthkit.autopsy.coreutils.Logger;
+
+@ActionID(category = "Tools", id = "org.sleuthkit.autopsy.timeline.Timeline")
+@ActionRegistration(displayName = "#CTL_MakeTimeline", lazy = false)
+@ActionReferences(value = {
+    @ActionReference(path = "Menu/Tools", position = 100)})
+public class OpenTimelineAction extends CallableSystemAction {
+
+    private static final Logger LOGGER = Logger.getLogger(OpenTimelineAction.class.getName());
+
+    private static final boolean fxInited = Installer.isJavaFxInited();
+
+    synchronized static void invalidateController() {
+        timeLineController = null;
+    }
+
+    private static TimeLineController timeLineController = null;
+
+    public OpenTimelineAction() {
+        super();
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return Case.isCaseOpen() && fxInited && Case.getCurrentCase().hasData();
+    }
+
+    @Override
+    public void performAction() {
+
+        //check case
+        if (!Case.existsCurrentCase()) {
+            return;
+        }
+        final Case currentCase = Case.getCurrentCase();
+
+        if (currentCase.hasData() == false) {
+            JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), "Error creating timeline, there are no data sources.");
+            LOGGER.log(Level.INFO, "Error creating timeline, there are no data sources.");
+            return;
+        }
+        synchronized (OpenTimelineAction.class) {
+            if (timeLineController == null) {
+                timeLineController = new TimeLineController();
+                LOGGER.log(Level.WARNING, "Failed to get TimeLineController from lookup. Instantiating one directly.S");
+            }
+        }
+        timeLineController.openTimeLine();
+    }
+
+    @Override
+    public String getName() {
+        return "Timeline";
+    }
+
+    @Override
+    public HelpCtx getHelpCtx() {
+        return HelpCtx.DEFAULT_HELP;
+    }
+
+    @Override
+    public boolean asynchronous() {
+        return false; // run on edt
+    }
+}
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimelineProgressDialog.form b/Timeline/src/org/sleuthkit/autopsy/timeline/ProgressWindow.form
similarity index 81%
rename from Core/src/org/sleuthkit/autopsy/timeline/TimelineProgressDialog.form
rename to Timeline/src/org/sleuthkit/autopsy/timeline/ProgressWindow.form
index 0a8a131d097019f41d3cd82e78a62ba822fbeebe..6c99e006c8d2b9d47d2dacfde1fb5ad5cce3e577 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/TimelineProgressDialog.form
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ProgressWindow.form
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 
-<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JDialogFormInfo">
+<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JFrameFormInfo">
   <SyntheticProperties>
     <SyntheticProperty name="formSizePolicy" type="int" value="1"/>
     <SyntheticProperty name="generateCenter" type="boolean" value="false"/>
@@ -28,7 +28,7 @@
               <Group type="103" groupAlignment="0" attributes="0">
                   <Component id="progressBar" pref="504" max="32767" attributes="0"/>
                   <Group type="102" alignment="0" attributes="0">
-                      <Component id="jLabel1" min="-2" max="-2" attributes="0"/>
+                      <Component id="progressHeader" min="-2" max="-2" attributes="0"/>
                       <EmptySpace min="0" pref="0" max="32767" attributes="0"/>
                   </Group>
               </Group>
@@ -40,23 +40,23 @@
       <Group type="103" groupAlignment="0" attributes="0">
           <Group type="102" alignment="0" attributes="0">
               <EmptySpace max="-2" attributes="0"/>
-              <Component id="jLabel1" min="-2" max="-2" attributes="0"/>
-              <EmptySpace min="-2" pref="7" max="-2" attributes="0"/>
+              <Component id="progressHeader" min="-2" max="-2" attributes="0"/>
+              <EmptySpace max="-2" attributes="0"/>
               <Component id="progressBar" min="-2" max="-2" attributes="0"/>
-              <EmptySpace pref="16" max="32767" attributes="0"/>
+              <EmptySpace max="32767" attributes="0"/>
           </Group>
       </Group>
     </DimensionLayout>
   </Layout>
   <SubComponents>
-    <Component class="javax.swing.JLabel" name="jLabel1">
+    <Component class="javax.swing.JProgressBar" name="progressBar">
+    </Component>
+    <Component class="javax.swing.JLabel" name="progressHeader">
       <Properties>
         <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
-          <ResourceString bundle="org/sleuthkit/autopsy/timeline/Bundle.properties" key="TimelineProgressDialog.jLabel1.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+          <ResourceString bundle="org/sleuthkit/autopsy/advancedtimeline/Bundle.properties" key="ProgressWindow.progressHeader.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
         </Property>
       </Properties>
     </Component>
-    <Component class="javax.swing.JProgressBar" name="progressBar">
-    </Component>
   </SubComponents>
 </Form>
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ProgressWindow.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ProgressWindow.java
new file mode 100644
index 0000000000000000000000000000000000000000..3fb9624f2a5c88d31d900e98d68484c54402af3a
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ProgressWindow.java
@@ -0,0 +1,278 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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;
+
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import javax.annotation.concurrent.Immutable;
+import javax.swing.AbstractAction;
+import javax.swing.ActionMap;
+import javax.swing.GroupLayout;
+import javax.swing.InputMap;
+import javax.swing.JComponent;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JProgressBar;
+import javax.swing.KeyStroke;
+import javax.swing.LayoutStyle;
+import javax.swing.SwingUtilities;
+import javax.swing.SwingWorker;
+import org.openide.awt.Mnemonics;
+import org.openide.util.NbBundle;
+import org.openide.windows.WindowManager;
+
+/**
+ * Dialog with progress bar that pops up when timeline is being generated
+ */
+public class ProgressWindow extends JFrame {
+
+    private final SwingWorker<?,?> worker;
+
+    /**
+     * Creates new form TimelineProgressDialog
+     */
+    public ProgressWindow(Component parent, boolean modal, SwingWorker<?,?> worker) {
+        super();
+        initComponents();
+
+        setLocationRelativeTo(parent);
+
+        setAlwaysOnTop(modal);
+
+        //set icon the same as main app
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                setIconImage(WindowManager.getDefault().getMainWindow().getIconImage());
+            }
+        });
+
+
+        //progressBar.setIndeterminate(true);
+
+        setName("Advanced Timeline");
+        setTitle("Generating Advanced Timeline data");
+        // Close the dialog when Esc is pressed
+        String cancelName = "cancel";
+        InputMap inputMap = getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
+
+        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), cancelName);
+        ActionMap actionMap = getRootPane().getActionMap();
+
+        actionMap.put(cancelName, new AbstractAction() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                cancel();
+            }
+        });
+        this.worker = worker;
+    }
+
+    public void updateProgress(final int progress) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+
+                progressBar.setValue(progress);
+            }
+        });
+    }
+
+    public void updateProgress(final int progress, final String message) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+
+                progressBar.setValue(progress);
+                progressBar.setString(message);
+            }
+        });
+    }
+
+    public void updateProgress(final String message) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+
+                progressBar.setString(message);
+            }
+        });
+    }
+
+    public void setProgressTotal(final int total) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                progressBar.setIndeterminate(false);
+                progressBar.setMaximum(total);
+                progressBar.setStringPainted(true);
+
+            }
+        });
+    }
+
+    public void updateHeaderMessage(final String headerMessage) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                progressHeader.setText(headerMessage);
+
+            }
+        });
+    }
+
+    public void setIndeterminate() {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                progressBar.setIndeterminate(true);
+                progressBar.setStringPainted(true);
+
+            }
+        });
+    }
+
+    /**
+     * This method is called from within the constructor to initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is always
+     * regenerated by the Form Editor.
+     */
+    @SuppressWarnings("unchecked")
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        progressBar = new JProgressBar();
+        progressHeader = new JLabel();
+
+        addWindowListener(new WindowAdapter() {
+            public void windowClosing(WindowEvent evt) {
+                closeDialog(evt);
+            }
+        });
+
+        Mnemonics.setLocalizedText(progressHeader, NbBundle.getMessage(ProgressWindow.class, "ProgressWindow.progressHeader.text")); // NOI18N
+
+        GroupLayout layout = new GroupLayout(getContentPane());
+        getContentPane().setLayout(layout);
+        layout.setHorizontalGroup(
+            layout.createParallelGroup(GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addContainerGap()
+                .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
+                    .addComponent(progressBar, GroupLayout.DEFAULT_SIZE, 504, Short.MAX_VALUE)
+                    .addGroup(layout.createSequentialGroup()
+                        .addComponent(progressHeader)
+                        .addGap(0, 0, Short.MAX_VALUE)))
+                .addContainerGap())
+        );
+        layout.setVerticalGroup(
+            layout.createParallelGroup(GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addContainerGap()
+                .addComponent(progressHeader)
+                .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
+                .addComponent(progressBar, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
+                .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
+        );
+
+        pack();
+    }// </editor-fold>//GEN-END:initComponents
+
+    /**
+     * Closes the dialog
+     */
+    private void closeDialog(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_closeDialog
+        cancel();
+    }//GEN-LAST:event_closeDialog
+
+    public void cancel() {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                if (isVisible()) {
+                    int showConfirmDialog = JOptionPane.showConfirmDialog(ProgressWindow.this, "Do you want to cancel time line creation?", "Cancel timeline creation?", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
+                    if (showConfirmDialog == JOptionPane.YES_OPTION) {
+                        close();
+                    }
+                } else {
+                    close();
+                }
+            }
+        });
+    }
+
+    public void close() {
+        worker.cancel(false);
+        setVisible(false);
+        dispose();
+    }
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private JProgressBar progressBar;
+    private JLabel progressHeader;
+    // End of variables declaration//GEN-END:variables
+
+    public void update(ProgressUpdate chunk) {
+        updateHeaderMessage(chunk.getHeaderMessage());
+        if (chunk.getTotal() >= 0) {
+            setProgressTotal(chunk.getTotal());
+            updateProgress(chunk.getProgress(), chunk.getDetailMessage());
+        } else {
+            setIndeterminate();
+            updateProgress(chunk.getDetailMessage());
+        }
+    }
+
+    /** bundles up progress information to be shown in the progress dialog */
+    @Immutable
+    public static class ProgressUpdate {
+
+        private final int progress;
+        private final int total;
+        private final String headerMessage;
+        private final String detailMessage;
+
+        public int getProgress() {
+            return progress;
+        }
+
+        public int getTotal() {
+            return total;
+        }
+
+        public String getHeaderMessage() {
+            return headerMessage;
+        }
+
+        public String getDetailMessage() {
+            return detailMessage;
+        }
+
+        public ProgressUpdate(int progress, int total, String headerMessage, String detailMessage) {
+            super();
+            this.progress = progress;
+            this.total = total;
+            this.headerMessage = headerMessage;
+            this.detailMessage = detailMessage;
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineController.java
new file mode 100644
index 0000000000000000000000000000000000000000..97426a5f4e350baf883195e52c328b95cac255df
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineController.java
@@ -0,0 +1,736 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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;
+
+import java.awt.HeadlessException;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.ZoneId;
+import java.util.Collection;
+import java.util.MissingResourceException;
+import java.util.TimeZone;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.logging.Level;
+import javafx.application.Platform;
+import javafx.beans.Observable;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
+import javafx.beans.property.ReadOnlyDoubleProperty;
+import javafx.beans.property.ReadOnlyDoubleWrapper;
+import javafx.beans.property.ReadOnlyListProperty;
+import javafx.beans.property.ReadOnlyListWrapper;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.property.ReadOnlyStringProperty;
+import javafx.beans.property.ReadOnlyStringWrapper;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.concurrent.Task;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.Immutable;
+import javax.swing.JOptionPane;
+import javax.swing.SwingUtilities;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Interval;
+import org.joda.time.ReadablePeriod;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.openide.util.Exceptions;
+import org.openide.util.NbBundle;
+import org.openide.windows.WindowManager;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+import org.sleuthkit.autopsy.timeline.events.db.EventsRepository;
+import org.sleuthkit.autopsy.timeline.events.type.EventType;
+import org.sleuthkit.autopsy.timeline.filters.Filter;
+import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
+import org.sleuthkit.autopsy.timeline.utils.IntervalUtils;
+import org.sleuthkit.autopsy.timeline.utils.ObservableStack;
+import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
+import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
+import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
+import org.sleuthkit.autopsy.casemodule.Case;
+import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE;
+import static org.sleuthkit.autopsy.casemodule.Case.Events.DATA_SOURCE_ADDED;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.ingest.IngestManager;
+import org.sleuthkit.datamodel.SleuthkitCase;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/** Controller in the MVC design along with model = {@link FilteredEventsModel}
+ * and views = {@link TimeLineView}. Forwards interpreted user gestures form
+ * views to model. Provides model to view. Is entry point for timeline module.
+ *
+ * Concurrency Policy:<ul>
+ * <li>Since filteredEvents is internally synchronized, only compound access to
+ * it needs external synchronization</li>
+ * * <li>Since eventsRepository is internally synchronized, only compound
+ * access to it needs external synchronization <li>
+ * <li>Other state including listeningToAutopsy, mainFrame, viewMode, and the
+ * listeners should only be accessed with this object's intrinsic lock held
+ * </li>
+ * <ul>
+ */
+public class TimeLineController {
+
+    private static final Logger LOGGER = Logger.getLogger(TimeLineController.class.getName());
+
+    private static final String DO_REPOPULATE_MESSAGE = "The events databse was prevously populated while ingest was running.\n" + "Some events may not have been populated or may have been populated inaccurately.\n" + "Do you want to repopulate the events database now?";
+
+    private static final ReadOnlyObjectWrapper<TimeZone> timeZone = new ReadOnlyObjectWrapper<>(TimeZone.getDefault());
+
+    public static ZoneId getTimeZoneID() {
+        return timeZone.get().toZoneId();
+    }
+
+    public static DateTimeFormatter getZonedFormatter() {
+        return DateTimeFormat.forPattern("YYYY-MM-dd HH:mm:ss").withZone(getJodaTimeZone());
+    }
+
+    public static DateTimeZone getJodaTimeZone() {
+        return DateTimeZone.forTimeZone(getTimeZone().get());
+    }
+
+    public static ReadOnlyObjectProperty<TimeZone> getTimeZone() {
+        return timeZone.getReadOnlyProperty();
+    }
+
+    private final ExecutorService executor = Executors.newSingleThreadExecutor();
+
+    private final ReadOnlyListWrapper<Task<?>> tasks = new ReadOnlyListWrapper<>(FXCollections.observableArrayList());
+
+    private final ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper(-1);
+
+    private final ReadOnlyStringWrapper message = new ReadOnlyStringWrapper();
+
+    private final ReadOnlyStringWrapper taskTitle = new ReadOnlyStringWrapper();
+
+    synchronized public ReadOnlyListProperty<Task<?>> getTasks() {
+        return tasks.getReadOnlyProperty();
+    }
+
+    synchronized public ReadOnlyDoubleProperty getProgress() {
+        return progress.getReadOnlyProperty();
+    }
+
+    synchronized public ReadOnlyStringProperty getMessage() {
+        return message.getReadOnlyProperty();
+    }
+
+    synchronized public ReadOnlyStringProperty getTaskTitle() {
+        return taskTitle.getReadOnlyProperty();
+    }
+
+    @GuardedBy("this")
+    private TimeLineTopComponent mainFrame;
+
+    //are the listeners currently attached
+    @GuardedBy("this")
+    private boolean listeningToAutopsy = false;
+
+    private final PropertyChangeListener caseListener;
+
+    private final PropertyChangeListener ingestJobListener = new AutopsyIngestJobListener();
+
+    private final PropertyChangeListener ingestModuleListener = new AutopsyIngestModuleListener();
+
+    @GuardedBy("this")
+    private final ReadOnlyObjectWrapper<VisualizationMode> viewMode = new ReadOnlyObjectWrapper<>(VisualizationMode.COUNTS);
+
+    synchronized public ReadOnlyObjectProperty<VisualizationMode> getViewMode() {
+        return viewMode.getReadOnlyProperty();
+    }
+
+    @GuardedBy("filteredEvents")
+    private final FilteredEventsModel filteredEvents;
+
+    @GuardedBy("eventsRepository")
+    private final EventsRepository eventsRepository;
+
+    @GuardedBy("this")
+    private final ZoomParams InitialZoomState;
+
+    /** list based stack to hold history, 'top' is at index 0; */
+    @GuardedBy("this")
+    private final ObservableStack<ZoomParams> historyStack = new ObservableStack<>();
+
+    @GuardedBy("this")
+    private final ObservableStack<ZoomParams> forwardStack = new ObservableStack<>();
+
+    synchronized public ObservableStack<ZoomParams> getHistoryStack() {
+        return historyStack;
+    }
+
+    synchronized public ObservableStack<ZoomParams> getForwardStack() {
+        return forwardStack;
+    }
+
+    //all members should be access with the intrinsict lock of this object held
+    //selected events (ie shown in the result viewer)
+    @GuardedBy("this")
+    private final ObservableList<Long> selectedEventIDs = FXCollections.<Long>synchronizedObservableList(FXCollections.<Long>observableArrayList());
+
+    /**
+     * @return an unmodifiable list of the selected event ids
+     */
+    synchronized public ObservableList<Long> getSelectedEventIDs() {
+        return selectedEventIDs;
+    }
+
+    @GuardedBy("this")
+    private final ReadOnlyObjectWrapper<Interval> selectedTimeRange = new ReadOnlyObjectWrapper<>();
+
+    /**
+     * @return a read only view of the selected interval.
+     */
+    synchronized public ReadOnlyObjectProperty<Interval> getSelectedTimeRange() {
+        return selectedTimeRange.getReadOnlyProperty();
+    }
+
+    public ReadOnlyBooleanProperty getNewEventsFlag() {
+        return newEventsFlag.getReadOnlyProperty();
+    }
+
+    private final ReadOnlyBooleanWrapper needsHistogramRebuild = new ReadOnlyBooleanWrapper(false);
+
+    public ReadOnlyBooleanProperty getNeedsHistogramRebuild() {
+        return needsHistogramRebuild.getReadOnlyProperty();
+    }
+
+    private final ReadOnlyBooleanWrapper newEventsFlag = new ReadOnlyBooleanWrapper(false);
+
+    public TimeLineController() {
+        //initalize repository and filteredEvents on creation
+        eventsRepository = new EventsRepository();
+
+        filteredEvents = eventsRepository.getEventsModel();
+        InitialZoomState = new ZoomParams(filteredEvents.getSpanningInterval(),
+                                          EventTypeZoomLevel.BASE_TYPE,
+                                          Filter.getDefaultFilter(),
+                                          DescriptionLOD.SHORT);
+        filteredEvents.requestZoomState(InitialZoomState, true);
+        //persistent listener instances
+        caseListener = new AutopsyCaseListener();
+
+    }
+
+    /** @return a shared events model */
+    public FilteredEventsModel getEventsModel() {
+        return filteredEvents;
+    }
+
+    public void applyDefaultFilters() {
+        pushFilters(Filter.getDefaultFilter());
+    }
+
+    public void zoomOutToActivity() {
+        Interval boundingEventsInterval = filteredEvents.getBoundingEventsInterval();
+        pushZoom(filteredEvents.getRequestedZoomParamters().get().withTimeRange(boundingEventsInterval));
+    }
+
+    boolean rebuildRepo() {
+        if (IngestManager.getInstance().isIngestRunning()) {
+            //confirm timeline during ingest
+            if (showIngestConfirmation() != JOptionPane.YES_OPTION) {
+                return false;
+            }
+        }
+        LOGGER.log(Level.INFO, "Beginning generation of timeline");
+        try {
+            SwingUtilities.invokeLater(() -> {
+                if (mainFrame != null) {
+                    mainFrame.close();
+                }
+            });
+            final SleuthkitCase sleuthkitCase = Case.getCurrentCase().getSleuthkitCase();
+            final long lastObjId = sleuthkitCase.getLastObjectId();
+            final long lastArtfID = getCaseLastArtifactID(sleuthkitCase);
+            final Boolean injestRunning = IngestManager.getInstance().isIngestRunning();
+            //TODO: verify this locking is correct? -jm
+            synchronized (eventsRepository) {
+                eventsRepository.rebuildRepository(() -> {
+
+                    synchronized (eventsRepository) {
+                        eventsRepository.recordLastObjID(lastObjId);
+                        eventsRepository.recordLastArtifactID(lastArtfID);
+                        eventsRepository.recordWasIngestRunning(injestRunning);
+                    }
+                    synchronized (TimeLineController.this) {
+                        needsHistogramRebuild.set(true);
+                        needsHistogramRebuild.set(false);
+                        showWindow();
+                    }
+
+                    Platform.runLater(() -> {
+                        newEventsFlag.set(false);
+                        TimeLineController.this.showFullRange();
+                    });
+                });
+            }
+        } catch (TskCoreException ex) {
+            LOGGER.log(Level.SEVERE, "Error when generating timeline, ", ex);
+            return false;
+        }
+        return true;
+    }
+
+    public void showFullRange() {
+        synchronized (filteredEvents) {
+            pushTimeRange(filteredEvents.getSpanningInterval());
+        }
+    }
+
+    synchronized public void closeTimeLine() {
+        if (mainFrame != null) {
+            listeningToAutopsy = false;
+            IngestManager.getInstance().removeIngestModuleEventListener(ingestModuleListener);
+            IngestManager.getInstance().removeIngestJobEventListener(ingestJobListener);
+            Case.removePropertyChangeListener(caseListener);
+            mainFrame.close();
+            mainFrame.setVisible(false);
+            mainFrame = null;
+        }
+    }
+
+    /** show the timeline window and prompt for rebuilding database */
+    synchronized void openTimeLine() {
+
+        // listen for case changes (specifically images being added, and case changes).
+        if (Case.isCaseOpen() && !listeningToAutopsy) {
+            IngestManager.getInstance().addIngestModuleEventListener(ingestModuleListener);
+            IngestManager.getInstance().addIngestJobEventListener(ingestJobListener);
+            Case.addPropertyChangeListener(caseListener);
+            listeningToAutopsy = true;
+        }
+
+        try {
+            long timeLineLastObjectId = eventsRepository.getLastObjID();
+
+            boolean rebuildingRepo = false;
+            if (timeLineLastObjectId == -1) {
+                rebuildingRepo = rebuildRepo();
+            }
+            if (rebuildingRepo == false
+                    && eventsRepository.getWasIngestRunning()) {
+                if (showLastPopulatedWhileIngestingConfirmation() == JOptionPane.YES_OPTION) {
+                    rebuildingRepo = rebuildRepo();
+                }
+            }
+            final SleuthkitCase sleuthkitCase = Case.getCurrentCase().getSleuthkitCase();
+            if ((rebuildingRepo == false)
+                    && (sleuthkitCase.getLastObjectId() != timeLineLastObjectId
+                    || getCaseLastArtifactID(sleuthkitCase) != eventsRepository.getLastArtfactID())) {
+                rebuildingRepo = outOfDatePromptAndRebuild();
+            }
+
+            if (rebuildingRepo == false) {
+                showWindow();
+                showFullRange();
+            }
+
+        } catch (TskCoreException ex) {
+            LOGGER.log(Level.SEVERE, "Error when generating timeline, ", ex);
+        } catch (HeadlessException | MissingResourceException ex) {
+            LOGGER.log(Level.SEVERE, "Unexpected error when generating timeline, ", ex);
+        }
+    }
+
+    private long getCaseLastArtifactID(final SleuthkitCase sleuthkitCase) {
+        long caseLastArtfId = -1;
+        try (ResultSet runQuery = sleuthkitCase.runQuery("select Max(artifact_id) as max_id from blackboard_artifacts")) {
+            while (runQuery.next()) {
+                caseLastArtfId = runQuery.getLong("max_id");
+            }
+            sleuthkitCase.closeRunQuery(runQuery);
+        } catch (SQLException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        return caseLastArtfId;
+    }
+
+    /**
+     * request a time range the same length as the given period and centered
+     * around the middle of the currently selected range
+     *
+     * @param period
+     */
+    synchronized public void pushPeriod(ReadablePeriod period) {
+        synchronized (filteredEvents) {
+            final DateTime middleOf = IntervalUtils.middleOf(filteredEvents.timeRange().get());
+            pushTimeRange(IntervalUtils.getIntervalAround(middleOf, period));
+        }
+    }
+
+    synchronized public void pushZoomOutTime() {
+        final Interval timeRange = filteredEvents.timeRange().get();
+        long toDurationMillis = timeRange.toDurationMillis() / 4;
+        DateTime start = timeRange.getStart().minus(toDurationMillis);
+        DateTime end = timeRange.getEnd().plus(toDurationMillis);
+        pushTimeRange(new Interval(start, end));
+    }
+
+    synchronized public void pushZoomInTime() {
+        final Interval timeRange = filteredEvents.timeRange().get();
+        long toDurationMillis = timeRange.toDurationMillis() / 4;
+        DateTime start = timeRange.getStart().plus(toDurationMillis);
+        DateTime end = timeRange.getEnd().minus(toDurationMillis);
+        pushTimeRange(new Interval(start, end));
+    }
+
+    synchronized public void setViewMode(VisualizationMode visualizationMode) {
+        if (viewMode.get() != visualizationMode) {
+            viewMode.set(visualizationMode);
+        }
+    }
+
+    public void selectEventIDs(Collection<Long> events) {
+        final LoggedTask<Interval> selectEventIDsTask = new LoggedTask<Interval>("Select Event IDs", true) {
+            @Override
+            protected Interval call() throws Exception {
+                return filteredEvents.getSpanningInterval(events);
+            }
+
+            @Override
+            protected void succeeded() {
+                super.succeeded();
+                try {
+                    synchronized (TimeLineController.this) {
+                        selectedTimeRange.set(get());
+                        selectedEventIDs.setAll(events);
+                    }
+                } catch (InterruptedException ex) {
+                    Logger.getLogger(FilteredEventsModel.class
+                            .getName()).log(Level.SEVERE, getTitle() + " interrupted unexpectedly", ex);
+                } catch (ExecutionException ex) {
+                    Logger.getLogger(FilteredEventsModel.class
+                            .getName()).log(Level.SEVERE, getTitle() + " unexpectedly threw " + ex.getCause(), ex);
+                }
+            }
+        };
+
+        monitorTask(selectEventIDsTask);
+    }
+
+    /**
+     * private method to build gui if necessary and make it visible.
+     */
+    synchronized private void showWindow() {
+        if (mainFrame == null) {
+            LOGGER.log(Level.WARNING, "Tried to show timeline with invalid window. Rebuilding GUI.");
+            mainFrame = (TimeLineTopComponent) WindowManager.getDefault().findTopComponent("TimeLineTopComponent");
+            if (mainFrame == null) {
+                mainFrame = new TimeLineTopComponent();
+            }
+            mainFrame.setController(this);
+        }
+        SwingUtilities.invokeLater(() -> {
+            mainFrame.open();
+            mainFrame.setVisible(true);
+            mainFrame.toFront();
+        });
+    }
+
+    synchronized public void pushEventTypeZoom(EventTypeZoomLevel typeZoomeLevel) {
+        ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get();
+        if (currentZoom == null) {
+            pushZoom(InitialZoomState.withTypeZoomLevel(typeZoomeLevel));
+        } else if (currentZoom.hasTypeZoomLevel(typeZoomeLevel) == false) {
+            pushZoom(currentZoom.withTypeZoomLevel(typeZoomeLevel));
+        }
+    }
+
+    synchronized public void pushTimeRange(Interval timeRange) {
+//        timeRange = this.filteredEvents.getSpanningInterval().overlap(timeRange);
+        ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get();
+        if (currentZoom == null) {
+            pushZoom(InitialZoomState.withTimeRange(timeRange));
+        } else if (currentZoom.hasTimeRange(timeRange) == false) {
+            pushZoom(currentZoom.withTimeRange(timeRange));
+        }
+    }
+
+    synchronized public void pushDescrLOD(DescriptionLOD newLOD) {
+        ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get();
+        if (currentZoom == null) {
+            pushZoom(InitialZoomState.withDescrLOD(newLOD));
+        } else if (currentZoom.hasDescrLOD(newLOD) == false) {
+            pushZoom(currentZoom.withDescrLOD(newLOD));
+        }
+    }
+
+    synchronized public void pushTimeAndType(Interval timeRange, EventTypeZoomLevel typeZoom) {
+        timeRange = this.filteredEvents.getSpanningInterval().overlap(timeRange);
+        ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get();
+        if (currentZoom == null) {
+            pushZoom(InitialZoomState.withTimeAndType(timeRange, typeZoom));
+        } else if (currentZoom.hasTimeRange(timeRange) == false && currentZoom.hasTypeZoomLevel(typeZoom) == false) {
+            pushZoom(currentZoom.withTimeAndType(timeRange, typeZoom));
+        } else if (currentZoom.hasTimeRange(timeRange) == false) {
+            pushZoom(currentZoom.withTimeRange(timeRange));
+        } else if (currentZoom.hasTypeZoomLevel(typeZoom) == false) {
+            pushZoom(currentZoom.withTypeZoomLevel(typeZoom));
+        }
+    }
+
+    synchronized public void pushFilters(Filter filter) {
+        ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get();
+        if (currentZoom == null) {
+            pushZoom(InitialZoomState.withFilter(filter.copyOf()));
+        } else if (currentZoom.hasFilter(filter) == false) {
+            pushZoom(currentZoom.withFilter(filter.copyOf()));
+        }
+    }
+
+    synchronized public ZoomParams goForward() {
+
+        final ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get();
+
+        final ZoomParams fpeek = forwardStack.peek();
+
+        if (fpeek != null && currentZoom.equals(fpeek) == false) {
+            historyStack.push(currentZoom);
+            filteredEvents.requestZoomState(fpeek, false);
+            forwardStack.pop();
+        }
+        return fpeek;
+    }
+
+    synchronized public ZoomParams goBack() {
+        final ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get();
+        final ZoomParams peek = historyStack.peek();
+
+        if (peek != null && peek.equals(currentZoom) == false) {
+            forwardStack.push(currentZoom);
+            filteredEvents.requestZoomState(historyStack.pop(), false);
+        } else if (peek != null && peek.equals(currentZoom)) {
+            historyStack.pop();
+            return goBack();
+        }
+        return peek;
+    }
+
+    @Deprecated
+    synchronized public void popZoomUpTo(ZoomParams zCrumb) {
+        ZoomParams popped = historyStack.peek();
+
+        while (popped.equals(zCrumb) == false) {
+            popped = historyStack.pop();
+        }
+        pushZoom(zCrumb);
+    }
+
+    synchronized private void pushZoom(ZoomParams zCrumb, boolean force) {
+        final ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get();
+
+        if (force || currentZoom.equals(zCrumb) == false) {
+            historyStack.push(currentZoom);
+            filteredEvents.requestZoomState(zCrumb, force);
+            if (zCrumb.equals(forwardStack.peek())) {
+                forwardStack.pop();
+            } else {
+                forwardStack.clear();
+            }
+        }
+    }
+
+    synchronized private void pushZoom(ZoomParams zCrumb) {
+        pushZoom(zCrumb, false);
+    }
+
+    public void selectTimeAndType(Interval interval, EventType type) {
+        final Interval timeRange = filteredEvents.getSpanningInterval().overlap(interval);
+
+        final LoggedTask<Collection<Long>> selectTimeAndTypeTask = new LoggedTask<Collection<Long>>("Select Time and Type", true) {
+            @Override
+            protected Collection< Long> call() throws Exception {
+                synchronized (TimeLineController.this) {
+                    return filteredEvents.getEventIDs(timeRange, new TypeFilter(type));
+                }
+            }
+
+            @Override
+            protected void succeeded() {
+                super.succeeded();
+                try {
+                    synchronized (TimeLineController.this) {
+                        selectedTimeRange.set(timeRange);
+                        selectedEventIDs.setAll(get());
+                    }
+                } catch (InterruptedException ex) {
+                    Logger.getLogger(FilteredEventsModel.class
+                            .getName()).log(Level.SEVERE, getTitle() + " interrupted unexpectedly", ex);
+                } catch (ExecutionException ex) {
+                    Logger.getLogger(FilteredEventsModel.class
+                            .getName()).log(Level.SEVERE, getTitle() + " unexpectedly threw " + ex.getCause(), ex);
+                }
+            }
+        };
+
+        monitorTask(selectTimeAndTypeTask);
+    }
+
+    synchronized public void monitorTask(final Task<?> task) {
+        if (task != null) {
+            Platform.runLater(() -> {
+
+                //is this actually threadsafe, could we get a finished task stuck in the list?
+                task.stateProperty().addListener((Observable observable) -> {
+                    switch (task.getState()) {
+                        case READY:
+                        case RUNNING:
+                        case SCHEDULED:
+                            break;
+                        case SUCCEEDED:
+                        case CANCELLED:
+                        case FAILED:
+                            tasks.remove(task);
+                            if (tasks.isEmpty() == false) {
+                                progress.bind(tasks.get(0).progressProperty());
+                                message.bind(tasks.get(0).messageProperty());
+                                taskTitle.bind(tasks.get(0).titleProperty());
+                            }
+                            break;
+                    }
+                });
+                tasks.add(task);
+                progress.bind(task.progressProperty());
+                message.bind(task.messageProperty());
+                taskTitle.bind(task.titleProperty());
+                switch (task.getState()) {
+                    case READY:
+                        executor.submit(task);
+                        break;
+                    case SCHEDULED:
+                    case RUNNING:
+                    case SUCCEEDED:
+                    case CANCELLED:
+                    case FAILED:
+                        break;
+                }
+            });
+        }
+    }
+
+    static synchronized public void setTimeZone(TimeZone timeZone) {
+        TimeLineController.timeZone.set(timeZone);
+    }
+
+    Interval getSpanningInterval(Collection<Long> eventIDs) {
+        return filteredEvents.getSpanningInterval(eventIDs);
+
+    }
+
+    /**
+     * prompt the user to rebuild and then rebuild if the user chooses to
+     */
+    synchronized public boolean outOfDatePromptAndRebuild() {
+        return showOutOfDateConfirmation() == JOptionPane.YES_OPTION
+               ? rebuildRepo()
+               : false;
+    }
+
+    synchronized int showLastPopulatedWhileIngestingConfirmation() {
+        return JOptionPane.showConfirmDialog(mainFrame,
+                                             DO_REPOPULATE_MESSAGE,
+                                             "re populate events?",
+                                             JOptionPane.YES_NO_OPTION,
+                                             JOptionPane.QUESTION_MESSAGE);
+
+    }
+
+    synchronized int showOutOfDateConfirmation() throws MissingResourceException, HeadlessException {
+        return JOptionPane.showConfirmDialog(mainFrame,
+                                             NbBundle.getMessage(TimeLineController.class,
+                                                                 "AdvTimeline.propChg.confDlg.timelineOOD.msg"),
+                                             NbBundle.getMessage(TimeLineController.class,
+                                                                 "AdvTimeline.propChg.confDlg.timelineOOD.details"),
+                                             JOptionPane.YES_NO_OPTION);
+    }
+
+    synchronized int showIngestConfirmation() throws MissingResourceException, HeadlessException {
+        return JOptionPane.showConfirmDialog(mainFrame,
+                                             NbBundle.getMessage(TimeLineController.class,
+                                                                 "AdvTimeline.initTimeline.confDlg.genBeforeIngest.msg"),
+                                             NbBundle.getMessage(TimeLineController.class,
+                                                                 "AdvTimeline.initTimeline.confDlg.genBeforeIngest.details"),
+                                             JOptionPane.YES_NO_OPTION);
+    }
+
+    private class AutopsyIngestModuleListener implements PropertyChangeListener {
+
+        @Override
+        public void propertyChange(PropertyChangeEvent evt) {
+            switch (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName())) {
+                case CONTENT_CHANGED:
+//                    ((ModuleContentEvent)evt.getOldValue())????
+                    //ModuleContentEvent doesn't seem to provide any usefull information...
+                    break;
+                case DATA_ADDED:
+//                    Collection<BlackboardArtifact> artifacts = ((ModuleDataEvent) evt.getOldValue()).getArtifacts();
+                    //new artifacts, insert them into db
+                    break;
+                case FILE_DONE:
+//                    Long fileID = (Long) evt.getOldValue();
+                    //update file (known status) for file with id
+                    Platform.runLater(() -> {
+                        newEventsFlag.set(true);
+                    });
+                    break;
+            }
+        }
+    }
+
+    @Immutable
+    private class AutopsyIngestJobListener implements PropertyChangeListener {
+
+        @Override
+        public void propertyChange(PropertyChangeEvent evt) {
+            switch (IngestManager.IngestJobEvent.valueOf(evt.getPropertyName())) {
+                case CANCELLED:
+                case COMPLETED:
+                    //if we are doing incremental updates, drop this
+                    outOfDatePromptAndRebuild();
+                    break;
+            }
+        }
+    }
+
+    @Immutable
+    class AutopsyCaseListener implements PropertyChangeListener {
+
+        @Override
+        public void propertyChange(PropertyChangeEvent evt) {
+            switch (Case.Events.valueOf(evt.getPropertyName())) {
+                case DATA_SOURCE_ADDED:
+//                    Content content = (Content) evt.getNewValue();
+                    //if we are doing incremental updates, drop this
+                    outOfDatePromptAndRebuild();
+                    break;
+                case CURRENT_CASE:
+                    OpenTimelineAction.invalidateController();
+                    closeTimeLine();
+                    break;
+            }
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineException.java b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineException.java
new file mode 100644
index 0000000000000000000000000000000000000000..82aa4df51f2908dc3f9b578d3da93db5d7f4d01d
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineException.java
@@ -0,0 +1,33 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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;
+
+/**
+ *
+ */
+public class TimeLineException extends Exception {
+
+    public TimeLineException(String string, Exception e) {
+        super(string, e);
+    }
+
+    public TimeLineException(String string) {
+        super(string);
+    }
+}
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimelineFrame.form b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.form
similarity index 75%
rename from Core/src/org/sleuthkit/autopsy/timeline/TimelineFrame.form
rename to Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.form
index 545399bf0dfbce86954fd2126b71d91d1faef608..1f0f367115b175ab2204726be65ee489e31a3880 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/TimelineFrame.form
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.form
@@ -1,19 +1,6 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 
-<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JFrameFormInfo">
-  <Properties>
-    <Property name="defaultCloseOperation" type="int" value="2"/>
-    <Property name="title" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
-      <ResourceString bundle="org/sleuthkit/autopsy/timeline/Bundle.properties" key="TimelineFrame.title" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
-    </Property>
-    <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
-      <Dimension value="[1200, 700]"/>
-    </Property>
-  </Properties>
-  <SyntheticProperties>
-    <SyntheticProperty name="formSizePolicy" type="int" value="1"/>
-    <SyntheticProperty name="generateCenter" type="boolean" value="false"/>
-  </SyntheticProperties>
+<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
   <AuxValues>
     <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
     <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
@@ -29,21 +16,37 @@
   <Layout>
     <DimensionLayout dim="0">
       <Group type="103" groupAlignment="0" attributes="0">
-          <Component id="splitYPane" alignment="1" max="32767" attributes="0"/>
+          <Component id="splitYPane" alignment="0" pref="972" max="32767" attributes="0"/>
+          <Component id="jFXstatusPanel" alignment="0" max="32767" attributes="0"/>
       </Group>
     </DimensionLayout>
     <DimensionLayout dim="1">
       <Group type="103" groupAlignment="0" attributes="0">
-          <Component id="splitYPane" alignment="1" max="32767" attributes="0"/>
+          <Group type="102" alignment="0" attributes="0">
+              <Component id="splitYPane" pref="482" max="32767" attributes="0"/>
+              <EmptySpace min="0" pref="0" max="-2" attributes="0"/>
+              <Component id="jFXstatusPanel" min="-2" pref="29" max="-2" attributes="0"/>
+          </Group>
       </Group>
     </DimensionLayout>
   </Layout>
   <SubComponents>
+    <Container class="javafx.embed.swing.JFXPanel" name="jFXstatusPanel">
+      <Properties>
+        <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
+          <Dimension value="[100, 16]"/>
+        </Property>
+      </Properties>
+
+      <Layout class="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout">
+        <Property name="useNullLayout" type="boolean" value="true"/>
+      </Layout>
+    </Container>
     <Container class="javax.swing.JSplitPane" name="splitYPane">
       <Properties>
         <Property name="dividerLocation" type="int" value="420"/>
         <Property name="orientation" type="int" value="0"/>
-        <Property name="resizeWeight" type="double" value="0.5"/>
+        <Property name="resizeWeight" type="double" value="0.9"/>
         <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
           <Dimension value="[1024, 400]"/>
         </Property>
@@ -51,21 +54,18 @@
 
       <Layout class="org.netbeans.modules.form.compat2.layouts.support.JSplitPaneSupportLayout"/>
       <SubComponents>
-        <Container class="javax.swing.JPanel" name="topPane">
-          <Properties>
-            <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
-              <Dimension value="[1200, 400]"/>
-            </Property>
-          </Properties>
+        <Container class="javafx.embed.swing.JFXPanel" name="jFXVizPanel">
           <Constraints>
             <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.support.JSplitPaneSupportLayout" value="org.netbeans.modules.form.compat2.layouts.support.JSplitPaneSupportLayout$JSplitPaneConstraintsDescription">
               <JSplitPaneConstraints position="left"/>
             </Constraint>
           </Constraints>
 
-          <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/>
+          <Layout class="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout">
+            <Property name="useNullLayout" type="boolean" value="true"/>
+          </Layout>
         </Container>
-        <Container class="javax.swing.JSplitPane" name="splitXPane">
+        <Container class="javax.swing.JSplitPane" name="lowerSplitXPane">
           <Properties>
             <Property name="dividerLocation" type="int" value="600"/>
             <Property name="resizeWeight" type="double" value="0.5"/>
@@ -82,7 +82,7 @@
 
           <Layout class="org.netbeans.modules.form.compat2.layouts.support.JSplitPaneSupportLayout"/>
           <SubComponents>
-            <Container class="javax.swing.JPanel" name="leftPane">
+            <Container class="javax.swing.JPanel" name="resultContainerPanel">
               <Properties>
                 <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
                   <Dimension value="[700, 300]"/>
@@ -96,7 +96,7 @@
 
               <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/>
             </Container>
-            <Container class="javax.swing.JPanel" name="rightPane">
+            <Container class="javax.swing.JPanel" name="contentViewerContainerPanel">
               <Properties>
                 <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
                   <Dimension value="[500, 300]"/>
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java
new file mode 100644
index 0000000000000000000000000000000000000000..f326576862bc05ab783a8c5a490b751fc8b7e5e6
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java
@@ -0,0 +1,287 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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;
+
+import java.awt.BorderLayout;
+import java.util.Collections;
+import java.util.List;
+import javafx.application.Platform;
+import javafx.beans.Observable;
+import javafx.event.ActionEvent;
+import javafx.scene.Scene;
+import javafx.scene.control.SplitPane;
+import javafx.scene.control.Tab;
+import javafx.scene.control.TabPane;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCodeCombination;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import org.netbeans.api.settings.ConvertAsProperties;
+import org.openide.explorer.ExplorerManager;
+import org.openide.explorer.ExplorerUtils;
+import org.openide.util.NbBundle.Messages;
+import org.openide.windows.Mode;
+import org.openide.windows.TopComponent;
+import static org.openide.windows.TopComponent.PROP_UNDOCKING_DISABLED;
+import org.openide.windows.WindowManager;
+import org.sleuthkit.autopsy.corecomponents.DataContentPanel;
+import org.sleuthkit.autopsy.corecomponents.DataResultPanel;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.timeline.actions.Back;
+import org.sleuthkit.autopsy.timeline.actions.Forward;
+import org.sleuthkit.autopsy.timeline.ui.StatusBar;
+import org.sleuthkit.autopsy.timeline.ui.TimeLineResultView;
+import org.sleuthkit.autopsy.timeline.ui.TimeZonePanel;
+import org.sleuthkit.autopsy.timeline.ui.VisualizationPanel;
+import org.sleuthkit.autopsy.timeline.ui.detailview.tree.NavPanel;
+import org.sleuthkit.autopsy.timeline.ui.filtering.FilterSetPanel;
+import org.sleuthkit.autopsy.timeline.zooming.ZoomSettingsPane;
+
+/**
+ * TopComponent for the advanced timeline module.
+ */
+@ConvertAsProperties(
+        dtd = "-//org.sleuthkit.autopsy.timeline//TimeLine//EN",
+        autostore = false)
+@TopComponent.Description(
+        preferredID = "TimeLineTopComponent",
+        //iconBase="SET/PATH/TO/ICON/HERE", 
+        persistenceType = TopComponent.PERSISTENCE_NEVER)
+@TopComponent.Registration(mode = "timeline", openAtStartup = false)
+@Messages({
+    "CTL_TimeLineTopComponentAction=TimeLineTopComponent",
+    "CTL_TimeLineTopComponent=Timeline Window",
+    "HINT_TimeLineTopComponent=This is a Timeline window"
+})
+public final class TimeLineTopComponent extends TopComponent implements ExplorerManager.Provider, TimeLineUI {
+
+    private static final Logger LOGGER = Logger.getLogger(TimeLineTopComponent.class.getName());
+
+    private DataContentPanel dataContentPanel;
+
+    private TimeLineResultView tlrv;
+
+    private final ExplorerManager em = new ExplorerManager();
+
+    private TimeLineController controller;
+
+    ////jfx componenets that make up the interface
+    private final FilterSetPanel filtersPanel = new FilterSetPanel();
+
+    private final Tab eventsTab = new Tab("Events");
+
+    private final Tab filterTab = new Tab("Filters");
+
+    private final VBox leftVBox = new VBox(5);
+
+    private final NavPanel navPanel = new NavPanel();
+
+    private final StatusBar statusBar = new StatusBar();
+
+    private final TabPane tabPane = new TabPane();
+
+    private final ZoomSettingsPane zoomSettingsPane = new ZoomSettingsPane();
+
+    private final VisualizationPanel visualizationPanel = new VisualizationPanel(navPanel);
+
+    private final SplitPane splitPane = new SplitPane();
+
+    private final TimeZonePanel timeZonePanel = new TimeZonePanel();
+
+    public TimeLineTopComponent() {
+        initComponents();
+
+        associateLookup(ExplorerUtils.createLookup(em, getActionMap()));
+
+        setName(Bundle.CTL_TimeLineTopComponent());
+        setToolTipText(Bundle.HINT_TimeLineTopComponent());
+        setIcon(WindowManager.getDefault().getMainWindow().getIconImage()); //use the same icon as main application
+
+        customizeComponents();
+    }
+
+    synchronized private void customizeComponents() {
+
+        dataContentPanel = DataContentPanel.createInstance();
+        this.contentViewerContainerPanel.add(dataContentPanel, BorderLayout.CENTER);
+        tlrv = new TimeLineResultView(dataContentPanel);
+        DataResultPanel dataResultPanel = tlrv.getDataResultPanel();
+        this.resultContainerPanel.add(dataResultPanel, BorderLayout.CENTER);
+        dataResultPanel.open();
+
+        Platform.runLater(() -> {
+            //assemble ui componenets together
+            jFXstatusPanel.setScene(new Scene(statusBar));
+
+            splitPane.setDividerPositions(0);
+            jFXVizPanel.setScene(new Scene(splitPane));
+
+            filterTab.setClosable(false);
+            filterTab.setContent(filtersPanel);
+            filterTab.setGraphic(new ImageView("org/sleuthkit/autopsy/timeline/images/funnel.png"));
+
+            eventsTab.setClosable(false);
+            eventsTab.setContent(navPanel);
+            eventsTab.setGraphic(new ImageView("org/sleuthkit/autopsy/timeline/images/timeline_marker.png"));
+
+            tabPane.getTabs().addAll(filterTab, eventsTab);
+            VBox.setVgrow(tabPane, Priority.ALWAYS);
+
+            VBox.setVgrow(timeZonePanel, Priority.SOMETIMES);
+            leftVBox.getChildren().addAll(timeZonePanel, zoomSettingsPane, tabPane);
+
+            SplitPane.setResizableWithParent(leftVBox, Boolean.FALSE);
+            splitPane.getItems().addAll(leftVBox, visualizationPanel);
+
+        });
+    }
+
+    public synchronized void setController(TimeLineController controller) {
+        this.controller = controller;
+
+        tlrv.setController(controller);
+        Platform.runLater(() -> {
+            jFXVizPanel.getScene().addEventFilter(KeyEvent.KEY_PRESSED, (
+                                                  KeyEvent event) -> {
+                                                      if (new KeyCodeCombination(KeyCode.LEFT, KeyCodeCombination.ALT_DOWN).match(event)) {
+                                                          new Back(controller).handle(new ActionEvent());
+                                                      } else if (new KeyCodeCombination(KeyCode.BACK_SPACE).match(event)) {
+                                                          new Back(controller).handle(new ActionEvent());
+                                                      } else if (new KeyCodeCombination(KeyCode.RIGHT, KeyCodeCombination.ALT_DOWN).match(event)) {
+                                                          new Forward(controller).handle(new ActionEvent());
+                                                      } else if (new KeyCodeCombination(KeyCode.BACK_SPACE, KeyCodeCombination.SHIFT_DOWN).match(event)) {
+                                                          new Forward(controller).handle(new ActionEvent());
+                                                      }
+                                                  });
+            controller.getViewMode().addListener((Observable observable) -> {
+                if (controller.getViewMode().get().equals(VisualizationMode.COUNTS)) {
+                    tabPane.getSelectionModel().select(filterTab);
+                }
+            });
+            eventsTab.disableProperty().bind(controller.getViewMode().isEqualTo(VisualizationMode.COUNTS));
+            visualizationPanel.setController(controller);
+            navPanel.setController(controller);
+            filtersPanel.setController(controller);
+            zoomSettingsPane.setController(controller);
+            statusBar.setController(controller);
+        });
+    }
+
+    @Override
+    public List<Mode> availableModes(List<Mode> modes) {
+        return Collections.emptyList();
+    }
+
+    /**
+     * This method is called from within the constructor to initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is always
+     * regenerated by the Form Editor.
+     */
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        jFXstatusPanel = new javafx.embed.swing.JFXPanel();
+        splitYPane = new javax.swing.JSplitPane();
+        jFXVizPanel = new javafx.embed.swing.JFXPanel();
+        lowerSplitXPane = new javax.swing.JSplitPane();
+        resultContainerPanel = new javax.swing.JPanel();
+        contentViewerContainerPanel = new javax.swing.JPanel();
+
+        jFXstatusPanel.setPreferredSize(new java.awt.Dimension(100, 16));
+
+        splitYPane.setDividerLocation(420);
+        splitYPane.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT);
+        splitYPane.setResizeWeight(0.9);
+        splitYPane.setPreferredSize(new java.awt.Dimension(1024, 400));
+        splitYPane.setLeftComponent(jFXVizPanel);
+
+        lowerSplitXPane.setDividerLocation(600);
+        lowerSplitXPane.setResizeWeight(0.5);
+        lowerSplitXPane.setPreferredSize(new java.awt.Dimension(1200, 300));
+        lowerSplitXPane.setRequestFocusEnabled(false);
+
+        resultContainerPanel.setPreferredSize(new java.awt.Dimension(700, 300));
+        resultContainerPanel.setLayout(new java.awt.BorderLayout());
+        lowerSplitXPane.setLeftComponent(resultContainerPanel);
+
+        contentViewerContainerPanel.setPreferredSize(new java.awt.Dimension(500, 300));
+        contentViewerContainerPanel.setLayout(new java.awt.BorderLayout());
+        lowerSplitXPane.setRightComponent(contentViewerContainerPanel);
+
+        splitYPane.setRightComponent(lowerSplitXPane);
+
+        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
+        this.setLayout(layout);
+        layout.setHorizontalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addComponent(splitYPane, javax.swing.GroupLayout.DEFAULT_SIZE, 972, Short.MAX_VALUE)
+            .addGroup(layout.createSequentialGroup()
+                .addGap(0, 0, 0)
+                .addComponent(jFXstatusPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
+                .addGap(0, 0, 0))
+        );
+        layout.setVerticalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addComponent(splitYPane, javax.swing.GroupLayout.DEFAULT_SIZE, 482, Short.MAX_VALUE)
+                .addGap(0, 0, 0)
+                .addComponent(jFXstatusPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 29, javax.swing.GroupLayout.PREFERRED_SIZE))
+        );
+    }// </editor-fold>//GEN-END:initComponents
+
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JPanel contentViewerContainerPanel;
+    private javafx.embed.swing.JFXPanel jFXVizPanel;
+    private javafx.embed.swing.JFXPanel jFXstatusPanel;
+    private javax.swing.JSplitPane lowerSplitXPane;
+    private javax.swing.JPanel resultContainerPanel;
+    private javax.swing.JSplitPane splitYPane;
+    // End of variables declaration//GEN-END:variables
+
+    @Override
+    public void componentOpened() {
+        WindowManager.getDefault().setTopComponentFloating(this, true);
+
+        putClientProperty(PROP_UNDOCKING_DISABLED, true);
+    }
+
+    @Override
+    public void componentClosed() {
+        // TODO add custom code on component closing
+    }
+
+    void writeProperties(java.util.Properties p) {
+        // better to version settings since initial version as advocated at
+        // http://wiki.apidesign.org/wiki/PropertyFiles
+        p.setProperty("version", "1.0");
+        // TODO store your settings
+    }
+
+    void readProperties(java.util.Properties p) {
+        String version = p.getProperty("version");
+        // TODO read your settings according to their version
+    }
+
+    @Override
+    public ExplorerManager getExplorerManager() {
+        return em;
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineUI.java b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineUI.java
new file mode 100644
index 0000000000000000000000000000000000000000..bb14f76aeee6a38639bb8891b4281202dcafcff8
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineUI.java
@@ -0,0 +1,27 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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;
+
+/**
+ *
+ */
+public interface TimeLineUI {
+
+    void setController(TimeLineController controller);
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineView.java b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineView.java
new file mode 100644
index 0000000000000000000000000000000000000000..a29058ac6fa17a9251ce94aef9577103d145c121
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineView.java
@@ -0,0 +1,35 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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;
+
+/** Interface to be implemented by views of the data.
+ *
+ * Most implementations should install the relevant listeners in their
+ * {@link #setController} and {@link #setModel} methods */
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+
+public interface TimeLineView extends TimeLineUI {
+
+    @Override
+    void setController(TimeLineController controller);
+
+    void setModel(final FilteredEventsModel filteredEvents);
+
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/VisualizationMode.java b/Timeline/src/org/sleuthkit/autopsy/timeline/VisualizationMode.java
new file mode 100644
index 0000000000000000000000000000000000000000..8990b792bce6ac1ad39174bdfea97f37b19a8ecb
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/VisualizationMode.java
@@ -0,0 +1,27 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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;
+
+/**
+ *
+ */
+public enum VisualizationMode {
+
+    COUNTS, DETAIL;
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/actions/Back.java b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/Back.java
new file mode 100644
index 0000000000000000000000000000000000000000..b25c79b55cb283b9e9a718700ab907f189f7c33b
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/Back.java
@@ -0,0 +1,50 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.actions;
+
+import javafx.event.ActionEvent;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCodeCombination;
+import org.controlsfx.control.action.AbstractAction;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+
+/**
+ *
+ */
+public class Back extends AbstractAction {
+
+    private static final Image BACK_IMAGE = new Image("/org/sleuthkit/autopsy/timeline/images/arrow-180.png", 16, 16, true, true, true);
+
+    private final TimeLineController controller;
+
+    public Back(TimeLineController controller) {
+        super("Back");
+        setGraphic(new ImageView(BACK_IMAGE));
+        setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCodeCombination.ALT_DOWN));
+        this.controller = controller;
+        disabledProperty().bind(controller.getHistoryStack().sizeProperty().isEqualTo(0));
+    }
+
+    @Override
+    public void handle(ActionEvent ae) {
+        controller.goBack();
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/actions/DefaultFilters.java b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/DefaultFilters.java
new file mode 100644
index 0000000000000000000000000000000000000000..f9182bc90aee9e868001a2a4726f8c2157f4f4ba
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/DefaultFilters.java
@@ -0,0 +1,57 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.actions;
+
+import javafx.beans.binding.BooleanBinding;
+import javafx.event.ActionEvent;
+import org.controlsfx.control.action.AbstractAction;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+import org.sleuthkit.autopsy.timeline.filters.Filter;
+
+/**
+ *
+ */
+public class DefaultFilters extends AbstractAction {
+
+    private final TimeLineController controller;
+
+    private FilteredEventsModel eventsModel;
+
+    public DefaultFilters(final TimeLineController controller) {
+        super("apply default filters");
+        this.controller = controller;
+        eventsModel = controller.getEventsModel();
+        disabledProperty().bind(new BooleanBinding() {
+            {
+                bind(eventsModel.getRequestedZoomParamters());
+            }
+
+            @Override
+            protected boolean computeValue() {
+                return eventsModel.getRequestedZoomParamters().getValue().getFilter().equals(Filter.getDefaultFilter());
+            }
+        });
+    }
+
+    @Override
+    public void handle(ActionEvent event) {
+        controller.applyDefaultFilters();
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/actions/Forward.java b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/Forward.java
new file mode 100644
index 0000000000000000000000000000000000000000..d68a3e4734b1663447e14c6c8836fda7cfef57ea
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/Forward.java
@@ -0,0 +1,50 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.actions;
+
+import javafx.event.ActionEvent;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCodeCombination;
+import org.controlsfx.control.action.AbstractAction;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+
+/**
+ *
+ */
+public class Forward extends AbstractAction {
+
+    private static final Image BACK_IMAGE = new Image("/org/sleuthkit/autopsy/timeline/images/arrow.png", 16, 16, true, true, true);
+
+    private final TimeLineController controller;
+
+    public Forward(TimeLineController controller) {
+        super("Forward");
+        setGraphic(new ImageView(BACK_IMAGE));
+        setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCodeCombination.ALT_DOWN));
+        this.controller = controller;
+        disabledProperty().bind(controller.getForwardStack().sizeProperty().isEqualTo(0));
+    }
+
+    @Override
+    public void handle(ActionEvent ae) {
+        controller.goForward();
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshot.java b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshot.java
new file mode 100644
index 0000000000000000000000000000000000000000..0be925b4eb32048dca126a97cb53026cebe9958e
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshot.java
@@ -0,0 +1,134 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.actions;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import javafx.embed.swing.SwingFXUtils;
+import javafx.event.ActionEvent;
+import javafx.scene.image.WritableImage;
+import javafx.stage.DirectoryChooser;
+import javafx.util.Pair;
+import javax.imageio.ImageIO;
+import org.controlsfx.control.action.AbstractAction;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ */
+public class SaveSnapshot extends AbstractAction {
+
+    private static final String HTML_EXT = ".html";
+
+    private static final String REPORT_IMAGE_EXTENSION = ".png";
+
+    private static final Logger LOGGER = Logger.getLogger(SaveSnapshot.class.getName());
+
+    private final TimeLineController controller;
+
+    private final WritableImage snapshot;
+
+    public SaveSnapshot(TimeLineController controller, WritableImage snapshot) {
+        super("save snapshot");
+        this.controller = controller;
+        this.snapshot = snapshot;
+    }
+
+    @Override
+    public void handle(ActionEvent event) {
+        //choose location/name
+        DirectoryChooser fileChooser = new DirectoryChooser();
+        fileChooser.setTitle("Save snapshot to");
+        fileChooser.setInitialDirectory(new File(Case.getCurrentCase().getCaseDirectory() + File.separator + "Reports"));
+        File outFolder = fileChooser.showDialog(null);
+        if (outFolder == null) {
+            return;
+        }
+        outFolder.mkdir();
+        String name = outFolder.getName();
+
+        //gather metadata
+        List<Pair<String, String>> reportMetaData = new ArrayList<>();
+
+        reportMetaData.add(new Pair<>("Case", Case.getCurrentCase().getName()));
+
+        ZoomParams get = controller.getEventsModel().getRequestedZoomParamters().get();
+        reportMetaData.add(new Pair<>("Time Range", get.getTimeRange().toString()));
+        reportMetaData.add(new Pair<>("Description Level of Detail", get.getDescrLOD().getDisplayName()));
+        reportMetaData.add(new Pair<>("Event Type Zoom Level", get.getTypeZoomLevel().getDisplayName()));
+        reportMetaData.add(new Pair<>("Filters", get.getFilter().getHTMLReportString()));
+
+        //save snapshot as png
+        try {
+            ImageIO.write(SwingFXUtils.fromFXImage(snapshot, null), "png", new File(outFolder.getPath() + File.separator + outFolder.getName() + REPORT_IMAGE_EXTENSION));
+        } catch (IOException ex) {
+            LOGGER.log(Level.WARNING, "failed to write snapshot to disk", ex);
+            return;
+        }
+
+        //build html string
+        StringBuilder wrapper = new StringBuilder();
+        wrapper.append("<html>\n<head>\n\t<title>").append("timeline snapshot").append("</title>\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"index.css\" />\n</head>\n<body>\n");
+        wrapper.append("<div id=\"content\">\n<h1>").append(outFolder.getName()).append("</h1>\n");
+        wrapper.append("<img src = \"").append(outFolder.getName()).append(REPORT_IMAGE_EXTENSION + "\" alt = \"snaphot\">");
+        wrapper.append("<table>\n");
+        for (Pair<String, String> pair : reportMetaData) {
+            wrapper.append("<tr><td>").append(pair.getKey()).append(": </td><td>").append(pair.getValue()).append("</td></tr>\n");
+        }
+        wrapper.append("</table>\n");
+        wrapper.append("</div>\n</body>\n</html>");
+
+        //write html wrapper
+        try (Writer htmlWriter = new FileWriter(new File(outFolder, name + HTML_EXT))) {
+            htmlWriter.write(wrapper.toString());
+        } catch (FileNotFoundException ex) {
+            LOGGER.log(Level.WARNING, "failed to open html wrapper file for writing ", ex);
+            return;
+        } catch (IOException ex) {
+            LOGGER.log(Level.WARNING, "failed to write html wrapper file", ex);
+            return;
+        }
+
+        //copy css
+        try (InputStream resource = this.getClass().getResourceAsStream("/org/sleuthkit/autopsy/timeline/index.css")) {
+            Files.copy(resource, Paths.get(outFolder.getPath(), "index.css"));
+        } catch (IOException ex) {
+            LOGGER.log(Level.WARNING, "failed to copy css file", ex);
+        }
+
+        //add html file as report to case
+        try {
+            Case.getCurrentCase().addReport(outFolder.getPath() + File.separator + outFolder.getName() + HTML_EXT, "Timeline", outFolder.getName() + HTML_EXT);
+        } catch (TskCoreException ex) {
+            LOGGER.log(Level.WARNING, "failed add html wrapper as a report", ex);
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/actions/SwingMenuItemAdapter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/SwingMenuItemAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..85f7e5552d3118e2c42225372c74c097690d3271
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/SwingMenuItemAdapter.java
@@ -0,0 +1,91 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 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.actions;
+
+import javafx.event.ActionEvent;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuItem;
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.JPopupMenu;
+import javax.swing.MenuElement;
+import javax.swing.SwingUtilities;
+
+public class SwingMenuItemAdapter extends MenuItem {
+
+    JMenuItem jMenuItem;
+
+    SwingMenuItemAdapter(final JMenuItem jMenuItem) {
+        super(jMenuItem.getText());
+        this.jMenuItem = jMenuItem;
+        setOnAction((ActionEvent t) -> {
+            SwingUtilities.invokeLater(() -> {
+                jMenuItem.doClick();
+            });
+
+        });
+    }
+
+    public static MenuItem create(MenuElement jmenuItem) {
+        if (jmenuItem instanceof JMenu) {
+            return new SwingMenuAdapter((JMenu) jmenuItem);
+        } else if (jmenuItem instanceof JPopupMenu) {
+            return new SwingMenuAdapter((JPopupMenu) jmenuItem);
+        } else {
+            return new SwingMenuItemAdapter((JMenuItem) jmenuItem);
+        }
+
+    }
+}
+
+class SwingMenuAdapter extends Menu {
+
+    private final MenuElement jMenu;
+
+    SwingMenuAdapter(final JMenu jMenu) {
+        super(jMenu.getText());
+        this.jMenu = jMenu;
+        buildChildren(jMenu);
+
+    }
+
+    SwingMenuAdapter(JPopupMenu jPopupMenu) {
+        super(jPopupMenu.getLabel());
+        this.jMenu = jPopupMenu;
+
+        buildChildren(jMenu);
+    }
+
+    private void buildChildren(MenuElement jMenu) {
+
+        for (MenuElement menuE : jMenu.getSubElements()) {
+            if (menuE instanceof JMenu) {
+                getItems().add(SwingMenuItemAdapter.create((JMenu) menuE));
+            } else if (menuE instanceof JMenuItem) {
+                getItems().add(SwingMenuItemAdapter.create((JMenuItem) menuE));
+            } else if (menuE instanceof JPopupMenu) {
+                buildChildren(menuE);
+            } else {
+
+                System.out.println(menuE.toString());
+//                throw new UnsupportedOperationException();
+            }
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java
new file mode 100644
index 0000000000000000000000000000000000000000..3925efc9f3a858e73c6f59c919fc8c5bc5fea05d
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java
@@ -0,0 +1,56 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.actions;
+
+import javafx.beans.binding.BooleanBinding;
+import javafx.event.ActionEvent;
+import org.controlsfx.control.action.AbstractAction;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+
+/**
+ *
+ */
+public class ZoomOut extends AbstractAction {
+
+    private final TimeLineController controller;
+
+    private final FilteredEventsModel eventsModel;
+
+    public ZoomOut(final TimeLineController controller) {
+        super("apply default filters");
+        this.controller = controller;
+        eventsModel = controller.getEventsModel();
+        disabledProperty().bind(new BooleanBinding() {
+            {
+                bind(eventsModel.getRequestedZoomParamters());
+            }
+
+            @Override
+            protected boolean computeValue() {
+                return eventsModel.getRequestedZoomParamters().getValue().getTimeRange().contains(eventsModel.getSpanningInterval());
+            }
+        });
+    }
+
+    @Override
+    public void handle(ActionEvent event) {
+        controller.zoomOutToActivity();
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/AggregateEvent.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/AggregateEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..c97008328d5762c39b32c408aaa646277ef5fb12
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/AggregateEvent.java
@@ -0,0 +1,114 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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 com.google.common.collect.Collections2;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.concurrent.Immutable;
+import org.joda.time.Interval;
+import org.openide.util.NbBundle;
+import org.sleuthkit.autopsy.timeline.events.type.EventType;
+import org.sleuthkit.autopsy.timeline.utils.IntervalUtils;
+import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
+
+/** An event that represent a set of other events aggregated together. All the
+ * sub events should have the same type and matching descriptions at the
+ * designated 'zoom level'.
+ */
+@Immutable
+public class AggregateEvent {
+
+    final private Interval span;
+
+    final private EventType type;
+
+    final private Set<Long> eventIDs;
+
+    final private String description;
+
+    private final DescriptionLOD lod;
+
+    public AggregateEvent(Interval spanningInterval, EventType type, Set<Long> eventIDs, String description, DescriptionLOD lod) {
+
+        this.span = spanningInterval;
+        this.type = type;
+        this.description = description;
+
+        this.eventIDs = eventIDs;
+        this.lod = lod;
+    }
+
+    public AggregateEvent(Interval spanningInterval, EventType type, List<String> events, String description, DescriptionLOD lod) {
+
+        this.span = spanningInterval;
+        this.type = type;
+        this.description = description;
+
+        this.eventIDs = new HashSet<>(Collections2.transform(events, Long::valueOf));
+        this.lod = lod;
+    }
+
+    /** @return the actual interval from the first event to the last event */
+    public Interval getSpan() {
+        return span;
+    }
+
+    public Set<Long> getEventIDs() {
+        return Collections.unmodifiableSet(eventIDs);
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public EventType getType() {
+        return type;
+    }
+
+    /**
+     * merge two aggregate events into one new aggregate event.
+     *
+     * @param ag1
+     * @param ag2
+     *
+     * @return
+     */
+    public static AggregateEvent merge(AggregateEvent ag1, AggregateEvent ag2) {
+
+        if (ag1.getType() != ag2.getType()) {
+            throw new IllegalArgumentException(NbBundle.getMessage(AggregateEvent.class, "AggregateEvent.differentTypes"));
+        }
+
+        if (!ag1.getDescription().equals(ag2.getDescription())) {
+            throw new IllegalArgumentException(NbBundle.getMessage(AggregateEvent.class, "AggregateEvent.differentDescriptions"));
+        }
+        HashSet<Long> ids = new HashSet<>(ag1.getEventIDs());
+        ids.addAll(ag2.getEventIDs());
+
+        //TODO: check that types/descriptions are actually the same -jm
+        return new AggregateEvent(IntervalUtils.span(ag1.span, ag2.span), ag1.getType(), ids, ag1.getDescription(), ag1.lod);
+    }
+
+    DescriptionLOD getLOD() {
+        return lod;
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/FilteredEventsModel.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/FilteredEventsModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..6bd3481d42dee7672315f3ec6cfcc2a95e328c6a
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/FilteredEventsModel.java
@@ -0,0 +1,250 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javax.annotation.concurrent.GuardedBy;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Interval;
+import org.sleuthkit.autopsy.timeline.TimeLineView;
+import org.sleuthkit.autopsy.timeline.events.db.EventsRepository;
+import org.sleuthkit.autopsy.timeline.events.type.EventType;
+import org.sleuthkit.autopsy.timeline.filters.Filter;
+import org.sleuthkit.autopsy.timeline.filters.IntersectionFilter;
+import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
+import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
+import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
+
+/**
+ * This class acts as the model for a {@link TimeLineView}
+ *
+ * Views can register listeners on properties returned by methods.
+ *
+ * This class is implemented as a filtered view into an underlying
+ * {@link EventsRepository}.
+ *
+ * TODO: as many methods as possible should cache their results so as to avoid
+ * unnecessary db calls through the {@link EventsRepository} -jm
+ *
+ * Concurrency Policy: repo is internally synchronized, so methods that only
+ * access the repo atomicaly do not need further synchronization
+ *
+ * all other member state variables should only be accessed with intrinsic lock
+ * of containing FilteredEventsModel held. Many methods delegate to a task
+ * submitted to the dbQueryThread executor. These methods should synchronize on
+ * this object, and the tasks should too. Since the tasks execute asynchronously
+ * from the invoking methods, the methods will return and release the lock for
+ * the tasks to obtain.
+ *
+ */
+public class FilteredEventsModel {
+
+    /**
+     * time range that spans the filtered events
+     */
+    //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.
+    @GuardedBy("this")
+    private final ReadOnlyObjectWrapper<Interval> requestedTimeRange = new ReadOnlyObjectWrapper<>();
+
+    @GuardedBy("this")
+    private final ReadOnlyObjectWrapper<Filter> requestedFilter = new ReadOnlyObjectWrapper<>(Filter.getDefaultFilter());
+
+    @GuardedBy("this")
+    private final ReadOnlyObjectWrapper< EventTypeZoomLevel> requestedTypeZoom = new ReadOnlyObjectWrapper<>(EventTypeZoomLevel.BASE_TYPE);
+
+    @GuardedBy("this")
+    private final ReadOnlyObjectWrapper< DescriptionLOD> requestedLOD = new ReadOnlyObjectWrapper<>(DescriptionLOD.SHORT);
+
+    @GuardedBy("this")
+    private final ReadOnlyObjectWrapper<ZoomParams> requestedZoomParamters = new ReadOnlyObjectWrapper<>();
+
+    /**
+     * The underlying repo for events. Atomic access to repo is synchronized
+     * internally, but compound access should be done with the intrinsic lock of
+     * this FilteredEventsModel object
+     */
+    @GuardedBy("this")
+    private final EventsRepository repo;
+
+    public FilteredEventsModel(EventsRepository repo) {
+        this.repo = repo;
+        requestedZoomParamters.set(new ZoomParams(new Interval(repo.getMinTime(), repo.getMaxTime(), DateTimeZone.UTC),
+                                                  EventTypeZoomLevel.BASE_TYPE,
+                                                  Filter.getDefaultFilter(),
+                                                  DescriptionLOD.SHORT,
+                                                  EnumSet.noneOf(ZoomParams.Field.class)));
+        this.requestedTimeRange.set(getSpanningInterval());
+    }
+
+    public Interval getBoundingEventsInterval() {
+        return repo.getBoundingEventsInterval(getRequestedZoomParamters().get().getTimeRange(), getRequestedZoomParamters().get().getFilter());
+    }
+
+    synchronized public ReadOnlyObjectProperty<ZoomParams> getRequestedZoomParamters() {
+        return requestedZoomParamters.getReadOnlyProperty();
+    }
+
+    public TimeLineEvent getEventById(Long eventID) {
+        return repo.getEventById(eventID);
+    }
+
+    public Set<Long> getEventIDs(Interval timeRange, Filter filter) {
+        final Interval overlap;
+        final IntersectionFilter intersect;
+        synchronized (this) {
+            overlap = getSpanningInterval().overlap(timeRange);
+            intersect = Filter.intersect(new Filter[]{filter, requestedFilter.get()});
+        }
+        return repo.getEventIDs(overlap, intersect);
+    }
+
+    /**
+     * return the number of events that pass the requested filter and are within
+     * the given time range.
+     *
+     * NOTE: this method does not change the requested time range
+     *
+     * @param timeRange
+     *
+     * @return
+     */
+    public Map<EventType, Long> getEventCounts(Interval timeRange) {
+
+        final Filter filter;
+        final EventTypeZoomLevel typeZoom;
+        synchronized (this) {
+            filter = requestedFilter.get();
+            typeZoom = requestedTypeZoom.get();
+        }
+        return repo.countEvents(new ZoomParams(timeRange, typeZoom, filter, null));
+    }
+
+    /**
+     * @return a read only view of the time range requested via
+     *         {@link #requestTimeRange(org.joda.time.Interval)}
+     */
+    synchronized public ReadOnlyObjectProperty<Interval> timeRange() {
+        if (requestedTimeRange.get() == null) {
+            requestedTimeRange.set(getSpanningInterval());
+        }
+        return requestedTimeRange.getReadOnlyProperty();
+    }
+
+    synchronized public ReadOnlyObjectProperty<DescriptionLOD> descriptionLOD() {
+        return requestedLOD.getReadOnlyProperty();
+    }
+
+    synchronized public ReadOnlyObjectProperty<Filter> filter() {
+        return requestedFilter.getReadOnlyProperty();
+    }
+
+    /**
+     * @return the smallest interval spanning all the events from the
+     *         repository, ignoring any filters or requested ranges
+     */
+    public final Interval getSpanningInterval() {
+        return new Interval(getMinTime() * 1000, 1000 + getMaxTime() * 1000, DateTimeZone.UTC);
+    }
+
+    /**
+     * @return the smallest interval spanning all the given events
+     */
+    public Interval getSpanningInterval(Collection<Long> eventIDs) {
+        return repo.getSpanningInterval(eventIDs);
+    }
+
+    /**
+     * @return the time (in seconds from unix epoch) of the absolutely first
+     *         event available from the repository, ignoring any filters or requested
+     *         ranges
+     */
+    public final Long getMinTime() {
+        return repo.getMinTime();
+    }
+
+    /**
+     * @return the time (in seconds from unix epoch) of the absolutely last
+     *         event available from the repository, ignoring any filters or requested
+     *         ranges
+     */
+    public final Long getMaxTime() {
+        return repo.getMaxTime();
+    }
+
+    /**
+     * @param aggregation
+     *
+     * @return a list of aggregated events that are within the requested time
+     *         range and pass the requested filter, using the given aggregation to
+     *         control the grouping of events
+     */
+    public List<AggregateEvent> getAggregatedEvents() {
+        final Interval range;
+        final Filter filter;
+        final EventTypeZoomLevel zoom;
+        final DescriptionLOD lod;
+        synchronized (this) {
+            range = requestedTimeRange.get();
+            filter = requestedFilter.get();
+            zoom = requestedTypeZoom.get();
+            lod = requestedLOD.get();
+        }
+        return repo.getAggregatedEvents(new ZoomParams(range, zoom, filter, lod));
+    }
+
+    /**
+     * @param aggregation
+     *
+     * @return a list of aggregated events that are within the requested time
+     *         range and pass the requested filter, using the given aggregation to
+     *         control the grouping of events
+     */
+    public List<AggregateEvent> getAggregatedEvents(ZoomParams params) {
+        return repo.getAggregatedEvents(params);
+    }
+
+    synchronized public ReadOnlyObjectProperty<EventTypeZoomLevel> eventTypeZoom() {
+        return requestedTypeZoom.getReadOnlyProperty();
+    }
+
+    synchronized public EventTypeZoomLevel getEventTypeZoom() {
+        return requestedTypeZoom.get();
+    }
+
+    synchronized public void requestZoomState(ZoomParams zCrumb, boolean force) {
+        if (force
+                || zCrumb.getTypeZoomLevel().equals(requestedTypeZoom.get()) == false
+                || zCrumb.getDescrLOD().equals(requestedLOD.get()) == false
+                || zCrumb.getFilter().equals(requestedFilter.get()) == false
+                || zCrumb.getTimeRange().equals(requestedTimeRange.get()) == false) {
+
+            requestedZoomParamters.set(zCrumb);
+            requestedTypeZoom.set(zCrumb.getTypeZoomLevel());
+            requestedFilter.set(zCrumb.getFilter().copyOf());
+            requestedTimeRange.set(zCrumb.getTimeRange());
+            requestedLOD.set(zCrumb.getDescrLOD());
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/TimeLineEvent.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/TimeLineEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..b1340db2347f780a3b98d93c9fd110a9b03d19c6
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/TimeLineEvent.java
@@ -0,0 +1,91 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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 org.sleuthkit.autopsy.timeline.events.type.EventType;
+import org.sleuthkit.datamodel.TskData;
+
+/**
+ *
+ */
+public class TimeLineEvent {
+
+    private final Long eventID;
+
+    private final Long fileID, time;
+
+    private final Long artifactID;
+
+    private final EventType subType;
+
+    private final String fullDescription, medDescription, shortDescription;
+
+    private final TskData.FileKnown known;
+
+    public Long getArtifactID() {
+        return artifactID;
+    }
+
+    public Long getEventID() {
+        return eventID;
+    }
+
+    public Long getFileID() {
+        return fileID;
+    }
+
+    /** @return the time in seconds from unix epoch */
+    public Long getTime() {
+        return time;
+    }
+
+    public EventType getType() {
+        return subType;
+    }
+
+    public String getFullDescription() {
+        return fullDescription;
+    }
+
+    public String getMedDescription() {
+        return medDescription;
+    }
+
+    public String getShortDescription() {
+        return shortDescription;
+    }
+
+    public TimeLineEvent(Long eventID, Long objID, Long artifactID, Long time,
+                         EventType type, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known) {
+        this.eventID = eventID;
+        this.fileID = objID;
+        this.artifactID = artifactID;
+        this.time = time;
+        this.subType = type;
+
+        this.fullDescription = fullDescription;
+        this.medDescription = medDescription;
+        this.shortDescription = shortDescription;
+        this.known = known;
+    }
+
+    public TskData.FileKnown getKnown() {
+        return known;
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/db/EventDB.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/db/EventDB.java
new file mode 100644
index 0000000000000000000000000000000000000000..979317bee41c97eac43434c5a5d790d50e012df7
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/db/EventDB.java
@@ -0,0 +1,1122 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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.db;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.SetMultimap;
+import java.io.File;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Interval;
+import org.joda.time.Period;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
+import org.sleuthkit.autopsy.timeline.events.TimeLineEvent;
+import org.sleuthkit.autopsy.timeline.events.type.BaseTypes;
+import org.sleuthkit.autopsy.timeline.events.type.EventType;
+import org.sleuthkit.autopsy.timeline.events.type.RootEventType;
+import org.sleuthkit.autopsy.timeline.filters.Filter;
+import org.sleuthkit.autopsy.timeline.filters.HideKnownFilter;
+import org.sleuthkit.autopsy.timeline.filters.IntersectionFilter;
+import org.sleuthkit.autopsy.timeline.filters.TextFilter;
+import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
+import org.sleuthkit.autopsy.timeline.filters.UnionFilter;
+import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;
+import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
+import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
+import org.sleuthkit.autopsy.timeline.zooming.TimeUnits;
+import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.datamodel.TskData;
+import org.sqlite.SQLiteJDBCLoader;
+
+/**
+ * This class provides access to the Advanced Timeline SQLite database. This
+ * class borrows a lot of ideas and techniques from {@link  SleuthkitCase},
+ * Creating an abstract base class for sqlite databases, or using a higherlevel
+ * persistence api may make sense in the future.
+ */
+public class EventDB {
+
+    private static final String ARTIFACT_ID_COLUMN = "artifact_id";
+
+    private static final String BASE_TYPE_COLUMN = "base_type";
+
+    private static final String EVENT_ID_COLUMN = "event_id";
+
+    //column name constants//////////////////////
+    private static final String FILE_ID_COLUMN = "file_id";
+
+    private static final String FULL_DESCRIPTION_COLUMN = "full_description";
+
+    private static final String KNOWN_COLUMN = "known_state";
+
+    private static final String LAST_ARTIFACT_ID_KEY = "last_artifact_id";
+
+    private static final String LAST_OBJECT_ID_KEY = "last_object_id";
+
+    private static final java.util.logging.Logger LOGGER = Logger.getLogger(EventDB.class.getName());
+
+    private static final String MED_DESCRIPTION_COLUMN = "med_description";
+
+    private static final String SHORT_DESCRIPTION_COLUMN = "short_description";
+
+    private static final String SUB_TYPE_COLUMN = "sub_type";
+
+    private static final String TIME_COLUMN = "time";
+
+    private static final String WAS_INGEST_RUNNING_KEY = "was_ingest_running";
+
+    static {
+        //make sure sqlite driver is loaded // possibly redundant
+        try {
+            Class.forName("org.sqlite.JDBC");
+        } catch (ClassNotFoundException ex) {
+            LOGGER.log(Level.SEVERE, "Failed to load sqlite JDBC driver", ex);
+        }
+    }
+
+    /**
+     * public factory method. Creates and opens a connection to a database at
+     * the given path. If a database does not already exist at that path, one is
+     * created.
+     *
+     * @param dbPath
+     *
+     * @return
+     */
+    public static EventDB getEventDB(String dbPath) {
+        try {
+            EventDB eventDB = new EventDB(dbPath + File.separator + "events.db");
+
+            return eventDB;
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "sql error creating database connection", ex);
+            return null;
+        } catch (Exception ex) {
+            LOGGER.log(Level.SEVERE, "error creating database connection", ex);
+            return null;
+        }
+    }
+
+    static List<Integer> getActiveSubTypes(TypeFilter filter) {
+        if (filter.isActive()) {
+            if (filter.getSubFilters().isEmpty()) {
+                return Collections.singletonList(RootEventType.allTypes.indexOf(filter.getEventType()));
+            } else {
+                return filter.getSubFilters().stream().flatMap((Filter t) -> getActiveSubTypes((TypeFilter) t).stream()).collect(Collectors.toList());
+            }
+        } else {
+            return Collections.emptyList();
+        }
+    }
+
+    static String getSQLWhere(IntersectionFilter filter) {
+        return filter.getSubFilters().stream()
+                .filter(Filter::isActive)
+                .map(EventDB::getSQLWhere)
+                .collect(Collectors.joining(" and ", "( ", ")"));
+    }
+
+    static String getSQLWhere(UnionFilter filter) {
+        return filter.getSubFilters().stream()
+                .filter(Filter::isActive)
+                .map(EventDB::getSQLWhere)
+                .collect(Collectors.joining(" or ", "( ", ")"));
+    }
+
+    private static String getSQLWhere(Filter filter) {
+        //TODO: this is here so that the filters don't depend, even implicitly, on the db, but it leads to some nasty code
+        //it would all be much easier if all the getSQLWhere methods where moved to their respective filter classes
+        String result = "";
+        if (filter == null) {
+            return "1";
+        } else if (filter instanceof HideKnownFilter) {
+            result = getSQLWhere((HideKnownFilter) filter);
+        } else if (filter instanceof TextFilter) {
+            result = getSQLWhere((TextFilter) filter);
+        } else if (filter instanceof TypeFilter) {
+            result = getSQLWhere((TypeFilter) filter);
+        } else if (filter instanceof IntersectionFilter) {
+            result = getSQLWhere((IntersectionFilter) filter);
+        } else if (filter instanceof UnionFilter) {
+            result = getSQLWhere((UnionFilter) filter);
+        } else {
+            return "1";
+        }
+        result = StringUtils.deleteWhitespace(result).equals("(1and1and1)") ? "1" : result;
+        System.out.println(result);
+        return result;
+    }
+
+    private static String getSQLWhere(HideKnownFilter filter) {
+        return (filter.isActive())
+               ? "(known_state is not '" + TskData.FileKnown.KNOWN.getFileKnownValue() + "')"
+               : "1";
+    }
+
+    private static String getSQLWhere(TextFilter filter) {
+        if (filter.isActive()) {
+            if (StringUtils.isBlank(filter.getText())) {
+                return "1";
+            }
+            String strip = StringUtils.strip(filter.getText());
+            return "((" + MED_DESCRIPTION_COLUMN + " like '%" + strip + "%') or ("
+                    + FULL_DESCRIPTION_COLUMN + " like '%" + strip + "%') or ("
+                    + SHORT_DESCRIPTION_COLUMN + " like '%" + strip + "%'))";
+        } else {
+            return "1";
+        }
+    }
+
+    /**
+     * generate a sql where clause for the given type filter, while trying to be
+     * as simple as possible to improve performance.
+     *
+     * @param filter
+     *
+     * @return
+     */
+    private static String getSQLWhere(TypeFilter filter) {
+        if (filter.isActive() == false) {
+            return "0";
+        } else if (filter.getEventType() instanceof RootEventType) {
+            //if the filter is a root filter and all base type filtes and subtype filters are active,
+            if (filter.getSubFilters().stream().allMatch(f
+                    -> f.isActive() && ((TypeFilter) f).getSubFilters().stream().allMatch(Filter::isActive))) {
+                return "1"; //then collapse clause to true
+            }
+        }
+        return "(" + SUB_TYPE_COLUMN + " in (" + StringUtils.join(getActiveSubTypes(filter), ",") + "))";
+    }
+
+    private volatile Connection con;
+
+    private final String dbPath;
+
+    private PreparedStatement getDBInfoStmt;
+
+    private PreparedStatement getEventByIDStmt;
+
+    private PreparedStatement getMaxTimeStmt;
+
+    private PreparedStatement getMinTimeStmt;
+
+    private PreparedStatement insertRowStmt;
+
+    private final Set<PreparedStatement> preparedStatements = new HashSet<>();
+
+    private PreparedStatement recordDBInfoStmt;
+
+    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true); //use fairness policy
+
+    private final Lock DBLock = rwLock.writeLock(); //using exclusing lock for all db ops for now
+
+    private EventDB(String dbPath) throws SQLException, Exception {
+        this.dbPath = dbPath;
+        initializeDB();
+    }
+
+    @Override
+    public void finalize() throws Throwable {
+        try {
+            closeDBCon();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    public Interval getSpanningInterval(Collection<Long> eventIDs) {
+
+        Interval span = null;
+        dbReadLock();
+        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, ", ") + ")");) {
+            while (rs.next()) {
+                span = new Interval(rs.getLong("Min(time)"), rs.getLong("Max(time)") + 1, DateTimeZone.UTC);
+
+            }
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "Error executing get spanning interval query.", ex);
+        } finally {
+            dbReadUnlock();
+        }
+        return span;
+    }
+
+    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);
+            }
+        }
+        con = null;
+    }
+
+    void commitTransaction(EventTransaction tr, Boolean notify) {
+        if (tr.isClosed()) {
+            throw new IllegalArgumentException("can't close already closed transaction");
+        }
+        tr.commit(notify);
+    }
+
+    int countAllEvents() {
+        int result = -1;
+        dbReadLock();
+        //TODO convert this to prepared statement -jm
+        try (ResultSet rs = con.createStatement().executeQuery("select count(*) as count from events")) {
+            while (rs.next()) {
+                result = rs.getInt("count");
+                break;
+            }
+        } catch (SQLException ex) {
+            Exceptions.printStackTrace(ex);
+        } finally {
+            dbReadUnlock();
+        }
+        return result;
+    }
+
+    Map<EventType, Long> countEvents(ZoomParams params) {
+        if (params.getTimeRange() != null) {
+            return countEvents(params.getTimeRange().getStartMillis() / 1000, params.getTimeRange().getEndMillis() / 1000, params.getFilter(), params.getTypeZoomLevel());
+        } else {
+            return Collections.emptyMap();
+        }
+    }
+
+    /**
+     * Lock to protect against read while it is in a write transaction state.
+     * Supports multiple concurrent readers if there is no writer. MUST always
+     * call dbReadUnLock() as early as possible, in the same thread where
+     * dbReadLock() was called.
+     */
+    void dbReadLock() {
+        DBLock.lock();
+    }
+
+    /**
+     * Release previously acquired read lock acquired in this thread using
+     * dbReadLock(). Call in "finally" block to ensure the lock is always
+     * released.
+     */
+    void dbReadUnlock() {
+        DBLock.unlock();
+    }
+
+    //////////////general database logic , mostly borrowed from sleuthkitcase
+    void dbWriteLock() {
+        //Logger.getLogger("LOCK").log(Level.INFO, "Locking " + rwLock.toString());
+        DBLock.lock();
+    }
+
+    /**
+     * Release previously acquired write lock acquired in this thread using
+     * dbWriteLock(). Call in "finally" block to ensure the lock is always
+     * released.
+     */
+    void dbWriteUnlock() {
+        //Logger.getLogger("LOCK").log(Level.INFO, "UNLocking " + rwLock.toString());
+        DBLock.unlock();
+    }
+
+    void dropTable() {
+        //TODO: use prepared statement - jm
+        dbWriteLock();
+        try (Statement createStatement = con.createStatement()) {
+            createStatement.execute("drop table if exists events");
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "could not drop old events table", ex);
+        } finally {
+            dbWriteUnlock();
+        }
+    }
+
+    List<AggregateEvent> getAggregatedEvents(ZoomParams params) {
+        return getAggregatedEvents(params.getTimeRange(), params.getFilter(), params.getTypeZoomLevel(), params.getDescrLOD());
+    }
+
+    Interval getBoundingEventsInterval(Interval timeRange, Filter filter) {
+        long start = timeRange.getStartMillis() / 1000;
+        long end = timeRange.getEndMillis() / 1000;
+        final String sqlWhere = getSQLWhere(filter);
+
+        dbReadLock();
+        try (Statement stmt = con.createStatement(); //can't use prepared statement because of complex where clause
+             ResultSet rs = stmt.executeQuery(" select (select Max(time) from events where time <=" + start + " and " + sqlWhere + ") as start,(select Min(time) from events where time >= " + end + " and " + sqlWhere + ") as end")) {
+            while (rs.next()) {
+
+                long start2 = rs.getLong("start");
+                long end2 = rs.getLong("end");
+
+                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) {
+            LOGGER.log(Level.SEVERE, "Failed to get MIN time.", ex);
+        } finally {
+            dbReadUnlock();
+        }
+        return null;
+    }
+
+    TimeLineEvent getEventById(Long eventID) {
+        TimeLineEvent result = null;
+        dbReadLock();
+        try {
+            getEventByIDStmt.clearParameters();
+            getEventByIDStmt.setLong(1, eventID);
+            try (ResultSet rs = getEventByIDStmt.executeQuery()) {
+                while (rs.next()) {
+                    result = constructTimeLineEvent(rs);
+                    break;
+                }
+            }
+        } catch (SQLException sqlEx) {
+            LOGGER.log(Level.SEVERE, "exception while querying for event with id = " + eventID, sqlEx);
+        } finally {
+            dbReadUnlock();
+        }
+        return result;
+    }
+
+    Set<Long> getEventIDs(Interval timeRange, Filter filter) {
+        return getEventIDs(timeRange.getStartMillis() / 1000, timeRange.getEndMillis() / 1000, filter);
+    }
+
+    Set<Long> getEventIDs(Long startTime, Long endTime, Filter filter) {
+        if (Objects.equals(startTime, endTime)) {
+            endTime++;
+        }
+        Set<Long> resultIDs = new HashSet<>();
+
+        dbReadLock();
+        final String query = "select event_id from events where time >=  " + startTime + " and time <" + endTime + " and " + getSQLWhere(filter);
+        System.out.println(query);
+        try (Statement stmt = con.createStatement();
+             ResultSet rs = stmt.executeQuery(query)) {
+
+            while (rs.next()) {
+                resultIDs.add(rs.getLong(EVENT_ID_COLUMN));
+            }
+
+        } catch (SQLException sqlEx) {
+            LOGGER.log(Level.SEVERE, "failed to execute query for event ids in range", sqlEx);
+        } finally {
+            dbReadUnlock();
+        }
+
+        return resultIDs;
+    }
+
+    long getLastArtfactID() {
+        return getDBInfo(LAST_ARTIFACT_ID_KEY, -1);
+    }
+
+    long getLastObjID() {
+        return getDBInfo(LAST_OBJECT_ID_KEY, -1);
+    }
+
+    /** @return maximum time in seconds from unix epoch */
+    Long getMaxTime() {
+        dbReadLock();
+        try (ResultSet rs = getMaxTimeStmt.executeQuery()) {
+            while (rs.next()) {
+                return rs.getLong("max");
+            }
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "Failed to get MAX time.", ex);
+        } finally {
+            dbReadUnlock();
+        }
+        return -1l;
+    }
+
+    /** @return maximum time in seconds from unix epoch */
+    Long getMinTime() {
+        dbReadLock();
+        try (ResultSet rs = getMinTimeStmt.executeQuery()) {
+            while (rs.next()) {
+                return rs.getLong("min");
+            }
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "Failed to get MIN time.", ex);
+        } finally {
+            dbReadUnlock();
+        }
+        return -1l;
+    }
+
+    boolean getWasIngestRunning() {
+        return getDBInfo(WAS_INGEST_RUNNING_KEY, 0) != 0;
+    }
+
+    /**
+     * create the table and indices if they don't already exist
+     *
+     *
+     * @return the number of rows in the table , count > 0 indicating an
+     *         existing table
+     */
+    final synchronized void initializeDB() {
+        try {
+            if (isClosed()) {
+                openDBCon();
+            }
+            configureDB();
+
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "problem accessing  database", ex);
+        }
+
+        dbWriteLock();
+        try {
+            try (Statement stmt = con.createStatement()) {
+                String sql = "CREATE TABLE if not exists db_info "
+                        + " ( key TEXT, "
+                        + " value INTEGER, "
+                        + "PRIMARY KEY (key))";
+                stmt.execute(sql);
+            } catch (SQLException ex) {
+                LOGGER.log(Level.SEVERE, "problem creating  db_info table", ex);
+            }
+
+            try (Statement stmt = con.createStatement()) {
+                String sql = "CREATE TABLE if not exists events "
+                        + " (event_id INTEGER PRIMARY KEY, "
+                        + " file_id INTEGER, "
+                        + " artifact_id INTEGER, "
+                        + " time INTEGER, "
+                        + " sub_type INTEGER, "
+                        + " base_type INTEGER, "
+                        + " full_description TEXT, "
+                        + " med_description TEXT, "
+                        + " short_description TEXT, "
+                        + " known_state INTEGER)";
+                stmt.execute(sql);
+            } catch (SQLException ex) {
+                LOGGER.log(Level.SEVERE, "problem creating  database table", ex);
+            }
+
+            try (Statement stmt = con.createStatement()) {
+                String sql = "CREATE INDEX if not exists file_idx ON events(file_id)";
+                stmt.execute(sql);
+            } catch (SQLException ex) {
+                LOGGER.log(Level.SEVERE, "problem creating file_idx", ex);
+            }
+            try (Statement stmt = con.createStatement()) {
+                String sql = "CREATE INDEX if not exists artifact_idx ON events(artifact_id)";
+                stmt.execute(sql);
+            } catch (SQLException ex) {
+                LOGGER.log(Level.SEVERE, "problem creating artifact_idx", ex);
+            }
+
+            //for common queries the covering indexes below were better, but having the time index 'blocke' them
+//            try (Statement stmt = con.createStatement()) {
+//                String sql = "CREATE INDEX if not exists time_idx ON events(time)";
+//                stmt.execute(sql);
+//            } catch (SQLException ex) {
+//                LOGGER.log(Level.SEVERE, "problem creating time_idx", ex);
+//            }
+            try (Statement stmt = con.createStatement()) {
+                String sql = "CREATE INDEX if not exists sub_type_idx ON events(sub_type, time)";
+                stmt.execute(sql);
+            } catch (SQLException ex) {
+                LOGGER.log(Level.SEVERE, "problem creating sub_type_idx", ex);
+            }
+
+            try (Statement stmt = con.createStatement()) {
+                String sql = "CREATE INDEX if not exists base_type_idx ON events(base_type, time)";
+                stmt.execute(sql);
+            } catch (SQLException ex) {
+                LOGGER.log(Level.SEVERE, "problem creating base_type_idx", ex);
+            }
+
+            try (Statement stmt = con.createStatement()) {
+                String sql = "CREATE INDEX if not exists known_idx ON events(known_state)";
+                stmt.execute(sql);
+            } catch (SQLException ex) {
+                LOGGER.log(Level.SEVERE, "problem creating known_idx", ex);
+            }
+
+            try {
+                insertRowStmt = prepareStatement(
+                        "INSERT INTO events (file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description, known_state) "
+                        + "VALUES (?,?,?,?,?,?,?,?,?)");
+
+                getMaxTimeStmt = prepareStatement("select Max(time) as max from events");
+                getMinTimeStmt = prepareStatement("select Min(time) as min from events");
+                getEventByIDStmt = prepareStatement("select * from events where event_id =  ?");
+                recordDBInfoStmt = prepareStatement("insert or replace into db_info (key, value) values (?, ?)");
+                getDBInfoStmt = prepareStatement("select value from db_info where key = ?");
+            } catch (SQLException sQLException) {
+                LOGGER.log(Level.SEVERE, "failed to prepareStatment", sQLException);
+            }
+
+        } finally {
+            dbWriteUnlock();
+        }
+
+    }
+
+    void insertEvent(long time, EventType type, Long objID, Long artifactID, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known) {
+        EventTransaction trans = beginTransaction();
+        insertEvent(time, type, objID, artifactID, fullDescription, medDescription, shortDescription, known, trans);
+        commitTransaction(trans, true);
+    }
+
+    /**
+     * use transactions to update files
+     *
+     * @param f
+     * @param tr
+     */
+    void insertEvent(long time, EventType type, Long objID, Long artifactID, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known, EventTransaction tr) {
+        if (tr.isClosed()) {
+            throw new IllegalArgumentException("can't update database with closed transaction");
+        }
+        int typeNum;
+        int superTypeNum;
+
+        typeNum = RootEventType.allTypes.indexOf(type);
+        superTypeNum = type.getSuperType().ordinal();
+
+        dbWriteLock();
+        try {
+
+            //"INSERT INTO events (file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description) "
+            insertRowStmt.clearParameters();
+            if (objID != null) {
+                insertRowStmt.setLong(1, objID);
+            } else {
+                insertRowStmt.setNull(1, Types.INTEGER);
+            }
+            if (artifactID != null) {
+                insertRowStmt.setLong(2, artifactID);
+            } else {
+                insertRowStmt.setNull(2, Types.INTEGER);
+            }
+            insertRowStmt.setLong(3, time);
+
+            if (typeNum != -1) {
+                insertRowStmt.setInt(4, typeNum);
+            } else {
+                insertRowStmt.setNull(4, Types.INTEGER);
+            }
+
+            insertRowStmt.setInt(5, superTypeNum);
+            insertRowStmt.setString(6, fullDescription);
+            insertRowStmt.setString(7, medDescription);
+            insertRowStmt.setString(8, shortDescription);
+
+            insertRowStmt.setByte(9, known == null ? TskData.FileKnown.UNKNOWN.getFileKnownValue() : known.getFileKnownValue());
+
+            insertRowStmt.executeUpdate();
+
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "failed to insert event", ex);
+        } finally {
+            dbWriteUnlock();
+        }
+    }
+
+    boolean isClosed() throws SQLException {
+        if (con == null) {
+            return true;
+        }
+        return con.isClosed();
+    }
+
+    void openDBCon() {
+        try {
+            if (con == null || con.isClosed()) {
+                con = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
+            }
+        } catch (SQLException ex) {
+            LOGGER.log(Level.WARNING, "Failed to open connection to events.db", ex);
+        }
+    }
+
+    void recordLastArtifactID(long lastArtfID) {
+        recordDBInfo(LAST_ARTIFACT_ID_KEY, lastArtfID);
+    }
+
+    void recordLastObjID(Long lastObjID) {
+        recordDBInfo(LAST_OBJECT_ID_KEY, lastObjID);
+    }
+
+    void recordWasIngestRunning(boolean wasIngestRunning) {
+        recordDBInfo(WAS_INGEST_RUNNING_KEY, (wasIngestRunning ? 1 : 0));
+    }
+
+    void rollBackTransaction(EventTransaction trans) {
+        trans.rollback();
+    }
+
+    boolean tableExists() {
+        //TODO: use prepared statement - jm
+        try (Statement createStatement = con.createStatement();
+             ResultSet executeQuery = createStatement.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='events'")) {
+            if (executeQuery.getString("name").equals("events") == false) {
+                return false;
+            }
+        } catch (SQLException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        return true;
+    }
+
+    private void closeStatements() throws SQLException {
+        for (PreparedStatement pStmt : preparedStatements) {
+            pStmt.close();
+        }
+    }
+
+    private void configureDB() throws SQLException {
+        dbWriteLock();
+        //this should match Sleuthkit db setupt
+        try (Statement statement = con.createStatement()) {
+            //reduce i/o operations, we have no OS crash recovery anyway
+            statement.execute("PRAGMA synchronous = OFF;");
+            //we don't use this feature, so turn it off for minimal speed up on queries
+            //this is deprecated and not recomended
+            statement.execute("PRAGMA count_changes = OFF;");
+            //this made a big difference to query speed
+            statement.execute("PRAGMA temp_store = MEMORY");
+            //this made a modest improvement in query speeds
+            statement.execute("PRAGMA cache_size = 50000");
+            //we never delete anything so...
+            statement.execute("PRAGMA auto_vacuum = 0");
+            //allow to query while in transaction - no need read locks
+            statement.execute("PRAGMA read_uncommitted = True;");
+        } finally {
+            dbWriteUnlock();
+        }
+
+        try {
+            LOGGER.log(Level.INFO, String.format("sqlite-jdbc version %s loaded in %s mode",
+                                                 SQLiteJDBCLoader.getVersion(), SQLiteJDBCLoader.isNativeMode()
+                                                                                ? "native" : "pure-java"));
+        } catch (Exception exception) {
+        }
+    }
+
+    private TimeLineEvent constructTimeLineEvent(ResultSet rs) throws SQLException {
+        EventType type = RootEventType.allTypes.get(rs.getInt(SUB_TYPE_COLUMN));
+        return new TimeLineEvent(rs.getLong(EVENT_ID_COLUMN),
+                                 rs.getLong(FILE_ID_COLUMN),
+                                 rs.getLong(ARTIFACT_ID_COLUMN),
+                                 rs.getLong(TIME_COLUMN),
+                                 type,
+                                 rs.getString(FULL_DESCRIPTION_COLUMN),
+                                 rs.getString(MED_DESCRIPTION_COLUMN),
+                                 rs.getString(SHORT_DESCRIPTION_COLUMN),
+                                 TskData.FileKnown.valueOf(rs.getByte(KNOWN_COLUMN)));
+    }
+
+    /**
+     * count all the events with the given options and return a map organizing
+     * the counts in a hierarchy from date > eventtype> count
+     *
+     *
+     * @param startTime events before this time will be excluded (seconds from
+     *                  unix epoch)
+     * @param endTime   events at or after this time will be excluded (seconds
+     *                  from unix epoch)
+     * @param filter    only events that pass this filter will be counted
+     * @param zoomLevel only events of this type or a subtype will be counted
+     *                  and the counts will be organized into bins for each of the subtypes of
+     *                  the given event type
+     *
+     * @return a map organizing the counts in a hierarchy from date > eventtype>
+     *         count
+     */
+    private Map<EventType, Long> countEvents(Long startTime, Long endTime, Filter filter, EventTypeZoomLevel zoomLevel) {
+        if (Objects.equals(startTime, endTime)) {
+            endTime++;
+        }
+
+        Map<EventType, Long> typeMap = new HashMap<>();
+
+        //do we want the root or subtype column of the databse
+        final boolean useSubTypes = (zoomLevel == EventTypeZoomLevel.SUB_TYPE);
+
+        //get some info about the range of dates requested
+        final String queryString = "select count(*), " + (useSubTypes ? SUB_TYPE_COLUMN : BASE_TYPE_COLUMN)
+                + " from events where time >= " + startTime + " and time < " + endTime + " and " + getSQLWhere(filter)
+                + " GROUP BY " + (useSubTypes ? SUB_TYPE_COLUMN : BASE_TYPE_COLUMN);
+
+        ResultSet rs = null;
+        dbReadLock();
+        System.out.println(queryString);
+        try (Statement stmt = con.createStatement();) {
+            Stopwatch stopwatch = new Stopwatch();
+            stopwatch.start();
+            rs = stmt.executeQuery(queryString);
+            stopwatch.stop();
+            System.out.println(stopwatch.elapsedMillis() / 1000.0 + " seconds");
+            while (rs.next()) {
+
+                EventType type = useSubTypes
+                                 ? RootEventType.allTypes.get(rs.getInt(SUB_TYPE_COLUMN))
+                                 : BaseTypes.values()[rs.getInt(BASE_TYPE_COLUMN)];
+
+                typeMap.put(type, rs.getLong("count(*)"));
+            }
+
+        } catch (Exception ex) {
+            LOGGER.log(Level.SEVERE, "error getting count of events from db.", ex);
+        } finally {
+            try {
+                rs.close();
+            } catch (SQLException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+            dbReadUnlock();
+        }
+        return typeMap;
+    }
+
+    /**
+     * //TODO: update javadoc //TODO: split this into helper methods
+     *
+     * get a list of {@link AggregateEvent}s.
+     *
+     * 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
+     *
+     *
+     * @param timeRange the Interval within in which all returned aggregate
+     *                  events will be.
+     * @param filter    only events that pass the filter will be included in
+     *                  aggregates events returned
+     * @param zoomLevel only events of this level will be included
+     * @param lod       description level of detail to use when grouping events
+     *
+     *
+     * @return a list of aggregate events within the given timerange, that pass
+     *         the supplied filter, aggregated according to the given event type and
+     *         description zoom levels
+     */
+    private List<AggregateEvent> getAggregatedEvents(Interval timeRange, Filter filter, EventTypeZoomLevel zoomLevel, DescriptionLOD lod) {
+        String descriptionColumn = getDescriptionColumn(lod);
+        final boolean useSubTypes = (zoomLevel.equals(EventTypeZoomLevel.SUB_TYPE));
+
+        //get some info about the time range requested
+        RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(timeRange);
+        //use 'rounded out' range
+        long start = timeRange.getStartMillis() / 1000;//.getLowerBound();
+        long end = timeRange.getEndMillis() / 1000;//Millis();//rangeInfo.getUpperBound();
+        if (Objects.equals(start, end)) {
+            end++;
+        }
+
+        //get a sqlite srtftime format string
+        String strfTimeFormat = getStrfTimeFormat(rangeInfo.getPeriodSize());
+
+        //effectively map from type to (map from description to events)
+        Map<EventType, SetMultimap< String, AggregateEvent>> typeMap = new HashMap<>();
+
+        //get all agregate events in this time unit
+        dbReadLock();
+        String query = "select strftime('" + strfTimeFormat + "',time , 'unixepoch'" + (TimeLineController.getTimeZone().get().equals(TimeZone.getDefault()) ? ", 'localtime'" : "") + ") as interval,  group_concat(event_id) as event_ids, Min(time), Max(time),  " + descriptionColumn + ", " + (useSubTypes ? SUB_TYPE_COLUMN : BASE_TYPE_COLUMN)
+                + " from events where time >= " + start + " and time < " + end + " and " + getSQLWhere(filter)
+                + " group by interval, " + (useSubTypes ? SUB_TYPE_COLUMN : BASE_TYPE_COLUMN) + " , " + descriptionColumn
+                + " order by Min(time)";
+        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");
+            while (rs.next()) {
+                EventType type = useSubTypes ? RootEventType.allTypes.get(rs.getInt(SUB_TYPE_COLUMN)) : BaseTypes.values()[rs.getInt(BASE_TYPE_COLUMN)];
+
+                AggregateEvent aggregateEvent = new AggregateEvent(
+                        new Interval(rs.getLong("Min(time)") * 1000, rs.getLong("Max(time)") * 1000, TimeLineController.getJodaTimeZone()),
+                        type,
+                        Arrays.asList(rs.getString("event_ids").split(",")),
+                        rs.getString(descriptionColumn), lod);
+
+                //put events in map from type/descrition -> event
+                SetMultimap<String, AggregateEvent> descrMap = typeMap.get(type);
+                if (descrMap == null) {
+                    descrMap = HashMultimap.<String, AggregateEvent>create();
+                    typeMap.put(type, descrMap);
+                }
+                descrMap.put(aggregateEvent.getDescription(), aggregateEvent);
+            }
+
+        } catch (SQLException ex) {
+            Exceptions.printStackTrace(ex);
+        } finally {
+            try {
+                rs.close();
+            } catch (SQLException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+            dbReadUnlock();
+        }
+
+        //result list to return
+        ArrayList<AggregateEvent> aggEvents = new ArrayList<>();
+
+        //save this for use when comparing gap size
+        Period timeUnitLength = rangeInfo.getPeriodSize().getPeriod();
+
+        //For each (type, description) key, merge agg events
+        for (SetMultimap<String, AggregateEvent> descrMap : typeMap.values()) {
+            for (String descr : descrMap.keySet()) {
+                //run through the sorted events, merging together adjacent events
+                Iterator<AggregateEvent> iterator = descrMap.get(descr).stream()
+                        .sorted((AggregateEvent o1, AggregateEvent o2)
+                                -> Long.compare(o1.getSpan().getStartMillis(), o2.getSpan().getStartMillis()))
+                        .iterator();
+                AggregateEvent current = iterator.next();
+                while (iterator.hasNext()) {
+                    AggregateEvent next = iterator.next();
+                    Interval gap = current.getSpan().gap(next.getSpan());
+
+                    //if they overlap or gap is less one quarter timeUnitLength
+                    //TODO: 1/4 factor is arbitrary. review! -jm
+                    if (gap == null || gap.toDuration().getMillis() <= timeUnitLength.toDurationFrom(gap.getStart()).getMillis() / 4) {
+                        //merge them
+                        current = AggregateEvent.merge(current, next);
+                    } else {
+                        //done merging into current, set next as new current
+                        aggEvents.add(current);
+                        current = next;
+                    }
+                }
+                aggEvents.add(current);
+            }
+        }
+
+        //at this point we should have a list of aggregate events.
+        //one per type/description spanning consecutive time units as determined in rangeInfo
+        return aggEvents;
+    }
+
+    private long getDBInfo(String key, long defaultValue) {
+        dbReadLock();
+        try {
+            getDBInfoStmt.setString(1, key);
+
+            try (ResultSet rs = getDBInfoStmt.executeQuery()) {
+                long result = defaultValue;
+                while (rs.next()) {
+                    result = rs.getLong("value");
+                }
+                return result;
+            } catch (SQLException ex) {
+                LOGGER.log(Level.SEVERE, "failed to read key: " + key + " from db_info", ex);
+            } finally {
+                dbReadUnlock();
+            }
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "failed to set key: " + key + " on getDBInfoStmt ", ex);
+        }
+
+        return defaultValue;
+    }
+
+    private String getDescriptionColumn(DescriptionLOD lod) {
+        switch (lod) {
+            case FULL:
+                return FULL_DESCRIPTION_COLUMN;
+            case MEDIUM:
+                return MED_DESCRIPTION_COLUMN;
+            case SHORT:
+            default:
+                return SHORT_DESCRIPTION_COLUMN;
+        }
+    }
+
+    private String getStrfTimeFormat(TimeUnits info) {
+        switch (info) {
+            case DAYS:
+                return "%Y-%m-%dT00:00:00";
+            case HOURS:
+                return "%Y-%m-%dT%H:00:00";
+            case MINUTES:
+                return "%Y-%m-%dT%H:%M:00";
+            case MONTHS:
+                return "%Y-%m-01T00:00:00";
+            case SECONDS:
+                return "%Y-%m-%dT%H:%M:%S";
+            case YEARS:
+                return "%Y-01-01T00:00:00";
+            default:
+                return "%Y-%m-%dT%H:%M:%S";
+        }
+    }
+
+    private PreparedStatement prepareStatement(String queryString) throws SQLException {
+        PreparedStatement prepareStatement = con.prepareStatement(queryString);
+        preparedStatements.add(prepareStatement);
+        return prepareStatement;
+    }
+
+    private void recordDBInfo(String key, long value) {
+        dbWriteLock();
+        try {
+            recordDBInfoStmt.setString(1, key);
+            recordDBInfoStmt.setLong(2, value);
+            recordDBInfoStmt.executeUpdate();
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "failed to set dbinfo  key: " + key + " value: " + value, ex);
+        } finally {
+            dbWriteUnlock();
+        }
+    }
+
+    /**
+     * inner class that can reference access database connection
+     */
+    public class EventTransaction {
+
+        private boolean closed = false;
+
+        /**
+         * factory creation method
+         *
+         * @param con the {@link  ava.sql.Connection}
+         *
+         * @return a LogicalFileTransaction for the given connection
+         *
+         * @throws SQLException
+         */
+        private EventTransaction() {
+
+            //get the write lock, released in close()
+            dbWriteLock();
+            try {
+                con.setAutoCommit(false);
+
+            } catch (SQLException ex) {
+                LOGGER.log(Level.SEVERE, "failed to set auto-commit to to false", ex);
+            }
+
+        }
+
+        private void rollback() {
+            if (!closed) {
+                try {
+                    con.rollback();
+
+                } catch (SQLException ex1) {
+                    LOGGER.log(Level.SEVERE, "Exception while attempting to rollback!!", ex1);
+                } finally {
+                    close();
+                }
+            }
+        }
+
+        private void commit(Boolean notify) {
+            if (!closed) {
+                try {
+                    con.commit();
+                    // make sure we close before we update, bc they'll need locks
+                    close();
+
+                    if (notify) {
+//                        fireNewEvents(newEvents);
+                    }
+                } catch (SQLException ex) {
+                    LOGGER.log(Level.SEVERE, "Error commiting events.db.", ex);
+                    rollback();
+                }
+            }
+        }
+
+        private void close() {
+            if (!closed) {
+                try {
+                    con.setAutoCommit(true);
+                } catch (SQLException ex) {
+                    LOGGER.log(Level.SEVERE, "Error setting auto-commit to true.", ex);
+                } finally {
+                    closed = true;
+
+                    dbWriteUnlock();
+                }
+            }
+        }
+
+        public Boolean isClosed() {
+            return closed;
+        }
+    }
+
+    public class MultipleTransactionException extends IllegalStateException {
+
+        private static final String CANNOT_HAVE_MORE_THAN_ONE_OPEN_TRANSACTIO = "cannot have more than one open transaction";
+
+        public MultipleTransactionException() {
+            super(CANNOT_HAVE_MORE_THAN_ONE_OPEN_TRANSACTIO);
+        }
+
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/db/EventsRepository.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/db/EventsRepository.java
new file mode 100644
index 0000000000000000000000000000000000000000..da9bab52d8b97aef1095527120f2434361dbbe1f
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/db/EventsRepository.java
@@ -0,0 +1,360 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 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.db;
+
+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.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import javax.annotation.concurrent.GuardedBy;
+import javax.swing.JOptionPane;
+import javax.swing.SwingWorker;
+import org.apache.commons.lang3.StringUtils;
+import org.joda.time.Interval;
+import org.sleuthkit.autopsy.timeline.ProgressWindow;
+import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+import org.sleuthkit.autopsy.timeline.events.TimeLineEvent;
+import org.sleuthkit.autopsy.timeline.events.type.ArtifactEventType;
+import org.sleuthkit.autopsy.timeline.events.type.EventType;
+import org.sleuthkit.autopsy.timeline.events.type.FileSystemTypes;
+import org.sleuthkit.autopsy.timeline.events.type.RootEventType;
+import org.sleuthkit.autopsy.timeline.filters.Filter;
+import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.SleuthkitCase;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * Provides public API (over EventsDB) to access events. In theory this
+ * insulates the rest of the timeline module form the details of the db
+ * implementation. Since there are no other implementations of the database or
+ * clients of this class, and no Java Interface defined yet, in practice this
+ * just delegates everything to the eventDB
+ *
+ * Concurrency Policy:
+ *
+ * Since almost everything just delegates to the EventDB, which is internally
+ * synchronized, we only have to worry about rebuildRepository() which we
+ * synchronize on our intrinsic lock.
+ *
+ */
+public class EventsRepository {
+
+    private static final String FILES_AND_DIRS_WHERE_CLAUSE = "name != '.' AND name != '..'";
+
+    private final EventDB eventDB;
+
+    private final static Logger LOGGER = Logger.getLogger(EventsRepository.class.getName());
+
+    @GuardedBy("this")
+    private SwingWorker<Void, ProgressWindow.ProgressUpdate> dbPopulationWorker;
+
+    private final LoadingCache<Object, Long> maxCache;
+
+    private final LoadingCache<Object, Long> minCache;
+
+    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;
+
+    public Interval getBoundingEventsInterval(Interval timeRange, Filter filter) {
+        return eventDB.getBoundingEventsInterval(timeRange, filter);
+    }
+
+    /**
+     * @return a FilteredEvetns object with this repository as underlying source
+     *         of events
+     */
+    public FilteredEventsModel getEventsModel() {
+        return modelInstance;
+    }
+
+    public EventsRepository() {
+        //TODO: we should check that case is open, or get passed a case object/directory -jm
+        this.eventDB = EventDB.getEventDB(Case.getCurrentCase().getCaseDirectory());
+
+        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));
+        maxCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMaxTime));
+        minCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMinTime));
+        this.modelInstance = new FilteredEventsModel(this);
+    }
+
+  
+
+    /** @return min time (in seconds from unix epoch) */
+    public Long getMaxTime() {
+        return maxCache.getUnchecked("max");
+//        return eventDB.getMaxTime();
+    }
+
+    /** @return max tie (in seconds from unix epoch) */
+    public Long getMinTime() {
+        return minCache.getUnchecked("min");
+//        return eventDB.getMinTime();
+    }
+
+    public void recordLastArtifactID(long lastArtfID) {
+        eventDB.recordLastArtifactID(lastArtfID);
+    }
+
+    public void recordWasIngestRunning(Boolean wasIngestRunning) {
+        eventDB.recordWasIngestRunning(wasIngestRunning);
+    }
+
+    public void recordLastObjID(Long lastObjID) {
+        eventDB.recordLastObjID(lastObjID);
+    }
+
+    public boolean getWasIngestRunning() {
+        return eventDB.getWasIngestRunning();
+    }
+
+    public Long getLastObjID() {
+        return eventDB.getLastObjID();
+    }
+    public long getLastArtfactID() {
+        return eventDB.getLastArtfactID();
+    }
+    public TimeLineEvent getEventById(Long eventID) {
+        return idToEventCache.getUnchecked(eventID);
+    }
+
+    public List<AggregateEvent> getAggregatedEvents(ZoomParams params) {
+
+        return aggregateEventsCache.getUnchecked(params);
+    }
+
+    public Map<EventType, Long> countEvents(ZoomParams params) {
+        return eventCountsCache.getUnchecked(params);
+
+    }
+
+    private void invalidateCaches() {
+        minCache.invalidateAll();
+        maxCache.invalidateAll();
+        eventCountsCache.invalidateAll();
+        aggregateEventsCache.invalidateAll();
+    }
+
+    public Set<Long> getEventIDs(Interval timeRange, Filter filter) {
+        return eventDB.getEventIDs(timeRange, filter);
+    }
+
+    public Interval getSpanningInterval(Collection<Long> eventIDs) {
+        return eventDB.getSpanningInterval(eventIDs);
+    }
+
+    synchronized public void rebuildRepository(Runnable r) {
+        if (dbPopulationWorker != null) {
+            dbPopulationWorker.cancel(true);
+
+        }
+        dbPopulationWorker = new DBPopulationWorker(r);
+        dbPopulationWorker.execute();
+    }
+
+    /**
+     * SwingWorker to populate event db with data from main autopsy database.
+     *
+     * has only local state. accesses eventDB but that is internally
+     * synchronized/ thread-safe.
+     */
+    private class DBPopulationWorker extends SwingWorker<Void, ProgressWindow.ProgressUpdate> {
+
+        private final ProgressWindow progressDialog;
+
+        //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;
+
+        public DBPopulationWorker(Runnable r) {
+            progressDialog = new ProgressWindow(null, true, this);
+            progressDialog.setVisible(true);
+            this.r = r;
+        }
+
+        @Override
+        protected Void doInBackground() throws Exception {
+            process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, -1, "(re)initializing events database", "")));
+            //reset database 
+            //TODO: can we do more incremental updates? -jm
+            eventDB.dropTable();
+            eventDB.initializeDB();
+
+            //grab ids of all files
+            SleuthkitCase skCase = Case.getCurrentCase().getSleuthkitCase();
+            List<Long> files = skCase.findAllFileIdsWhere(FILES_AND_DIRS_WHERE_CLAUSE);
+
+            final int numFiles = files.size();
+            process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, numFiles, "populating mac events for files: ", "")));
+
+            //insert file events into db
+            int i = 1;
+            EventDB.EventTransaction trans = eventDB.beginTransaction();
+            for (final Long fID : files) {
+                if (isCancelled()) {
+                    break;
+                } else {
+                    try {
+                        AbstractFile f = skCase.getAbstractFileById(fID);
+                        //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();
+                        final String parentPath = f.getParentPath();
+                        String datasourceName = StringUtils.substringBefore(StringUtils.stripStart(uniquePath, "/"), parentPath);
+                        String rootFolder = StringUtils.substringBetween(parentPath, "/", "/");
+                        String shortDesc = datasourceName + "/" + StringUtils.defaultIfBlank(rootFolder, "");
+                        String medD = datasourceName  + parentPath;
+
+                        //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, fID, null, uniquePath, medD, shortDesc, f.getKnown(), trans);
+                        }
+                        if (f.getMtime() > 0) {
+                            eventDB.insertEvent(f.getMtime(), FileSystemTypes.FILE_MODIFIED, fID, null, uniquePath, medD, shortDesc, f.getKnown(), trans);
+                        }
+                        if (f.getCtime() > 0) {
+                            eventDB.insertEvent(f.getCtime(), FileSystemTypes.FILE_CHANGED, fID, null, uniquePath, medD, shortDesc, f.getKnown(), trans);
+                        }
+                        if (f.getCrtime() > 0) {
+                            eventDB.insertEvent(f.getCrtime(), FileSystemTypes.FILE_CREATED, fID, null, uniquePath, medD, shortDesc, f.getKnown(), trans);
+                        }
+
+                        process(Arrays.asList(new ProgressWindow.ProgressUpdate(i, numFiles, "populating mac events for files: ", f.getName())));
+                    } catch (TskCoreException tskCoreException) {
+                        LOGGER.log(Level.WARNING, "failed to insert mac event for file : " + fID, tskCoreException);
+                    }
+                }
+                i++;
+            }
+
+            //insert artifact based events
+            //TODO: use (not-yet existing api) to grab all artifacts with timestamps, rather than the hardcoded lists in EventType -jm
+            for (EventType type : RootEventType.allTypes) {
+                if (isCancelled()) {
+                    break;
+                }
+                //skip file_system events, they are already handled above.
+                if (type instanceof ArtifactEventType) {
+                    populateEventType((ArtifactEventType) type, trans, skCase);
+                }
+            }
+
+            process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, -1, "commiting events db", "")));
+            if (isCancelled()) {
+                eventDB.rollBackTransaction(trans);
+            } else {
+                eventDB.commitTransaction(trans, true);
+            }
+
+            invalidateCaches();
+
+            return null;
+        }
+
+        /**
+         * handle intermediate 'results': just update progress dialog
+         *
+         * @param chunks
+         */
+        @Override
+        protected void process(List<ProgressWindow.ProgressUpdate> chunks) {
+            super.process(chunks);
+            ProgressWindow.ProgressUpdate chunk = chunks.get(chunks.size() - 1);
+            progressDialog.update(chunk);
+        }
+
+        @Override
+        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);
+            } catch (InterruptedException | ExecutionException ex) {
+                LOGGER.log(Level.WARNING, "Exception while populating database.", ex);
+                JOptionPane.showMessageDialog(null, "There was a problem populating the timeline.  Not all events may be present or accurate. See the log for details.");
+            } catch (Exception ex) {
+                LOGGER.log(Level.WARNING, "Unexpected exception while populating database.", ex);
+                JOptionPane.showMessageDialog(null, "There was a problem populating the timeline.  Not all events may be present or accurate. See the log for details.");
+            }
+            r.run();  //execute post db population operation
+        }
+
+        /**
+         * populate all the events of one subtype
+         *
+         * @param subType the subtype to populate
+         * @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) {
+            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, "populating " + type.toString() + " events", "")));
+
+                int i = 0;
+                for (final BlackboardArtifact bbart : blackboardArtifacts) {
+                    //for each artifact, extract the relevant information for the descriptions
+                    ArtifactEventType.AttributeEventDescription eventDescription = ArtifactEventType.AttributeEventDescription.buildEventDescription(type, bbart);
+
+                    if (eventDescription != null && eventDescription.getTime() > 0L) {  //insert it into the db if time is > 0  => time is legitimate
+                        eventDB.insertEvent(eventDescription.getTime(), type, bbart.getObjectID(), bbart.getArtifactID(), eventDescription.getFullDescription(), eventDescription.getMedDescription(), eventDescription.getShortDescription(), null, trans);
+                    }
+
+                    i++;
+                    process(Arrays.asList(new ProgressWindow.ProgressUpdate(i, numArtifacts, "populating " + type.toString() + " events", "")));
+                }
+            } catch (TskCoreException ex) {
+                LOGGER.log(Level.SEVERE, "There was a problem getting events with sub type = " + type.toString() + ".", ex);
+            }
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/ArtifactEventType.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/ArtifactEventType.java
new file mode 100644
index 0000000000000000000000000000000000000000..38a0de16c0272ff62c4252bee1a4a0f712b93113
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/ArtifactEventType.java
@@ -0,0 +1,189 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.type;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.logging.Level;
+import org.apache.commons.lang3.StringUtils;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ *
+ */
+public interface ArtifactEventType extends EventType {
+
+    /**
+     * @return the Artifact type this event type is derived form, or null if
+     *         there is no artifact type (eg file system events)
+     */
+    public BlackboardArtifact.ARTIFACT_TYPE getArtifactType();
+
+    public BlackboardAttribute.ATTRIBUTE_TYPE getDateTimeAttrubuteType();
+
+    /** given an artifact, and a map from attribute types to attributes, pull
+     * out the time stamp, and compose the descriptions. Each implementation
+     * of {@link ArtifactEventType} needs to implement parseAttributesHelper()
+     * as hook for {@link buildEventDescription(org.sleuthkit.datamodel.BlackboardArtifact)
+     * to invoke. Most subtypes can use this default implementation.
+     *
+     * @param artf
+     * @param attrMap
+     *
+     * @return an {@link AttributeEventDescription} containing the timestamp
+     *         and description information
+     *
+     * @throws TskCoreException
+     */
+    default AttributeEventDescription parseAttributesHelper(BlackboardArtifact artf, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> attrMap) throws TskCoreException {
+        final BlackboardAttribute dateTimeAttr = attrMap.get(getDateTimeAttrubuteType());
+
+        long time = dateTimeAttr.getValueLong();
+        String shortDescription = getShortExtractor().apply(artf, attrMap);
+        String medDescription = shortDescription + " : " + getMedExtractor().apply(artf, attrMap);
+        String fullDescription = medDescription + " : " + getFullExtractor().apply(artf, attrMap);
+        return new AttributeEventDescription(time, shortDescription, medDescription, fullDescription);
+    }
+
+    /** @return a function from an artifact and a map of its attributes, to a
+     *          String to use as part of the full event description */
+    BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getFullExtractor();
+
+    /** @return a function from an artifact and a map of its attributes, to a
+     *          String to use as part of the medium event description */
+    BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getMedExtractor();
+
+    /** @return a function from an artifact and a map of its attributes, to a
+     *          String to use as part of the short event description */
+    BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getShortExtractor();
+
+    /**
+     * bundles the per event information derived from a BlackBoard Artifact into
+     * one object. Primarily used to have a single return value for
+     * {@link SubType#buildEventDescription(org.sleuthkit.datamodel.BlackboardArtifact).
+     */
+    static class AttributeEventDescription {
+
+        final private long time;
+
+        public long getTime() {
+            return time;
+        }
+
+        public String getShortDescription() {
+            return shortDescription;
+        }
+
+        public String getMedDescription() {
+            return medDescription;
+        }
+
+        public String getFullDescription() {
+            return fullDescription;
+        }
+
+        final private String shortDescription;
+
+        final private String medDescription;
+
+        final private String fullDescription;
+
+        public AttributeEventDescription(long time, String shortDescription,
+                                         String medDescription,
+                                         String fullDescription) {
+            this.time = time;
+            this.shortDescription = shortDescription;
+            this.medDescription = medDescription;
+            this.fullDescription = fullDescription;
+        }
+
+        /**
+         * Build a {@link AttributeEventDescription} derived from a
+         * {@link BlackboardArtifact}. This is a template method that relies on
+         * each {@link SubType}'s implementation of
+         * {@link SubType#parseAttributesHelper(org.sleuthkit.datamodel.BlackboardArtifact, java.util.Map)}
+         * know how to go from {@link BlackboardAttribute}s to the event
+         * description.
+         *
+         * @param artf the {@link BlackboardArtifact} to derive the event
+         *             description from
+         *
+         * @return an {@link AttributeEventDescription} derived from the given
+         *         artifact
+         *
+         * @throws TskCoreException is there is a problem accessing the
+         *                          blackboard data
+         */
+        static public AttributeEventDescription buildEventDescription(
+                ArtifactEventType type, BlackboardArtifact artf) throws TskCoreException {
+            //if we got passed an artifact that doesn't correspond to the type of the event, 
+            //something went very wrong. throw an exception.
+            if (type.getArtifactType().getTypeID() != artf.getArtifactTypeID()) {
+                throw new IllegalArgumentException();
+            }
+
+            /* build a map from attribute type to attribute, this makes
+             * implementing the parseAttributeHelper easier but could be
+             * ineffecient if we don't need most of the attributes. This would
+             * be unnessecary if there was an api on Blackboard artifacts to get
+             * specific attributes by type */
+            List<BlackboardAttribute> attributes = artf.getAttributes();
+            Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> attrMap = new HashMap<>();
+            for (BlackboardAttribute attr : attributes) {
+                attrMap.put(BlackboardAttribute.ATTRIBUTE_TYPE.fromLabel(attr.
+                        getAttributeTypeName()), attr);
+            }
+
+            if (attrMap.get(type.getDateTimeAttrubuteType()) == null) {
+                Logger.getLogger(AttributeEventDescription.class.getName()).log(Level.WARNING, "Artifact {0}  has no date/time attribute, skipping it.", artf.getArtifactID());
+                return null;
+            }
+            //use the hook provided by this subtype implementation
+            return type.parseAttributesHelper(artf, attrMap);
+        }
+    }
+
+    public static class AttributeExtractor implements BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> {
+
+        @Override
+        public String apply(BlackboardArtifact artf, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> attrMap) {
+            final BlackboardAttribute attr = attrMap.get(attribute);
+            return (attr != null) ? StringUtils.defaultString(attr.getValueString()) : " ";
+        }
+
+        private final BlackboardAttribute.ATTRIBUTE_TYPE attribute;
+
+        public AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE attribute) {
+            this.attribute = attribute;
+        }
+    }
+
+    public static class EmptyExtractor implements BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> {
+
+        @Override
+        public String apply(BlackboardArtifact t, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> u) {
+            return "";
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/BaseTypes.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/BaseTypes.java
new file mode 100644
index 0000000000000000000000000000000000000000..ff7fc925ff73edd462b772a870b2353c2d9d879e
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/BaseTypes.java
@@ -0,0 +1,108 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.type;
+
+import java.util.Arrays;
+import java.util.List;
+import javafx.scene.image.Image;
+import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
+
+/**
+ * RootTypes are event types that have no super type.
+ */
+public enum BaseTypes implements EventType {
+
+    FILE_SYSTEM("File System", "blue-document.png") {
+
+                @Override
+                public List<? extends EventType> getSubTypes() {
+                    return Arrays.asList(FileSystemTypes.values());
+                }
+
+                @Override
+                public EventType getSubType(String string) {
+                    return FileSystemTypes.valueOf(string);
+                }
+            },
+    WEB_ACTIVITY("Web Activity", "web-file.png") {
+
+                @Override
+                public List<? extends EventType> getSubTypes() {
+                    return Arrays.asList(WebTypes.values());
+                }
+
+                @Override
+                public EventType getSubType(String string) {
+                    return WebTypes.valueOf(string);
+                }
+            },
+    MISC_TYPES("Misc Types", "block.png") {
+
+                @Override
+                public List<? extends EventType> getSubTypes() {
+                    return Arrays.asList(MiscTypes.values());
+                }
+
+                @Override
+                public EventType getSubType(String string) {
+                    return MiscTypes.valueOf(string);
+                }
+            };
+
+    private final String displayName;
+
+    private final String iconBase;
+
+    private Image image;
+
+    public Image getFXImage() {
+        return image;
+    }
+
+    @Override
+    public String getIconBase() {
+        return iconBase;
+    }
+
+    @Override
+    public EventTypeZoomLevel getZoomLevel() {
+        return EventTypeZoomLevel.BASE_TYPE;
+    }
+
+    @Override
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    private BaseTypes(String displayName, String iconBase) {
+        this.displayName = displayName;
+        this.iconBase = iconBase;
+        this.image = new Image("org/sleuthkit/autopsy/timeline/images/" + iconBase, true);
+    }
+
+    @Override
+    public EventType getSuperType() {
+        return RootEventType.getInstance();
+    }
+
+    @Override
+    public EventType getSubType(String string) {
+        return BaseTypes.valueOf(string);
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/EventType.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/EventType.java
new file mode 100644
index 0000000000000000000000000000000000000000..fbbc95e0ecc2b41ecb7e464bf60e52161d17713d
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/EventType.java
@@ -0,0 +1,108 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.type;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import javafx.scene.image.Image;
+import javafx.scene.paint.Color;
+import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
+
+/**
+ * An Event Type represents a distinct kind of event ie file system or web
+ * activity. An EventType may have an optional super-type and 0 or more
+ * subtypes, allowing events to be organized in a type hierarchy.
+ */
+public interface EventType {
+
+    final static List<? extends EventType> allTypes = RootEventType.getInstance().getSubTypesRecusive();
+
+    static Comparator<EventType> getComparator() {
+        return Comparator.comparing(EventType.allTypes::indexOf);
+
+    }
+
+    default BaseTypes getBaseType() {
+        if (this instanceof BaseTypes) {
+            return (BaseTypes) this;
+        } else {
+            return getSuperType().getBaseType();
+        }
+    }
+
+    default List<? extends EventType> getSubTypesRecusive() {
+        ArrayList<EventType> flatList = new ArrayList<>();
+
+        for (EventType et : getSubTypes()) {
+            flatList.add(et);
+            flatList.addAll(et.getSubTypesRecusive());
+        }
+        return flatList;
+    }
+
+    /**
+     * @return the color used to represent this event type visually
+     */
+    default Color getColor() {
+
+        Color baseColor = this.getSuperType().getColor();
+        int siblings = getSuperType().getSiblingTypes().stream().max((
+                EventType t, EventType t1)
+                -> Integer.compare(t.getSubTypes().size(), t1.getSubTypes().size()))
+                .get().getSubTypes().size() + 1;
+        int superSiblings = this.getSuperType().getSiblingTypes().size();
+
+        double offset = (360.0 / superSiblings) / siblings;
+        final Color deriveColor = baseColor.deriveColor(ordinal() * offset, 1, 1, 1);
+
+        return Color.hsb(deriveColor.getHue(), deriveColor.getSaturation(), deriveColor.getBrightness());
+
+    }
+
+    default List<? extends EventType> getSiblingTypes() {
+        return this.getSuperType().getSubTypes();
+    }
+
+    /**
+     * @return the super type of this event
+     */
+    public EventType getSuperType();
+
+    public EventTypeZoomLevel getZoomLevel();
+
+    /**
+     * @return a list of event types, one for each subtype of this eventype, or
+     *         an empty list if this event type has no subtypes
+     */
+    public List<? extends EventType> getSubTypes();
+
+    /* return the name of the icon file for this type, it will be resolved in
+     * the org/sleuthkit/autopsy/timeline/images */
+    public String getIconBase();
+
+    public String getDisplayName();
+
+    public EventType getSubType(String string);
+
+    public Image getFXImage();
+
+    public int ordinal();
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/FileSystemTypes.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/FileSystemTypes.java
new file mode 100644
index 0000000000000000000000000000000000000000..771b692023e4e42c75fe261817001544f28b2ddf
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/FileSystemTypes.java
@@ -0,0 +1,82 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.type;
+
+import java.util.Collections;
+import java.util.List;
+import javafx.scene.image.Image;
+import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
+
+/**
+ *
+ */
+public enum FileSystemTypes implements EventType {
+
+    FILE_MODIFIED("File Modified", "blue-document-attribute-m.png"),
+    FILE_ACCESSED("File Accessed", "blue-document-attribute-a.png"),
+    FILE_CREATED("File Created", "blue-document-attribute-b.png"),
+    FILE_CHANGED("File Changed", "blue-document-attribute-c.png");
+
+    private final String iconBase;
+
+    private final Image image;
+
+    @Override
+    public Image getFXImage() {
+        return image;
+    }
+
+    @Override
+    public String getIconBase() {
+        return iconBase;
+    }
+
+    @Override
+    public EventTypeZoomLevel getZoomLevel() {
+        return EventTypeZoomLevel.SUB_TYPE;
+    }
+
+    private final String displayName;
+
+    @Override
+    public EventType getSubType(String string) {
+        return FileSystemTypes.valueOf(string);
+    }
+
+    @Override
+    public EventType getSuperType() {
+        return BaseTypes.FILE_SYSTEM;
+    }
+
+    @Override
+    public List<? extends EventType> getSubTypes() {
+        return Collections.emptyList();
+    }
+
+    private FileSystemTypes(String displayName, String iconBase) {
+        this.displayName = displayName;
+        this.iconBase = iconBase;
+        this.image = new Image("org/sleuthkit/autopsy/timeline/images/" + iconBase, true);
+    }
+
+    @Override
+    public String getDisplayName() {
+        return displayName;
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/MiscTypes.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/MiscTypes.java
new file mode 100644
index 0000000000000000000000000000000000000000..1d44a251611a8e013b2b67b2ca286622d394cf9b
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/MiscTypes.java
@@ -0,0 +1,253 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.type;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import javafx.scene.image.Image;
+import org.apache.commons.lang3.StringUtils;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ *
+ */
+public enum MiscTypes implements EventType, ArtifactEventType {
+
+    MESSAGE("Messages", "message.png",
+            BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE,
+            BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME,
+            new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_MESSAGE_TYPE),
+            (artf, attrMap) -> {
+                final BlackboardAttribute dir = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DIRECTION);
+                final BlackboardAttribute name = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME);
+                final BlackboardAttribute phoneNumber = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER);
+                final BlackboardAttribute subject = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SUBJECT);
+                List<String> asList = Arrays.asList(stringValueOf(dir), name != null || phoneNumber != null ? toFrom(dir) : "", stringValueOf(name != null ? name : phoneNumber), (subject == null ? "" : stringValueOf(subject)));
+                return StringUtils.join(asList, " ");
+            },
+            new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT)),
+    GPS_ROUTE("GPS Routes", "gps-search.png",
+              BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE,
+              BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME,
+              new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME),
+              new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_LOCATION),
+              (BlackboardArtifact artf, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> attrMap) -> {
+                  final BlackboardAttribute latStart = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START);
+                  final BlackboardAttribute longStart = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_START);
+                  final BlackboardAttribute latEnd = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END);
+                  final BlackboardAttribute longEnd = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END);
+                  return String.format("from %1$g %2$g to %3$g %4$g", latStart.getValueDouble(), longStart.getValueDouble(), latEnd.getValueDouble(), longEnd.getValueDouble());
+              }),
+    GPS_TRACKPOINT("Location History", "gps-trackpoint.png",
+                   BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACKPOINT,
+                   BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME,
+                   new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME),
+                   (artf, attrMap) -> {
+                       final BlackboardAttribute longitude = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE);
+                       final BlackboardAttribute latitude = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE);
+                       return (latitude != null ? latitude.getValueDouble() : "") + " " + (longitude != null ? longitude.getValueDouble() : "");
+                   },
+                   (artf, attrMap) -> ""),
+    CALL_LOG("Calls", "calllog.png",
+             BlackboardArtifact.ARTIFACT_TYPE.TSK_CALLLOG,
+             BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_START,
+             new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME),
+             new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER),
+             new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DIRECTION)),
+    EMAIL("Email", "mail-icon-16.png",
+          BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG,
+          BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_SENT,
+          (artifact, attrMap) -> {
+              final BlackboardAttribute emailFrom = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL_FROM);
+              final BlackboardAttribute emailTo = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL_TO);
+              return (emailFrom != null ? emailFrom.getValueString() : "") + " to " + (emailTo != null ? emailTo.getValueString() : "");
+          },
+          new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SUBJECT),
+          new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_PLAIN)),
+    RECENT_DOCUMENTS("Recent Documents", "recent_docs.png",
+                     BlackboardArtifact.ARTIFACT_TYPE.TSK_RECENT_OBJECT,
+                     BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME,
+                     new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH).andThen(
+                             (String t) -> (StringUtils.substringBeforeLast(StringUtils.substringBeforeLast(t, "\\"), "\\"))),
+                     new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH).andThen(
+                             (String t) -> StringUtils.substringBeforeLast(t, "\\")),
+                     new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH)) {
+
+                /** Override
+                 * {@link ArtifactEventType#parseAttributesHelper(org.sleuthkit.datamodel.BlackboardArtifact, java.util.Map)}
+                 * with non-default description construction */
+                @Override
+        public AttributeEventDescription parseAttributesHelper(BlackboardArtifact artf, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> attrMap) throws TskCoreException {
+                    final BlackboardAttribute dateTimeAttr = attrMap.get(getDateTimeAttrubuteType());
+
+                    long time = dateTimeAttr.getValueLong();
+
+                    //Non-default description construction
+                    String shortDescription = getShortExtractor().apply(artf, attrMap);
+                    String medDescription = getMedExtractor().apply(artf, attrMap);
+                    String fullDescription = getFullExtractor().apply(artf, attrMap);
+
+                    return new AttributeEventDescription(time, shortDescription, medDescription, fullDescription);
+                }
+            },
+    INSTALLED_PROGRAM("Installed Programs", "programs.png",
+                      BlackboardArtifact.ARTIFACT_TYPE.TSK_INSTALLED_PROG,
+                      BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME,
+                      new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME),
+                      new EmptyExtractor(),
+                      new EmptyExtractor()),
+    EXIF("Exif", "camera-icon-16.png",
+         BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF,
+         BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED,
+         new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MAKE),
+         new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MODEL),
+         (BlackboardArtifact t,
+          Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> u) -> {
+             try {
+                 return t.getSleuthkitCase().getAbstractFileById(t.getObjectID()).getName();
+             } catch (TskCoreException ex) {
+                 Exceptions.printStackTrace(ex);
+                 return " error loading file name";
+             }
+         }),
+    DEVICES_ATTACHED("Devices Attached", "usb_devices.png",
+                     BlackboardArtifact.ARTIFACT_TYPE.TSK_DEVICE_ATTACHED,
+                     BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME,
+                     new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MAKE),
+                     new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MODEL),
+                     new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_ID));
+
+    static public String stringValueOf(BlackboardAttribute attr) {
+        return attr != null ? attr.getValueString() : "";
+    }
+
+    public static String toFrom(BlackboardAttribute dir) {
+        if (dir == null) {
+            return "";
+        } else {
+            switch (dir.getValueString()) {
+                case "Incoming":
+                    return "from";
+                case "Outgoing":
+                    return "to";
+                default:
+                    return "";
+            }
+        }
+    }
+
+    private final BlackboardAttribute.ATTRIBUTE_TYPE dateTimeAttributeType;
+
+    private final String iconBase;
+
+    private final Image image;
+
+    @Override
+    public Image getFXImage() {
+        return image;
+    }
+
+    private final BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> longExtractor;
+
+    private final BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> medExtractor;
+
+    private final BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> shortExtractor;
+
+    @Override
+    public BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getFullExtractor() {
+        return longExtractor;
+    }
+
+    @Override
+    public BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getMedExtractor() {
+        return medExtractor;
+    }
+
+    @Override
+    public BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getShortExtractor() {
+        return shortExtractor;
+    }
+
+    @Override
+    public BlackboardAttribute.ATTRIBUTE_TYPE getDateTimeAttrubuteType() {
+        return dateTimeAttributeType;
+    }
+
+    @Override
+    public EventTypeZoomLevel getZoomLevel() {
+        return EventTypeZoomLevel.SUB_TYPE;
+    }
+
+    private final String displayName;
+
+    private final BlackboardArtifact.ARTIFACT_TYPE artifactType;
+
+    @Override
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    @Override
+    public String getIconBase() {
+        return iconBase;
+    }
+
+    @Override
+    public EventType getSubType(String string) {
+        return MiscTypes.valueOf(string);
+    }
+
+    private MiscTypes(String displayName, String iconBase, BlackboardArtifact.ARTIFACT_TYPE artifactType,
+                      BlackboardAttribute.ATTRIBUTE_TYPE dateTimeAttributeType,
+                      BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> shortExtractor,
+                      BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> medExtractor,
+                      BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> longExtractor) {
+        this.displayName = displayName;
+        this.iconBase = iconBase;
+        this.artifactType = artifactType;
+        this.dateTimeAttributeType = dateTimeAttributeType;
+        this.shortExtractor = shortExtractor;
+        this.medExtractor = medExtractor;
+        this.longExtractor = longExtractor;
+        this.image = new Image("org/sleuthkit/autopsy/timeline/images/" + iconBase, true);
+    }
+
+    @Override
+    public EventType getSuperType() {
+        return BaseTypes.MISC_TYPES;
+    }
+
+    @Override
+    public List<? extends EventType> getSubTypes() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public BlackboardArtifact.ARTIFACT_TYPE getArtifactType() {
+        return artifactType;
+    }
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/RootEventType.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/RootEventType.java
new file mode 100644
index 0000000000000000000000000000000000000000..79716db2f7693745513d308fc4da5460fe42f2eb
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/RootEventType.java
@@ -0,0 +1,95 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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.type;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javafx.scene.image.Image;
+import javafx.scene.paint.Color;
+import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
+
+/** A singleton {@link } EventType to represent the root type of all event
+ * types. */
+public class RootEventType implements EventType {
+
+    @Override
+    public List<RootEventType> getSiblingTypes() {
+        return Collections.singletonList(this);
+    }
+
+    @Override
+    public EventTypeZoomLevel getZoomLevel() {
+        return EventTypeZoomLevel.ROOT_TYPE;
+    }
+
+    private RootEventType() {
+    }
+
+    public static RootEventType getInstance() {
+        return RootEventTypeHolder.INSTANCE;
+    }
+
+    @Override
+    public EventType getSubType(String string) {
+        return BaseTypes.valueOf(string);
+    }
+
+    @Override
+    public int ordinal() {
+        return 0;
+    }
+
+    private static class RootEventTypeHolder {
+
+        private static final RootEventType INSTANCE = new RootEventType();
+    }
+
+    @Override
+    public Color getColor() {
+        return Color.hsb(359, .9, .9, 0);
+    }
+
+    @Override
+    public RootEventType getSuperType() {
+        return this;
+    }
+
+    @Override
+    public List<BaseTypes> getSubTypes() {
+        return Arrays.asList(BaseTypes.values());
+    }
+
+   
+
+    @Override
+    public String getIconBase() {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public String getDisplayName() {
+        return "Event Types";
+    }
+
+    @Override
+    public Image getFXImage() {
+        return null;
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/WebTypes.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/WebTypes.java
new file mode 100644
index 0000000000000000000000000000000000000000..63093209940307fb63de9abef9bcdb760ddc56ad
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/WebTypes.java
@@ -0,0 +1,186 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.type;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import javafx.scene.image.Image;
+import org.apache.commons.lang3.StringUtils;
+import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+
+/**
+ *
+ */
+public enum WebTypes implements EventType, ArtifactEventType {
+
+    WEB_DOWNLOADS("Web Downloads",
+                  "downloads.png",
+                  BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_DOWNLOAD,
+                  BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED,
+                  new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN),
+                  new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH),
+                  new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL)) {
+
+                /** Override
+                 * {@link ArtifactEventType#parseAttributesHelper(org.sleuthkit.datamodel.BlackboardArtifact, java.util.Map)}
+                 * with non default descritpion construction */
+                @Override
+                public AttributeEventDescription parseAttributesHelper(BlackboardArtifact artf, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> attrMap) {
+                    long time = attrMap.get(getDateTimeAttrubuteType()).getValueLong();
+                    String domain = getShortExtractor().apply(artf, attrMap);
+                    String path = getMedExtractor().apply(artf, attrMap);
+                    String fileName = StringUtils.substringAfterLast(path, "/");
+                    String url = getFullExtractor().apply(artf, attrMap);
+
+                    //TODO: review non default descritpion construction 
+                    String shortDescription = fileName + " from " + domain;
+                    String medDescription = fileName + " from " + url;
+                    String fullDescription = path + " from " + url;
+                    return new AttributeEventDescription(time, shortDescription, medDescription, fullDescription);
+                }
+            },
+    //TODO: review description seperators
+    WEB_COOKIE("Web Cookies",
+               "cookies.png",
+               BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_COOKIE,
+               BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME,
+               new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN),
+               new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME),
+               new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_VALUE)),
+    //TODO: review description seperators
+    WEB_BOOKMARK("Web Bookmarks",
+                 "bookmarks.png",
+                 BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_BOOKMARK,
+                 BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED,
+                 new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN),
+                 new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL),
+                 new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TITLE)),
+    //TODO: review description seperators
+    WEB_HISTORY("Web History",
+                "history.png",
+                BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_HISTORY,
+                BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED,
+                new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN),
+                new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL),
+                new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TITLE)),
+    //TODO: review description seperators
+    WEB_SEARCH("Web Searches",
+               "searchquery.png",
+               BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY,
+               BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED,
+               new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT),
+               new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN),
+               new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME));
+
+    private final BlackboardAttribute.ATTRIBUTE_TYPE dateTimeAttributeType;
+
+    private final String iconBase;
+
+    private final Image image;
+
+    @Override
+    public Image getFXImage() {
+        return image;
+    }
+
+    @Override
+    public BlackboardAttribute.ATTRIBUTE_TYPE getDateTimeAttrubuteType() {
+        return dateTimeAttributeType;
+    }
+
+    @Override
+    public EventTypeZoomLevel getZoomLevel() {
+        return EventTypeZoomLevel.SUB_TYPE;
+    }
+
+    private final BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> longExtractor;
+
+    private final BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> medExtractor;
+
+    private final BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> shortExtractor;
+
+    @Override
+    public BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getFullExtractor() {
+        return longExtractor;
+    }
+
+    @Override
+    public BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getMedExtractor() {
+        return medExtractor;
+    }
+
+    @Override
+    public BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getShortExtractor() {
+        return shortExtractor;
+    }
+
+    private final String displayName;
+
+    BlackboardArtifact.ARTIFACT_TYPE artifactType;
+
+    @Override
+    public String getIconBase() {
+        return iconBase;
+    }
+
+    @Override
+    public BlackboardArtifact.ARTIFACT_TYPE getArtifactType() {
+        return artifactType;
+    }
+
+    private WebTypes(String displayName, String iconBase, BlackboardArtifact.ARTIFACT_TYPE artifactType,
+                     BlackboardAttribute.ATTRIBUTE_TYPE dateTimeAttributeType,
+                     BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> shortExtractor,
+                     BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> medExtractor,
+                     BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> longExtractor) {
+        this.displayName = displayName;
+        this.iconBase = iconBase;
+        this.artifactType = artifactType;
+        this.dateTimeAttributeType = dateTimeAttributeType;
+        this.shortExtractor = shortExtractor;
+        this.medExtractor = medExtractor;
+        this.longExtractor = longExtractor;
+        this.image = new Image("org/sleuthkit/autopsy/timeline/images/" + iconBase, true);
+    }
+
+    @Override
+    public EventType getSuperType() {
+        return BaseTypes.WEB_ACTIVITY;
+    }
+
+    @Override
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    @Override
+    public EventType getSubType(String string) {
+        return WebTypes.valueOf(string);
+    }
+
+    @Override
+    public List<? extends EventType> getSubTypes() {
+        return Collections.emptyList();
+    }
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java b/Timeline/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..5e44cb9d79d5f96224237f9c3a3e564ea4e461be
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java
@@ -0,0 +1,151 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.explorernodes;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import javafx.beans.Observable;
+import javax.swing.Action;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.openide.nodes.Children;
+import org.openide.nodes.PropertySupport;
+import org.openide.nodes.Sheet;
+import org.openide.util.Exceptions;
+import org.openide.util.lookup.Lookups;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.events.TimeLineEvent;
+import org.sleuthkit.autopsy.datamodel.DataModelActionsFactory;
+import org.sleuthkit.autopsy.datamodel.DisplayableItemNode;
+import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor;
+import org.sleuthkit.autopsy.datamodel.NodeProperty;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.Content;
+
+/** * Node for {@link TimeLineEvent}s. */
+class EventNode extends DisplayableItemNode {
+
+    private final TimeLineEvent e;
+
+    EventNode(TimeLineEvent eventById, AbstractFile file, BlackboardArtifact artifact) {
+        super(Children.LEAF, Lookups.fixed(eventById, file, artifact));
+        this.e = eventById;
+        this.setIconBaseWithExtension("org/sleuthkit/autopsy/timeline/images/" + e.getType().getIconBase());
+    }
+
+    EventNode(TimeLineEvent eventById, AbstractFile file) {
+        super(Children.LEAF, Lookups.fixed(eventById, file));
+        this.e = eventById;
+        this.setIconBaseWithExtension("org/sleuthkit/autopsy/timeline/images/" + e.getType().getIconBase());
+    }
+
+    @Override
+    protected Sheet createSheet() {
+        Sheet s = super.createSheet();
+        Sheet.Set properties = s.get(Sheet.PROPERTIES);
+        if (properties == null) {
+            properties = Sheet.createPropertiesSet();
+            s.put(properties);
+        }
+
+        final TimeProperty timePropery = new TimeProperty("time", "Date/Time", "time ", getDateTimeString());
+
+        TimeLineController.getTimeZone().addListener((Observable observable) -> {
+            try {
+                timePropery.setValue(getDateTimeString());
+            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        });
+
+        //  String name, String displayName, String desc, T value
+        properties.put(new NodeProperty<>("icon", "Icon", "icon", true)); //gets overriden with icon
+
+        properties.put(timePropery);
+        properties.put(new NodeProperty<>("eventID", "Event ID", "event id", e.getEventID()));
+        properties.put(new NodeProperty<>("fileID", "File ID", "File id", e.getFileID()));
+        properties.put(new NodeProperty<>("artifactID", "Result ID", "result id", e.getArtifactID() == 0 ? "" : e.getArtifactID()));
+        properties.put(new NodeProperty<>("eventBaseType", "Base Type", "base type", e.getType().getSuperType().getDisplayName()));
+        properties.put(new NodeProperty<>("eventSubType", "Sub Type", "sub type", e.getType().getDisplayName()));
+        properties.put(new NodeProperty<>("Known", "Known", "known", e.getKnown().toString()));
+        properties.put(new NodeProperty<>("description", "Description", "description", e.getFullDescription()));
+        return s;
+    }
+
+    private String getDateTimeString() {
+        return new DateTime(e.getTime() * 1000, DateTimeZone.UTC).toString(TimeLineController.getZonedFormatter());
+    }
+
+    @Override
+    public Action[] getActions(boolean context) {
+        Action[] superActions = super.getActions(context);
+        List<Action> actionsList = new ArrayList<>();
+        actionsList.addAll(Arrays.asList(superActions));
+
+        final Content content = getLookup().lookup(Content.class);
+        final BlackboardArtifact artifact = getLookup().lookup(BlackboardArtifact.class);
+
+        final List<Action> factoryActions = DataModelActionsFactory.getActions(content, artifact != null);
+
+        actionsList.addAll(factoryActions);
+        return actionsList.toArray(new Action[0]);
+    }
+
+    @Override
+    public boolean isLeafTypeNode() {
+        return true;
+    }
+
+    @Override
+    public <T> T accept(DisplayableItemNodeVisitor<T> dinv) {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    class TimeProperty extends PropertySupport.ReadWrite<String> {
+
+        private String value;
+
+        @Override
+        public boolean canWrite() {
+            return false;
+        }
+
+        public TimeProperty(String name, String displayName, String shortDescription, String value) {
+            super(name, String.class, displayName, shortDescription);
+            setValue("suppressCustomEditor", Boolean.TRUE); // remove the "..." (editing) button NON-NLS
+            this.value = value;
+        }
+
+        @Override
+        public String getValue() throws IllegalAccessException, InvocationTargetException {
+            return value;
+        }
+
+        @Override
+        public void setValue(String t) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+            String oldValue = getValue();
+            value = t;
+            firePropertyChange("time", oldValue, t);
+        }
+    }
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java b/Timeline/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..7716d392d9ab199bae4be9216cef4be8e7c8bcb5
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java
@@ -0,0 +1,130 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.explorernodes;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.logging.Level;
+import org.openide.nodes.AbstractNode;
+import org.openide.nodes.ChildFactory;
+import org.openide.nodes.Children;
+import org.openide.nodes.Node;
+import org.openide.util.lookup.Lookups;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+import org.sleuthkit.autopsy.timeline.events.TimeLineEvent;
+import org.sleuthkit.autopsy.timeline.events.type.BaseTypes;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.datamodel.DisplayableItemNode;
+import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ *
+ */
+public class EventRootNode extends DisplayableItemNode {
+
+    public static final int MAX_EVENTS_TO_DISPLAY = 5000;
+
+    private final int childCount;
+
+    public EventRootNode(String NAME, Collection<Long> fileIds, FilteredEventsModel filteredEvents) {
+        super(Children.create(new EventNodeChildFactory(fileIds, filteredEvents), true), Lookups.singleton(fileIds));
+
+        super.setName(NAME);
+        super.setDisplayName(NAME);
+
+        childCount = fileIds.size();
+    }
+
+    @Override
+    public boolean isLeafTypeNode() {
+        return false;
+    }
+
+    @Override
+    public <T> T accept(DisplayableItemNodeVisitor<T> v) {
+        return null;
+    }
+
+    public int getChildCount() {
+        return childCount;
+    }
+
+    /** The node factories used to make lists of files to send to the result
+     * viewer using the lazy loading (rather than background) loading option to
+     * facilitate */
+    private static class EventNodeChildFactory extends ChildFactory<Long> {
+
+        private static final Logger LOGGER = Logger.getLogger(EventNodeChildFactory.class.getName());
+
+        private final Collection<Long> eventIDs;
+
+        private final FilteredEventsModel filteredEvents;
+
+        EventNodeChildFactory(Collection<Long> fileIds, FilteredEventsModel filteredEvents) {
+            this.eventIDs = fileIds;
+            this.filteredEvents = filteredEvents;
+        }
+
+        @Override
+        protected boolean createKeys(List<Long> toPopulate) {
+            if (eventIDs.size() < MAX_EVENTS_TO_DISPLAY) {
+                toPopulate.addAll(eventIDs);
+            } else {
+                toPopulate.add(-1l);
+            }
+            return true;
+        }
+
+        @Override
+        protected Node createNodeForKey(Long eventID) {
+            if (eventID >= 0) {
+                final TimeLineEvent eventById = filteredEvents.getEventById(eventID);
+                try {
+                    if (eventById.getType().getSuperType() == BaseTypes.FILE_SYSTEM) {
+                        return new EventNode(eventById, Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(eventById.getFileID()));
+                    } else {
+                        AbstractFile file = Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(eventById.getFileID());
+                        BlackboardArtifact blackboardArtifact = Case.getCurrentCase().getSleuthkitCase().getBlackboardArtifact(eventById.getArtifactID());
+
+                        return new EventNode(eventById, file, blackboardArtifact);
+                    }
+
+                } catch (TskCoreException tskCoreException) {
+                    LOGGER.log(Level.WARNING, "Failed to lookup sleuthkit object backing TimeLineEvent.", tskCoreException);
+                    return null;
+                }
+            } else {
+                return new TooManyNode(eventIDs.size());
+            }
+        }
+    }
+
+    private static class TooManyNode extends AbstractNode {
+
+        public TooManyNode(int size) {
+            super(Children.LEAF);
+            this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/info-icon-16.png");
+            setDisplayName("Too many events to display.  Maximum = " + MAX_EVENTS_TO_DISPLAY + ". But there are " + size + " to display.");
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..4e4f122f24b8731f66c0e19d82893baa931e7e25
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java
@@ -0,0 +1,53 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.timeline.filters;
+
+import javafx.beans.property.SimpleBooleanProperty;
+
+/**
+ * Base implementation of a {@link Filter}. Implements active property.
+ *
+ */
+public abstract class AbstractFilter implements Filter {
+
+    private final SimpleBooleanProperty active = new SimpleBooleanProperty(true);
+
+    private final SimpleBooleanProperty disabled = new SimpleBooleanProperty(false);
+
+    @Override
+    public SimpleBooleanProperty getActiveProperty() {
+        return active;
+    }
+
+    @Override
+    public SimpleBooleanProperty getDisabledProperty() {
+        return disabled;
+    }
+
+    @Override
+    public void setActive(Boolean act) {
+        active.set(act);
+    }
+
+    @Override
+    public boolean isActive() {
+        return active.get();
+    }
+
+    @Override
+    public void setDisabled(Boolean act) {
+        disabled.set(act);
+    }
+
+    @Override
+    public boolean isdisabled() {
+        return disabled.get();
+    }
+
+    @Override
+    public String getStringCheckBox() {
+        return "[" + (isActive() ? "x" : " ") + "]";
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..f1fab113c2581f24efd1b2d42ace28d4636ef2a3
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java
@@ -0,0 +1,106 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.timeline.filters;
+
+import java.util.List;
+import javafx.beans.Observable;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+
+/**
+ * A Filter with a collection of {@link Filter} sub-filters. If this
+ * filter is not active than none of its sub-filters are applied either.
+ * Concrete implementations can decide how to combine the sub-filters.
+ *
+ * a {@link CompoundFilter} uses listeners to enforce the following
+ * relationships between it and its sub-filters:
+ * <ol>
+ * <le>if a filter becomes active, and all its sub-filters were inactive, make
+ * them all active</le>
+ * <le>if a filter becomes inactive and all its sub-filters were active, make
+ * them all inactive</le>
+ * <le>if a sub-filter changes active state set the parent filter active if any
+ * of its sub-filters are active.</le>
+ * </ol>
+ */
+public abstract class CompoundFilter extends AbstractFilter {
+
+    /** the list of sub-filters that make up this filter */
+    private final ObservableList<Filter> subFilters;
+
+    public final ObservableList<Filter> getSubFilters() {
+        return subFilters;
+    }
+
+    /** construct a compound filter from a list of other filters to combine.
+     *
+     * @param subFilters
+     */
+    public CompoundFilter(ObservableList<Filter> subFilters) {
+        super();
+        this.subFilters = FXCollections.<Filter>synchronizedObservableList(subFilters);
+
+        //listen to changes in list of subfilters and add active state listener to newly added filters
+        this.subFilters.addListener((ListChangeListener.Change<? extends Filter> c) -> {
+            while (c.next()) {
+                addListeners(c.getAddedSubList());
+                //TODO: remove listeners from removed subfilters
+            }
+        });
+
+        //add listeners to subfilters
+        addListeners(subFilters);
+
+        //disable subfilters if this filter is disabled
+        getDisabledProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
+            if (newValue) {
+                getSubFilters().forEach((Filter t) -> {
+                    t.setDisabled(true);
+                });
+            } else {
+                final boolean isActive = !isActive();
+                getSubFilters().forEach((Filter t) -> {
+                    t.setDisabled(isActive);
+                });
+            }
+        });
+
+        //listen to active property and adjust subfilters active property
+        getActiveProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
+            if (newValue) {
+                // if this filter become active, and all its subfilters were inactive, make them all active
+                if (getSubFilters().stream().noneMatch(Filter::isActive)) {
+                    getSubFilters().forEach((Filter filter) -> {
+                        filter.setActive(true);
+                    });
+                }
+            } else {
+                //if this filter beceoms inactive and all its subfilters where active, make them inactive
+                if (getSubFilters().stream().allMatch(Filter::isActive)) {
+                    getSubFilters().forEach((Filter filter) -> {
+                        filter.setActive(false);
+                    });
+                }
+            }
+
+            //disabled subfilters if this filter is not active
+            getSubFilters().forEach((Filter t) -> {
+                t.setDisabled(!newValue);
+            });
+        });
+    }
+
+    private void addListeners(List<? extends Filter> newSubfilters) {
+        for (Filter sf : newSubfilters) {
+            //if a subfilter changes active state
+            sf.getActiveProperty().addListener((Observable observable) -> {
+                //set this filter acttive af any of the subfilters are active.
+                setActive(getSubFilters().parallelStream().anyMatch(Filter::isActive));
+            });
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/Filter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/Filter.java
new file mode 100644
index 0000000000000000000000000000000000000000..df07d23ffa7443ef6bcd71d75e1fd35799a4767b
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/Filter.java
@@ -0,0 +1,81 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.filters;
+
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import org.sleuthkit.autopsy.timeline.events.type.RootEventType;
+
+/** Interface for Filters */
+public interface Filter {
+
+    /** @return the default filter used at startup */
+    static Filter getDefaultFilter() {
+        return Filter.intersect(new Filter[]{new HideKnownFilter(), new TextFilter(), new TypeFilter(RootEventType.getInstance())});
+    }
+
+    /** @param <S>     the type of the given filters
+     * @param filters a set of filters to intersect
+     *
+     * @return a filter that is the intersection of the given filters */
+    public static IntersectionFilter intersect(ObservableList<Filter> filters) {
+        return new IntersectionFilter(filters);
+    }
+
+    /** @param <S>     the type of the given filters
+     * @param filters a set of filters to intersect
+     *
+     * @return a filter that is the intersection of the given filters */
+    public static IntersectionFilter intersect(Filter[] filters) {
+        return new IntersectionFilter(FXCollections.observableArrayList(filters));
+    }
+
+    /** since filters have mutable state (active) and are observed in various
+     * places, we need a mechanism to copy the current state to keep in history.
+     *
+     * Concrete subtasks should implement this in a way that preserves the
+     * active state and any subfilters.
+     *
+     * @return a copy of this filter. */
+    Filter copyOf();
+
+    String getDisplayName();
+
+    String getHTMLReportString();
+
+    String getStringCheckBox();
+
+    boolean isActive();
+
+    void setActive(Boolean act);
+
+    SimpleBooleanProperty getActiveProperty();
+
+    /* TODO: disabled state only affects the state of the checkboxes in the ui
+     * and not the actual filters and shouldn't be implemented here, but it
+     * was too hard to figure out how it should be implemented without intruding
+     * on the ui-ignorant filters */
+    void setDisabled(Boolean act);
+
+    SimpleBooleanProperty getDisabledProperty();
+
+    boolean isdisabled();
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/HideKnownFilter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/HideKnownFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..86c36ce5fe26534861500879d11ee24c5c2640f8
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/HideKnownFilter.java
@@ -0,0 +1,69 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.filters;
+
+/**
+ * Filter to hide known files
+ */
+public class HideKnownFilter extends AbstractFilter {
+
+    @Override
+    public String getDisplayName() {
+        return "Hide Known Files";
+    }
+
+    public HideKnownFilter() {
+        super();
+        getActiveProperty().set(false);
+    }
+
+    @Override
+    public HideKnownFilter copyOf() {
+        HideKnownFilter hideKnownFilter = new HideKnownFilter();
+        hideKnownFilter.setActive(isActive());
+        hideKnownFilter.setDisabled(isdisabled());
+        return hideKnownFilter;
+    }
+
+    @Override
+    public String getHTMLReportString() {
+        return "hide known" + getStringCheckBox();
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final HideKnownFilter other = (HideKnownFilter) obj;
+
+        return isActive() == other.isActive();
+
+    }
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/IntersectionFilter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/IntersectionFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..b11b0f53b7f5718ec4efad9d46e72a91e599be4d
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/IntersectionFilter.java
@@ -0,0 +1,70 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.timeline.filters;
+
+import java.util.stream.Collectors;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+/** Intersection(And) filter */
+public class IntersectionFilter extends CompoundFilter {
+    
+    public IntersectionFilter(ObservableList<Filter> subFilters) {
+        super(subFilters);
+    }
+    
+    public IntersectionFilter() {
+        super(FXCollections.<Filter>observableArrayList());
+    }
+    
+    @Override
+    public IntersectionFilter copyOf() {
+        IntersectionFilter filter = new IntersectionFilter(FXCollections.observableArrayList(
+                this.getSubFilters().stream()
+                .map(Filter::copyOf)
+                .collect(Collectors.toList())));
+        filter.setActive(isActive());
+        filter.setDisabled(isdisabled());
+        return filter;
+    }
+    
+    @Override
+    public String getDisplayName() {
+        return "Intersection";
+    }
+    
+    @Override
+    public String getHTMLReportString() {
+        return getSubFilters().stream().filter(Filter::isActive).map(Filter::getHTMLReportString).collect(Collectors.joining("</li><li>", "<ul><li>", "</li></ul>"));
+    }
+    
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final IntersectionFilter other = (IntersectionFilter) obj;
+        
+        if (isActive() != other.isActive()) {
+            return false;
+        }
+        
+        for (int i = 0; i < getSubFilters().size(); i++) {
+            if (getSubFilters().get(i).equals(other.getSubFilters().get(i)) == false) {
+                return false;
+            }
+        }
+        return true;
+    }
+    
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        return hash;
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/TextFilter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/TextFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..44a38c30e9a793ee1cb9593e100f921aa784c0a2
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/TextFilter.java
@@ -0,0 +1,91 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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.filters;
+
+import java.util.Objects;
+import javafx.beans.property.Property;
+import javafx.beans.property.SimpleStringProperty;
+import org.apache.commons.lang3.StringUtils;
+
+/** Filter for text matching */
+public class TextFilter extends AbstractFilter {
+    
+    public TextFilter() {
+    }
+    
+    public TextFilter(String text) {
+        this.text.set(text);
+    }
+    
+    private final SimpleStringProperty text = new SimpleStringProperty();
+    
+    synchronized public void setText(String text) {
+        this.text.set(text);
+    }
+    
+    @Override
+    public String getDisplayName() {
+        return "Text Filter";
+    }
+    
+    synchronized public String getText() {
+        return text.getValue();
+    }
+    
+    public Property<String> textProperty() {
+        return text;
+    }
+    
+    @Override
+    synchronized public TextFilter copyOf() {
+        TextFilter textFilter = new TextFilter(getText());
+        textFilter.setActive(isActive());
+        textFilter.setDisabled(isdisabled());
+        return textFilter;
+    }
+    
+    @Override
+    public String getHTMLReportString() {
+        return "text like \"" + StringUtils.defaultIfBlank(text.getValue(), "") + "\"" + getStringCheckBox();
+    }
+    
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final TextFilter other = (TextFilter) obj;
+        
+        if (isActive() != other.isActive()) {
+            return false;
+        }
+        return Objects.equals(text.get(), other.text.get());
+    }
+    
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 29 * hash + Objects.hashCode(this.text.get());
+        return hash;
+    }
+    
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/TypeFilter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/TypeFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..0306e463d9203f0cbe73a56f4b763ccf306d9aa5
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/TypeFilter.java
@@ -0,0 +1,136 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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.filters;
+
+import java.util.Objects;
+import java.util.stream.Collectors;
+import javafx.collections.FXCollections;
+import javafx.scene.image.Image;
+import javafx.scene.paint.Color;
+import org.sleuthkit.autopsy.timeline.events.type.EventType;
+import org.sleuthkit.autopsy.timeline.events.type.RootEventType;
+
+/** Event Type Filter. An instance of TypeFilter is usually a tree that
+ * parallels the event type hierarchy with one filter/node for each event type. */
+public class TypeFilter extends UnionFilter {
+
+    /** the event type this filter passes */
+    private final EventType eventType;
+
+    /** private constructor that enables non recursive/tree construction of the
+     * filter hierarchy for use in {@link TypeFilter#copyOf()}.
+     *
+     * @param et        the event type this filter passes
+     * @param recursive true if subfilters should be added for each subtype.
+     *                  False if no subfilters should be added. */
+    private TypeFilter(EventType et, boolean recursive) {
+        super(FXCollections.observableArrayList());
+        this.eventType = et;
+        
+        if (recursive) { // add subfilters for each subtype
+            for (EventType subType : et.getSubTypes()) {
+                this.getSubFilters().add(new TypeFilter(subType));
+            }
+        }
+    }
+
+    /** public constructor. creates a subfilter for each subtype of the given
+     * event type
+     *
+     * @param et the event type this filter will pass */
+    public TypeFilter(EventType et) {
+        this(et, true);
+    }
+    
+    public EventType getEventType() {
+        return eventType;
+    }
+    
+    @Override
+    public String getDisplayName() {
+        return eventType == RootEventType.getInstance() ? "Event Type Filter" : eventType.getDisplayName();
+    }
+
+    /** @return a color to use in GUI components representing this filter */
+    public Color getColor() {
+        return eventType.getColor();
+    }
+
+    /** @return an image to use in GUI components representing this filter */
+    public Image getFXImage() {
+        return eventType.getFXImage();
+    }
+    
+    @Override
+    public TypeFilter copyOf() {
+        //make a nonrecursive copy of this filter
+        final TypeFilter typeFilter = new TypeFilter(eventType, false);
+        typeFilter.setActive(isActive());
+        typeFilter.setDisabled(isdisabled());
+        //add a copy of each subfilter
+        this.getSubFilters().forEach((Filter t) -> {
+            typeFilter.getSubFilters().add(t.copyOf());
+        });
+        
+        return typeFilter;
+    }
+    
+    @Override
+    public String getHTMLReportString() {
+        String string = getEventType().getDisplayName() + getStringCheckBox();
+        if (getSubFilters().isEmpty() == false) {
+            string = string + " : " + getSubFilters().stream().filter(Filter::isActive).map(Filter::getHTMLReportString).collect(Collectors.joining("</li><li>", "<ul><li>", "</li></ul>"));
+        }
+        return string;
+    }
+    
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final TypeFilter other = (TypeFilter) obj;
+        
+        if (isActive() != other.isActive()) {
+            return false;
+        }
+        
+        if (this.eventType != other.eventType) {
+            return false;
+        }
+        
+        for (int i = 0; i < getSubFilters().size(); i++) {
+            if (getSubFilters().get(i).equals(other.getSubFilters().get(i)) == false) {
+                return false;
+            }
+        }
+        return true;
+    }
+    
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 67 * hash + Objects.hashCode(this.eventType);
+        return hash;
+    }
+    
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..b4d4db64f669c058d0d53389c59fa2e70db8f658
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java
@@ -0,0 +1,25 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.timeline.filters;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+/**
+ *
+ * Union(or) filter
+ */
+abstract public class UnionFilter extends CompoundFilter{
+
+    public UnionFilter(ObservableList<Filter> subFilters) {
+        super(subFilters);
+    }
+
+    public UnionFilter() {
+        super(FXCollections.<Filter>observableArrayList());
+    }
+    
+   
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/20140521121247760_easyicon_net_32.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/20140521121247760_easyicon_net_32.png
new file mode 100644
index 0000000000000000000000000000000000000000..cd3148ba91fe520543b2173128458f64b827deae
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/20140521121247760_easyicon_net_32.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/20140521121247760_easyicon_net_32_colorized.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/20140521121247760_easyicon_net_32_colorized.png
new file mode 100644
index 0000000000000000000000000000000000000000..af23a9822867ab17b5d1d69f80af0c66879909ea
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/20140521121247760_easyicon_net_32_colorized.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-090.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-090.png
new file mode 100644
index 0000000000000000000000000000000000000000..f62345e491b0af46cf10f59d46467c257d5be8d9
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-090.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-180.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-180.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d2aa3ccb2d59ecf6cc1db2006bc330a7b32423c
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-180.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-270.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-270.png
new file mode 100644
index 0000000000000000000000000000000000000000..56e9a63d6233853be9c14ec522652611d7b9056c
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-270.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-circle-double-135.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-circle-double-135.png
new file mode 100644
index 0000000000000000000000000000000000000000..4f40ba52062c62295a89856c8459052c0b78eadb
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-circle-double-135.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-in.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-in.png
new file mode 100644
index 0000000000000000000000000000000000000000..f4ac44f93fc06039a89d8b3f11e9c0e2e654dcfd
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-in.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-out.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-out.png
new file mode 100644
index 0000000000000000000000000000000000000000..5bbb31178d34a03a47a98395d9ceaec27042ee8c
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-out.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-step-out.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-step-out.png
new file mode 100644
index 0000000000000000000000000000000000000000..04bb4f415ef2ab96888bf36a3e48a97fcf5c44dd
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-step-out.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-step.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-step.png
new file mode 100644
index 0000000000000000000000000000000000000000..5d13ddc048345ef451c73afab190ab6cb0a2ceb3
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-step.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow.png
new file mode 100644
index 0000000000000000000000000000000000000000..12077d33242acb0933fb8a1eb1e9b283173bf3b6
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow_in.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow_in.png
new file mode 100644
index 0000000000000000000000000000000000000000..745c65134db478a64016d63a7104e585452f2b9f
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow_in.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow_out.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow_out.png
new file mode 100644
index 0000000000000000000000000000000000000000..2e9bc42bec16e3077a9680e7af0f90395bfeb60c
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow_out.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/block.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/block.png
new file mode 100644
index 0000000000000000000000000000000000000000..d38c058f2ff9cb262a9d0874e57d579f6f8f8b53
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/block.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-a.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-a.png
new file mode 100644
index 0000000000000000000000000000000000000000..aa5ee3f37c43430aefa8fc75f6027fa9c4192516
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-a.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-b.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-b.png
new file mode 100644
index 0000000000000000000000000000000000000000..57f91ce198f61a5fde189f07148b27f712bbd87f
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-b.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-c.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-c.png
new file mode 100644
index 0000000000000000000000000000000000000000..5d12ddd67b3b1439a56113a8b84fecbdbebc1a74
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-c.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-m.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-m.png
new file mode 100644
index 0000000000000000000000000000000000000000..a61fd8627a4b0debb3a1ff964c7a1f05911d6ff9
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-m.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b2545a5a31915b051147d705e8ae6563b08b8cd
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/bookmark--plus.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/bookmark--plus.png
new file mode 100644
index 0000000000000000000000000000000000000000..6c0eb49fcff6172f3d47c9bf77b1ff594c12ced4
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/bookmark--plus.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/bookmarks.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/bookmarks.png
new file mode 100644
index 0000000000000000000000000000000000000000..cbb650ad4c18ea5542ed8fe043e2f9acd57a3c70
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/bookmarks.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back.png
new file mode 100644
index 0000000000000000000000000000000000000000..b9d9ffe6229c1ed53b008f4b06a127373fd494f9
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back_disabled.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back_disabled.png
new file mode 100644
index 0000000000000000000000000000000000000000..c71ad536c6012a29f4249e75eb42586044b6f4c7
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back_disabled.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back_hover.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back_hover.png
new file mode 100644
index 0000000000000000000000000000000000000000..387374f5b6a5a9ee66df3b5868dfbc2e0b041e28
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back_hover.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward.png
new file mode 100644
index 0000000000000000000000000000000000000000..c88640951f972d734f6c09a0555321537bbd65e3
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward_disabled.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward_disabled.png
new file mode 100644
index 0000000000000000000000000000000000000000..61a0867c7922509044d27dc000ca2b0f7142b9a9
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward_disabled.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward_hover.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward_hover.png
new file mode 100644
index 0000000000000000000000000000000000000000..d201b31bf5dbdd46f01b2edc8261f788b9f94627
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward_hover.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/calllog.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/calllog.png
new file mode 100644
index 0000000000000000000000000000000000000000..83eb9c448d592e1cc125710cd5d02e9520543457
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/calllog.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/camera-icon-16.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/camera-icon-16.png
new file mode 100644
index 0000000000000000000000000000000000000000..046f049a487675b35e0f28f3ef09ebe7f75ab714
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/camera-icon-16.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/chart_bar.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/chart_bar.png
new file mode 100644
index 0000000000000000000000000000000000000000..9051fbc609b92b15af9be410e368b7adc20283b8
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/chart_bar.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/checker64.jpg b/Timeline/src/org/sleuthkit/autopsy/timeline/images/checker64.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..919d4507951966610867b410060bd43f66c14e45
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/checker64.jpg differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/checkerboard.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/checkerboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..b757ac3cbef6a6b3c6ca9f65908a73947beb0c28
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/checkerboard.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/checkerboard_transparent.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/checkerboard_transparent.png
new file mode 100644
index 0000000000000000000000000000000000000000..9387a300dae695532dfa5162d94a7f1fe943fa08
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/checkerboard_transparent.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/clock-history.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/clock-history.png
new file mode 100644
index 0000000000000000000000000000000000000000..7ce390d4c9095e4e755586e7e52917afda0a83da
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/clock-history.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/cookies.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/cookies.png
new file mode 100644
index 0000000000000000000000000000000000000000..793964127b0d6213342eea899523870a21b4b576
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/cookies.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/cross-circle (2).png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/cross-circle (2).png
new file mode 100644
index 0000000000000000000000000000000000000000..be8bdd2731127872a12ac1e92a6e507547ace35a
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/cross-circle (2).png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/cross-circle.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/cross-circle.png
new file mode 100644
index 0000000000000000000000000000000000000000..d3b37afb85093a9b9578adf66d543559e9578143
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/cross-circle.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/downloads.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/downloads.png
new file mode 100644
index 0000000000000000000000000000000000000000..a976964fc543a7506a0b232413d3114d1799ca44
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/downloads.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/funnel--minus.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/funnel--minus.png
new file mode 100644
index 0000000000000000000000000000000000000000..4f3534ee4454471c972b00610f590acd76eddaf1
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/funnel--minus.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/funnel.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/funnel.png
new file mode 100644
index 0000000000000000000000000000000000000000..1f69604528f29ca95e3b124de2849067797d839f
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/funnel.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/geolocation.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/geolocation.png
new file mode 100644
index 0000000000000000000000000000000000000000..08857a3ae6db9adbcfbc3ea7de73a6f693460d4f
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/geolocation.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/gps-search.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/gps-search.png
new file mode 100644
index 0000000000000000000000000000000000000000..26c97d488809de8088c6896490014015a5beb5f0
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/gps-search.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/gps-trackpoint.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/gps-trackpoint.png
new file mode 100644
index 0000000000000000000000000000000000000000..78f1f97dcc3026b5cbe20a356df6eb7ea56d97ff
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/gps-trackpoint.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/history.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/history.png
new file mode 100644
index 0000000000000000000000000000000000000000..48f4d5f3240f031daf482f300bbd50194daced1e
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/history.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/image.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/image.png
new file mode 100644
index 0000000000000000000000000000000000000000..fc3c393caa3bc4371d12d0c67ffd6d333ecf1d8e
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/image.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/info-icon-16.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/info-icon-16.png
new file mode 100644
index 0000000000000000000000000000000000000000..a9e499782c6e78fc6b23d743f4b29baa398699ce
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/info-icon-16.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/information.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/information.png
new file mode 100644
index 0000000000000000000000000000000000000000..5d353a191098dee1e1e8f4bdf99c7f4bccb0f292
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/information.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-left.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-left.png
new file mode 100644
index 0000000000000000000000000000000000000000..f865c5787fd53ae4809bdecc318ed273d8c60e31
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-left.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-actual-equal.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-actual-equal.png
new file mode 100644
index 0000000000000000000000000000000000000000..52972052c5e9f68880c12353b5008eab9df665d8
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-actual-equal.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-actual.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-actual.png
new file mode 100644
index 0000000000000000000000000000000000000000..bf0137c5b4e66a12edf613daf6578ec9ee076d2c
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-actual.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-fit.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-fit.png
new file mode 100644
index 0000000000000000000000000000000000000000..e29d97104517b832e17261a2c768784198ea2f5e
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-fit.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-in-green.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-in-green.png
new file mode 100644
index 0000000000000000000000000000000000000000..53bb613f8cebaf171fde016a9c5fca4bfabc303e
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-in-green.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-in.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-in.png
new file mode 100644
index 0000000000000000000000000000000000000000..281af0cde172dac2a38734350dfd35d840881fde
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-in.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out-red.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out-red.png
new file mode 100644
index 0000000000000000000000000000000000000000..04446b048ca454541a9bc74b5f34200a35afe041
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out-red.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out.png
new file mode 100644
index 0000000000000000000000000000000000000000..c11249f5286caa237c1128a6e500fc0e6e9619fb
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom.png
new file mode 100644
index 0000000000000000000000000000000000000000..a705d33d9231f2ca64f90a4505422f45bec80935
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier.png
new file mode 100644
index 0000000000000000000000000000000000000000..cf3d97f75e9cde9c143980d89272fe61fc2d64ee
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier_zoom_in.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier_zoom_in.png
new file mode 100644
index 0000000000000000000000000000000000000000..af4fe07477243b9b2099899d1ef47b8e3fd87b09
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier_zoom_in.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier_zoom_out.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier_zoom_out.png
new file mode 100644
index 0000000000000000000000000000000000000000..81f28199ac1c979f440f0586e6e0da48672e74a4
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier_zoom_out.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/mail-icon-16.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/mail-icon-16.png
new file mode 100644
index 0000000000000000000000000000000000000000..a919ae3e50664bd01a78732d9a07e223608b3aa4
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/mail-icon-16.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/marker.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/marker.png
new file mode 100644
index 0000000000000000000000000000000000000000..74c3b9143461531ce53eaa91a394309139cd10d1
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/marker.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/message.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/message.png
new file mode 100644
index 0000000000000000000000000000000000000000..6223516e3e122204aef38296213d0a4d2b382a36
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/message.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/programs.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/programs.png
new file mode 100644
index 0000000000000000000000000000000000000000..b8872bbcf9f54f7afd563bf0e43fffb387cea35f
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/programs.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/prohibition.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/prohibition.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a368df09f18b1bfced6e4d179b9c0053f0b2748
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/prohibition.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/recent_docs.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/recent_docs.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6d2a9c65cbd9e63c8858cabe2beea80ec8ae424
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/recent_docs.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/searchquery.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/searchquery.png
new file mode 100644
index 0000000000000000000000000000000000000000..c6cb1709304c3e81d1f55dfd30aa38f1830cbd4f
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/searchquery.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/tick.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/tick.png
new file mode 100644
index 0000000000000000000000000000000000000000..a7d7a96be3f2282a62e3c0733bac89c7f6de7b4a
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/tick.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/timeline_marker.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/timeline_marker.png
new file mode 100644
index 0000000000000000000000000000000000000000..a3fbddf88b7661e9ee2a434ad4152cc724db24c5
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/timeline_marker.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/usb_devices.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/usb_devices.png
new file mode 100644
index 0000000000000000000000000000000000000000..e49540dccced741b99d90ebb10a959769dea6439
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/usb_devices.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/web-file.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/web-file.png
new file mode 100644
index 0000000000000000000000000000000000000000..ac5957ad62d73408cd754a27453b4ce601a2b042
Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/web-file.png differ
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/index.css b/Timeline/src/org/sleuthkit/autopsy/timeline/index.css
new file mode 100644
index 0000000000000000000000000000000000000000..fd6e48afd9746bd5b8089cbad0908bc59a389067
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/index.css
@@ -0,0 +1,16 @@
+body {margin: 0px; padding: 0px; background: #FFFFFF; font: 13px/20px Arial, Helvetica, sans-serif; color: #535353;}
+#content {padding: 30px;}
+#header {width:100%; padding: 10px; line-height: 25px; background: #07A; color: #FFF; font-size: 20px;}
+h1 {font-size: 20px; font-weight: normal; color: #07A; padding: 0 0 7px 0; margin-top: 25px; border-bottom: 1px solid #D6D6D6;}
+h2 {font-size: 20px; font-weight: bolder; color: #07A;}
+h3 {font-size: 16px; color: #07A;}
+h4 {background: #07A; color: #FFF; font-size: 16px; margin: 0 0 0 25px; padding: 0; padding-left: 15px;}
+ul.nav {list-style-type: none; line-height: 35px; padding: 0px; margin-left: 15px;}
+ul li a {font-size: 14px; color: #444; text-decoration: none; padding-left: 25px;}
+ul li a:hover {text-decoration: underline;}
+p {margin: 0 0 20px 0;}
+table {max-width: 100%; min-width: 700px; padding: 0; margin: 0; border-collapse: collapse; border-bottom: 2px solid #e5e5e5;}
+.keyword_list table {width: 100%; margin: 0 0 25px 25px; border-bottom: 2px solid #dedede;}
+table th {display: table-cell; text-align: left; padding: 8px 16px; background: #e5e5e5; color: #777; font-size: 11px; text-shadow: #e9f9fd 0 1px 0; border-top: 1px solid #dedede; border-bottom: 2px solid #e5e5e5;}
+table td {display: table-cell; padding: 8px 16px; font: 13px/20px Arial, Helvetica, sans-serif; max-width: 500px; min-width: 125px; word-break: break-all; overflow: auto;}
+table tr:nth-child(even) td {background: #f3f3f3;}
\ No newline at end of file
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/license-advanced_timeline.txt b/Timeline/src/org/sleuthkit/autopsy/timeline/license-advanced_timeline.txt
new file mode 100644
index 0000000000000000000000000000000000000000..df5d98b91af0bc83731262657fe918d7b28586a5
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/license-advanced_timeline.txt
@@ -0,0 +1,18 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.
+ */
\ No newline at end of file
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java
new file mode 100644
index 0000000000000000000000000000000000000000..aabac6e1bb5b91e0c1410e490b063e2e4c932197
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java
@@ -0,0 +1,412 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui;
+
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.ReadOnlyListProperty;
+import javafx.beans.property.ReadOnlyListWrapper;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.concurrent.Task;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.chart.Axis;
+import javafx.scene.chart.BarChart;
+import javafx.scene.chart.Chart;
+import javafx.scene.chart.XYChart;
+import javafx.scene.control.Label;
+import javafx.scene.control.OverrunStyle;
+import javafx.scene.effect.Effect;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Region;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontWeight;
+import javafx.scene.text.Text;
+import javafx.scene.text.TextAlignment;
+import javax.annotation.concurrent.Immutable;
+import org.apache.commons.lang3.StringUtils;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.TimeLineView;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+
+/** Abstract base class for {@link Chart} based {@link TimeLineView}s used in
+ * the main visualization area.
+ *
+ * @param <X> the type of data plotted along the x axis
+ * @param <Y> the type of data plotted along the y axis
+ * @param <N> the type of nodes used to represent data items
+ * @param <C> the type of the {@link XYChart<X,Y>} this class uses to plot the
+ *            data.
+ *
+ * TODO: this is becoming (too?) closely tied to the notion that their is a
+ * {@link XYChart} doing the rendering. Is this a good idea? -jm
+ * TODO: pull up common history context menu items out of derived classes? -jm
+ */
+public abstract class AbstractVisualization<X, Y, N extends Node, C extends XYChart<X, Y> & TimeLineChart<X>> extends BorderPane implements TimeLineView {
+
+    protected final SimpleBooleanProperty hasEvents = new SimpleBooleanProperty(true);
+
+    protected final ObservableList<BarChart.Series<X, Y>> dataSets = FXCollections.<BarChart.Series<X, Y>>observableArrayList();
+
+    protected C chart;
+
+    //// replacement axis label componenets
+    private final Pane leafPane; // container for the leaf lables in the declutterd axis
+
+    private final Pane branchPane;// container for the branch lables in the declutterd axis
+
+    protected final Region spacer;
+
+    /** task used to reload the content of this visualization */
+    private Task<Boolean> updateTask;
+
+    protected TimeLineController controller;
+
+    protected FilteredEventsModel filteredEvents;
+
+    protected ReadOnlyListWrapper<N> selectedNodes = new ReadOnlyListWrapper<>(FXCollections.observableArrayList());
+
+    public ReadOnlyListProperty<N> getSelectedNodes() {
+        return selectedNodes.getReadOnlyProperty();
+    }
+
+    /** list of {@link Node}s to insert into the toolbar. This should be
+     * set in an implementations constructor. */
+    protected List<Node> settingsNodes;
+
+    /** @return the list of nodes containing settings widgets to insert into
+     *          this visualization's header */
+    protected List<Node> getSettingsNodes() {
+        return settingsNodes;
+    }
+
+    /** @param value a value along this visualization's x axis
+     *
+     * @return true if the tick label for the given value should be bold ( has
+     *         relevant data),
+     *         false* otherwise */
+    protected abstract Boolean isTickBold(X value);
+
+    /** apply this visualization's 'selection effect' to the given node
+     *
+     * @param node    the node to apply the 'effect' to
+     * @param applied true if the effect should be applied, false if the effect
+     *                should
+     */
+    protected abstract void applySelectionEffect(N node, Boolean applied);
+
+    /** @return a task to execute on a background thread to reload this
+     *          visualization with different data. */
+    protected abstract Task<Boolean> getUpdateTask();
+
+    /** @return return the {@link Effect} applied to 'selected nodes' in this
+     *          visualization, or null if selection is visualized via another
+     *          mechanism */
+    protected abstract Effect getSelectionEffect();
+
+    /** @param tickValue
+     *
+     * @return a String to use for a tick mark label given a tick value
+     */
+    protected abstract String getTickMarkLabel(X tickValue);
+
+    /** the spacing (in pixels) between tick marks of the horizontal
+     * axis. This will be used to layout the decluttered replacement labels.
+     *
+     * @return the spacing in pixels between tick marks of the horizontal
+     *         axis */
+    protected abstract double getTickSpacing();
+
+    /** @return the horizontal axis used by this Visualization's chart */
+    protected abstract Axis<X> getXAxis();
+
+    /** @return the vertical axis used by this Visualization's chart */
+    protected abstract Axis<Y> getYAxis();
+
+    /** * update this visualization based on current state of zoom /
+     * filters.Primarily this invokes the background {@link Task} returned
+     * by {@link #getUpdateTask()} which derived classes must implement. */
+    synchronized public void update() {
+        if (updateTask != null) {
+            updateTask.cancel(true);
+            updateTask = null;
+        }
+        updateTask = getUpdateTask();
+        updateTask.stateProperty().addListener((Observable observable) -> {
+            switch (updateTask.getState()) {
+                case CANCELLED:
+                case FAILED:
+                case READY:
+                case RUNNING:
+                case SCHEDULED:
+                    break;
+                case SUCCEEDED:
+                    try {
+                        this.hasEvents.set(updateTask.get());
+                    } catch (InterruptedException | ExecutionException ex) {
+                        Exceptions.printStackTrace(ex);
+                    }
+                    break;
+            }
+        });
+        controller.monitorTask(updateTask);
+    }
+
+    synchronized public void dispose() {
+        if (updateTask != null) {
+            updateTask.cancel(true);
+        }
+        this.filteredEvents.getRequestedZoomParamters().removeListener(invalidationListener);
+        invalidationListener = null;
+    }
+
+    protected AbstractVisualization(Pane partPane, Pane contextPane, Region spacer) {
+        this.leafPane = partPane;
+        this.branchPane = contextPane;
+        this.spacer = spacer;
+        selectedNodes.addListener((ListChangeListener.Change<? extends N> c) -> {
+            while (c.next()) {
+                c.getRemoved().forEach((N n) -> {
+                    applySelectionEffect(n, false);
+                });
+
+                c.getAddedSubList().forEach((N c1) -> {
+                    applySelectionEffect(c1, true);
+                });
+            }
+        });
+    }
+
+    @Override
+    synchronized public void setController(TimeLineController controller) {
+        this.controller = controller;
+        chart.setController(controller);
+
+        setModel(controller.getEventsModel());
+        TimeLineController.getTimeZone().addListener((Observable observable) -> {
+            update();
+        });
+    }
+
+    @Override
+    synchronized public void setModel(FilteredEventsModel filteredEvents) {
+        this.filteredEvents = filteredEvents;
+
+        this.filteredEvents.getRequestedZoomParamters().addListener(invalidationListener);
+        update();
+    }
+
+    protected InvalidationListener invalidationListener = (Observable observable) -> {
+        update();
+    };
+
+    /** iterate through the list of tick-marks building a two level structure of
+     * replacement tick marl labels. (Visually) upper level has most
+     * detailed/highest frequency part of date/time. Second level has rest of
+     * date/time grouped by unchanging part.
+     * eg:
+     *
+     *
+     * october-30_october-31_september-01_september-02_september-03
+     *
+     * becomes
+     *
+     * _________30_________31___________01___________02___________03
+     *
+     * _________october___________|_____________september___________
+     *
+     *
+     * NOTE: This method should only be invoked on the JFX thread
+     */
+    public synchronized void layoutDateLabels() {
+
+        //clear old labels
+        branchPane.getChildren().clear();
+        leafPane.getChildren().clear();
+        //since the tickmarks aren't necessarily in value/position order,
+        //make a clone of the list sorted by position along axis
+        ObservableList<Axis.TickMark<X>> tickMarks = FXCollections.observableArrayList(getXAxis().getTickMarks());
+        tickMarks.sort((Axis.TickMark<X> t, Axis.TickMark<X> t1) -> Double.compare(t.getPosition(), t1.getPosition()));
+
+        if (tickMarks.isEmpty() == false) {
+            //get the spacing between ticks in the underlying axis
+            double spacing = getTickSpacing();
+
+            //initialize values from first tick
+            TwoPartDateTime dateTime = new TwoPartDateTime(getTickMarkLabel(tickMarks.get(0).getValue()));
+            String lastSeenBranchLabel = dateTime.branch;
+            //cumulative width of the current branch label
+
+            //x-positions (pixels) of the current branch and leaf labels
+            double leafLabelX = 0;
+
+            if (dateTime.branch.equals("")) {
+                //if there is only one part to the date (ie only year), just add a label for each tick
+                for (Axis.TickMark<X> t : tickMarks) {
+                    assignLeafLabel(new TwoPartDateTime(getTickMarkLabel(t.getValue())).leaf,
+                                    spacing,
+                                    leafLabelX,
+                                    isTickBold(t.getValue())
+                    );
+
+                    leafLabelX += spacing;  //increment x
+                }
+            } else {
+                //there are two parts so ...
+                //initialize additional state 
+                double branchLabelX = 0;
+                double branchLabelWidth = 0;
+
+                for (Axis.TickMark<X> t : tickMarks) {               //for each tick
+
+                    //split the label into a TwoPartDateTime
+                    dateTime = new TwoPartDateTime(getTickMarkLabel(t.getValue()));
+
+                    //if we are still on the same branch
+                    if (lastSeenBranchLabel.equals(dateTime.branch)) {
+                        //increment branch width
+                        branchLabelWidth += spacing;
+                    } else {// we are on to a new branch, so ...
+                        assignBranchLabel(lastSeenBranchLabel, branchLabelWidth, branchLabelX);
+                        //and then update label, x-pos, and width
+                        lastSeenBranchLabel = dateTime.branch;
+                        branchLabelX += branchLabelWidth;
+                        branchLabelWidth = spacing;
+                    }
+                    //add the label for the leaf (highest frequency part)
+                    assignLeafLabel(dateTime.leaf, spacing, leafLabelX, isTickBold(t.getValue()));
+
+                    //increment leaf position
+                    leafLabelX += spacing;
+                }
+                //we have reached end so add branch label for current branch
+                assignBranchLabel(lastSeenBranchLabel, branchLabelWidth, branchLabelX);
+            }
+        }
+        //request layout since we have modified scene graph structure
+        requestParentLayout();
+    }
+
+    protected void setChartClickHandler() {
+        chart.addEventHandler(MouseEvent.MOUSE_CLICKED, (MouseEvent event) -> {
+            if (event.getButton() == MouseButton.PRIMARY && event.isStillSincePress()) {
+                selectedNodes.clear();
+            }
+        });
+    }
+
+    /** add a {@link Text} node to the leaf container for the decluttered axis
+     * labels
+     *
+     * @param labelText  the string to add
+     * @param labelWidth the width of the space available for the text
+     * @param labelX     the horizontal position in the partPane of the text
+     * @param bold       true if the text should be bold, false otherwise
+     */
+    private synchronized void assignLeafLabel(String labelText, double labelWidth, double labelX, boolean bold) {
+
+        Text label = new Text(" " + labelText + " ");
+        label.setTextAlignment(TextAlignment.CENTER);
+        label.setFont(Font.font(null, bold ? FontWeight.BOLD : FontWeight.NORMAL, 10));
+        //position label accounting for width
+        label.relocate(labelX + labelWidth / 2 - label.getBoundsInLocal().getWidth() / 2, 0);
+        label.autosize();
+
+        if (leafPane.getChildren().isEmpty()) {
+            //just add first label
+            leafPane.getChildren().add(label);
+        } else {
+            //otherwise don't actually add the label if it would intersect with previous label
+            final Text lastLabel = (Text) leafPane.getChildren().get(leafPane.getChildren().size() - 1);
+
+            if (!lastLabel.getBoundsInParent().intersects(label.getBoundsInParent())) {
+                leafPane.getChildren().add(label);
+            }
+        }
+    }
+
+    /** add a {@link Label} node to the branch container for the decluttered
+     * axis labels
+     *
+     * @param labelText  the string to add
+     * @param labelWidth the width of the space to use for the label
+     * @param labelX     the horizontal position in the partPane of the text
+     */
+    private synchronized void assignBranchLabel(String labelText, double labelWidth, double labelX) {
+
+        Label label = new Label(labelText);
+        label.setAlignment(Pos.CENTER);
+        label.setTextAlignment(TextAlignment.CENTER);
+        label.setFont(Font.font(10));
+        //use a leading ellipse since that is the lowest frequency part,
+        //and can be infered more easily from other surrounding labels
+        label.setTextOverrun(OverrunStyle.LEADING_ELLIPSIS);
+        //force size
+        label.setMinWidth(labelWidth);
+        label.setPrefWidth(labelWidth);
+        label.setMaxWidth(labelWidth);
+        label.relocate(labelX, 0);
+
+        if (labelX == 0) { // first label has no border
+            label.setStyle("-fx-border-width: 0 0 0 0 ; -fx-border-color:black;");
+        } else {  // subsequent labels have border on left to create dividers
+            label.setStyle("-fx-border-width: 0 0 0 1; -fx-border-color:black;");
+        }
+
+        branchPane.getChildren().add(label);
+    }
+
+    /** A simple data object used to represent a partial date as up to two
+     * parts. A low frequency part (branch) containing all but the most
+     * specific element, and a highest frequency part (leaf) containing the most
+     * specific element. The branch and leaf names come from thinking of the
+     * space of all date times as a tree with higher frequency information
+     * further from the root. If there is only one part, it will be in the
+     * branch and the leaf will equal an empty string */
+    @Immutable
+    private static final class TwoPartDateTime {
+
+        /** the low frequency part of a date/time eg 2001-May-4 */
+        private final String branch;
+
+        /** the highest frequency part of a date/time eg 14 (2pm) */
+        private final String leaf;
+
+        TwoPartDateTime(String dateString) {
+            //find index of separator to spit on
+            int splitIndex = StringUtils.lastIndexOfAny(dateString, " ", "-", ":");
+            if (splitIndex < 0) { // there is only one part
+                leaf = dateString;
+                branch = "";
+            } else { //split at index
+                leaf = StringUtils.substring(dateString, splitIndex + 1);
+                branch = StringUtils.substring(dateString, 0, splitIndex);
+            }
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties
new file mode 100644
index 0000000000000000000000000000000000000000..3b123bc3b076322166ddd3f653556e2dd7eca4ce
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties
@@ -0,0 +1,21 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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.
+ */
+AggregateEvent.diferentDescriptions=aggregate events are not compatible they have different descriptions
+
+AdvTimeline.node.root=Root
\ No newline at end of file
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/NoEventsDialog.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/NoEventsDialog.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..926f634900e93fa3a20b8cb8b0055f015d1acb3c
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/NoEventsDialog.fxml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+<?import javafx.scene.text.*?>
+
+<fx:root collapsible="false" contentDisplay="RIGHT" maxHeight="-Infinity" maxWidth="-Infinity" text="No Visible Events" type="TitledPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+<graphic><HBox>
+<children><Region prefWidth="150.0" /><Button fx:id="dismissButton" contentDisplay="GRAPHIC_ONLY" graphicTextGap="0.0" minHeight="16.0" minWidth="16.0" mnemonicParsing="false" prefHeight="16.0" prefWidth="16.0">
+<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+<image>
+<Image url="@../images/cross-circle.png" />
+</image></ImageView>
+</graphic></Button>
+</children></HBox>
+</graphic>
+<content>
+<VBox alignment="CENTER_RIGHT" fillWidth="false" prefHeight="130.0" prefWidth="259.0" spacing="5.0">
+<children><Label graphicTextGap="10.0" text="There are no events visible with the current zoom / filter settings. " wrapText="true" GridPane.columnIndex="1" VBox.vgrow="ALWAYS">
+<GridPane.margin>
+<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
+</GridPane.margin>
+<font>
+<Font size="14.0" />
+</font>
+<VBox.margin>
+<Insets bottom="10.0" />
+</VBox.margin>
+<graphic><ImageView fitHeight="32.0" fitWidth="32.0" pickOnBounds="true" preserveRatio="true" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
+<image>
+<Image url="@../images/information.png" />
+</image>
+<GridPane.margin>
+<Insets />
+</GridPane.margin>
+<HBox.margin>
+<Insets />
+</HBox.margin></ImageView>
+</graphic></Label><Button fx:id="zoomButton" alignment="BASELINE_LEFT" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Zoom to events" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
+<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+<image>
+<Image url="@../images/magnifier-zoom-out-red.png" />
+</image></ImageView>
+</graphic>
+<GridPane.margin>
+<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
+</GridPane.margin>
+<VBox.margin>
+<Insets />
+</VBox.margin></Button><Button fx:id="resetFiltersButton" alignment="BASELINE_LEFT" maxWidth="1.7976931348623157E308" mnemonicParsing="false" prefHeight="25.0" prefWidth="120.0" text="Reset all filters" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="2" GridPane.valignment="CENTER">
+<graphic>
+<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+<image>
+<Image url="@../images/arrow-circle-double-135.png" />
+</image>
+</ImageView>
+</graphic>
+<GridPane.margin>
+<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
+</GridPane.margin>
+<VBox.margin>
+<Insets />
+</VBox.margin>
+</Button>
+</children>
+<HBox.margin>
+<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
+</HBox.margin>
+</VBox>
+</content>
+</fx:root>
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..e5bfae1b99b64da81263cd18922fbf4600bf7e4d
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.fxml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+
+<fx:root maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" type="ToolBar" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+<items><Label fx:id="refreshLabel" text="New events may be available, re-open the timeline to refresh.">
+<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+<image>
+<Image url="@../images/info-icon-16.png" />
+</image></ImageView>
+</graphic></Label><Separator orientation="VERTICAL" /><Region fx:id="spacer" /><Separator orientation="VERTICAL" /><Label fx:id="taskLabel" contentDisplay="RIGHT" text="Label">
+<graphic><StackPane>
+<children><ProgressBar fx:id="progressBar" progress="0.0" /><Label fx:id="messageLabel" text="Label" />
+</children></StackPane>
+</graphic></Label>
+</items>
+</fx:root>
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.java
new file mode 100644
index 0000000000000000000000000000000000000000..0a80d3146dc91ee7e15f010fd31fadc067815084
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.java
@@ -0,0 +1,80 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.control.ProgressBar;
+import javafx.scene.control.ToolBar;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import org.sleuthkit.autopsy.timeline.FXMLConstructor;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.TimeLineUI;
+
+/**
+ * simple status bar that only shows one possible message determined by
+ * {@link TimeLineController#newEventsFlag}
+ */
+public class StatusBar extends ToolBar implements TimeLineUI {
+
+    private TimeLineController controller;
+
+    @FXML
+    private Label refreshLabel;
+
+    @FXML
+    private ProgressBar progressBar;
+
+    @FXML
+    private Region spacer;
+
+    @FXML
+    private Label taskLabel;
+
+    @FXML
+    private Label messageLabel;
+
+    public StatusBar() {
+        FXMLConstructor.construct(this, "StatusBar.fxml");
+    }
+
+    @FXML
+    void initialize() {
+        assert refreshLabel != null : "fx:id=\"refreshLabel\" was not injected: check your FXML file 'StatusBar.fxml'.";
+        assert progressBar != null : "fx:id=\"progressBar\" was not injected: check your FXML file 'StatusBar.fxml'.";
+        assert spacer != null : "fx:id=\"spacer\" was not injected: check your FXML file 'StatusBar.fxml'.";
+        assert taskLabel != null : "fx:id=\"taskLabel\" was not injected: check your FXML file 'StatusBar.fxml'.";
+        assert messageLabel != null : "fx:id=\"messageLabel\" was not injected: check your FXML file 'StatusBar.fxml'.";
+        refreshLabel.setVisible(false);
+        taskLabel.setVisible(false);
+        HBox.setHgrow(spacer, Priority.ALWAYS);
+    }
+
+    @Override
+    public void setController(TimeLineController controller) {
+        this.controller = controller;
+        refreshLabel.visibleProperty().bind(this.controller.getNewEventsFlag());
+        taskLabel.textProperty().bind(this.controller.getTaskTitle());
+        messageLabel.textProperty().bind(this.controller.getMessage());
+        progressBar.progressProperty().bind(this.controller.getProgress());
+        taskLabel.visibleProperty().bind(this.controller.getTasks().emptyProperty().not());
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java
new file mode 100644
index 0000000000000000000000000000000000000000..70e76b2fe017b61bf96fbcd60c2e629383b142e9
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java
@@ -0,0 +1,275 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui;
+
+import javafx.event.EventHandler;
+import javafx.geometry.Point2D;
+import javafx.scene.Cursor;
+import javafx.scene.chart.Axis;
+import javafx.scene.chart.Chart;
+import javafx.scene.control.Tooltip;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.TimeLineView;
+
+/** Interface for TimeLineViews that are 'charts'.
+ *
+ * @param <X> the type of values along the horizontal axis */
+public interface TimeLineChart<X> extends TimeLineView {
+
+    IntervalSelector<? extends X> getIntervalSelector();
+
+    void setIntervalSelector(IntervalSelector<? extends X> newIntervalSelector);
+
+    /** derived classes should implement this so as to supply an appropriate
+     * subclass of {@link IntervalSelector}
+     *
+     * @param x    the initial x position of the new interval selector
+     * @param axis the axis the new interval selector will be over
+     *
+     * @return a new interval selector
+     */
+    IntervalSelector<X> newIntervalSelector(double x, Axis<X> axis);
+
+    /** clear any references to previous interval selectors , including removing
+     * the interval selector from the ui / scene-graph */
+    void clearIntervalSelector();
+
+    /**
+     * drag handler class used by {@link TimeLineChart}s to create
+     * {@link IntervalSelector}s
+     *
+     * @param <X> the type of values along the horizontal axis
+     * @param <Y> the type of chart this is a drag handler for
+     */
+    class ChartDragHandler<X, Y extends Chart & TimeLineChart<X>> implements EventHandler<MouseEvent> {
+
+        private final Y chart;
+
+        private final Axis<X> dateAxis;
+
+        private double startX;  //hanlder mainstains position of drag start
+
+        public ChartDragHandler(Y chart, Axis<X> dateAxis) {
+            this.chart = chart;
+            this.dateAxis = dateAxis;
+        }
+
+        @Override
+        public void handle(MouseEvent t) {
+            if (t.getButton() == MouseButton.SECONDARY) {
+
+                if (t.getEventType() == MouseEvent.MOUSE_PRESSED) {
+                    //caputure  x-position, incase we are repositioning existing selector
+                    startX = t.getX();
+                    chart.setCursor(Cursor.E_RESIZE);
+                } else if (t.getEventType() == MouseEvent.MOUSE_DRAGGED) {
+                    if (chart.getIntervalSelector() == null) {
+                        //make new interval selector
+                        chart.setIntervalSelector(chart.newIntervalSelector(t.getX(), dateAxis));
+                        chart.getIntervalSelector().heightProperty().bind(chart.heightProperty().subtract(dateAxis.heightProperty().subtract(dateAxis.tickLengthProperty())));
+                        chart.getIntervalSelector().addEventHandler(MouseEvent.MOUSE_CLICKED, (MouseEvent event) -> {
+                            if (event.getButton() == MouseButton.SECONDARY) {
+                                chart.clearIntervalSelector();
+                                event.consume();
+                            }
+                        });
+                        startX = t.getX();
+                    } else {
+                        //resize/position existing selector
+                        if (t.getX() > startX) {
+                            chart.getIntervalSelector().setX(startX);
+                            chart.getIntervalSelector().setWidth(t.getX() - startX);
+                        } else {
+                            chart.getIntervalSelector().setX(t.getX());
+                            chart.getIntervalSelector().setWidth(startX - t.getX());
+                        }
+                    }
+                } else if (t.getEventType() == MouseEvent.MOUSE_RELEASED) {
+                    chart.setCursor(Cursor.DEFAULT);
+                }
+                t.consume();
+            }
+        }
+    }
+
+    /** Visually represents a 'selected' time range, and allows mouse
+     * interactions with it.
+     *
+     * @param <X> the type of values along the x axis this is a selector for
+     *
+     * This abstract class requires concrete implementations to implement
+     * hook methods to handle formating and date 'lookup' of the
+     * generic x-axis type */
+    static abstract class IntervalSelector<X> extends Rectangle {
+
+        private static final double STROKE_WIDTH = 3;
+
+        private static final double HALF_STROKE = STROKE_WIDTH / 2;
+
+        /** the Axis this is a selector over */
+        private final Axis<X> dateAxis;
+
+        protected Tooltip tooltip;
+
+        /////////drag state
+        private DragPosition dragPosition;
+
+        private double startLeft;
+
+        private double startX;
+
+        private double startWidth;
+        /////////end drag state
+
+        /**
+         *
+         * @param x          the initial x position of this selector
+         * @param height     the initial height of this selector
+         * @param axis       the {@link Axis<X>} this is a selector over
+         * @param controller the controller to invoke when this selector is
+         *                   double clicked
+         */
+        public IntervalSelector(double x, double height, Axis<X> axis, TimeLineController controller) {
+            super(x, 0, x, height);
+            dateAxis = axis;
+            setStroke(Color.BLUE);
+            setStrokeWidth(STROKE_WIDTH);
+            setFill(Color.BLUE.deriveColor(0, 1, 1, 0.5));
+            setOpacity(0.5);
+            widthProperty().addListener(o -> {
+                setTooltip();
+            });
+            xProperty().addListener(o -> {
+                setTooltip();
+            });
+            setTooltip();
+
+            setOnMouseMoved((MouseEvent event) -> {
+                Point2D localMouse = sceneToLocal(new Point2D(event.getSceneX(), event.getSceneY()));
+                final double diffX = getX() - localMouse.getX();
+                if (Math.abs(diffX) <= HALF_STROKE || Math.abs(diffX + getWidth()) <= HALF_STROKE) {
+                    setCursor(Cursor.E_RESIZE);
+                } else {
+                    setCursor(Cursor.HAND);
+                }
+            });
+            setOnMousePressed((MouseEvent event) -> {
+                Point2D localMouse = sceneToLocal(new Point2D(event.getSceneX(), event.getSceneY()));
+                final double diffX = getX() - localMouse.getX();
+                startX = event.getX();
+                startWidth = getWidth();
+                startLeft = getX();
+                if (Math.abs(diffX) <= HALF_STROKE) {
+                    dragPosition = IntervalSelector.DragPosition.LEFT;
+                } else if (Math.abs(diffX + getWidth()) <= HALF_STROKE) {
+                    dragPosition = IntervalSelector.DragPosition.RIGHT;
+                } else {
+                    dragPosition = IntervalSelector.DragPosition.CENTER;
+                }
+            });
+            setOnMouseDragged((MouseEvent event) -> {
+                double dX = event.getX() - startX;
+                switch (dragPosition) {
+                    case CENTER:
+                        setX(startLeft + dX);
+                        break;
+                    case LEFT:
+                        setX(startLeft + dX);
+                        setWidth(startWidth - dX);
+                        break;
+                    case RIGHT:
+                        setWidth(startWidth + dX);
+                        break;
+                }
+            });
+            //have to add handler rather than use convenience methods so that charts can listen for dismisal click
+            addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
+
+                public void handle(MouseEvent event) {
+                    if (event.getClickCount() >= 2) {
+                        //convert to DateTimes, using max/min if null(off axis)
+                        DateTime start = parseDateTime(getSpanStart());
+                        DateTime end = parseDateTime(getSpanEnd());
+
+                        Interval i = adjustInterval(start.isBefore(end) ? new Interval(start, end) : new Interval(end, start));
+
+                        controller.pushTimeRange(i);
+                    }
+                }
+            });
+        }
+
+        /**
+         *
+         * @param i the interval represented by this selector
+         *
+         * @return a modified version of {@code i} adjusted to suite the needs
+         *         of the concrete implementation
+         */
+        protected abstract Interval adjustInterval(Interval i);
+
+        /** format a string representation of the given x-axis value to use in
+         * the tooltip
+         *
+         * @param date a x-axis value of type X
+         *
+         * @return a string representation of the given x-axis value */
+        protected abstract String formatSpan(final X date);
+
+        /** parse an x-axis value to a {@link DateTime}
+         *
+         * @param date a x-axis value of type X
+         *
+         * @return a {@link DateTime} corresponding to the given x-axis value */
+        protected abstract DateTime parseDateTime(X date);
+
+        private void setTooltip() {
+            final X start = getSpanStart();
+            final X end = getSpanEnd();
+            Tooltip.uninstall(this, tooltip);
+            tooltip = new Tooltip("Double-click to zoom into range:\n" + formatSpan(start) + " to " + formatSpan(end) + "\nRight-click to clear.");
+            Tooltip.install(this, tooltip);
+        }
+
+        /** @return the value along the x-axis corresponding to the left edge of
+         *          the selector */
+        public X getSpanEnd() {
+            return dateAxis.getValueForDisplay(dateAxis.parentToLocal(getBoundsInParent().getMaxX(), 0).getX());
+        }
+
+        /** @return the value along the x-axis corresponding to the right edge
+         *          of the selector */
+        public X getSpanStart() {
+            return dateAxis.getValueForDisplay(dateAxis.parentToLocal(getBoundsInParent().getMinX(), 0).getX());
+        }
+
+        /** enum to represent whether the drag is a left/right-edge modification
+         * or a horizontal slide triggered by dragging the center */
+        private enum DragPosition {
+
+            LEFT, CENTER, RIGHT
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeLineResultView.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeLineResultView.java
new file mode 100644
index 0000000000000000000000000000000000000000..2700f86aace31cf9ba4b563c693bda4ba3a1fd39
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeLineResultView.java
@@ -0,0 +1,120 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui;
+
+import java.util.HashSet;
+import java.util.Set;
+import javafx.beans.Observable;
+import javax.swing.SwingUtilities;
+import org.joda.time.format.DateTimeFormatter;
+import org.openide.nodes.Node;
+import org.openide.util.NbBundle;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.TimeLineView;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+import org.sleuthkit.autopsy.timeline.explorernodes.EventRootNode;
+import org.sleuthkit.autopsy.corecomponentinterfaces.DataContent;
+import org.sleuthkit.autopsy.corecomponents.DataResultPanel;
+
+/**
+ * Since it was too hard to derive from {@link DataResultPanel}, this class
+ * implements {@link TimeLineView}, listens to the events/state of a the
+ * assigned {@link FilteredEventsModel} and acts appropriately on its
+ * {@link DataResultPanel}. That is, this class acts as a sort of
+ * bridge/adapter between a FilteredEventsModel instance and a
+ * DataResultPanel instance.
+ */
+public class TimeLineResultView implements TimeLineView {
+
+    /** the {@link DataResultPanel} that is the real view proxied by this
+     * class */
+    private final DataResultPanel dataResultPanel;
+
+    private TimeLineController controller;
+
+    private FilteredEventsModel filteredEvents;
+
+    private Set<Long> selectedEventIDs = new HashSet<>();
+
+    public DataResultPanel getDataResultPanel() {
+        return dataResultPanel;
+    }
+
+    public TimeLineResultView(DataContent dataContent) {
+        dataResultPanel = DataResultPanel.createInstanceUninitialized("", "", Node.EMPTY, 0, dataContent);
+    }
+
+    /** Set the Controller for this class. Also sets the model provided by the
+     * controller as the model for this view.
+     *
+     * @param controller
+     */
+    @Override
+    public void setController(TimeLineController controller) {
+        this.controller = controller;
+
+        //set up listeners on relevant properties
+        TimeLineController.getTimeZone().addListener((Observable observable) -> {
+            dataResultPanel.setPath(getSummaryString());
+        });
+
+        controller.getSelectedEventIDs().addListener((Observable o) -> {
+            refresh();
+        });
+
+        setModel(controller.getEventsModel());
+    }
+
+    /** Set the Model for this View
+     *
+     * @param filteredEvents */
+    @Override
+    synchronized public void setModel(final FilteredEventsModel filteredEvents) {
+        this.filteredEvents = filteredEvents;
+    }
+
+    /** @return a String representation of all the Events displayed */
+    private String getSummaryString() {
+        if (controller.getSelectedTimeRange().get() != null) {
+            final DateTimeFormatter zonedFormatter = TimeLineController.getZonedFormatter();
+            return controller.getSelectedTimeRange().get().getStart().withZone(TimeLineController.getJodaTimeZone()).toString(zonedFormatter)
+                    + " to "
+                    + controller.getSelectedTimeRange().get().getEnd().withZone(TimeLineController.getJodaTimeZone()).toString(zonedFormatter);
+        }
+        return "";
+    }
+
+    /** refresh this view with the events selected in the controller */
+    public final void refresh() {
+
+        Set<Long> newSelectedEventIDs = new HashSet<>(controller.getSelectedEventIDs());
+        if (selectedEventIDs.equals(newSelectedEventIDs) == false) {
+            selectedEventIDs = newSelectedEventIDs;
+            final EventRootNode root = new EventRootNode(
+                    NbBundle.getMessage(this.getClass(), "AdvTimeline.node.root"), selectedEventIDs,
+                    filteredEvents);
+
+            //this must be in edt or exception is thrown
+            SwingUtilities.invokeLater(() -> {
+                dataResultPanel.setPath(getSummaryString());
+                dataResultPanel.setNode(root);
+            });
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeZonePanel.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeZonePanel.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..ae444c2817d38250e959c8900cce86c0076d32fd
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeZonePanel.fxml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+
+<fx:root alignment="TOP_LEFT" animated="false" collapsible="false" contentDisplay="RIGHT" minHeight="-Infinity" minWidth="-Infinity" text="Display Times In:" type="TitledPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+<graphic>
+<HBox spacing="5.0">
+<children><RadioButton fx:id="localRadio" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" selected="true" text="Local Time Zone">
+<HBox.margin>
+<Insets />
+</HBox.margin>
+<toggleGroup>
+<ToggleGroup fx:id="localOtherGroup" />
+</toggleGroup></RadioButton><RadioButton fx:id="otherRadio" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" text="GMT / UTC" toggleGroup="$localOtherGroup">
+<HBox.margin>
+<Insets />
+</HBox.margin></RadioButton>
+</children>
+<padding>
+<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+</padding>
+</HBox>
+</graphic>
+</fx:root>
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeZonePanel.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeZonePanel.java
new file mode 100644
index 0000000000000000000000000000000000000000..6882781b64d57acc0a728bd7cbd87cc7b1b0e3f9
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeZonePanel.java
@@ -0,0 +1,70 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui;
+
+import java.time.ZoneOffset;
+import java.util.Date;
+import java.util.TimeZone;
+import javafx.beans.value.ObservableValue;
+import javafx.fxml.FXML;
+import javafx.scene.control.RadioButton;
+import javafx.scene.control.TitledPane;
+import javafx.scene.control.Toggle;
+import javafx.scene.control.ToggleGroup;
+import org.sleuthkit.autopsy.timeline.FXMLConstructor;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+
+/**
+ * FXML Controller class for timezone picker ui
+ *
+ */
+public class TimeZonePanel extends TitledPane {
+
+    @FXML
+    private RadioButton localRadio;
+
+    @FXML
+    private RadioButton otherRadio;
+
+    @FXML
+    private ToggleGroup localOtherGroup;
+
+    static private String getTimeZoneString(final TimeZone timeZone) {
+        final String id = ZoneOffset.ofTotalSeconds(timeZone.getOffset(System.currentTimeMillis()) / 1000).getId();
+        final String timeZoneString = "(GMT" + ("Z".equals(id) ? "+00:00" : id) + ") " + timeZone.getID() + " [" + timeZone.getDisplayName(timeZone.observesDaylightTime() && timeZone.inDaylightTime(new Date()), TimeZone.SHORT) + "]";
+        return timeZoneString;
+    }
+
+    @FXML
+    public void initialize() {
+
+//        localRadio.setText("Local Time Zone: " + getTimeZoneString(TimeZone.getDefault()));
+        localOtherGroup.selectedToggleProperty().addListener((ObservableValue<? extends Toggle> observable, Toggle oldValue, Toggle newValue) -> {
+            if (newValue == localRadio) {
+                TimeLineController.setTimeZone(TimeZone.getDefault());
+            } else {
+                TimeLineController.setTimeZone(TimeZone.getTimeZone(ZoneOffset.UTC));
+            }
+        });
+    }
+
+    public TimeZonePanel() {
+        FXMLConstructor.construct(this, "TimeZonePanel.fxml");
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..7aa00b91b4f8adcf4f91e593427c552f96c0bcef
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.fxml
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+<?import jfxtras.scene.control.*?>
+<?import org.controlsfx.control.*?>
+
+<fx:root prefHeight="-1.0" prefWidth="-1.0" type="javafx.scene.layout.BorderPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+    <top>
+        <ToolBar fx:id="toolBar" prefWidth="200.0" BorderPane.alignment="CENTER">
+            <items>
+                <HBox alignment="CENTER" BorderPane.alignment="CENTER">
+                    <children>
+                        <Label text="Mode:">
+                            <HBox.margin>
+                                <Insets right="5.0" />
+                            </HBox.margin>
+                        </Label>
+                             
+                        <org.controlsfx.control.SegmentedButton maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity">
+                            <buttons>   
+                                <ToggleButton fx:id="countsToggle" alignment="TOP_LEFT" mnemonicParsing="false" selected="true" text="Counts">
+                                    <graphic>
+                                        <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
+                                            <image>
+                                                <Image url="@../images/chart_bar.png" />
+                                            </image>
+                                        </ImageView>
+                                    </graphic>
+                                </ToggleButton>
+                                <ToggleButton fx:id="detailsToggle" alignment="CENTER_RIGHT" layoutX="74.0" mnemonicParsing="false" selected="false" text="Details">
+                                    <graphic>
+                                        <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true" rotate="0.0" smooth="true" style="-fx-background-color:white;" x="2.0" y="1.0">
+                                            <image>
+                                                <Image url="@../images/20140521121247760_easyicon_net_32_colorized.png" />
+                                            </image>
+                                        </ImageView>
+                                    </graphic>
+                                </ToggleButton>
+                            </buttons>
+                                    
+                        </org.controlsfx.control.SegmentedButton>
+                    </children>
+                    <padding>
+                        <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" />
+                    </padding>
+                    <BorderPane.margin>
+                        <Insets left="10.0" />
+                    </BorderPane.margin>
+                </HBox>
+                <Separator orientation="VERTICAL" />
+                <Button fx:id="snapShotButton" mnemonicParsing="false" text="snapshot">
+                    <graphic>
+                        <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+                            <image>
+                                <Image url="@../images/image.png" />
+                            </image>
+                        </ImageView>
+                    </graphic>
+                </Button>
+                <Separator orientation="VERTICAL" />
+            </items>
+        </ToolBar>
+    </top> 
+    <bottom>
+        <VBox maxHeight="-Infinity">
+            <children>
+                <HBox fillHeight="false" BorderPane.alignment="CENTER">
+                    <children>
+                        <Region fx:id="spacer" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="5.0" prefWidth="20.0" />
+                        <VBox fillWidth="false">
+                            <children>
+                                <Pane fx:id="partPane" />
+                                <Pane fx:id="contextPane" />
+                            </children>
+                        </VBox>
+                    </children>
+                </HBox>
+                <Separator />
+                <Separator />
+                <StackPane fx:id="rangeHistogramStack" maxHeight="-Infinity" BorderPane.alignment="CENTER">
+                    <children>
+                        <HBox fx:id="histogramBox" alignment="BOTTOM_LEFT" fillHeight="false" maxHeight="-Infinity" minHeight="-Infinity" prefHeight="32.0" StackPane.alignment="BOTTOM_CENTER" />
+                    </children>
+                </StackPane>
+                <Separator />
+                <ToolBar>
+                    <items>
+                        <Label contentDisplay="RIGHT" minWidth="-Infinity" text="Start:">
+                            <graphic>
+                                <LocalDateTimeTextField fx:id="startPicker" minWidth="200.0" prefWidth="200.0">
+                                    <padding>
+                                        <Insets top="3.0" />
+                                    </padding>
+                                </LocalDateTimeTextField>
+                            </graphic>
+                        </Label>
+                        <Separator fx:id="leftSeperator" halignment="LEFT" maxWidth="1.7976931348623157E308" minWidth="-Infinity" orientation="VERTICAL" HBox.hgrow="ALWAYS">
+                            <HBox.margin>
+                                <Insets left="10.0" right="10.0" />
+                            </HBox.margin>
+                        </Separator>
+                        <HBox>
+                            <children>
+                                <Button fx:id="zoomOutButton" mnemonicParsing="false">
+                                    <graphic>
+                                        <ImageView pickOnBounds="true" preserveRatio="true">
+                                            <image>
+                                                <Image url="@../images/magnifier-zoom-out-red.png" />
+                                            </image>
+                                        </ImageView>
+                                    </graphic>
+                                    <HBox.margin>
+                                        <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" />
+                                    </HBox.margin>
+                                </Button>
+                                <Button fx:id="zoomInButton" mnemonicParsing="false">
+                                    <graphic>
+                                        <ImageView pickOnBounds="true" preserveRatio="true">
+                                            <image>
+                                                <Image url="@../images/magnifier-zoom-in-green.png" />
+                                            </image>
+                                        </ImageView>
+                                    </graphic>
+                                    <HBox.margin>
+                                        <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" />
+                                    </HBox.margin>
+                                </Button>
+                            </children>
+                        </HBox>
+                        <MenuButton fx:id="zoomMenuButton" mnemonicParsing="false" text="Zoom in/out to">
+                            <graphic>
+                                <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+                                    <image>
+                                        <Image url="@../images/magnifier-left.png" />
+                                    </image>
+                                </ImageView>
+                            </graphic>
+                            <HBox.margin>
+                                <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" />
+                            </HBox.margin>
+                        </MenuButton>
+                        <Separator fx:id="rightSeperator" halignment="RIGHT" maxWidth="1.7976931348623157E308" minWidth="-Infinity" orientation="VERTICAL" HBox.hgrow="ALWAYS">
+                            <HBox.margin>
+                                <Insets left="10.0" right="10.0" />
+                            </HBox.margin>
+                        </Separator>
+                        <Label contentDisplay="RIGHT" minWidth="-Infinity" text="End:">
+                            <graphic>
+                                <LocalDateTimeTextField fx:id="endPicker" minWidth="200.0" nodeOrientation="LEFT_TO_RIGHT" prefWidth="200.0">
+                                    <padding>
+                                        <Insets top="3.0" />
+                                    </padding>
+                                </LocalDateTimeTextField>
+                            </graphic>
+                        </Label>
+                    </items>
+                </ToolBar>
+            </children>
+        </VBox>
+    </bottom>
+   </fx:root>
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java
new file mode 100644
index 0000000000000000000000000000000000000000..e13f7624a12c198390631a45f8edfdbeb203b9e7
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java
@@ -0,0 +1,583 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui;
+
+import impl.org.controlsfx.skin.RangeSliderSkin;
+import java.net.URL;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.ResourceBundle;
+import javafx.application.Platform;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.geometry.Insets;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.Node;
+import javafx.scene.SnapshotParameters;
+import javafx.scene.control.Button;
+import javafx.scene.control.MenuButton;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.Separator;
+import javafx.scene.control.Skin;
+import javafx.scene.control.TitledPane;
+import javafx.scene.control.Toggle;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.control.ToolBar;
+import javafx.scene.control.Tooltip;
+import javafx.scene.effect.Lighting;
+import javafx.scene.image.WritableImage;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.BackgroundFill;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.CornerRadii;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import static javafx.scene.layout.Region.USE_PREF_SIZE;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javax.annotation.concurrent.GuardedBy;
+import jfxtras.scene.control.LocalDateTimeTextField;
+import org.controlsfx.control.RangeSlider;
+import org.controlsfx.control.action.AbstractAction;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Interval;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.timeline.FXMLConstructor;
+import org.sleuthkit.autopsy.timeline.LoggedTask;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.TimeLineView;
+import org.sleuthkit.autopsy.timeline.VisualizationMode;
+import org.sleuthkit.autopsy.timeline.actions.DefaultFilters;
+import org.sleuthkit.autopsy.timeline.actions.SaveSnapshot;
+import org.sleuthkit.autopsy.timeline.actions.ZoomOut;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+import org.sleuthkit.autopsy.timeline.ui.countsview.CountsViewPane;
+import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane;
+import org.sleuthkit.autopsy.timeline.ui.detailview.tree.NavPanel;
+import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;
+
+/** A Container for an {@link AbstractVisualization}, has a toolbar on top to
+ * hold settings widgets supplied by contained {@link AbstractVisualization},
+ * and the histogram / timeselection on bottom. Also supplies containers for
+ * replacement axis to contained {@link AbstractVisualization}
+ *
+ * TODO: refactor common code out of histogram and CountsView? -jm
+ */
+public class VisualizationPanel extends BorderPane implements TimeLineView {
+
+    @GuardedBy("this")
+    private LoggedTask<Void> histogramTask;
+
+    private static final Logger LOGGER = Logger.getLogger(VisualizationPanel.class.getName());
+
+    private final NavPanel navPanel;
+
+    private AbstractVisualization<?, ?, ?, ?> visualization;
+
+    @FXML // ResourceBundle that was given to the FXMLLoader
+    private ResourceBundle resources;
+
+    @FXML // URL location of the FXML file that was given to the FXMLLoader
+    private URL location;
+
+    //// range slider and histogram componenets
+    @FXML // fx:id="histogramBox"
+    protected HBox histogramBox; // Value injected by FXMLLoader
+
+    @FXML // fx:id="rangeHistogramStack"
+    protected StackPane rangeHistogramStack; // Value injected by FXMLLoader
+
+    private final RangeSlider rangeSlider = new RangeSlider(0, 1.0, .25, .75);
+
+    //// time range selection componenets
+    @FXML
+    protected MenuButton zoomMenuButton;
+
+    @FXML
+    private Separator rightSeperator;
+
+    @FXML
+    private Separator leftSeperator;
+
+    @FXML
+    protected Button zoomOutButton;
+
+    @FXML
+    protected Button zoomInButton;
+
+    @FXML
+    protected LocalDateTimeTextField startPicker;
+
+    @FXML
+    protected LocalDateTimeTextField endPicker;
+
+    //// replacemetn axis label componenets
+    @FXML
+    protected Pane partPane;
+
+    @FXML
+    protected Pane contextPane;
+
+    @FXML
+    protected Region spacer;
+
+    //// header toolbar componenets
+    @FXML
+    private ToolBar toolBar;
+
+    @FXML
+    private ToggleButton countsToggle;
+
+    @FXML
+    private ToggleButton detailsToggle;
+
+    @FXML
+    private Button snapShotButton;
+
+    private double preDragPos;
+
+    protected TimeLineController controller;
+
+    protected FilteredEventsModel filteredEvents;
+
+    private final ChangeListener<Object> rangeSliderListener
+            = (observable1, oldValue, newValue) -> {
+                if (rangeSlider.isHighValueChanging() == false && rangeSlider.isLowValueChanging() == false) {
+                    Long minTime = filteredEvents.getMinTime() * 1000;
+                    controller.pushTimeRange(new Interval(
+                                    new Double(rangeSlider.getLowValue() + minTime).longValue(),
+                                    new Double(rangeSlider.getHighValue() + minTime).longValue(),
+                                    DateTimeZone.UTC));
+                }
+            };
+
+    private final InvalidationListener endListener = (Observable observable) -> {
+        if (endPicker.getLocalDateTime() != null) {
+            controller.pushTimeRange(VisualizationPanel.this.filteredEvents.timeRange().get().withEndMillis(
+                    ZonedDateTime.of(endPicker.getLocalDateTime(), TimeLineController.getTimeZoneID()).toInstant().toEpochMilli()));
+        }
+    };
+
+    private final InvalidationListener startListener = (Observable observable) -> {
+        if (startPicker.getLocalDateTime() != null) {
+            controller.pushTimeRange(VisualizationPanel.this.filteredEvents.timeRange().get().withStartMillis(
+                    ZonedDateTime.of(startPicker.getLocalDateTime(), TimeLineController.getTimeZoneID()).toInstant().toEpochMilli()));
+        }
+    };
+
+    static private final Background background = new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY));
+
+    static private final Lighting lighting = new Lighting();
+
+    public VisualizationPanel(NavPanel navPanel) {
+        this.navPanel = navPanel;
+        FXMLConstructor.construct(this, "VisualizationPanel.fxml");
+    }
+
+    @FXML // This method is called by the FXMLLoader when initialization is complete
+    protected void initialize() {
+        assert endPicker != null : "fx:id=\"endPicker\" was not injected: check your FXML file 'ViewWrapper.fxml'.";
+        assert histogramBox != null : "fx:id=\"histogramBox\" was not injected: check your FXML file 'ViewWrapper.fxml'.";
+        assert startPicker != null : "fx:id=\"startPicker\" was not injected: check your FXML file 'ViewWrapper.fxml'.";
+        assert rangeHistogramStack != null : "fx:id=\"rangeHistogramStack\" was not injected: check your FXML file 'ViewWrapper.fxml'.";
+        assert countsToggle != null : "fx:id=\"countsToggle\" was not injected: check your FXML file 'VisToggle.fxml'.";
+        assert detailsToggle != null : "fx:id=\"eventsToggle\" was not injected: check your FXML file 'VisToggle.fxml'.";
+
+        HBox.setHgrow(leftSeperator, Priority.ALWAYS);
+        HBox.setHgrow(rightSeperator, Priority.ALWAYS);
+        ChangeListener<Toggle> toggleListener = (ObservableValue<? extends Toggle> observable,
+                                                 Toggle oldValue,
+                                                 Toggle newValue) -> {
+                    if (newValue == null) {
+                        countsToggle.getToggleGroup().selectToggle(oldValue != null ? oldValue : countsToggle);
+                    } else if (newValue == countsToggle && oldValue != null) {
+                        controller.setViewMode(VisualizationMode.COUNTS);
+                    } else if (newValue == detailsToggle && oldValue != null) {
+                        controller.setViewMode(VisualizationMode.DETAIL);
+                    }
+                };
+
+        if (countsToggle.getToggleGroup() != null) {
+            countsToggle.getToggleGroup().selectedToggleProperty().addListener(toggleListener);
+        } else {
+            countsToggle.toggleGroupProperty().addListener((Observable observable) -> {
+                countsToggle.getToggleGroup().selectedToggleProperty().addListener(toggleListener);
+            });
+        }
+
+        //setup rangeslider
+        rangeSlider.setOpacity(.7);
+        rangeSlider.setMin(0);
+
+        /** this is still needed to not get swamped by low/high value changes.
+         * https://bitbucket.org/controlsfx/controlsfx/issue/241/rangeslider-high-low-properties
+         * TODO: committ an appropriate version of this fix to the ControlsFX
+         * repo on bitbucket, remove this after next release -jm */
+        Skin<?> skin = rangeSlider.getSkin();
+        if (skin != null) {
+            attachDragListener((RangeSliderSkin) skin);
+        } else {
+            rangeSlider.skinProperty().addListener((Observable observable) -> {
+                RangeSliderSkin skin1 = (RangeSliderSkin) rangeSlider.getSkin();
+                attachDragListener(skin1);
+            });
+        }
+
+        rangeSlider.setBlockIncrement(1);
+
+        rangeHistogramStack.getChildren().add(rangeSlider);
+
+        /* this padding attempts to compensates for the fact that the
+         * rangeslider track doesn't extend to edge of node,and so the
+         * histrogram doesn't quite line up with the rangeslider */
+        histogramBox.setStyle("   -fx-padding: 0,0.5em,0,.5em; ");
+
+        zoomMenuButton.getItems().clear();
+        for (ZoomRanges b : ZoomRanges.values()) {
+
+            MenuItem menuItem = new MenuItem(b.getDisplayName());
+            menuItem.setOnAction((event) -> {
+                if (b != ZoomRanges.ALL) {
+                    controller.pushPeriod(b.getPeriod());
+                } else {
+                    controller.showFullRange();
+                }
+            });
+            zoomMenuButton.getItems().add(menuItem);
+        }
+
+        zoomOutButton.setOnAction(e -> {
+            controller.pushZoomOutTime();
+        });
+        zoomInButton.setOnAction(e -> {
+            controller.pushZoomInTime();
+        });
+
+        snapShotButton.setOnAction((ActionEvent event) -> {
+            //take snapshot
+            final SnapshotParameters snapshotParameters = new SnapshotParameters();
+            snapshotParameters.setViewport(new Rectangle2D(visualization.getBoundsInParent().getMinX(), visualization.getBoundsInParent().getMinY(),
+                                                           visualization.getBoundsInParent().getWidth(),
+                                                           contextPane.getLayoutBounds().getHeight() + visualization.getLayoutBounds().getHeight() + partPane.getLayoutBounds().getHeight()
+            ));
+            WritableImage snapshot = this.snapshot(snapshotParameters, null);
+            //pass snapshot to save action
+            new SaveSnapshot(controller, snapshot).handle(event);
+        });
+    }
+
+    /**
+     * TODO: committed an appropriate version of this fix to the ControlsFX repo
+     * on bitbucket, remove this after next release -jm
+     *
+     * @param skin
+     */
+    private void attachDragListener(RangeSliderSkin skin) {
+        if (skin != null) {
+            for (Node n : skin.getChildren()) {
+                if (n.getStyleClass().contains("track")) {
+                    n.setOpacity(.3);
+                }
+                if (n.getStyleClass().contains("range-bar")) {
+                    StackPane rangeBar = (StackPane) n;
+                    rangeBar.setOnMousePressed((MouseEvent e) -> {
+                        rangeBar.requestFocus();
+                        preDragPos = e.getX();
+                    });
+
+                    //don't mark as not changing until mouse is released
+                    rangeBar.setOnMouseReleased((MouseEvent event) -> {
+                        rangeSlider.setLowValueChanging(false);
+                        rangeSlider.setHighValueChanging(false);
+                    });
+                    rangeBar.setOnMouseDragged((MouseEvent event) -> {
+                        final double min = rangeSlider.getMin();
+                        final double max = rangeSlider.getMax();
+
+                        ///!!!  compensate for range and width so that rangebar actualy stays with the slider
+                        double delta = (event.getX() - preDragPos) * (max - min) / rangeSlider.
+                                getWidth();
+                        ////////////////////////////////////////////////////
+
+                        final double lowValue = rangeSlider.getLowValue();
+                        final double newLowValue = Math.min(Math.max(min, lowValue + delta),
+                                                            max);
+                        final double highValue = rangeSlider.getHighValue();
+                        final double newHighValue = Math.min(Math.max(min, highValue + delta),
+                                                             max);
+
+                        if (newLowValue <= min || newHighValue >= max) {
+                            return;
+                        }
+
+                        rangeSlider.setLowValueChanging(true);
+                        rangeSlider.setHighValueChanging(true);
+                        rangeSlider.setLowValue(newLowValue);
+                        rangeSlider.setHighValue(newHighValue);
+                    });
+                }
+            }
+        }
+    }
+
+    @Override
+    public synchronized void setController(TimeLineController controller) {
+        this.controller = controller;
+        setModel(controller.getEventsModel());
+
+        setViewMode(controller.getViewMode().get());
+
+        controller.getNeedsHistogramRebuild().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
+            if (newValue) {
+                refreshHistorgram();
+            }
+        });
+
+        controller.getViewMode().addListener((ObservableValue<? extends VisualizationMode> ov, VisualizationMode t, VisualizationMode t1) -> {
+            setViewMode(t1);
+        });
+        refreshHistorgram();
+    }
+
+    private void setViewMode(VisualizationMode visualizationMode) {
+        switch (visualizationMode) {
+            case COUNTS:
+                setVisualization(new CountsViewPane(partPane, contextPane, spacer));
+                countsToggle.setSelected(true);
+                break;
+            case DETAIL:
+                setVisualization(new DetailViewPane(partPane, contextPane, spacer));
+                detailsToggle.setSelected(true);
+                break;
+        }
+    }
+
+    synchronized void setVisualization(final AbstractVisualization<?, ?, ?, ?> newViz) {
+        Platform.runLater(() -> {
+            synchronized (VisualizationPanel.this) {
+                if (visualization != null) {
+                    toolBar.getItems().removeAll(visualization.getSettingsNodes());
+                    visualization.dispose();
+                }
+
+                visualization = newViz;
+                toolBar.getItems().addAll(newViz.getSettingsNodes());
+
+                visualization.setController(controller);
+                setCenter(visualization);
+                if (visualization instanceof DetailViewPane) {
+                    navPanel.setChart((DetailViewPane) visualization);
+                }
+                visualization.hasEvents.addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
+                    if (newValue == false) {
+
+                        setCenter(new StackPane(visualization, new Region() {
+                            {
+                                setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)));
+                                setOpacity(.3);
+                            }
+                        }, new NoEventsDialog(() -> {
+                            setCenter(visualization);
+                        })));
+                    } else {
+                        setCenter(visualization);
+                    }
+                });
+            }
+        });
+    }
+
+    synchronized private void refreshHistorgram() {
+
+        if (histogramTask != null) {
+            histogramTask.cancel(true);
+        }
+
+        histogramTask = new LoggedTask<Void>("Rebuild Histogram", true) {
+
+            @Override
+            protected Void call() throws Exception {
+
+                updateMessage("preparing");
+
+                long max = 0;
+                final RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.getSpanningInterval());
+                final long lowerBound = rangeInfo.getLowerBound();
+                final long upperBound = rangeInfo.getUpperBound();
+                Interval timeRange = new Interval(new DateTime(lowerBound, TimeLineController.getJodaTimeZone()), new DateTime(upperBound, TimeLineController.getJodaTimeZone()));
+
+                //extend range to block bounderies (ie day, month, year)
+                int p = 0; // progress counter
+
+                //clear old data, and reset ranges and series
+                Platform.runLater(() -> {
+                    updateMessage("resetting ui");
+
+                });
+
+                ArrayList<Long> bins = new ArrayList<>();
+
+                DateTime start = timeRange.getStart();
+                while (timeRange.contains(start)) {
+                    if (isCancelled()) {
+                        return null;
+                    }
+                    DateTime end = start.plus(rangeInfo.getPeriodSize().getPeriod());
+                    final Interval interval = new Interval(start, end);
+                    //increment for next iteration
+
+                    start = end;
+
+                    updateMessage("querying db");
+                    //query for current range
+                    long count = filteredEvents.getEventCounts(interval).values().stream().mapToLong(Long::valueOf).sum();
+                    bins.add(count);
+
+                    max = Math.max(count, max);
+
+                    final double fMax = Math.log(max);
+                    final ArrayList<Long> fbins = new ArrayList<>(bins);
+                    Platform.runLater(() -> {
+                        updateMessage("updating ui");
+
+                        histogramBox.getChildren().clear();
+
+                        for (Long bin : fbins) {
+                            if (isCancelled()) {
+                                break;
+                            }
+                            Region bar = new Region();
+                            //scale them to fit in histogram height
+                            bar.prefHeightProperty().bind(histogramBox.heightProperty().multiply(Math.log(bin)).divide(fMax));
+                            bar.setMaxHeight(USE_PREF_SIZE);
+                            bar.setMinHeight(USE_PREF_SIZE);
+                            bar.setBackground(background);
+                            bar.setOnMouseEntered((MouseEvent event) -> {
+                                Tooltip.install(bar, new Tooltip(bin.toString()));
+                            });
+                            bar.setEffect(lighting);
+                            //they each get equal width to fill the histogram horizontaly
+                            HBox.setHgrow(bar, Priority.ALWAYS);
+                            histogramBox.getChildren().add(bar);
+                        }
+                    });
+                }
+                return null;
+            }
+
+        };
+        new Thread(histogramTask).start();
+        controller.monitorTask(histogramTask);
+    }
+
+    @Override
+    public void setModel(FilteredEventsModel filteredEvents) {
+        this.filteredEvents = filteredEvents;
+
+        refreshTimeUI(filteredEvents.timeRange().get());
+        this.filteredEvents.timeRange().addListener((Observable observable) -> {
+            refreshTimeUI(filteredEvents.timeRange().get());
+        });
+        TimeLineController.getTimeZone().addListener((Observable observable) -> {
+            refreshTimeUI(filteredEvents.timeRange().get());
+        });
+    }
+
+    private void refreshTimeUI(Interval interval) {
+        RangeDivisionInfo rangeDivisionInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.getSpanningInterval());
+
+        final Long minTime = rangeDivisionInfo.getLowerBound();
+        final long maxTime = rangeDivisionInfo.getUpperBound();
+
+        if (minTime > 0 && maxTime > minTime) {
+
+            Platform.runLater(() -> {
+                startPicker.localDateTimeProperty().removeListener(startListener);
+                endPicker.localDateTimeProperty().removeListener(endListener);
+                rangeSlider.highValueChangingProperty().removeListener(rangeSliderListener);
+                rangeSlider.lowValueChangingProperty().removeListener(rangeSliderListener);
+
+                rangeSlider.setMax((Long) (maxTime - minTime));
+                rangeSlider.setHighValue(interval.getEndMillis() - minTime);
+                rangeSlider.setLowValue(interval.getStartMillis() - minTime);
+                endPicker.setLocalDateTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(interval.getEndMillis()), TimeLineController.getTimeZoneID()));
+                startPicker.setLocalDateTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(interval.getStartMillis()), TimeLineController.getTimeZoneID()));
+
+                rangeSlider.highValueChangingProperty().addListener(rangeSliderListener);
+                rangeSlider.lowValueChangingProperty().addListener(rangeSliderListener);
+                startPicker.localDateTimeProperty().addListener(startListener);
+                endPicker.localDateTimeProperty().addListener(endListener);
+            });
+        }
+    }
+
+    private class NoEventsDialog extends TitledPane {
+
+        private final Runnable closeCallback;
+
+        @FXML // ResourceBundle that was given to the FXMLLoader
+        private ResourceBundle resources;
+
+        @FXML // URL location of the FXML file that was given to the FXMLLoader
+        private URL location;
+
+        @FXML
+        private Button resetFiltersButton;
+
+        @FXML
+        private Button dismissButton;
+
+        @FXML
+        private Button zoomButton;
+
+        public NoEventsDialog(Runnable closeCallback) {
+            this.closeCallback = closeCallback;
+            FXMLConstructor.construct(this, "NoEventsDialog.fxml");
+
+        }
+
+        @FXML
+        void initialize() {
+            assert resetFiltersButton != null : "fx:id=\"resetFiltersButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'.";
+            assert dismissButton != null : "fx:id=\"dismissButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'.";
+            assert zoomButton != null : "fx:id=\"zoomButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'.";
+
+            AbstractAction zoomOutAction = new ZoomOut(controller);
+            zoomButton.setOnAction(zoomOutAction);
+            zoomButton.disableProperty().bind(zoomOutAction.disabledProperty());
+
+            dismissButton.setOnAction(e -> {
+                closeCallback.run();
+            });
+            AbstractAction defaultFiltersAction = new DefaultFilters(controller);
+            resetFiltersButton.setOnAction(defaultFiltersAction);
+            resetFiltersButton.disableProperty().bind(defaultFiltersAction.disabledProperty());
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/ZoomRanges.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/ZoomRanges.java
new file mode 100644
index 0000000000000000000000000000000000000000..30b9f2fd1bfe47698455860e5c663b7bf7331fe6
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/ZoomRanges.java
@@ -0,0 +1,51 @@
+package org.sleuthkit.autopsy.timeline.ui;
+
+import org.joda.time.Days;
+import org.joda.time.Hours;
+import org.joda.time.Minutes;
+import org.joda.time.Months;
+import org.joda.time.ReadablePeriod;
+import org.joda.time.Weeks;
+import org.joda.time.Years;
+
+/**
+ *
+ */
+public enum ZoomRanges {
+
+    ONE_MINUTE("One Minute", Minutes.ONE),
+    FIFTEEN_MINUTES("Fifteen Minutes", Minutes.minutes(15)),
+    ONE_HOUR("One Hour", Hours.ONE),
+    SIX_HOURS("Six Hours", Hours.SIX), 
+    TWELVE_HOURS("Twelve Hours", Hours.hours(12)),
+    ONE_DAY("One Day", Days.ONE),
+    THREE_DAYS("Three Days", Days.THREE),
+    ONE_WEEK("One Week", Weeks.ONE),
+    TWO_WEEK("Two Weeks", Weeks.TWO), 
+    ONE_MONTH("One Month", Months.ONE),
+    THREE_MONTHS("Three Months", Months.THREE), 
+    SIX_MONTHS("Six Months", Months.SIX),
+    ONE_YEAR("One Year", Years.ONE), 
+    THREE_YEARS("Three Years", Years.THREE), 
+    FIVE_YEARS("Five Years", Years.years(5)), 
+    TEN_YEARS("Ten Years", Years.years(10)), 
+    ALL("All", Minutes.ONE);
+
+    private ZoomRanges(String displayName, ReadablePeriod period) {
+        this.displayName = displayName;
+        this.period = period;
+    }
+
+    
+    private String displayName;
+    private ReadablePeriod period;
+
+    String getDisplayName() {
+        return displayName;
+    }
+
+    ReadablePeriod getPeriod() {
+        return period;
+    }
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/Bundle.properties b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/Bundle.properties
new file mode 100644
index 0000000000000000000000000000000000000000..284a121fc9668d7ed625e5a0674f883d6491f1d0
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/Bundle.properties
@@ -0,0 +1,19 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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.
+ */
+CountsChartPane.numberOfEvents=Number of Events
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java
new file mode 100644
index 0000000000000000000000000000000000000000..5d9f60120272b7d521f0cc2250d7e0058c21da0f
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java
@@ -0,0 +1,520 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.countsview;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import javafx.application.Platform;
+import javafx.beans.Observable;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.concurrent.Task;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.fxml.FXML;
+import javafx.scene.Cursor;
+import javafx.scene.Node;
+import javafx.scene.chart.BarChart;
+import javafx.scene.chart.CategoryAxis;
+import javafx.scene.chart.NumberAxis;
+import javafx.scene.chart.StackedBarChart;
+import javafx.scene.chart.XYChart;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.RadioButton;
+import javafx.scene.control.SeparatorMenuItem;
+import javafx.scene.control.ToggleGroup;
+import javafx.scene.control.Tooltip;
+import javafx.scene.effect.DropShadow;
+import javafx.scene.effect.Effect;
+import javafx.scene.effect.Lighting;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Region;
+import org.controlsfx.control.action.ActionGroup;
+import org.controlsfx.control.action.ActionUtils;
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.joda.time.Seconds;
+import org.sleuthkit.autopsy.timeline.FXMLConstructor;
+import org.sleuthkit.autopsy.timeline.LoggedTask;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.TimeLineView;
+import org.sleuthkit.autopsy.timeline.actions.Back;
+import org.sleuthkit.autopsy.timeline.actions.Forward;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+import org.sleuthkit.autopsy.timeline.events.type.EventType;
+import org.sleuthkit.autopsy.timeline.events.type.RootEventType;
+import org.sleuthkit.autopsy.timeline.ui.AbstractVisualization;
+import org.sleuthkit.autopsy.timeline.utils.ColorUtilities;
+import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;
+import org.sleuthkit.autopsy.coreutils.Logger;
+
+/**
+ * FXML Controller class for a {@link StackedBarChart<String,Number>} based
+ * implementation of a {@link TimeLineView}.
+ *
+ * This class listens to changes in the assigned {@link FilteredEventsModel} and
+ * updates the internal {@link StackedBarChart} to reflect the currently
+ * requested events.
+ *
+ * This class captures input from the user in the form of mouse clicks on graph
+ * bars, and forwards them to the assigned {@link TimeLineController} *
+ *
+ * Concurrency Policy: Access to the private members stackedBarChart, countAxis,
+ * dateAxis, EventTypeMap, and dataSets affects the stackedBarChart so
+ * they all must only be manipulated on the JavaFx thread (through {@link Platform#runLater(java.lang.Runnable)}
+ *
+ * {@link CountsChartPane#filteredEvents} should encapsulate all need
+ * synchronization internally.
+ *
+ * TODO: refactor common code out of this class and ClusterChartPane into
+ * AbstractChartView
+ */
+public class CountsViewPane extends AbstractVisualization<String, Number, Node, EventCountsChart> {
+
+    private static final Effect SELECTED_NODE_EFFECT = new Lighting();
+
+    private static final Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName());
+
+    private final NumberAxis countAxis = new NumberAxis();
+
+    private final CategoryAxis dateAxis = new CategoryAxis(FXCollections.<String>observableArrayList());
+
+    private final SimpleObjectProperty<ScaleType> scale = new SimpleObjectProperty<>(ScaleType.LINEAR);
+
+    //private access to barchart data
+    private final Map<EventType, XYChart.Series<String, Number>> eventTypeMap = new ConcurrentHashMap<>();
+
+    @Override
+    protected String getTickMarkLabel(String labelValueString) {
+        return labelValueString;
+    }
+
+    @Override
+    protected Boolean isTickBold(String value) {
+        return dataSets.stream().flatMap((series) -> series.getData().stream())
+                .anyMatch((data) -> data.getXValue().equals(value) && data.getYValue().intValue() > 0);
+    }
+
+    private ContextMenu getContextMenu() {
+
+        ContextMenu chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new ActionGroup("Zoom History", new Back(controller), new Forward(controller))));
+        chartContextMenu.setAutoHide(true);
+        return chartContextMenu;
+    }
+
+    @Override
+    protected Task<Boolean> getUpdateTask() {
+        return new LoggedTask<Boolean>("Updating Counts Graph", true) {
+
+            @Override
+            protected Boolean call() throws Exception {
+                if (isCancelled()) {
+                    return null;
+                }
+                updateProgress(-1, 1);
+                updateMessage("preparing update");
+                Platform.runLater(() -> {
+                    setCursor(Cursor.WAIT);
+                });
+
+                final RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.timeRange().get());
+                chart.setRangeInfo(rangeInfo);
+                //extend range to block bounderies (ie day, month, year)
+                final long lowerBound = rangeInfo.getLowerBound();
+                final long upperBound = rangeInfo.getUpperBound();
+                final Interval timeRange = new Interval(new DateTime(lowerBound, TimeLineController.getJodaTimeZone()), new DateTime(upperBound, TimeLineController.getJodaTimeZone()));
+
+                int max = 0;
+                int p = 0; // progress counter
+
+                //clear old data, and reset ranges and series
+                Platform.runLater(() -> {
+                    updateMessage("resetting ui");
+                    eventTypeMap.clear();
+                    dataSets.clear();
+                    dateAxis.getCategories().clear();
+
+                    DateTime start = timeRange.getStart();
+                    while (timeRange.contains(start)) {
+                        //add bar/'category' label for the current interval
+                        final String dateString = start.toString(rangeInfo.getTickFormatter());
+                        dateAxis.getCategories().add(dateString);
+
+                        //increment for next iteration
+                        start = start.plus(rangeInfo.getPeriodSize().getPeriod());
+                    }
+
+                    //make all series to ensure they get created in consistent order
+                    EventType.allTypes.forEach(CountsViewPane.this::getSeries);
+                });
+
+                DateTime start = timeRange.getStart();
+                while (timeRange.contains(start)) {
+
+                    final String dateString = start.toString(rangeInfo.getTickFormatter());
+                    DateTime end = start.plus(rangeInfo.getPeriodSize().getPeriod());
+                    final Interval interval = new Interval(start, end);
+
+                    //query for current range
+                    Map<EventType, Long> eventCounts = filteredEvents.getEventCounts(interval);
+
+                    //increment for next iteration
+                    start = end;
+
+                    int dateMax = 0; //used in max tracking
+
+                    //for each type add data to graph
+                    for (final EventType et : eventCounts.keySet()) {
+                        if (isCancelled()) {
+                            return null;
+                        }
+
+                        final Long count = eventCounts.get(et);
+                        final int fp = p++;
+                        if (count > 0) {
+                            final double adjustedCount = count == 0 ? 0 : scale.get().adjust(count);
+
+                            dateMax += adjustedCount;
+                            final XYChart.Data<String, Number> xyData = new BarChart.Data<>(dateString, adjustedCount);
+
+                            xyData.nodeProperty().addListener((Observable o) -> {
+                                final Node node = xyData.getNode();
+                                if (node != null) {
+                                    node.setStyle("-fx-border-width: 2; -fx-border-color: " + ColorUtilities.getRGBCode(et.getSuperType().getColor()) + "; -fx-bar-fill: " + ColorUtilities.getRGBCode(et.getColor()));
+                                    node.setCursor(Cursor.HAND);
+
+                                    node.setOnMouseEntered((MouseEvent event) -> {
+                                        //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart
+                                        final Tooltip tooltip = new Tooltip(count + " " + et.getDisplayName() + " events\n"
+                                                + "between " + dateString + "\n"
+                                                + "and     "
+                                                + interval.getEnd().toString(rangeInfo.getTickFormatter()));
+                                        tooltip.setGraphic(new ImageView(et.getFXImage()));
+                                        Tooltip.install(node, tooltip);
+                                        node.setEffect(new DropShadow(10, et.getColor()));
+                                    });
+                                    node.setOnMouseExited((MouseEvent event) -> {
+                                        if (selectedNodes.contains(node)) {
+                                            node.setEffect(SELECTED_NODE_EFFECT);
+                                        } else {
+                                            node.setEffect(null);
+                                        }
+                                    });
+
+                                    node.addEventHandler(MouseEvent.MOUSE_CLICKED, new BarClickHandler(node, dateString, interval, et));
+                                }
+                            });
+
+                            max = Math.max(max, dateMax);
+
+                            final double fmax = max;
+
+                            Platform.runLater(() -> {
+                                updateMessage("updating counts");
+                                getSeries(et).getData().add(xyData);
+                                if (scale.get().equals(ScaleType.LINEAR)) {
+                                    countAxis.setTickUnit(Math.pow(10, Math.max(0, Math.floor(Math.log10(fmax)) - 1)));
+                                } else {
+                                    countAxis.setTickUnit(Double.MAX_VALUE);
+                                }
+                                countAxis.setUpperBound(1 + fmax * 1.2);
+                                layoutDateLabels();
+                                updateProgress(fp, rangeInfo.getPeriodsInRange());
+                            });
+                        } else {
+                            final double fmax = max;
+
+                            Platform.runLater(() -> {
+                                updateMessage("updating counts");
+                                updateProgress(fp, rangeInfo.getPeriodsInRange());
+                            });
+                        }
+                    }
+                }
+
+                Platform.runLater(() -> {
+                    updateMessage("wrapping up");
+                    updateProgress(1, 1);
+                    layoutDateLabels();
+                    setCursor(Cursor.NONE);
+                });
+
+                return max > 0;
+            }
+        };
+    }
+
+    public CountsViewPane(Pane partPane, Pane contextPane, Region spacer) {
+        super(partPane, contextPane, spacer);
+        chart = new EventCountsChart(dateAxis, countAxis);
+        setChartClickHandler();
+        chart.setData(dataSets);
+        setCenter(chart);
+
+        settingsNodes = new ArrayList<>(new CountsViewSettingsPane().getChildrenUnmodifiable());
+
+        dateAxis.getTickMarks().addListener((Observable observable) -> {
+            layoutDateLabels();
+        });
+        dateAxis.categorySpacingProperty().addListener((Observable observable) -> {
+            layoutDateLabels();
+        });
+        dateAxis.getCategories().addListener((Observable observable) -> {
+            layoutDateLabels();
+        });
+
+        spacer.minWidthProperty().bind(countAxis.widthProperty().add(countAxis.tickLengthProperty()).add(dateAxis.startMarginProperty().multiply(2)));
+        spacer.prefWidthProperty().bind(countAxis.widthProperty().add(countAxis.tickLengthProperty()).add(dateAxis.startMarginProperty().multiply(2)));
+        spacer.maxWidthProperty().bind(countAxis.widthProperty().add(countAxis.tickLengthProperty()).add(dateAxis.startMarginProperty().multiply(2)));
+
+        scale.addListener(o -> {
+            countAxis.tickLabelsVisibleProperty().bind(scale.isEqualTo(ScaleType.LINEAR));
+            countAxis.tickMarkVisibleProperty().bind(scale.isEqualTo(ScaleType.LINEAR));
+            countAxis.minorTickVisibleProperty().bind(scale.isEqualTo(ScaleType.LINEAR));
+            update();
+        });
+    }
+
+    @Override
+    protected NumberAxis getYAxis() {
+        return countAxis;
+    }
+
+    @Override
+    protected CategoryAxis getXAxis() {
+        return dateAxis;
+    }
+
+    @Override
+    protected double getTickSpacing() {
+        return dateAxis.getCategorySpacing();
+    }
+
+    @Override
+    protected Effect getSelectionEffect() {
+        return SELECTED_NODE_EFFECT;
+    }
+
+    @Override
+    protected void applySelectionEffect(Node c1, Boolean applied) {
+        if (applied) {
+            c1.setEffect(getSelectionEffect());
+        } else {
+            c1.setEffect(null);
+        }
+    }
+
+    /**
+     * NOTE: Because this method modifies data directly used by the chart, this
+     * method should only be called from JavaFX thread!
+     *
+     * @param et the EventType to get the series for
+     *
+     * @return a Series object to contain all the events with the given
+     *         EventType
+     */
+    private XYChart.Series<String, Number> getSeries(final EventType et) {
+        XYChart.Series<String, Number> series = eventTypeMap.get(et);
+        if (series == null) {
+            series = new XYChart.Series<>();
+            series.setName(et.getDisplayName());
+            eventTypeMap.put(et, series);
+
+            dataSets.add(series);
+        }
+        return series;
+
+    }
+
+    /**
+     * EventHandler for click events on nodes representing a bar(segment) in the
+     * stacked bar chart.
+     *
+     * Concurrency Policy: This only accesses immutable state or javafx nodes
+     * (from the jfx thread) and the internally synchronized
+     * {@link TimeLineController}
+     *
+     * TODO: review for thread safety -jm
+     */
+    private class BarClickHandler implements EventHandler<MouseEvent> {
+
+        private ContextMenu barContextMenu;
+
+        private final Interval interval;
+
+        private final EventType type;
+
+        private final Node node;
+
+        private final String startDateString;
+
+        public BarClickHandler(Node node, String dateString, Interval countInterval, EventType type) {
+            this.interval = countInterval;
+            this.type = type;
+            this.node = node;
+            this.startDateString = dateString;
+        }
+
+        @Override
+        public void handle(final MouseEvent e) {
+            e.consume();
+            if (e.getClickCount() == 1) {     //single click => selection
+                if (e.getButton().equals(MouseButton.PRIMARY)) {
+                    controller.selectTimeAndType(interval, type);
+                    selectedNodes.setAll(node);
+                } else if (e.getButton().equals(MouseButton.SECONDARY)) {
+                    Platform.runLater(() -> {
+                        chart.getContextMenu().hide();
+
+                        if (barContextMenu == null) {
+                            barContextMenu = new ContextMenu();
+                            barContextMenu.setAutoHide(true);
+                            barContextMenu.getItems().addAll(
+                                    new MenuItem("Select Time Range") {
+                                        {
+                                            setOnAction((ActionEvent t) -> {
+                                                controller.selectTimeAndType(interval, RootEventType.getInstance());
+
+                                                selectedNodes.clear();
+                                                for (XYChart.Series<String, Number> s : dataSets) {
+                                                    s.getData().forEach((XYChart.Data<String, Number> d) -> {
+                                                        if (startDateString.contains(d.getXValue())) {
+                                                            selectedNodes.add(d.getNode());
+                                                        }
+                                                    });
+                                                }
+                                            });
+                                        }
+                                    },
+                                    new MenuItem("Select Event Type") {
+                                        {
+                                            setOnAction((ActionEvent t) -> {
+                                                controller.selectTimeAndType(filteredEvents.getSpanningInterval(), type);
+
+                                                selectedNodes.clear();
+                                                eventTypeMap.get(type).getData().forEach((d) -> {
+                                                    selectedNodes.add(d.getNode());
+
+                                                });
+                                            });
+                                        }
+                                    },
+                                    new MenuItem("Select Time and Type") {
+                                        {
+                                            setOnAction((ActionEvent t) -> {
+                                                controller.selectTimeAndType(interval, type);
+                                                selectedNodes.setAll(node);
+                                            });
+                                        }
+                                    },
+                                    new SeparatorMenuItem(),
+                                    new MenuItem("Zoom into Time Range") {
+                                        {
+                                            setOnAction((ActionEvent t) -> {
+                                                if (interval.toDuration().isShorterThan(Seconds.ONE.toStandardDuration()) == false) {
+                                                    controller.pushTimeRange(interval);
+                                                }
+                                            });
+                                        }
+                                    });
+                            barContextMenu.getItems().addAll(getContextMenu().getItems());
+                        }
+
+                        barContextMenu.show(node, e.getScreenX(), e.getScreenY());
+                    });
+
+                }
+            } else if (e.getClickCount() >= 2) {  //double-click => zoom in time
+                if (interval.toDuration().isLongerThan(Seconds.ONE.toStandardDuration())) {
+                    controller.pushTimeRange(interval);
+                }
+            }
+        }
+
+    }
+
+    class CountsViewSettingsPane extends HBox {
+
+        @FXML
+        private ResourceBundle resources;
+
+        @FXML
+        private URL location;
+
+        @FXML
+        private RadioButton logRadio;
+
+        @FXML
+        private RadioButton sqrtRadio;
+
+        @FXML
+        private RadioButton linearRadio;
+
+        @FXML
+        private ToggleGroup scaleGroup;
+
+        @FXML
+        void initialize() {
+            assert logRadio != null : "fx:id=\"logRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'.";
+            assert sqrtRadio != null : "fx:id=\"sqrtRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'.";
+            assert linearRadio != null : "fx:id=\"linearRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'.";
+            linearRadio.setSelected(true);
+            scaleGroup.selectedToggleProperty().addListener(observable -> {
+                if (scaleGroup.getSelectedToggle() == linearRadio) {
+                    scale.set(ScaleType.LINEAR);
+                } else if (scaleGroup.getSelectedToggle() == sqrtRadio) {
+                    scale.set(ScaleType.SQUARE_ROOT);
+                } else if (scaleGroup.getSelectedToggle() == logRadio) {
+                    scale.set(ScaleType.LOGARITHMIC);
+                }
+            });
+        }
+
+        public CountsViewSettingsPane() {
+            FXMLConstructor.construct(this, "CountsViewSettingsPane.fxml");
+        }
+    }
+
+    private static enum ScaleType {
+
+        LINEAR(t -> t.doubleValue()),
+        SQUARE_ROOT(t -> Math.sqrt(t)),
+        LOGARITHMIC(t -> Math.log10(t) + 1);
+
+        private final Function<Long, Double> func;
+
+        ScaleType(Function<Long, Double> func) {
+            this.func = func;
+        }
+
+        double adjust(Long c) {
+            return func.apply(c);
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewSettingsPane.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewSettingsPane.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..533eff15aae377fa704efc89b0e144af91963a6c
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewSettingsPane.fxml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+
+<fx:root alignment="CENTER_LEFT" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" type="HBox" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+<children><FlowPane alignment="CENTER_LEFT" hgap="5.0" prefWrapLength="250.0" vgap="5.0" HBox.hgrow="ALWAYS">
+<children><Label minHeight="-Infinity" minWidth="-Infinity" text="Scale: ">
+<HBox.margin>
+<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+</HBox.margin></Label><FlowPane alignment="CENTER_LEFT" hgap="5.0" prefWrapLength="250.0" vgap="5.0">
+<children><RadioButton fx:id="linearRadio" mnemonicParsing="false" selected="true" text="Linear">
+<toggleGroup>
+<ToggleGroup fx:id="scaleGroup" />
+</toggleGroup></RadioButton><RadioButton fx:id="sqrtRadio" mnemonicParsing="false" text="Square Root" toggleGroup="$scaleGroup" /><RadioButton fx:id="logRadio" mnemonicParsing="false" text="Logarithmic" toggleGroup="$scaleGroup" />
+</children>
+<HBox.margin>
+<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+</HBox.margin>
+<FlowPane.margin>
+<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+</FlowPane.margin></FlowPane>
+</children>
+<HBox.margin>
+<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+</HBox.margin></FlowPane>
+</children></fx:root>
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java
new file mode 100644
index 0000000000000000000000000000000000000000..4ee393cdf4fb8ce58736add83a546dc6a28a4de3
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java
@@ -0,0 +1,196 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.countsview;
+
+import java.util.Arrays;
+import java.util.Collections;
+import javafx.scene.chart.Axis;
+import javafx.scene.chart.CategoryAxis;
+import javafx.scene.chart.NumberAxis;
+import javafx.scene.chart.StackedBarChart;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.util.StringConverter;
+import org.controlsfx.control.action.ActionGroup;
+import org.controlsfx.control.action.ActionUtils;
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.openide.util.NbBundle;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.actions.Back;
+import org.sleuthkit.autopsy.timeline.actions.Forward;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+import org.sleuthkit.autopsy.timeline.ui.TimeLineChart;
+import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;
+
+/** Customized {@link StackedBarChart<String, Number>} used to display the event
+ * counts in {@link CountsViewPane} */
+class EventCountsChart extends StackedBarChart<String, Number> implements TimeLineChart<String> {
+
+    private ContextMenu contextMenu;
+
+    private TimeLineController controller;
+
+    private IntervalSelector<? extends String> intervalSelector;
+
+    /** * the RangeDivisionInfo for the currently displayed time range,
+     * used to correct the interval provided
+     * {@link  EventCountsChart#intervalSelector} by padding the end with one
+     * 'period' */
+    private RangeDivisionInfo rangeInfo;
+
+    EventCountsChart(CategoryAxis dateAxis, NumberAxis countAxis) {
+        super(dateAxis, countAxis);
+        //configure constant properties on axes and chart
+        dateAxis.setAnimated(true);
+        dateAxis.setLabel(null);
+        dateAxis.setTickLabelsVisible(false);
+        dateAxis.setTickLabelGap(0);
+
+        countAxis.setLabel(NbBundle.getMessage(CountsViewPane.class, "CountsChartPane.numberOfEvents"));
+        countAxis.setAutoRanging(false);
+        countAxis.setLowerBound(0);
+        countAxis.setAnimated(true);
+        countAxis.setMinorTickCount(0);
+        countAxis.setTickLabelFormatter(new IntegerOnlyStringConverter());
+
+        setAlternativeRowFillVisible(true);
+        setCategoryGap(2);
+        setLegendVisible(false);
+        setAnimated(true);
+        setTitle(null);
+
+        //use one handler with an if chain because it maintains state
+        ChartDragHandler<String, EventCountsChart> dragHandler = new ChartDragHandler<>(this, getXAxis());
+        setOnMousePressed(dragHandler);
+        setOnMouseReleased(dragHandler);
+        setOnMouseDragged(dragHandler);
+
+        setOnMouseClicked((MouseEvent clickEvent) -> {
+            contextMenu.hide();
+            if (clickEvent.getButton() == MouseButton.SECONDARY && clickEvent.isStillSincePress()) {
+                contextMenu.show(EventCountsChart.this, clickEvent.getScreenX(), clickEvent.getScreenY());
+                clickEvent.consume();
+            }
+        });
+    }
+
+    @Override
+    public void clearIntervalSelector() {
+        getChartChildren().remove(intervalSelector);
+        intervalSelector = null;
+    }
+
+    @Override
+    public final synchronized void setController(TimeLineController controller) {
+        this.controller = controller;
+        setModel(this.controller.getEventsModel());
+        //we have defered creating context menu until control is available
+        contextMenu = ActionUtils.createContextMenu(
+                Arrays.asList(new ActionGroup("Zoom History", new Back(controller), new Forward(controller))));
+        contextMenu.setAutoHide(true);
+    }
+
+    @Override
+    public IntervalSelector<? extends String> getIntervalSelector() {
+        return intervalSelector;
+    }
+
+    @Override
+    public void setIntervalSelector(IntervalSelector<? extends String> newIntervalSelector) {
+        intervalSelector = newIntervalSelector;
+        getChartChildren().add(getIntervalSelector());
+    }
+
+    @Override
+    public void setModel(FilteredEventsModel filteredEvents) {
+        filteredEvents.getRequestedZoomParamters().addListener(o -> {
+            clearIntervalSelector();
+            controller.selectEventIDs(Collections.emptyList());
+        });
+    }
+
+    @Override
+    public CountsIntervalSelector newIntervalSelector(double x, Axis<String> dateAxis) {
+        return new CountsIntervalSelector(x, getHeight() - dateAxis.getHeight() - dateAxis.getTickLength(), dateAxis, controller);
+    }
+
+    /** used by {@link CountsViewPane#BarClickHandler} to close the context menu
+     * when the bar menu is requested
+     *
+     * @return the context menu for this chart */
+    ContextMenu getContextMenu() {
+        return contextMenu;
+    }
+
+    void setRangeInfo(RangeDivisionInfo rangeInfo) {
+        this.rangeInfo = rangeInfo;
+    }
+
+    /**
+     * StringConvereter used to 'format' vertical axis labels
+     */
+    private static class IntegerOnlyStringConverter extends StringConverter<Number> {
+
+        @Override
+        public String toString(Number n) {
+            //suppress non-integer values
+            return n.intValue() == n.doubleValue()
+                   ? Integer.toString(n.intValue()) : "";
+        }
+
+        @Override
+        public Number fromString(String string) {
+            //this is unused but here for symmetry
+            return Double.valueOf(string).intValue();
+        }
+    }
+
+    /** Interval Selector for the counts chart, adjusts interval based on
+     * rangeInfo to include final period */
+    private class CountsIntervalSelector extends IntervalSelector<String> {
+
+        public CountsIntervalSelector(double x, double height, Axis<String> axis, TimeLineController controller) {
+            super(x, height, axis, controller);
+        }
+
+        @Override
+        protected String formatSpan(String date) {
+            return date;
+        }
+
+        @Override
+        protected Interval adjustInterval(Interval i) {
+            //extend range to block bounderies (ie day, month, year)
+            RangeDivisionInfo iInfo = RangeDivisionInfo.getRangeDivisionInfo(i);
+            final long lowerBound = iInfo.getLowerBound();
+            final long upperBound = iInfo.getUpperBound();
+            final DateTime lowerDate = new DateTime(lowerBound, TimeLineController.getJodaTimeZone());
+            final DateTime upperDate = new DateTime(upperBound, TimeLineController.getJodaTimeZone());
+            //add extra block to end that gets cut of by conversion from string/category.
+            return new Interval(lowerDate, upperDate.plus(rangeInfo.getPeriodSize().getPeriod()));
+        }
+
+        @Override
+        protected DateTime parseDateTime(String date) {
+            return date == null ? new DateTime(rangeInfo.getLowerBound()) : rangeInfo.getTickFormatter().parseDateTime(date);
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..edcb8ef407fb2b8b9416fcc828277a199276b681
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java
@@ -0,0 +1,266 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.detailview;
+
+import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
+import javafx.application.Platform;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Cursor;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Label;
+import javafx.scene.control.OverrunStyle;
+import javafx.scene.control.Tooltip;
+import javafx.scene.effect.DropShadow;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.BackgroundFill;
+import javafx.scene.layout.Border;
+import javafx.scene.layout.BorderStroke;
+import javafx.scene.layout.BorderStrokeStyle;
+import javafx.scene.layout.BorderWidths;
+import javafx.scene.layout.CornerRadii;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.utils.ColorUtilities;
+
+/** Represents an {@link AggregateEvent} in a {@link EventDetailChart}. */
+public class AggregateEventNode extends StackPane {
+
+    private static final CornerRadii CORNER_RADII = new CornerRadii(3);
+
+    /** the border to apply when this node is 'selected' */
+    private static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2)));
+
+    /** The event this AggregateEventNode represents visually */
+    private final AggregateEvent event;
+
+    private final AggregateEventNode parentEventNode;
+
+    /** the region that represents the time span of this node's event */
+    private final Region spanRegion = new Region();
+
+    /** The label used to display this node's event's description */
+    private final Label descrLabel = new Label();
+
+    /** The IamgeView used to show the icon for this node's event's type */
+    private final ImageView eventTypeImageView = new ImageView();
+
+    /** Pane that contains AggregateEventNodes of any 'subevents' if they are
+     * displayed
+     *
+     * //TODO: move more of the control of subnodes/events here and out of
+     * EventDetail Chart */
+    private final Pane subNodePane = new Pane();
+
+    /** the context menu that with the slider that controls subnode/event
+     * display
+     *
+     * //TODO: move more of the control of subnodes/events here and out
+     * of EventDetail Chart */
+    private final SimpleObjectProperty<ContextMenu> contextMenu = new SimpleObjectProperty<>();
+
+    /** the Background used to fill the spanRegion, this varies epending on the
+     * selected/highlighted state of this node in its parent EventDetailChart */
+    private Background spanFill;
+
+    public AggregateEventNode(final AggregateEvent event, AggregateEventNode parentEventNode) {
+        this.event = event;
+        this.parentEventNode = parentEventNode;
+        //set initial properties
+        getChildren().addAll(spanRegion, subNodePane, descrLabel);
+
+        setAlignment(Pos.TOP_LEFT);
+        setMinHeight(24);
+        minWidthProperty().bind(spanRegion.widthProperty());
+        setPrefHeight(USE_COMPUTED_SIZE);
+        setMaxHeight(USE_PREF_SIZE);
+        setMargin(descrLabel, new Insets(2, 5, 2, 5));
+
+        //set up subnode pane sizing contraints
+        subNodePane.setPrefHeight(USE_COMPUTED_SIZE);
+        subNodePane.setMinHeight(USE_PREF_SIZE);
+        subNodePane.setMinWidth(USE_PREF_SIZE);
+        subNodePane.setMaxHeight(USE_PREF_SIZE);
+        subNodePane.setMaxWidth(USE_PREF_SIZE);
+        subNodePane.setPickOnBounds(false);
+
+        //setup description label
+        eventTypeImageView.setImage(event.getType().getFXImage());
+        descrLabel.setGraphic(eventTypeImageView);
+        descrLabel.setPrefWidth(USE_COMPUTED_SIZE);
+        descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
+
+        descrLabel.setMouseTransparent(true);
+        setDescriptionVisibility(DescriptionVisibility.SHOWN);
+
+        //setup backgrounds
+        final Color evtColor = event.getType().getColor();
+        spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY));
+        setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)));
+        setCursor(Cursor.HAND);
+        spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor) + ";");
+        spanRegion.setBackground(spanFill);
+
+        //set up mouse hover effect and tooltip
+        setOnMouseEntered((MouseEvent e) -> {
+            //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart
+            installTooltip();
+            spanRegion.setEffect(new DropShadow(10, evtColor));
+        });
+
+        setOnMouseExited((MouseEvent e) -> {
+            spanRegion.setEffect(null);
+        });
+
+    }
+
+    private void installTooltip() {
+        Tooltip.install(AggregateEventNode.this, new Tooltip(getEvent().getEventIDs().size() + " " + getEvent().getType() + " events\n"
+                        + getEvent().getDescription()
+                        + "\nbetween " + getEvent().getSpan().getStart().toString(TimeLineController.getZonedFormatter())
+                        + "\nand      " + getEvent().getSpan().getEnd().toString(TimeLineController.getZonedFormatter())
+                        + "\nright-click to adjust local description zoom."));
+    }
+
+    public Pane getSubNodePane() {
+        return subNodePane;
+    }
+
+    public AggregateEvent getEvent() {
+        return event;
+    }
+
+    /**
+     * sets the width of the {@link Region} with border and background used to
+     * indicate the temporal span of this aggregate event
+     *
+     * @param w
+     */
+    public void setSpanWidth(double w) {
+        spanRegion.setPrefWidth(w);
+        spanRegion.setMaxWidth(w);
+        spanRegion.setMinWidth(Math.max(2, w));
+    }
+
+    /**
+     *
+     * @param w the maximum width the description label should have
+     */
+    public void setDescriptionLabelMaxWidth(double w) {
+        descrLabel.setMaxWidth(w);
+    }
+
+    /** @param descrVis the level of description that should be displayed */
+    final public void setDescriptionVisibility(DescriptionVisibility descrVis) {
+        switch (descrVis) {
+            case SHOWN:
+                descrLabel.setText(event.getDescription() + " (" + event.getEventIDs().size() + ")");
+                break;
+            case COUNT_ONLY:
+                descrLabel.setText("(" + event.getEventIDs().size() + ")");
+                break;
+            case HIDDEN:
+                descrLabel.setText("");
+                break;
+        }
+    }
+
+    /** apply the 'effect' to visually indicate selection
+     *
+     * @param applied true to apply the selection 'effect', false to remove it
+     */
+    void applySelectionEffect(final boolean applied) {
+        Platform.runLater(() -> {
+            if (applied) {
+                setBorder(selectionBorder);
+            } else {
+                setBorder(null);
+            }
+        });
+    }
+
+    /** apply the 'effect' to visually indicate highlighted nodes
+     *
+     * @param applied true to apply the highlight 'effect', false to remove it
+     */
+    void applyHighlightEffect(boolean applied) {
+
+        if (applied) {
+            descrLabel.setStyle("-fx-font-weight: bold;");
+            spanFill = new Background(new BackgroundFill(getEvent().getType().getColor().deriveColor(0, 1, 1, .5), CORNER_RADII, Insets.EMPTY));
+            spanRegion.setBackground(spanFill);
+            setBackground(new Background(new BackgroundFill(getEvent().getType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY)));
+        } else {
+            descrLabel.setStyle("-fx-font-weight: normal;");
+            spanFill = new Background(new BackgroundFill(getEvent().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)));
+        }
+    }
+
+    /** set the span background and description label visible or not
+     * (the span background is not visible when this node is displaying
+     * subnodes)
+     * //TODO: move more of the control of subnodes/events here and out
+     * of EventDetail Chart
+     *
+     * @param applied true to set the span fill visible, false to hide it
+     */
+    void setEventDetailsVisible(boolean b) {
+        if (b) {
+            spanRegion.setBackground(spanFill);
+        } else {
+            spanRegion.setBackground(null);
+        }
+        descrLabel.setVisible(b);
+    }
+
+    String getDisplayedDescription() {
+        return descrLabel.getText();
+    }
+
+    double getLayoutXCompensation() {
+        if (parentEventNode != null) {
+            return parentEventNode.getLayoutXCompensation() + getBoundsInParent().getMinX();
+        } else {
+            return getBoundsInParent().getMinX();
+        }
+    }
+
+    /**
+     * @return the contextMenu
+     */
+    public ContextMenu getContextMenu() {
+        return contextMenu.get();
+    }
+
+    /**
+     * @param contextMenu the contextMenu to set
+     */
+    public void setContextMenu(ContextMenu contextMenu) {
+        this.contextMenu.set(contextMenu);
+    }
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java
new file mode 100644
index 0000000000000000000000000000000000000000..df9092c794ad115ba053e65045bebc664bddb180
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java
@@ -0,0 +1,338 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013, Christian Schudt
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ * 
+ * 
+ * 
+ */
+package org.sleuthkit.autopsy.timeline.ui.detailview;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ObjectPropertyBase;
+import javafx.beans.property.ReadOnlyDoubleProperty;
+import javafx.beans.property.ReadOnlyDoubleWrapper;
+import javafx.scene.chart.Axis;
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;
+
+/**
+ * from <a
+ * href="https://bitbucket.org/sco0ter/extfx/src/a61710e99c63bfa288672f0d99861c8fe8571293/src/main/java/extfx/scene/chart/DateAxis.java?at=0.3"
+ * >here</a>
+ *
+ * <strong> with extreme modifications.</strong>
+ *
+ * @author Christian Schudt
+ * @author Diego Cirujano
+ */
+final class DateAxis extends Axis<DateTime> {
+
+    private ObjectProperty<DateTime> lowerBound = new ObjectPropertyBase<DateTime>() {
+        @Override
+        protected void invalidated() {
+            if (!isAutoRanging()) {
+                invalidateRange();
+                requestAxisLayout();
+            }
+        }
+
+        @Override
+        public Object getBean() {
+            return DateAxis.this;
+        }
+
+        @Override
+        public String getName() {
+            return "lowerBound";
+        }
+    };
+
+    /**
+     * Stores the min and max date of the list of dates which is used. If
+     * {@link #autoRanging} is true, these values are used as lower and upper
+     * bounds.
+     */
+    private DateTime maxDate;
+
+    /**
+     * Stores the min and max date of the list of dates which is used. If
+     * {@link #autoRanging} is true, these values are used as lower and upper
+     * bounds.
+     */
+    private DateTime minDate;
+
+    private RangeDivisionInfo rangeDivisionInfo;
+
+    private final ReadOnlyDoubleWrapper tickSpacing = new ReadOnlyDoubleWrapper();
+
+    private final ObjectProperty<DateTime> upperBound = new ObjectPropertyBase<DateTime>() {
+        @Override
+        protected void invalidated() {
+            if (!isAutoRanging()) {
+                invalidateRange();
+                requestAxisLayout();
+            }
+        }
+
+        @Override
+        public Object getBean() {
+            return DateAxis.this;
+        }
+
+        @Override
+        public String getName() {
+            return "upperBound";
+        }
+    };
+
+    /** Default constructor. By default the lower and upper bound are calculated
+     * by the data. */
+    DateAxis() {
+        setAutoRanging(false);
+    }
+
+    @Override
+    public double getDisplayPosition(DateTime date) {
+        final double length = -200 + (getSide().isHorizontal() ? getWidth() : getHeight());
+
+        // Get the difference between the max and min date.
+        double diff = getUpperBound().getMillis() - getLowerBound().getMillis();
+
+        // Get the actual range of the visible area.
+        // The minimal date should start at the zero position, that's why we subtract it.
+        double range = length - getZeroPosition();
+
+        // Then get the difference from the actual date to the min date and divide it by the total difference.
+        // We get a value between 0 and 1, if the date is within the min and max date.
+        double d = (date.getMillis() - getLowerBound().getMillis()) / diff;
+
+        // Multiply this percent value with the range and add the zero offset.
+        if (getSide().isVertical()) {
+            return getHeight() - d * range + getZeroPosition();
+        } else {
+            return d * range + getZeroPosition();
+        }
+    }
+
+    /**
+     * Gets the lower bound of the axis.
+     *
+     * @return The lower bound.
+     *
+     * @see #lowerBoundProperty()
+     */
+    public final DateTime getLowerBound() {
+        return lowerBound.get();
+    }
+
+    /**
+     * Sets the lower bound of the axis.
+     *
+     * @param date The lower bound date.
+     *
+     * @see #lowerBoundProperty()
+     */
+    public final void setLowerBound(DateTime date) {
+        lowerBound.set(date);
+    }
+
+    /**
+     * Gets the upper bound of the axis.
+     *
+     * @return The upper bound.
+     *
+     * @see #upperBoundProperty()
+     */
+    public final DateTime getUpperBound() {
+        return upperBound.get();
+    }
+
+    /**
+     * Sets the upper bound of the axis.
+     *
+     * @param date The upper bound date.
+     *
+     * @see #upperBoundProperty() ()
+     */
+    public final void setUpperBound(DateTime date) {
+        upperBound.set(date);
+    }
+
+    @Override
+    public DateTime getValueForDisplay(double displayPosition) {
+        final double length = - 200 + (getSide().isHorizontal() ? getWidth() : getHeight());
+
+        // Get the difference between the max and min date.
+        double diff = getUpperBound().getMillis() - getLowerBound().getMillis();
+
+        // Get the actual range of the visible area.
+        // The minimal date should start at the zero position, that's why we subtract it.
+        double range = length - getZeroPosition();
+
+        if (getSide().isVertical()) {
+            // displayPosition = getHeight() - ((date - lowerBound) / diff) * range + getZero
+            // date = displayPosition - getZero - getHeight())/range * diff + lowerBound
+            return new DateTime((long) ((displayPosition - getZeroPosition() - getHeight()) / -range * diff + getLowerBound().getMillis()), TimeLineController.getJodaTimeZone());
+        } else {
+            // displayPosition = ((date - lowerBound) / diff) * range + getZero
+            // date = displayPosition - getZero)/range * diff + lowerBound
+            return new DateTime((long) ((displayPosition - getZeroPosition()) / range * diff + getLowerBound().getMillis()), TimeLineController.getJodaTimeZone());
+        }
+    }
+
+    @Override
+    public double getZeroPosition() {
+        return 0;
+    }
+
+    @Override
+    public void invalidateRange(List<DateTime> list) {
+        super.invalidateRange(list);
+
+        Collections.sort(list);
+        if (list.isEmpty()) {
+            minDate = maxDate = new DateTime();
+        } else if (list.size() == 1) {
+            minDate = maxDate = list.get(0);
+        } else if (list.size() > 1) {
+            minDate = list.get(0);
+            maxDate = list.get(list.size() - 1);
+        }
+    }
+
+    @Override
+    public boolean isValueOnAxis(DateTime date) {
+        return date.getMillis() > getLowerBound().getMillis() && date.getMillis() < getUpperBound().getMillis();
+    }
+
+    @Override
+    public double toNumericValue(DateTime date) {
+        return date.getMillis();
+    }
+
+    @Override
+    public DateTime toRealValue(double v) {
+        return new DateTime((long) v);
+    }
+
+    @Override
+    protected Object autoRange(double length) {
+        if (isAutoRanging()) {
+            return new Interval(minDate, maxDate);
+        } else {
+            if (getLowerBound() == null || getUpperBound() == null) {
+                return null;
+            }
+            return getRange();
+        }
+    }
+
+    @Override
+    protected List<DateTime> calculateTickValues(double length, Object range) {
+        List<DateTime> tickDates = new ArrayList<>();
+        if (range == null) {
+            return tickDates;
+        }
+        rangeDivisionInfo = RangeDivisionInfo.getRangeDivisionInfo((Interval) range);
+        final DateTime lowerBound1 = getLowerBound();
+        final DateTime upperBound1 = getUpperBound();
+
+        if (lowerBound1 == null || upperBound1 == null) {
+            return tickDates;
+        }
+        DateTime lower = lowerBound1.withZone(TimeLineController.getJodaTimeZone());
+        DateTime upper = upperBound1.withZone(TimeLineController.getJodaTimeZone());
+
+        DateTime current = lower;
+        // Loop as long we exceeded the upper bound.
+        while (current.isBefore(upper)) {
+            tickDates.add(current);
+            current = current.plus(rangeDivisionInfo.getPeriodSize().getPeriod());//.add(interval.interval, interval.amount);
+        }
+
+        // At last add the upper bound.
+        tickDates.add(upper);
+
+        // If there are at least three dates, check if the gap between the lower date and the second date is at least half the gap of the second and third date.
+        // Do the same for the upper bound.
+        // If gaps between dates are to small, remove one of them.
+        // This can occur, e.g. if the lower bound is 25.12.2013 and years are shown. Then the next year shown would be 2014 (01.01.2014) which would be too narrow to 25.12.2013.
+        if (tickDates.size() > 2) {
+            DateTime secondDate = tickDates.get(1);
+            DateTime thirdDate = tickDates.get(2);
+            DateTime lastDate = tickDates.get(tickDates.size() - 2);
+            DateTime previousLastDate = tickDates.get(tickDates.size() - 3);
+
+            // If the second date is too near by the lower bound, remove it.
+            if (secondDate.getMillis() - lower.getMillis() < (thirdDate.getMillis() - secondDate.getMillis()) / 2) {
+                tickDates.remove(lower);
+            }
+
+            // If difference from the upper bound to the last date is less than the half of the difference of the previous two dates,
+            // we better remove the last date, as it comes to close to the upper bound.
+            if (upper.getMillis() - lastDate.getMillis() < (lastDate.getMillis() - previousLastDate.getMillis()) / 2) {
+                tickDates.remove(lastDate);
+            }
+        }
+
+        if (tickDates.size() >= 2) {
+            tickSpacing.set(getDisplayPosition(tickDates.get(1)) - getDisplayPosition(tickDates.get(0)));
+        } else if (tickDates.size() >= 4) {
+            tickSpacing.set(getDisplayPosition(tickDates.get(2)) - getDisplayPosition(tickDates.get(1)));
+        }
+        return tickDates;
+    }
+
+    @Override
+    protected Object getRange() {
+
+        return new Interval(getLowerBound(), getUpperBound());
+    }
+
+    @Override
+    protected String getTickMarkLabel(DateTime date) {
+        return rangeDivisionInfo.getTickFormatter().print(date);
+    }
+
+    @Override
+    protected void layoutChildren() {
+        super.layoutChildren();
+    }
+
+    @Override
+    protected void setRange(Object range, boolean animating) {
+
+        rangeDivisionInfo = RangeDivisionInfo.getRangeDivisionInfo((Interval) range);
+
+        setLowerBound(new DateTime(rangeDivisionInfo.getLowerBound()));
+        setUpperBound(new DateTime(rangeDivisionInfo.getUpperBound()));
+    }
+
+    ReadOnlyDoubleProperty getTickSpacing() {
+        return tickSpacing.getReadOnlyProperty();
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java
new file mode 100644
index 0000000000000000000000000000000000000000..e398c68070a42f72858d2484d3c1e84e4f778b9e
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java
@@ -0,0 +1,27 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.detailview;
+
+/** Level of description shown in UI
+ * NOTE: this is a separate concept form {@link DescriptionLOD} */
+enum DescriptionVisibility {
+
+    HIDDEN, COUNT_ONLY, SHOWN;
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java
new file mode 100644
index 0000000000000000000000000000000000000000..3eceb5d9197874a0e607f4562ccb192674a67997
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java
@@ -0,0 +1,428 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.detailview;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.concurrent.ConcurrentHashMap;
+import javafx.application.Platform;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.concurrent.Task;
+import javafx.event.EventHandler;
+import javafx.fxml.FXML;
+import javafx.geometry.Orientation;
+import javafx.scene.Cursor;
+import javafx.scene.chart.Axis;
+import javafx.scene.chart.BarChart;
+import javafx.scene.chart.XYChart;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.MultipleSelectionModel;
+import javafx.scene.control.RadioButton;
+import javafx.scene.control.ScrollBar;
+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;
+import static javafx.scene.input.KeyCode.KP_UP;
+import static javafx.scene.input.KeyCode.PAGE_DOWN;
+import static javafx.scene.input.KeyCode.PAGE_UP;
+import static javafx.scene.input.KeyCode.UP;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.ScrollEvent;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+import org.joda.time.DateTime;
+import org.sleuthkit.autopsy.timeline.FXMLConstructor;
+import org.sleuthkit.autopsy.timeline.LoggedTask;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+import org.sleuthkit.autopsy.timeline.events.type.EventType;
+import org.sleuthkit.autopsy.timeline.ui.AbstractVisualization;
+import org.sleuthkit.autopsy.timeline.ui.countsview.CountsViewPane;
+import org.sleuthkit.autopsy.timeline.ui.detailview.tree.NavTreeNode;
+import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;
+import org.sleuthkit.autopsy.coreutils.Logger;
+
+/**
+ * FXML Controller class for a {@link EventDetailChart} based implementation of
+ * a TimeLineView.
+ *
+ * This class listens to changes in the assigned {@link FilteredEventsModel} and
+ * updates the internal {@link EventDetailChart} to reflect the currently
+ * requested events.
+ *
+ * This class captures input from the user in the form of mouse clicks on graph
+ * bars, and forwards them to the assigned {@link TimeLineController}
+ *
+ * Concurrency Policy: Access to the private members clusterChart, dateAxis,
+ * EventTypeMap, and dataSets is all linked directly to the ClusterChart which
+ * must only be manipulated on the JavaFx thread (through {@link Platform#runLater(java.lang.Runnable)
+ * }
+ *
+ * {@link CountsChartPane#filteredEvents} should encapsulate all needed
+ * synchronization internally.
+ *
+ * TODO: refactor common code out of this class and CountsChartPane into
+ * {@link AbstractVisualization}
+ */
+public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEvent, AggregateEventNode, EventDetailChart> {
+
+    private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName());
+
+    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();
+
+    private final Axis<AggregateEvent> verticalAxis = new EventAxis();
+
+    //private access to barchart data
+    private final Map<EventType, XYChart.Series<DateTime, AggregateEvent>> eventTypeToSeriesMap = new ConcurrentHashMap<>();
+
+    private final ScrollBar vertScrollBar = new ScrollBar();
+
+    private final Region region = new Region();
+
+    private final ObservableList<AggregateEvent> aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
+
+    private final ObservableList<AggregateEventNode> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
+
+    public ObservableList<AggregateEvent> getAggregatedEvents() {
+        return aggregatedEvents;
+    }
+
+    public DetailViewPane(Pane partPane, Pane contextPane, Region spacer) {
+        super(partPane, contextPane, spacer);
+        chart = new EventDetailChart(dateAxis, verticalAxis, selectedNodes);
+        setChartClickHandler();
+        chart.setData(dataSets);
+        setCenter(chart);
+
+        chart.setPrefHeight(USE_COMPUTED_SIZE);
+
+        settingsNodes = new ArrayList<>(new DetailViewSettingsPane().getChildrenUnmodifiable());
+
+        vertScrollBar.setOrientation(Orientation.VERTICAL);
+        VBox vBox = new VBox();
+        VBox.setVgrow(vertScrollBar, Priority.ALWAYS);
+        vBox.getChildren().add(vertScrollBar);
+        vBox.getChildren().add(region);
+        setRight(vBox);
+
+        dateAxis.setAutoRanging(false);
+        region.minHeightProperty().bind(dateAxis.heightProperty());
+        vertScrollBar.visibleAmountProperty().bind(chart.heightProperty().multiply(100).divide(chart.getMaxVScroll()));
+        requestLayout();
+
+        highlightedNodes.addListener((ListChangeListener.Change<? extends AggregateEventNode> change) -> {
+            while (change.next()) {
+                change.getAddedSubList().forEach(aeNode -> {
+                    aeNode.applyHighlightEffect(true);
+                });
+                change.getRemoved().forEach(aeNode -> {
+                    aeNode.applyHighlightEffect(false);
+                });
+            }
+        });
+        //request focus for keyboard scrolling
+        setOnMouseClicked((MouseEvent t) -> {
+            requestFocus();
+        });
+
+        //These scroll related handlers don't affect any other view or the model, so they are handled internally
+        //mouse wheel scroll handler
+        this.onScrollProperty().set((EventHandler<ScrollEvent>) (ScrollEvent t) -> {
+            vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() - t.getDeltaY() / 200.0)));
+        });
+
+        this.setOnKeyPressed((KeyEvent t) -> {
+            switch (t.getCode()) {
+                case PAGE_UP:
+                    incrementScrollValue(-70);
+                    break;
+                case PAGE_DOWN:
+                    incrementScrollValue(70);
+                    break;
+                case KP_UP:
+                case UP:
+                    incrementScrollValue(-10);
+                    break;
+                case KP_DOWN:
+                case DOWN:
+                    incrementScrollValue(10);
+                    break;
+            }
+            t.consume();
+        });
+
+        //scrollbar handler
+        this.vertScrollBar.valueProperty().addListener((o, oldValue, newValue) -> {
+            chart.setVScroll(newValue.doubleValue() / 100.0);
+        });
+        spacer.minWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty()));
+        spacer.prefWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty()));
+        spacer.maxWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty()));
+
+        dateAxis.setTickLabelsVisible(false);
+
+        dateAxis.getTickMarks().addListener((Observable observable) -> {
+            layoutDateLabels();
+        });
+        dateAxis.getTickSpacing().addListener((Observable observable) -> {
+            layoutDateLabels();
+        });
+
+        dateAxis.setTickLabelGap(0);
+
+        selectedNodes.addListener((Observable observable) -> {
+            highlightedNodes.clear();
+            selectedNodes.stream().forEach((tn) -> {
+                for (AggregateEventNode n : chart.getNodes((
+                        AggregateEventNode t) -> t.getEvent().getDescription().equals(tn.getEvent().getDescription()))) {
+                    highlightedNodes.add(n);
+                }
+            });
+        });
+
+    }
+
+    private void incrementScrollValue(int factor) {
+        vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() + factor * (chart.getHeight() / chart.getMaxVScroll().get()))));
+    }
+
+    public void setSelectionModel(MultipleSelectionModel<TreeItem<NavTreeNode>> selectionModel) {
+        this.treeSelectionModel = selectionModel;
+
+        treeSelectionModel.getSelectedItems().addListener((Observable observable) -> {
+            highlightedNodes.clear();
+            for (TreeItem<NavTreeNode> tn : treeSelectionModel.getSelectedItems()) {
+                for (AggregateEventNode n : chart.getNodes((
+                        AggregateEventNode t)
+                        -> t.getEvent().getDescription().equals(tn.getValue().getDescription()))) {
+                    highlightedNodes.add(n);
+                }
+            }
+        });
+    }
+
+    @Override
+    protected Boolean isTickBold(DateTime value) {
+        return false;
+    }
+
+    @Override
+    protected Axis<AggregateEvent> getYAxis() {
+        return verticalAxis;
+    }
+
+    @Override
+    protected Axis<DateTime> getXAxis() {
+        return dateAxis;
+    }
+
+    @Override
+    protected double getTickSpacing() {
+        return dateAxis.getTickSpacing().get();
+    }
+
+    @Override
+    protected String getTickMarkLabel(DateTime value) {
+        return dateAxis.getTickMarkLabel(value);
+    }
+
+    /** NOTE: Because this method modifies data directly used by the chart,
+     * this method should only be called from JavaFX thread!
+     *
+     * @param et the EventType to get the series for
+     *
+     * @return a Series object to contain all the events with the given
+     *         EventType */
+    private XYChart.Series<DateTime, AggregateEvent> getSeries(final EventType et) {
+        XYChart.Series<DateTime, AggregateEvent> series = eventTypeToSeriesMap.get(et);
+        if (series == null) {
+            series = new XYChart.Series<>();
+            series.setName(et.getDisplayName());
+            eventTypeToSeriesMap.put(et, series);
+            dataSets.add(series);
+        }
+        return series;
+    }
+
+    @Override
+    protected Task<Boolean> getUpdateTask() {
+
+        return new LoggedTask<Boolean>("Update Details", true) {
+
+            @Override
+            protected Boolean call() throws Exception {
+                if (isCancelled()) {
+                    return null;
+                }
+                Platform.runLater(() -> {
+                    if (isCancelled() == false) {
+                        setCursor(Cursor.WAIT);
+                    }
+                });
+
+                updateProgress(-1, 1);
+                updateMessage("preparing");
+
+                final RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.timeRange().get());
+                final long lowerBound = rangeInfo.getLowerBound();
+                final long upperBound = rangeInfo.getUpperBound();
+
+                updateMessage("querying db");
+                aggregatedEvents.setAll(filteredEvents.getAggregatedEvents());
+
+                Platform.runLater(() -> {
+                    if (isCancelled()) {
+                        return;
+                    }
+                    dateAxis.setLowerBound(new DateTime(lowerBound, TimeLineController.getJodaTimeZone()));
+                    dateAxis.setUpperBound(new DateTime(upperBound, TimeLineController.getJodaTimeZone()));
+//                    if (chart == null) {
+//                        initializeClusterChart();
+//                    }
+                    vertScrollBar.setValue(0);
+                    eventTypeToSeriesMap.clear();
+                    dataSets.clear();
+                });
+                final int size = aggregatedEvents.size();
+                int i = 0;
+                for (final AggregateEvent e : aggregatedEvents) {
+                    if (isCancelled()) {
+                        break;
+                    }
+                    updateProgress(i++, size);
+                    updateMessage("updating ui");
+                    final XYChart.Data<DateTime, AggregateEvent> xyData = new BarChart.Data<>(new DateTime(e.getSpan().getStartMillis()), e);
+
+                    Platform.runLater(() -> {
+                        if (isCancelled() == false) {
+                            getSeries(e.getType()).getData().add(xyData);
+                        }
+                    });
+                }
+
+                Platform.runLater(() -> {
+                    setCursor(Cursor.NONE);
+                    layoutDateLabels();
+                    updateProgress(1, 1);
+                });
+                return aggregatedEvents.isEmpty() == false;
+            }
+        };
+    }
+
+    @Override
+    protected Effect getSelectionEffect() {
+        return null;
+    }
+
+    @Override
+    protected void applySelectionEffect(AggregateEventNode c1, Boolean applied) {
+        c1.applySelectionEffect(applied);
+    }
+
+    class DetailViewSettingsPane extends HBox {
+
+        @FXML
+        private RadioButton hiddenRadio;
+
+        @FXML
+        private RadioButton showRadio;
+
+        @FXML
+        private ToggleGroup descrVisibility;
+
+        @FXML
+        private RadioButton countsRadio;
+
+        @FXML
+        private ResourceBundle resources;
+
+        @FXML
+        private URL location;
+
+        @FXML
+        private CheckBox bandByTypeBox;
+
+        @FXML
+        private CheckBox oneEventPerRowBox;
+
+        @FXML
+        private CheckBox truncateAllBox;
+
+        @FXML
+        private Slider truncateWidthSlider;
+
+        public DetailViewSettingsPane() {
+            FXMLConstructor.construct(this, "DetailViewSettingsPane.fxml");
+        }
+
+        @FXML
+        void initialize() {
+            assert bandByTypeBox != null : "fx:id=\"bandByTypeBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'.";
+            assert oneEventPerRowBox != null : "fx:id=\"oneEventPerRowBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'.";
+            assert truncateAllBox != null : "fx:id=\"truncateAllBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'.";
+            assert truncateWidthSlider != null : "fx:id=\"truncateAllSlider\" was not injected: check your FXML file 'DetailViewSettings.fxml'.";
+            bandByTypeBox.selectedProperty().bindBidirectional(chart.getBandByType());
+            truncateAllBox.selectedProperty().bindBidirectional(chart.getTruncateAll());
+            oneEventPerRowBox.selectedProperty().bindBidirectional(chart.getOneEventPerRow());
+            truncateWidthSlider.disableProperty().bind(truncateAllBox.selectedProperty().not());
+            final InvalidationListener sliderListener = o -> {
+                if (truncateWidthSlider.isValueChanging() == false) {
+                    chart.getTruncateWidth().set(truncateWidthSlider.getValue());
+                }
+            };
+            truncateWidthSlider.valueProperty().addListener(sliderListener);
+            truncateWidthSlider.valueChangingProperty().addListener(sliderListener);
+
+            descrVisibility.selectedToggleProperty().addListener((observable, oldToggle, newToggle) -> {
+                if (newToggle == countsRadio) {
+                    chart.getDescrVisibility().set(DescriptionVisibility.COUNT_ONLY);
+                } else if (newToggle == showRadio) {
+                    chart.getDescrVisibility().set(DescriptionVisibility.SHOWN);
+                } else if (newToggle == hiddenRadio) {
+                    chart.getDescrVisibility().set(DescriptionVisibility.HIDDEN);
+                }
+            });
+        }
+
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..47b444c4e550c9e318c9ef584aa01e1deeb1526c
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+
+<fx:root alignment="CENTER_LEFT" spacing="5.0" type="javafx.scene.layout.HBox" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+  <children>
+    <FlowPane alignment="CENTER_LEFT" hgap="5.0" prefHeight="-1.0" prefWidth="-1.0" prefWrapLength="220.0" vgap="5.0" HBox.hgrow="ALWAYS">
+      <children><Label text="Layout Options:" />
+<HBox spacing="5.0">
+<children>
+        <CheckBox fx:id="bandByTypeBox" mnemonicParsing="false" text="Band by Type" />
+        <CheckBox fx:id="oneEventPerRowBox" mnemonicParsing="false" text="One Event Per Row" />
+</children>
+<FlowPane.margin>
+<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+</FlowPane.margin>
+</HBox>
+      </children>
+<HBox.margin>
+<Insets bottom="5.0" left="5.0" right="5.0" />
+</HBox.margin>
+    </FlowPane>
+    <Separator orientation="VERTICAL" prefHeight="-1.0" />
+    <FlowPane columnHalignment="CENTER" hgap="5.0" prefHeight="-1.0" prefWidth="-1.0" prefWrapLength="350.0" vgap="5.0" HBox.hgrow="ALWAYS">
+      <children>
+        <CheckBox fx:id="truncateAllBox" mnemonicParsing="false" text="Truncate Descriptions to (px):" />
+            <Slider id="truncateAllSlider" fx:id="truncateWidthSlider" blockIncrement="50.0" disable="false" majorTickUnit="150.0" max="500.0" min="50.0" minorTickCount="0" prefHeight="33.0" prefWidth="146.0" showTickLabels="true" showTickMarks="false" value="200.0">
+<FlowPane.margin>
+<Insets bottom="3.0" left="3.0" right="3.0" top="3.0" />
+</FlowPane.margin></Slider>
+      </children>
+    </FlowPane>
+    <Separator orientation="VERTICAL" prefHeight="-1.0" /><FlowPane alignment="CENTER_LEFT" hgap="5.0" minHeight="-Infinity" minWidth="-Infinity" prefWrapLength="200.0" vgap="5.0" HBox.hgrow="ALWAYS">
+<children><Label text="Description Visbility:" />
+<HBox spacing="5.0">
+<children><RadioButton fx:id="showRadio" mnemonicParsing="false" selected="true" text="Show">
+<toggleGroup>
+<ToggleGroup fx:id="descrVisibility" />
+</toggleGroup></RadioButton><RadioButton fx:id="countsRadio" mnemonicParsing="false" text="Counts Only" toggleGroup="$descrVisibility" /><RadioButton fx:id="hiddenRadio" mnemonicParsing="false" text="Hide" toggleGroup="$descrVisibility" />
+</children>
+<FlowPane.margin>
+<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+</FlowPane.margin>
+</HBox>
+</children>
+<HBox.margin>
+<Insets />
+</HBox.margin></FlowPane>
+  </children>
+  <padding>
+    <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+  </padding>
+</fx:root>
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java
new file mode 100644
index 0000000000000000000000000000000000000000..78235664c6e9808a752f4f1e682dbebcefe5ddec
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java
@@ -0,0 +1,86 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.detailview;
+
+import java.util.Collections;
+import java.util.List;
+import javafx.scene.chart.Axis;
+import javafx.scene.chart.XYChart;
+import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
+
+/** No-Op axis that doesn't do anything usefull but is necessary to pass
+ * AggregateEvent as the second member of {@link XYChart.Data} objects */
+class EventAxis extends Axis<AggregateEvent> {
+
+    @Override
+    public double getDisplayPosition(AggregateEvent value) {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public AggregateEvent getValueForDisplay(double displayPosition) {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public double getZeroPosition() {
+        return 0;
+    }
+
+    @Override
+    public boolean isValueOnAxis(AggregateEvent value) {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public double toNumericValue(AggregateEvent value) {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public AggregateEvent toRealValue(double value) {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    protected Object autoRange(double length) {
+        return null;
+    }
+
+    @Override
+    protected List<AggregateEvent> calculateTickValues(double length, Object range) {
+        return Collections.emptyList();
+    }
+
+    @Override
+    protected Object getRange() {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    protected String getTickMarkLabel(AggregateEvent value) {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    protected void setRange(Object range, boolean animate) {
+//        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java
new file mode 100644
index 0000000000000000000000000000000000000000..5f0c8e7584be19da3f37c179d654ba06f39226ff
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java
@@ -0,0 +1,841 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.detailview;
+
+import com.google.common.collect.Collections2;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.ReadOnlyDoubleProperty;
+import javafx.beans.property.ReadOnlyDoubleWrapper;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.MapChangeListener;
+import javafx.collections.ObservableList;
+import javafx.collections.ObservableMap;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.scene.Cursor;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.chart.Axis;
+import javafx.scene.chart.NumberAxis;
+import javafx.scene.chart.XYChart;
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.CustomMenuItem;
+import javafx.scene.control.Label;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.Slider;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.shape.Line;
+import javafx.scene.shape.StrokeLineCap;
+import javafx.scene.text.Text;
+import javafx.util.Duration;
+import javafx.util.StringConverter;
+import javax.annotation.concurrent.GuardedBy;
+import org.controlsfx.control.action.AbstractAction;
+import org.controlsfx.control.action.ActionGroup;
+import org.controlsfx.control.action.ActionUtils;
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.timeline.LoggedTask;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.actions.Back;
+import org.sleuthkit.autopsy.timeline.actions.Forward;
+import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+import org.sleuthkit.autopsy.timeline.events.type.EventType;
+import org.sleuthkit.autopsy.timeline.filters.Filter;
+import org.sleuthkit.autopsy.timeline.filters.TextFilter;
+import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
+import org.sleuthkit.autopsy.timeline.ui.TimeLineChart;
+import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD;
+import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
+
+/**
+ * Custom implementation of {@link XYChart} to graph events on a horizontal
+ * timeline.
+ *
+ * The horizontal {@link DateAxis} controls the tick-marks and the horizontal
+ * layout of the nodes representing events. The vertical {@link NumberAxis} does
+ * nothing (although a custom implementation could help with the vertical
+ * layout?)
+ *
+ * Series help organize events for the banding by event type, we could add a
+ * node to contain each band if we need a place for per band controls.
+ *
+ * //TODO: refactor the projected lines to a separate class. -jm */
+public class EventDetailChart extends XYChart<DateTime, AggregateEvent> implements TimeLineChart<DateTime> {
+
+    private static final int PROJECTED_LINE_Y_OFFSET = 5;
+
+    private static final int PROJECTED_LINE_STROKE_WIDTH = 5;
+
+    /** true == layout each event type in its own band, false == mix all the
+     * events together during layout */
+    private final SimpleBooleanProperty bandByType = new SimpleBooleanProperty(false);
+
+    private ContextMenu chartContextMenu;
+
+    private TimeLineController controller;
+
+    /** how much detail of the description to show in the ui */
+    private final SimpleObjectProperty<DescriptionVisibility> descrVisibility = new SimpleObjectProperty<>(DescriptionVisibility.SHOWN);
+
+    private FilteredEventsModel filteredEvents;
+
+    /** a user position-able vertical line to help the compare events */
+    private Line guideLine;
+
+    /** * the user can drag out a time range to zoom into and this
+     * {@link IntervalSelector} is the visual representation of it while
+     * the user is dragging */
+    private IntervalSelector<? extends DateTime> intervalSelector;
+
+    /** listener that triggers layout pass */
+    private final InvalidationListener layoutInvalidationListener = (
+            Observable o) -> {
+                synchronized (EventDetailChart.this) {
+                    requiresLayout = true;
+                    requestChartLayout();
+                }
+            };
+
+    /** the maximum y value used so far during the most recent layout pass */
+    private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0);
+
+    /**
+     * the group that all event nodes are added to. This facilitates scrolling
+     * by allowing a single translation of this group.
+     */
+    private final Group nodeGroup = new Group();
+
+    /** map from event to node */
+    private final Map<AggregateEvent, AggregateEventNode> nodeMap = new TreeMap<>((
+            AggregateEvent o1,
+            AggregateEvent o2) -> {
+                int comp = Long.compare(o1.getSpan().getStartMillis(), o2.getSpan().getStartMillis());
+                if (comp != 0) {
+                    return comp;
+                } else {
+                    return Comparator.comparing(AggregateEvent::hashCode).compare(o1, o2);
+                }
+            });
+
+    /** true == enforce that no two events can share the same 'row', leading to
+     * sparser but possibly clearer layout. false == put unrelated events in the
+     * same 'row', creating a denser more compact layout */
+    private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false);
+
+    private final ObservableMap<AggregateEventNode, Line> projectionMap = FXCollections.observableHashMap();
+
+    /** flag indicating whether this chart actually needs a layout pass */
+    @GuardedBy(value = "this")
+    private boolean requiresLayout = true;
+
+    private final ObservableList<AggregateEventNode> selectedNodes;
+
+    /**
+     * list of series of data added to this chart TODO: replace this with a map
+     * from name to series? -jm
+     */
+    private final ObservableList<Series<DateTime, AggregateEvent>> seriesList
+            = FXCollections.<Series<DateTime, AggregateEvent>>observableArrayList();
+
+    private final ObservableList<Series<DateTime, AggregateEvent>> sortedSeriesList = seriesList
+            .sorted((s1, s2) -> {
+                final List<String> collect = EventType.allTypes.stream().map(EventType::getDisplayName).collect(Collectors.toList());
+                return Integer.compare(collect.indexOf(s1.getName()), collect.indexOf(s2.getName()));
+            });
+
+    /** true == truncate all the labels to the greater of the size of their
+     * timespan indicator or the value of truncateWidth. false == don't truncate
+     * the labels, alow them to extend past the timespan indicator and off the
+     * edge of the screen */
+    private final SimpleBooleanProperty truncateAll = new SimpleBooleanProperty(false);
+
+    /** the width to truncate all labels to if truncateAll is true. adjustable
+     * via slider if truncateAll is true */
+    private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0);
+
+    EventDetailChart(DateAxis dateAxis, final Axis<AggregateEvent> verticalAxis, ObservableList<AggregateEventNode> selectedNodes) {
+        super(dateAxis, verticalAxis);
+        dateAxis.setAutoRanging(false);
+
+        //yAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm
+        verticalAxis.setTickLabelsVisible(false);
+        verticalAxis.setTickMarkVisible(false);
+
+        setLegendVisible(false);
+        setPadding(Insets.EMPTY);
+        setAlternativeColumnFillVisible(true);
+
+        //all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly
+        getPlotChildren().add(nodeGroup);
+
+        //bind listener to events that should trigger layout
+        widthProperty().addListener(layoutInvalidationListener);
+        heightProperty().addListener(layoutInvalidationListener);
+//        boundsInLocalProperty().addListener(layoutInvalidationListener);
+        bandByType.addListener(layoutInvalidationListener);
+        oneEventPerRow.addListener(layoutInvalidationListener);
+        truncateAll.addListener(layoutInvalidationListener);
+        truncateWidth.addListener(layoutInvalidationListener);
+        descrVisibility.addListener(layoutInvalidationListener);
+
+        //this is needed to allow non circular binding of the guideline and timerangRect heights to the height of the chart
+        boundsInLocalProperty().addListener((Observable observable) -> {
+            setPrefHeight(boundsInLocalProperty().get().getHeight());
+        });
+
+        //set up mouse listeners
+        final EventHandler<MouseEvent> clickHandler = (MouseEvent clickEvent) -> {
+            if (chartContextMenu != null) {
+                chartContextMenu.hide();
+            }
+            if (clickEvent.getButton() == MouseButton.SECONDARY && clickEvent.isStillSincePress()) {
+
+                chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new AbstractAction("Place Marker") {
+                    {
+                        setGraphic(new ImageView(new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true)));
+                    }
+
+                    @Override
+                    public void handle(ActionEvent ae) {
+//                    
+                        if (guideLine == null) {
+                            guideLine = new GuideLine(0, 0, 0, getHeight(), dateAxis);
+                            guideLine.relocate(clickEvent.getX(), 0);
+                            guideLine.endYProperty().bind(heightProperty().subtract(dateAxis.heightProperty().subtract(dateAxis.tickLengthProperty())));
+
+                            getChartChildren().add(guideLine);
+
+                            guideLine.setOnMouseClicked((MouseEvent event) -> {
+                                if (event.getButton() == MouseButton.SECONDARY) {
+                                    clearGuideLine();
+                                    event.consume();
+                                }
+                            });
+
+//                           
+                        } else {
+
+                            guideLine.relocate(clickEvent.getX(), 0);
+                        }
+                    }
+                }, new ActionGroup("Zoom History", new Back(controller),
+                                   new Forward(controller))));
+                chartContextMenu.setAutoHide(true);
+                chartContextMenu.show(EventDetailChart.this, clickEvent.getScreenX(), clickEvent.getScreenY());
+                clickEvent.consume();
+            }
+        };
+
+        setOnMouseClicked(clickHandler);
+
+        //use one handler with an if chain because it maintains state
+        final ChartDragHandler<DateTime, EventDetailChart> dragHandler = new ChartDragHandler<>(this, getXAxis());
+        setOnMousePressed(dragHandler);
+        setOnMouseReleased(dragHandler);
+        setOnMouseDragged(dragHandler);
+
+        projectionMap.addListener((MapChangeListener.Change<? extends AggregateEventNode, ? extends Line> change) -> {
+            final Line valueRemoved = change.getValueRemoved();
+            if (valueRemoved != null) {
+                getChartChildren().removeAll(valueRemoved);
+            }
+            final Line valueAdded = change.getValueAdded();
+            if (valueAdded != null) {
+                getChartChildren().add(valueAdded);
+            }
+        });
+
+        this.selectedNodes = selectedNodes;
+        this.selectedNodes.addListener((
+                ListChangeListener.Change<? extends AggregateEventNode> c) -> {
+                    while (c.next()) {
+                        c.getRemoved().forEach((AggregateEventNode t) -> {
+                            projectionMap.remove(t);
+                        });
+                        c.getAddedSubList().forEach((AggregateEventNode t) -> {
+                            Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET,
+                                                 dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET
+                            );
+                            line.setStroke(t.getEvent().getType().getColor().deriveColor(0, 1, 1, .5));
+                            line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH);
+                            line.setStrokeLineCap(StrokeLineCap.ROUND);
+                            projectionMap.put(t, line);
+                        });
+
+                    }
+
+                    this.controller.selectEventIDs(selectedNodes.stream()
+                            .flatMap((AggregateEventNode aggNode) -> aggNode.getEvent().getEventIDs().stream())
+                            .collect(Collectors.toList()));
+                });
+
+        requestChartLayout();
+    }
+
+    @Override
+    public void clearIntervalSelector() {
+        getChartChildren().remove(intervalSelector);
+        intervalSelector = null;
+    }
+
+    public synchronized SimpleBooleanProperty getBandByType() {
+        return bandByType;
+    }
+
+    @Override
+    public final synchronized void setController(TimeLineController controller) {
+        this.controller = controller;
+        setModel(this.controller.getEventsModel());
+    }
+
+    @Override
+    public void setModel(FilteredEventsModel filteredEvents) {
+        this.filteredEvents = filteredEvents;
+        filteredEvents.getRequestedZoomParamters().addListener(o -> {
+            clearGuideLine();
+            clearIntervalSelector();
+
+            selectedNodes.clear();
+            projectionMap.clear();
+            controller.selectEventIDs(Collections.emptyList());
+        });
+    }
+
+    @Override
+    public IntervalSelector<DateTime> newIntervalSelector(double x, Axis<DateTime> axis) {
+        return new DetailIntervalSelector(x, getHeight() - axis.getHeight() - axis.getTickLength(), axis, controller);
+    }
+
+    synchronized void setBandByType(Boolean t1) {
+        bandByType.set(t1);
+    }
+
+    /** get the DateTime along the x-axis that corresponds to the given
+     * x-coordinate in the coordinate system of this {@link EventDetailChart}
+     *
+     * @param x a x-coordinate in the space of this {@link EventDetailChart}
+     *
+     * @return the DateTime along the x-axis corresponding to the given x value
+     *         (in the space of this {@link EventDetailChart}
+     */
+    public final DateTime getDateTimeForPosition(double x) {
+        return getXAxis().getValueForDisplay(getXAxis().parentToLocal(x, 0).getX());
+    }
+
+    @Override
+    public IntervalSelector<? extends DateTime> getIntervalSelector() {
+        return intervalSelector;
+    }
+
+    @Override
+    public void setIntervalSelector(IntervalSelector<? extends DateTime> newIntervalSelector) {
+        intervalSelector = newIntervalSelector;
+        getChartChildren().add(getIntervalSelector());
+    }
+
+    public synchronized SimpleBooleanProperty getOneEventPerRow() {
+        return oneEventPerRow;
+    }
+
+    public synchronized SimpleBooleanProperty getTruncateAll() {
+        return truncateAll;
+    }
+
+    synchronized void setEventOnePerRow(Boolean t1) {
+        oneEventPerRow.set(t1);
+    }
+
+    synchronized void setTruncateAll(Boolean t1) {
+        truncateAll.set(t1);
+
+    }
+
+    @Override
+    protected synchronized void dataItemAdded(Series<DateTime, AggregateEvent> series, int i, Data<DateTime, AggregateEvent> data) {
+        final AggregateEvent aggEvent = data.getYValue();
+        AggregateEventNode eventNode = nodeMap.get(aggEvent);
+        if (eventNode == null) {
+            eventNode = new AggregateEventNode(aggEvent, null);
+            eventNode.setOnMouseClicked(new EventMouseHandler(eventNode));
+
+            eventNode.setLayoutX(getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis())));
+            data.setNode(eventNode);
+            nodeMap.put(aggEvent, eventNode);
+            nodeGroup.getChildren().add(eventNode);
+            requiresLayout = true;
+        }
+    }
+
+    @Override
+    protected synchronized void dataItemChanged(Data<DateTime, AggregateEvent> data) {
+        //TODO: can we use this to help with local detail level adjustment -jm
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    protected synchronized void dataItemRemoved(Data<DateTime, AggregateEvent> data, Series<DateTime, AggregateEvent> series) {
+        nodeMap.remove(data.getYValue());
+        nodeGroup.getChildren().remove(data.getNode());
+        data.setNode(null);
+    }
+
+    @Override
+    protected void layoutChildren() {
+        super.layoutChildren();
+
+    }
+
+    /**
+     * Layout the nodes representing events via the following algorithm.
+     *
+     * we start with a list of nodes (each representing an event) - sort the
+     * list of nodes by span start time of the underlying event - initialize
+     * empty map (maxXatY) from y-position to max used x-value - for each
+     * node: -- autosize the node (based on text label) -- get the event's start
+     * and end positions from the dateaxis -- size the capsule representing
+     * event duration -- starting from the top of the chart: --- (1)check if
+     * maxXatY is to the left of the start position: -------if maxXatY less than
+     * start position , good, put the current node here, mark end
+     * position as maxXatY, go to next node -------if maxXatY greater than start
+     * position, increment y position, do -------------check(1) again until
+     * maxXatY less than start position
+     */
+    @Override
+    protected synchronized void layoutPlotChildren() {
+
+        if (requiresLayout) {
+            setCursor(Cursor.WAIT);
+            double minY = 0;
+
+            maxY.set(0.0);
+
+            if (bandByType.get() == false) {
+
+                ObservableList<Node> nodes = FXCollections.observableArrayList(nodeMap.values());
+                FXCollections.sort(nodes, new StartTimeComparator());
+                layoutNodes(nodes, minY, 0);
+//                layoutNodes(new ArrayList<>(nodeMap.values()), minY, 0);
+            } else {
+                for (Series<DateTime, AggregateEvent> s : sortedSeriesList) {
+                    ObservableList<Node> nodes = FXCollections.observableArrayList(Collections2.transform(s.getData(), Data::getNode));
+
+                    FXCollections.sort(nodes, new StartTimeComparator());
+                    layoutNodes(nodes.filtered((Node n) -> n != null), minY, 0);
+                    minY = maxY.get();
+                }
+            }
+            setCursor(null);
+            requiresLayout = false;
+        }
+        layoutProjectionMap();
+    }
+
+    @Override
+    protected synchronized void seriesAdded(Series<DateTime, AggregateEvent> series, int i) {
+        for (int j = 0; j < series.getData().size(); j++) {
+            dataItemAdded(series, j, series.getData().get(j));
+        }
+        seriesList.add(series);
+        requiresLayout = true;
+    }
+
+    @Override
+    protected synchronized void seriesRemoved(Series<DateTime, AggregateEvent> series) {
+        for (int j = 0; j < series.getData().size(); j++) {
+            dataItemRemoved(series.getData().get(j), series);
+        }
+        seriesList.remove(series);
+        requiresLayout = true;
+    }
+
+    synchronized SimpleObjectProperty<DescriptionVisibility> getDescrVisibility() {
+        return descrVisibility;
+    }
+
+    synchronized ReadOnlyDoubleProperty getMaxVScroll() {
+        return maxY.getReadOnlyProperty();
+    }
+
+    Iterable<AggregateEventNode> getNodes(Predicate<AggregateEventNode> p) {
+        List<AggregateEventNode> nodes = new ArrayList<>();
+
+        for (AggregateEventNode node : nodeMap.values()) {
+            checkNode(node, p, nodes);
+        }
+
+        return nodes;
+    }
+
+    synchronized SimpleDoubleProperty getTruncateWidth() {
+        return truncateWidth;
+    }
+
+    synchronized void setVScroll(double d) {
+        final double h = maxY.get() - (getHeight() * .9);
+        nodeGroup.setTranslateY(-d * h);
+    }
+
+    private void checkNode(AggregateEventNode node, Predicate<AggregateEventNode> p, List<AggregateEventNode> nodes) {
+        if (node != null) {
+            AggregateEvent event = node.getEvent();
+            if (p.test(node)) {
+                nodes.add(node);
+            }
+            for (Node n : node.getSubNodePane().getChildrenUnmodifiable()) {
+                checkNode((AggregateEventNode) n, p, nodes);
+            }
+        }
+    }
+
+    private void clearGuideLine() {
+        getChartChildren().remove(guideLine);
+        guideLine = null;
+    }
+
+    /**
+     * layout the nodes in the given list, starting form the given minimum y
+     * coordinate.
+     *
+     * @param nodes
+     * @param minY
+     */
+    private synchronized double layoutNodes(final List<Node> nodes, final double minY, final double xOffset) {
+        //hash map from y value to right most occupied x value.  This tells you for a given 'row' what is the first avaialable slot
+        Map<Integer, Double> maxXatY = new HashMap<>();
+        double localMax = minY;
+        //for each node lay size it and position it in first available slot
+        for (Node n : nodes) {
+            final AggregateEventNode tlNode = (AggregateEventNode) n;
+            tlNode.setDescriptionVisibility(descrVisibility.get());
+
+            AggregateEvent ie = tlNode.getEvent();
+            final double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(ie.getSpan().getStartMillis()));
+            //position of start and end according to range of axis
+            double xPos = rawDisplayPosition - xOffset;
+            double layoutNodesResultHeight = 0;
+            if (tlNode.getSubNodePane().getChildren().isEmpty() == false) {
+                FXCollections.sort(tlNode.getSubNodePane().getChildren(), new StartTimeComparator());
+                layoutNodesResultHeight = layoutNodes(tlNode.getSubNodePane().getChildren(), 0, rawDisplayPosition);
+            }
+            double xPos2 = getXAxis().getDisplayPosition(new DateTime(ie.getSpan().getEndMillis())) - xOffset;
+            double span = xPos2 - xPos;
+
+            //size timespan border
+            tlNode.setSpanWidth(span);
+            if (truncateAll.get()) { //if truncate option is selected limit width of description label
+                tlNode.setDescriptionLabelMaxWidth(Math.max(span, truncateWidth.get()));
+            } else { //else set it unbounded
+
+                tlNode.setDescriptionLabelMaxWidth(20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth());
+            }
+            tlNode.autosize(); //compute size of tlNode based on constraints and event data
+
+            //get position of right edge of node ( influenced by description label)
+            double xRight = xPos + tlNode.getWidth();
+
+            //get the height of the node
+            final double h = layoutNodesResultHeight == 0 ? tlNode.getHeight() : layoutNodesResultHeight;
+            //initial test position
+            double yPos = minY;
+
+            double yPos2 = yPos + h;
+
+            if (oneEventPerRow.get()) {
+                // if onePerRow, just put it at end
+                yPos = (localMax + 2);
+                yPos2 = yPos + h;
+
+            } else {//else
+
+                boolean overlapping = true;
+                while (overlapping) {
+                    //loop through y values looking for available slot.
+
+                    overlapping = false;
+                    //check each pixel from bottom to top.
+                    for (double y = yPos2; y >= yPos; y--) {
+                        final Double maxX = maxXatY.get((int) y);
+                        if (maxX != null && maxX >= xPos - 4) {
+                            //if that pixel is already used
+                            //jump top to this y value and repeat until free slot is found.
+                            overlapping = true;
+                            yPos = y + 4;
+                            yPos2 = yPos + h;
+                            break;
+                        }
+                    }
+                }
+                //mark used y values
+                for (double y = yPos; y <= yPos2; y++) {
+                    maxXatY.put((int) y, xRight);
+                }
+            }
+            localMax = Math.max(yPos2, localMax);
+
+            Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0),
+                                                    new KeyValue(tlNode.layoutXProperty(), xPos),
+                                                    new KeyValue(tlNode.layoutYProperty(), yPos)));
+
+            tm.play();
+//            tlNode.relocate(xPos, yPos);
+        }
+        maxY.set(Math.max(maxY.get(), localMax));
+        return localMax - minY;
+    }
+
+    private void layoutProjectionMap() {
+        for (final Map.Entry<AggregateEventNode, Line> entry : projectionMap.entrySet()) {
+            final AggregateEventNode aggNode = entry.getKey();
+            final Line line = entry.getValue();
+
+            line.setStartX(getParentXForValue(new DateTime(aggNode.getEvent().getSpan().getStartMillis(), TimeLineController.getJodaTimeZone())));
+            line.setEndX(getParentXForValue(new DateTime(aggNode.getEvent().getSpan().getEndMillis(), TimeLineController.getJodaTimeZone())));
+            line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
+            line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET);
+        }
+    }
+
+    private double getParentXForValue(DateTime dt) {
+        return getXAxis().localToParent(getXAxis().getDisplayPosition(dt), 0).getX();
+    }
+
+    private static final class DescriptionLODConverter extends StringConverter<Double> {
+
+        @Override
+        public String toString(Double value) {
+            return value == -1 ? "None"
+                   : DescriptionLOD.values()[value.intValue()].getDisplayName();
+        }
+
+        @Override
+        public Double fromString(String string) {
+            //we never convert from string to double (slider position)
+            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+        }
+    }
+
+    private static class StartTimeComparator implements Comparator<Node> {
+
+        @Override
+        public int compare(Node n1, Node n2) {
+
+            if (n1 == null) {
+                return 1;
+            } else if (n2 == null) {
+                return -1;
+            } else {
+
+                return Long.compare(((AggregateEventNode) n1).getEvent().getSpan().getStartMillis(),
+                                    (((AggregateEventNode) n2).getEvent().getSpan().getStartMillis()));
+            }
+        }
+
+    }
+
+    private class DetailIntervalSelector extends IntervalSelector<DateTime> {
+
+        public DetailIntervalSelector(double x, double height, Axis<DateTime> axis, TimeLineController controller) {
+            super(x, height, axis, controller);
+        }
+
+        @Override
+        protected String formatSpan(DateTime date) {
+            return date.toString(TimeLineController.getZonedFormatter());
+        }
+
+        @Override
+        protected Interval adjustInterval(Interval i) {
+            return i;
+        }
+
+        @Override
+        protected DateTime parseDateTime(DateTime date) {
+            return date;
+        }
+
+    }
+
+    /** event handler used for mouse events on {@link AggregateEventNode}s
+     * //TODO: refactor this to put more of the state(slider)in the node */
+    private class EventMouseHandler implements EventHandler<MouseEvent> {
+
+        private final AggregateEventNode aggNode;
+
+        private final AggregateEvent aggEvent;
+
+        private final Slider slider;
+
+        public EventMouseHandler(AggregateEventNode aggNode) {
+            this.aggNode = aggNode;
+            this.aggEvent = aggNode.getEvent();
+
+            //configure slider
+            this.slider = new Slider(-1, 2, -1);
+            slider.setShowTickMarks(true);
+            slider.setShowTickLabels(true);
+            slider.setSnapToTicks(true);
+            slider.setMajorTickUnit(1);
+            slider.setMinorTickCount(0);
+            slider.setBlockIncrement(1);
+            slider.setLabelFormatter(new DescriptionLODConverter());
+
+            //on slider change, reload subnodes
+            InvalidationListener invalidationListener = o -> {
+                if (slider.isValueChanging() == false) {
+                    reloadSubNodes();
+                }
+            };
+            slider.valueProperty().addListener(invalidationListener);
+            slider.valueChangingProperty().addListener(invalidationListener);
+        }
+
+        private void reloadSubNodes() {
+            final int value = Math.round(slider.valueProperty().floatValue());
+            aggNode.getSubNodePane().getChildren().clear();
+            aggNode.setEventDetailsVisible(true);
+            if (value == -1) {
+                aggNode.getSubNodePane().getChildren().clear();
+                aggNode.setEventDetailsVisible(true);
+                synchronized (EventDetailChart.this) {
+                    requiresLayout = true;
+                }
+                requestChartLayout();
+            } else {
+                final DescriptionLOD newLOD = DescriptionLOD.values()[value];
+
+                final Filter combinedFilter = Filter.intersect(new Filter[]{new TextFilter(aggEvent.getDescription()),
+                                                                            new TypeFilter(aggEvent.getType()),
+                                                                            filteredEvents.filter().get()});
+                final Interval span = aggEvent.getSpan().withEndMillis(aggEvent.getSpan().getEndMillis() + 1000);
+                LoggedTask<List<AggregateEventNode>> loggedTask = new LoggedTask<List<AggregateEventNode>>("Load sub events", true) {
+
+                    @Override
+                    protected List<AggregateEventNode> call() throws Exception {
+
+                        List<AggregateEvent> aggregatedEvents = filteredEvents.getAggregatedEvents(new ZoomParams(span,
+                                                                                                                  filteredEvents.eventTypeZoom().get(),
+                                                                                                                  combinedFilter,
+                                                                                                                  newLOD));
+                        return aggregatedEvents.stream().map((AggregateEvent t) -> {
+                            AggregateEventNode subNode = new AggregateEventNode(t, aggNode);
+                            subNode.setOnMouseClicked(new EventMouseHandler(subNode));
+                            subNode.setLayoutX(getXAxis().getDisplayPosition(new DateTime(t.getSpan().getStartMillis())) - aggNode.getLayoutXCompensation());
+                            return subNode;
+                        }).collect(Collectors.toList());
+                    }
+
+                    @Override
+                    protected void succeeded() {
+                        try {
+                            if (get().size() > 1) {
+                                setCursor(Cursor.WAIT);
+                                aggNode.setEventDetailsVisible(false);
+                                aggNode.getSubNodePane().getChildren().setAll(get());
+                                synchronized (EventDetailChart.this) {
+                                    requiresLayout = true;
+                                }
+                                requestChartLayout();
+                                setCursor(null);
+                            }
+                        } catch (InterruptedException | ExecutionException ex) {
+                            Exceptions.printStackTrace(ex);
+                        }
+                    }
+                };
+                controller.monitorTask(loggedTask);
+            }
+        }
+
+        @Override
+        public void handle(MouseEvent t) {
+            t.consume();
+            if (t.getButton() == MouseButton.PRIMARY) {
+                if (t.isShiftDown()) {
+                    if (selectedNodes.contains(aggNode) == false) {
+                        selectedNodes.add(aggNode);
+                    }
+                } else if (t.isShortcutDown()) {
+                    selectedNodes.removeAll(aggNode);
+                } else if (t.getClickCount() > 1) {
+                    slider.increment();
+                } else {
+                    selectedNodes.setAll(aggNode);
+                }
+            } else if (t.getButton() == MouseButton.SECONDARY) {
+
+                if (chartContextMenu != null) {
+                    chartContextMenu.hide();
+                }
+                //we use a per node menu to remember the slider position
+                ContextMenu nodeContextMenu = aggNode.getContextMenu();
+                if (nodeContextMenu == null) {
+                    nodeContextMenu = builContextMenu();
+                    aggNode.setContextMenu(nodeContextMenu);
+                }
+                nodeContextMenu.show(aggNode, t.getScreenX(), t.getScreenY());
+            }
+        }
+
+        private ContextMenu builContextMenu() {
+            //should we include a label to remind uer of what group this is for
+            //final MenuItem headingItem = new CustomMenuItem(new Label(aggEvent.getDescription()), false);
+            //headingItem.getStyleClass().remove("menu-item");
+            final Label sliderLabel = new Label("Nested Detail:", slider);
+            sliderLabel.setContentDisplay(ContentDisplay.RIGHT);
+            final MenuItem detailSliderItem = new CustomMenuItem(sliderLabel, false);
+            detailSliderItem.getStyleClass().remove("menu-item");
+            ContextMenu contextMenu = new ContextMenu(detailSliderItem);
+            //we don't reuse item from chartContextMenu because 'place marker' is location specific.
+            //TODO: refactor this so we can reuse chartContextMenu items
+            contextMenu.getItems().addAll(ActionUtils.createContextMenu(
+                    Arrays.asList(new ActionGroup("Zoom History", new Back(controller),
+                                                  new Forward(controller)))).getItems());
+            //TODO: add tagging actions here
+            contextMenu.setAutoHide(true);
+            return contextMenu;
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java
new file mode 100644
index 0000000000000000000000000000000000000000..febf055aada0cd8468682df3207396a5807d1a9c
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java
@@ -0,0 +1,82 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.detailview;
+
+import javafx.scene.Cursor;
+import javafx.scene.control.Tooltip;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Line;
+import org.joda.time.DateTime;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+
+/**
+ *
+ */
+class GuideLine extends Line {
+
+    private final DateAxis dateAxis;
+
+    private double startLayoutX;
+
+    protected Tooltip tooltip;
+
+    private double dragStartX = 0;
+
+    GuideLine(double startX, double startY, double endX, double endY, DateAxis axis) {
+        super(startX, startY, endX, endY);
+        dateAxis = axis;
+        setCursor(Cursor.E_RESIZE);
+        getStrokeDashArray().setAll(5.0, 5.0);
+        setStroke(Color.RED);
+        setOpacity(.5);
+        setStrokeWidth(3);
+
+        setOnMouseEntered((MouseEvent event) -> {
+            setTooltip();
+        });
+
+        setOnMousePressed((MouseEvent event) -> {
+            startLayoutX = getLayoutX();
+            dragStartX = event.getScreenX();
+        });
+        setOnMouseDragged((MouseEvent event) -> {
+            double dX = event.getScreenX() - dragStartX;
+
+            relocate(startLayoutX + dX, 0);
+        });
+    }
+
+    private void setTooltip() {
+        Tooltip.uninstall(this, tooltip);
+        tooltip = new Tooltip(formatSpan(getDateTime())
+                + "\nRight-click to remove."
+                + "\nRight-drag to reposition.");
+        Tooltip.install(this, tooltip);
+    }
+
+    private String formatSpan(DateTime date) {
+        return date.toString(TimeLineController.getZonedFormatter());
+    }
+
+    private DateTime getDateTime() {
+        return dateAxis.getValueForDisplay(dateAxis.parentToLocal(getLayoutX(), 0).getX());
+    }
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..7a662c28cd3611cebf29172f66a177f50d591ee5
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java
@@ -0,0 +1,61 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
+
+import java.util.Comparator;
+import javafx.scene.control.TreeItem;
+import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
+
+/**
+ *
+ */
+class EventDescriptionTreeItem extends NavTreeItem {
+
+    public EventDescriptionTreeItem(AggregateEvent g) {
+        setValue(new NavTreeNode(g.getType().getBaseType(), g.getDescription(), g.getEventIDs().size()));
+    }
+
+    @Override
+    public int getCount() {
+        return getValue().getCount();
+    }
+
+    @Override
+    public void insert(AggregateEvent g) {
+        NavTreeNode value = getValue();
+        if ((value.getType().getBaseType().equals(g.getType().getBaseType()) == false) || ((value.getDescription().equals(g.getDescription()) == false))) {
+            throw new IllegalArgumentException();
+        }
+
+        setValue(new NavTreeNode(value.getType().getBaseType(), value.getDescription(), value.getCount() + g.getEventIDs().size()));
+    }
+
+    @Override
+    public void resort(Comparator<TreeItem<NavTreeNode>> comp) {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+
+    @Override
+    public TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t) {
+        if (getValue().getType().getBaseType() == t.getType().getBaseType() && getValue().getDescription().equals(t.getDescription())) {
+            return this;
+        }
+        return null;
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..ba2367e3ea23cba319d13b710bad8ff901b38867
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java
@@ -0,0 +1,94 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
+
+import java.util.Comparator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.scene.control.TreeItem;
+import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
+
+class EventTypeTreeItem extends NavTreeItem {
+
+    /** maps a description to the child item of this item with that description */
+    private final Map<String, EventDescriptionTreeItem> childMap = new ConcurrentHashMap<>();
+
+    private final Comparator<TreeItem<NavTreeNode>> comparator = TreeComparator.Description;
+
+    EventTypeTreeItem(AggregateEvent g) {
+        setValue(new NavTreeNode(g.getType().getBaseType(), g.getType().getBaseType().getDisplayName(), 0));
+    }
+
+    @Override
+    public int getCount() {
+        return getValue().getCount();
+    }
+
+    /** Recursive method to add a grouping at a given path.
+     *
+     * @param path Full path (or subset not yet added) to add
+     * @param g    Group to add
+     * @param tree True if it is part of a tree (versus a list)
+     */
+    @Override
+    public void insert(AggregateEvent g) {
+
+        EventDescriptionTreeItem treeItem = childMap.get(g.getDescription());
+        if (treeItem == null) {
+            final EventDescriptionTreeItem newTreeItem = new EventDescriptionTreeItem(g);
+            newTreeItem.setExpanded(true);
+            childMap.put(g.getDescription(), newTreeItem);
+
+            Platform.runLater(() -> {
+                synchronized (getChildren()) {
+                    getChildren().add(newTreeItem);
+                    FXCollections.sort(getChildren(), comparator);
+                }
+            });
+        } else {
+            treeItem.insert(g);
+        }
+        Platform.runLater(() -> {
+            NavTreeNode value1 = getValue();
+            setValue(new NavTreeNode(value1.getType().getBaseType(), value1.getType().getBaseType().getDisplayName(), childMap.values().stream().mapToInt(EventDescriptionTreeItem::getCount).sum()));
+        });
+
+    }
+
+    @Override
+    public TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t) {
+        if (t.getType().getBaseType() == getValue().getType().getBaseType()) {
+
+            for (TreeItem<NavTreeNode> child : getChildren()) {
+                final TreeItem<NavTreeNode> findTreeItemForEvent = ((NavTreeItem) child).findTreeItemForEvent(t);
+                if (findTreeItemForEvent != null) {
+                    return findTreeItemForEvent;
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void resort(Comparator<TreeItem<NavTreeNode>> comp) {
+        FXCollections.sort(getChildren(), comp);
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..a1457451eba5613a21a1fa43655614092ac94181
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.fxml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.collections.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+
+
+                <fx:root prefHeight="-1.0" prefWidth="-1.0" type="BorderPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+                  <center>
+                    <TreeView fx:id="eventsTree" prefHeight="-1.0" prefWidth="-1.0" />
+                  </center>
+                  <top>
+                    <ToolBar>
+                      <items>
+                        <Label text="Sort By:" />
+                        <ComboBox fx:id="sortByBox">
+                          <items>
+                            <FXCollections fx:factory="observableArrayList">
+                              <String fx:value="Item 1" />
+                              <String fx:value="Item 2" />
+                              <String fx:value="Item 3" />
+                            </FXCollections>
+                          </items>
+                        </ComboBox>
+                      </items>
+                    </ToolBar>
+                  </top>
+                </fx:root>
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java
new file mode 100644
index 0000000000000000000000000000000000000000..dd368a5257318bba27804eb3adaf4d292d141f91
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java
@@ -0,0 +1,161 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
+
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.ResourceBundle;
+import javafx.application.Platform;
+import javafx.beans.Observable;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.SelectionMode;
+import javafx.scene.control.Tooltip;
+import javafx.scene.control.TreeCell;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.StackPane;
+import javafx.scene.shape.Rectangle;
+import org.sleuthkit.autopsy.timeline.FXMLConstructor;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.TimeLineView;
+import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+import org.sleuthkit.autopsy.timeline.ui.detailview.AggregateEventNode;
+import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane;
+
+/**
+ * Display two trees. one shows all folders (groups) and calls out folders with
+ * images. the user can select folders with images to see them in the main
+ * GroupListPane The other shows folders with hash set hits.
+ */
+public class NavPanel extends BorderPane implements TimeLineView {
+
+    private TimeLineController controller;
+
+    private FilteredEventsModel filteredEvents;
+
+    @FXML
+    private ResourceBundle resources;
+
+    @FXML
+    private URL location;
+
+    private DetailViewPane detailViewPane;
+
+    /**
+     * TreeView for folders with hash hits
+     */
+    @FXML
+    private TreeView< NavTreeNode> eventsTree;
+
+    @FXML
+    private ComboBox<Comparator<TreeItem<NavTreeNode>>> sortByBox;
+
+    public NavPanel() {
+
+        FXMLConstructor.construct(this, "NavPanel.fxml");
+    }
+
+    public void setChart(DetailViewPane detailViewPane) {
+        this.detailViewPane = detailViewPane;
+        detailViewPane.setSelectionModel(eventsTree.getSelectionModel());
+        setRoot();
+        detailViewPane.getAggregatedEvents().addListener((Observable observable) -> {
+            setRoot();
+        });
+        detailViewPane.getSelectedNodes().addListener((Observable observable) -> {
+            eventsTree.getSelectionModel().clearSelection();
+            detailViewPane.getSelectedNodes().forEach((AggregateEventNode t) -> {
+                eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getEvent()));
+            });
+        });
+
+    }
+
+    private void setRoot() {
+        RootItem root = new RootItem();
+        final ObservableList<AggregateEvent> aggregatedEvents = detailViewPane.getAggregatedEvents();
+
+        synchronized (aggregatedEvents) {
+            for (AggregateEvent agg : aggregatedEvents) {
+                root.insert(agg);
+            }
+        }
+        Platform.runLater(() -> {
+            eventsTree.setRoot(root);
+        });
+    }
+
+    @Override
+    public void setController(TimeLineController controller) {
+        this.controller = controller;
+        setModel(controller.getEventsModel());
+    }
+
+    @Override
+    public void setModel(FilteredEventsModel filteredEvents) {
+        this.filteredEvents = filteredEvents;
+
+    }
+
+    @FXML
+    void initialize() {
+        assert sortByBox != null : "fx:id=\"sortByBox\" was not injected: check your FXML file 'NavPanel.fxml'.";
+
+        sortByBox.getItems().setAll(Arrays.asList(TreeComparator.Description, TreeComparator.Count));
+        sortByBox.getSelectionModel().select(TreeComparator.Description);
+        sortByBox.getSelectionModel().selectedItemProperty().addListener((Observable o) -> {
+            ((RootItem) eventsTree.getRoot()).resort(sortByBox.getSelectionModel().getSelectedItem());
+        });
+        eventsTree.setShowRoot(false);
+        eventsTree.setCellFactory((TreeView<NavTreeNode> p) -> new EventTreeCell());
+        eventsTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
+    }
+
+    /** A tree cell to display {@link NavTreeNode}s. Shows the description, and
+     * count, as well a a "legend icon" for the event type. */
+    private static class EventTreeCell extends TreeCell<NavTreeNode> {
+
+        @Override
+        protected void updateItem(NavTreeNode item, boolean empty) {
+            super.updateItem(item, empty);
+            if (item != null) {
+                final String text = item.getDescription() + " (" + item.getCount() + ")";
+                setText(text);
+                setTooltip(new Tooltip(text));
+                Rectangle rect = new Rectangle(24, 24);
+                rect.setArcHeight(5);
+                rect.setArcWidth(5);
+                rect.setStrokeWidth(2);
+                rect.setStroke(item.getType().getColor());
+                rect.setFill(item.getType().getColor().deriveColor(0, 1, 1, 0.1));
+                setGraphic(new StackPane(rect, new ImageView(item.getType().getFXImage())));
+            } else {
+                setText(null);
+                setTooltip(null);
+                setGraphic(null);
+            }
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..c880c29b36799a796bc5a6d43ad0adc95cdf0e73
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java
@@ -0,0 +1,39 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
+
+import java.util.Comparator;
+import javafx.scene.control.TreeItem;
+import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
+
+/** A node in the nav tree. Manages inserts and resorts. Has parents
+ * and children. Does not have graphical properties these are configured in
+ * {@link EventTreeCell}. Each GroupTreeItem has a NavTreeNode which has a type,
+ * description , and count */
+abstract class NavTreeItem extends TreeItem<NavTreeNode> {
+
+    abstract void insert(AggregateEvent g);
+
+    abstract int getCount();
+
+    abstract void resort(Comparator<TreeItem<NavTreeNode>> comp);
+
+    abstract TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t);
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeNode.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..46f7689654c8d468878efc50a32bec3b047ea8c7
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeNode.java
@@ -0,0 +1,53 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
+
+import javax.annotation.concurrent.Immutable;
+import org.sleuthkit.autopsy.timeline.events.type.EventType;
+
+/** The data item for the nav tree. Represents a combination of type and
+ * description, as well as the corresponding number of events */
+@Immutable
+public class NavTreeNode {
+
+    final private EventType type;
+
+    final private String Description;
+
+    final private int count;
+
+    public EventType getType() {
+        return type;
+    }
+
+    public String getDescription() {
+        return Description;
+    }
+
+    public int getCount() {
+        return count;
+    }
+
+    public NavTreeNode(EventType type, String Description, int count) {
+        this.type = type;
+        this.Description = Description;
+        this.count = count;
+    }
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..f15f66f31b1c25e03ddcf963e9516526726b933e
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java
@@ -0,0 +1,92 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.scene.control.TreeItem;
+import org.sleuthkit.autopsy.timeline.events.type.EventType;
+import org.sleuthkit.autopsy.timeline.events.AggregateEvent;
+
+/**
+ *
+ */
+class RootItem extends NavTreeItem {
+
+    /** maps a description to the child item of this item with that description */
+    private final Map<EventType, EventTypeTreeItem> childMap = new HashMap<>();
+
+    /** the comparator if any used to sort the children of this item */
+//    private TreeNodeComparators comp;
+    RootItem() {
+
+    }
+
+    @Override
+    public int getCount() {
+        return getValue().getCount();
+    }
+
+    /** Recursive method to add a grouping at a given path.
+     *
+     * @param g Group to add
+     */
+    @Override
+    public void insert(AggregateEvent g) {
+
+        EventTypeTreeItem treeItem = childMap.get(g.getType().getBaseType());
+        if (treeItem == null) {
+            final EventTypeTreeItem newTreeItem = new EventTypeTreeItem(g);
+            newTreeItem.setExpanded(true);
+            childMap.put(g.getType().getBaseType(), newTreeItem);
+            newTreeItem.insert(g);
+
+            Platform.runLater(() -> {
+                synchronized (getChildren()) {
+                    getChildren().add(newTreeItem);
+
+                    FXCollections.sort(getChildren(), TreeComparator.Type);
+                }
+            });
+        } else {
+            treeItem.insert(g);
+        }
+    }
+
+    @Override
+    public void resort(Comparator<TreeItem<NavTreeNode>> comp) {
+        childMap.values().forEach((ti) -> {
+            ti.resort(comp);
+        });
+    }
+
+    @Override
+    public TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t) {
+        for (TreeItem<NavTreeNode> child : getChildren()) {
+            final TreeItem<NavTreeNode> findTreeItemForEvent = ((NavTreeItem) child).findTreeItemForEvent(t);
+            if (findTreeItemForEvent != null) {
+                return findTreeItemForEvent;
+            }
+        }
+        return null;
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java
new file mode 100644
index 0000000000000000000000000000000000000000..5da9a49d4251acd52a7938f369bb7c1f3c6048bd
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java
@@ -0,0 +1,47 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.detailview.tree;
+
+import java.util.Comparator;
+import javafx.scene.control.TreeItem;
+import org.sleuthkit.autopsy.timeline.events.type.EventType;
+
+enum TreeComparator implements Comparator<TreeItem<NavTreeNode>> {
+
+    Description {
+                @Override
+                public int compare(TreeItem<NavTreeNode> o1, TreeItem<NavTreeNode> o2) {
+                    return o1.getValue().getDescription().compareTo(o2.getValue().getDescription());
+                }
+            },
+    Count {
+                @Override
+                public int compare(TreeItem<NavTreeNode> o1, TreeItem<NavTreeNode> o2) {
+
+                    return -Integer.compare(o1.getValue().getCount(), o2.getValue().getCount());
+                }
+            },
+    Type {
+                @Override
+                public int compare(TreeItem<NavTreeNode> o1, TreeItem<NavTreeNode> o2) {
+                    return EventType.getComparator().compare(o1.getValue().getType(), o2.getValue().getType());
+                }
+            };
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCell.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCell.java
new file mode 100644
index 0000000000000000000000000000000000000000..026ca01b89d472fdd67bca2649cd164f24441ad3
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCell.java
@@ -0,0 +1,53 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.filtering;
+
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.TreeTableCell;
+import org.sleuthkit.autopsy.timeline.filters.AbstractFilter;
+
+/**
+ *
+ */
+class FilterCheckBoxCell extends TreeTableCell<AbstractFilter, AbstractFilter> {
+    
+    private CheckBox checkBox;
+    
+    public FilterCheckBoxCell() {
+    }
+    
+    @Override
+    protected void updateItem(AbstractFilter item, boolean empty) {
+        super.updateItem(item, empty);
+        
+        if (item == null) {
+            setText(null);
+            setGraphic(null);
+            checkBox = null;
+        } else {
+            setText(item.getDisplayName());
+            
+            checkBox = new CheckBox();
+            checkBox.selectedProperty().bindBidirectional(item.getActiveProperty());
+            checkBox.disableProperty().bind(item.getDisabledProperty());
+            setGraphic(checkBox);
+        }
+    }
+    
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..e3e1c13947802436319bf5819517d6217914cf09
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.fxml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+
+<fx:root type="BorderPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+<top><ToolBar prefWidth="200.0" BorderPane.alignment="CENTER">
+<items>
+        <Button fx:id="applyButton" mnemonicParsing="false" text="Apply">
+<HBox.margin>
+<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+</HBox.margin>
+<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+<image>
+<Image url="@../../images/tick.png" />
+</image></ImageView>
+</graphic></Button><Button fx:id="defaultButton" mnemonicParsing="false" text="Default">
+<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+<image>
+<Image url="@../../images/arrow-circle-double-135.png" />
+</image></ImageView>
+</graphic></Button>
+</items>
+</ToolBar>
+</top>
+<center><TreeTableView fx:id="filterTreeTable" editable="true" minWidth="-Infinity" showRoot="false" BorderPane.alignment="CENTER">
+  <columns>
+    <TreeTableColumn fx:id="treeColumn" minWidth="100.0" prefWidth="200.0" sortable="false" text="C1" />
+    <TreeTableColumn fx:id="legendColumn" editable="false" minWidth="50.0" prefWidth="50.0" sortable="false" text="C2" />
+  </columns>
+<columnResizePolicy>
+<TreeTableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
+</columnResizePolicy>
+</TreeTableView>
+</center></fx:root>
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java
new file mode 100644
index 0000000000000000000000000000000000000000..89f379f80f53969716b9b0bfe7fb7e36e825856e
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java
@@ -0,0 +1,172 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.filtering;
+
+import java.net.URL;
+import java.util.ResourceBundle;
+import javafx.beans.Observable;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeTableColumn;
+import javafx.scene.control.TreeTableRow;
+import javafx.scene.control.TreeTableView;
+import javafx.scene.layout.BorderPane;
+import org.controlsfx.control.action.AbstractAction;
+import org.sleuthkit.autopsy.timeline.FXMLConstructor;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.TimeLineView;
+import org.sleuthkit.autopsy.timeline.actions.DefaultFilters;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+import org.sleuthkit.autopsy.timeline.filters.AbstractFilter;
+import org.sleuthkit.autopsy.timeline.filters.Filter;
+
+/** The FXML controller for the filter ui.
+ *
+ * This also implements {@link TimeLineView} since it dynamically updates its
+ * filters based on the contents of a {@link FilteredEventsModel}
+ */
+public class FilterSetPanel extends BorderPane implements TimeLineView {
+
+    @FXML
+    private ResourceBundle resources;
+
+    @FXML
+    private URL location;
+
+    @FXML
+    private Button applyButton;
+
+    @FXML
+    private Button defaultButton;
+
+    @FXML
+    private TreeTableView<Filter> filterTreeTable;
+
+    @FXML
+    private TreeTableColumn<AbstractFilter, AbstractFilter> treeColumn;
+
+    @FXML
+    private TreeTableColumn<AbstractFilter, AbstractFilter> legendColumn;
+
+    private FilteredEventsModel filteredEvents;
+
+    private TimeLineController controller;
+
+    @FXML
+    void initialize() {
+        assert applyButton != null : "fx:id=\"applyButton\" was not injected: check your FXML file 'FilterSetPanel.fxml'.";
+
+        applyButton.setOnAction(e -> {
+            controller.pushFilters(filterTreeTable.getRoot().getValue().copyOf());
+        });
+
+        //remove column headers via css.
+        filterTreeTable.getStylesheets().addAll(getClass().getResource("FilterTable.css").toExternalForm());
+
+        //use row factory as hook to attach context menus to.
+        filterTreeTable.setRowFactory((TreeTableView<Filter> param) -> {
+            final TreeTableRow<Filter> row = new TreeTableRow<>();
+
+            MenuItem all = new MenuItem("all");
+            all.setOnAction(e -> {
+                row.getTreeItem().getParent().getChildren().forEach((TreeItem<Filter> t) -> {
+                    t.getValue().setActive(Boolean.TRUE);
+                });
+            });
+            MenuItem none = new MenuItem("none");
+            none.setOnAction(e -> {
+                row.getTreeItem().getParent().getChildren().forEach((TreeItem<Filter> t) -> {
+                    t.getValue().setActive(Boolean.FALSE);
+                });
+            });
+
+            MenuItem only = new MenuItem("only");
+            only.setOnAction(e -> {
+                row.getTreeItem().getParent().getChildren().forEach((TreeItem<Filter> t) -> {
+                    if (t == row.getTreeItem()) {
+                        t.getValue().setActive(Boolean.TRUE);
+                    } else {
+                        t.getValue().setActive(Boolean.FALSE);
+                    }
+                });
+            });
+            MenuItem others = new MenuItem("others");
+            others.setOnAction(e -> {
+                row.getTreeItem().getParent().getChildren().forEach((TreeItem<Filter> t) -> {
+                    if (t == row.getTreeItem()) {
+                        t.getValue().setActive(Boolean.FALSE);
+                    } else {
+                        t.getValue().setActive(Boolean.TRUE);
+                    }
+                });
+            });
+            final ContextMenu rowMenu = new ContextMenu();
+            Menu select = new Menu("select");
+            select.setOnAction(e -> {
+                row.getItem().setActive(!row.getItem().isActive());
+            });
+            select.getItems().addAll(all, none, only, others);
+            rowMenu.getItems().addAll(select);
+            row.setContextMenu(rowMenu);
+
+            return row;
+        });
+
+        //configure tree column to show name of filter and checkbox
+        treeColumn.setCellValueFactory(param -> param.getValue().valueProperty());
+        treeColumn.setCellFactory(col -> new FilterCheckBoxCell());
+
+        //configure legend column to show legend (or othe supplamantal ui, eg, text field for text filter)
+        legendColumn.setCellValueFactory(param -> param.getValue().valueProperty());
+        legendColumn.setCellFactory(col -> new LegendCell(this.controller));
+    }
+
+    public FilterSetPanel() {
+        FXMLConstructor.construct(this, "FilterSetPanel.fxml");
+    }
+
+    @Override
+    public void setController(TimeLineController timeLineController) {
+        this.controller = timeLineController;
+        AbstractAction defaultFiltersAction = new DefaultFilters(controller);
+        defaultButton.setOnAction(defaultFiltersAction);
+        defaultButton.disableProperty().bind(defaultFiltersAction.disabledProperty());
+        this.setModel(timeLineController.getEventsModel());
+    }
+
+    @Override
+    public void setModel(FilteredEventsModel filteredEvents) {
+        this.filteredEvents = filteredEvents;
+
+        refresh();
+
+        this.filteredEvents.filter().addListener((Observable o) -> {
+            refresh();
+        });
+
+    }
+
+    public void refresh() {
+        filterTreeTable.setRoot(new FilterTreeItem(this.filteredEvents.filter().get().copyOf()));
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTable.css b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTable.css
new file mode 100644
index 0000000000000000000000000000000000000000..fa3c5a76dbc43e0c298330ae1c379de8e9d31cef
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTable.css
@@ -0,0 +1,3 @@
+
+
+.column-header-background { visibility: hidden; -fx-padding: -1em; }
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..2302099939f36fc1ae3a4ae17f4e82172cc7e5a1
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java
@@ -0,0 +1,31 @@
+package org.sleuthkit.autopsy.timeline.ui.filtering;
+
+import javafx.scene.control.TreeItem;
+import org.sleuthkit.autopsy.timeline.filters.CompoundFilter;
+import org.sleuthkit.autopsy.timeline.filters.Filter;
+
+/** A TreeItem for a filter. */
+public class FilterTreeItem extends TreeItem<Filter> {
+
+    /**
+     * recursively construct a tree of treeitems to parallel the filter tree of
+     * the given filter
+     *
+     *
+     * @param f the filter for this item. if f has sub-filters, tree items will
+     *          be made for them added added to the children of this
+     *          FilterTreeItem
+     */
+    public FilterTreeItem(Filter f) {
+        super(f);
+        setExpanded(true);
+
+        if (f instanceof CompoundFilter) {
+            CompoundFilter cf = (CompoundFilter) f;
+
+            for (Filter af : cf.getSubFilters()) {
+                getChildren().add(new FilterTreeItem(af));
+            }
+        }
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/LegendCell.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/LegendCell.java
new file mode 100644
index 0000000000000000000000000000000000000000..23e6f25055f66fec680444a5a912b9472f229bf0
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/LegendCell.java
@@ -0,0 +1,135 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.timeline.ui.filtering;
+
+import javafx.application.Platform;
+import javafx.beans.value.ObservableValue;
+import javafx.geometry.Pos;
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TreeTableCell;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.HBox;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.TimeLineView;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+import org.sleuthkit.autopsy.timeline.filters.AbstractFilter;
+import org.sleuthkit.autopsy.timeline.filters.TextFilter;
+import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
+import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
+
+/**
+ * A TreeTableCell that shows an icon and color corresponding to the
+ * represented filter
+ */
+class LegendCell extends TreeTableCell<AbstractFilter, AbstractFilter> implements TimeLineView {
+
+    private static final Color CLEAR = Color.rgb(0, 0, 0, 0);
+
+    private TimeLineController controller;
+
+    private FilteredEventsModel filteredEvents;
+
+    //We need a controller so we can listen to changes in EventTypeZoom to show/hide legends
+    public LegendCell(TimeLineController controller) {
+        setEditable(false);
+        setController(controller);
+    }
+
+    @Override
+    public void updateItem(AbstractFilter item, boolean empty) {
+        super.updateItem(item, empty);
+        if (item == null) {
+            Platform.runLater(() -> {
+                setGraphic(null);
+                setBackground(null);
+            });
+        } else {
+
+            //TODO: have the filter return an appropriate node, rather than use instanceof
+            if (item instanceof TypeFilter) {
+                TypeFilter filter = (TypeFilter) item;
+                Rectangle rect = new Rectangle(20, 20);
+
+                rect.setArcHeight(5);
+                rect.setArcWidth(5);
+                rect.setStrokeWidth(3);
+                setLegendColor(filter, rect, this.filteredEvents.getEventTypeZoom());
+                this.filteredEvents.eventTypeZoom().addListener((
+                        ObservableValue<? extends EventTypeZoomLevel> observable,
+                        EventTypeZoomLevel oldValue,
+                        EventTypeZoomLevel newValue) -> {
+                            setLegendColor(filter, rect, newValue);
+                        });
+
+                HBox hBox = new HBox(new Rectangle(filter.getEventType().getZoomLevel().ordinal() * 10, 5, CLEAR),
+                                     new ImageView(((TypeFilter) item).getFXImage()), rect
+                );
+                hBox.setAlignment(Pos.CENTER);
+                Platform.runLater(() -> {
+                    setGraphic(hBox);
+                    setContentDisplay(ContentDisplay.CENTER);
+                });
+
+            } else if (item instanceof TextFilter) {
+                TextFilter f = (TextFilter) item;
+                TextField textField = new TextField();
+                textField.setPromptText("enter filter string");
+                textField.textProperty().bindBidirectional(f.textProperty());
+                Platform.runLater(() -> {
+                    setGraphic(textField);
+                });
+
+            } else {
+                Platform.runLater(() -> {
+                    setGraphic(null);
+                    setBackground(null);
+                });
+            }
+        }
+    }
+
+    private void setLegendColor(TypeFilter filter, Rectangle rect, EventTypeZoomLevel eventTypeZoom) {
+        //only show legend color if filter is of the same zoomlevel as requested in filteredEvents
+        if (eventTypeZoom.equals(filter.getEventType().getZoomLevel())) {
+            Platform.runLater(() -> {
+                rect.setStroke(filter.getEventType().getSuperType().getColor());
+                rect.setFill(filter.getColor());
+            });
+        } else {
+            Platform.runLater(() -> {
+                rect.setStroke(CLEAR);
+                rect.setFill(CLEAR);
+            });
+        }
+    }
+
+    @Override
+    synchronized public final void setController(TimeLineController controller) {
+        this.controller = controller;
+        setModel(this.controller.getEventsModel());
+    }
+
+    @Override
+    public void setModel(FilteredEventsModel filteredEvents) {
+        this.filteredEvents = filteredEvents;
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/utils/ColorUtilities.java b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/ColorUtilities.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a7e614f7d35ef9ca960815f91b3cc7c7f0c7085
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/ColorUtilities.java
@@ -0,0 +1,36 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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.utils;
+
+import javafx.scene.paint.Color;
+
+/**
+ *
+ */
+public class ColorUtilities {
+
+   
+    public static String getRGBCode(Color color) {
+        return String.format("#%02X%02X%02X%02X",
+                             (int) (color.getRed() * 255),
+                             (int) (color.getGreen() * 255),
+                             (int) (color.getBlue() * 255),
+                             (int) (color.getOpacity() * 255));
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/utils/IntervalUtils.java b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/IntervalUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..ab03330b2c7f57eee4af9750d9008d10d84085df
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/IntervalUtils.java
@@ -0,0 +1,83 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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.utils;
+
+import java.util.Collection;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Interval;
+import org.joda.time.ReadablePeriod;
+import org.sleuthkit.autopsy.timeline.zooming.TimeUnits;
+
+/**
+ *
+ */
+public class IntervalUtils {
+
+    static public Interval getSpanningInterval(Collection<DateTime> times) {
+        Interval trange = null;
+        for (DateTime t : times) {
+            if (trange == null) {
+                trange = new Interval(t.getMillis(), t.getMillis() + 1000,DateTimeZone.UTC);
+            } else {
+                trange = extendInterval(trange, t.getMillis());
+            }
+        }
+        return trange;
+    }
+
+    static public Interval span(Interval range, final Interval range2) {
+        return new Interval(Math.min(range.getStartMillis(), range2.getStartMillis()), Math.max(range.getEndMillis(), range2.getEndMillis()),DateTimeZone.UTC);
+    }
+
+    static public Interval extendInterval(Interval range, final Long eventTime) {
+        return new Interval(Math.min(range.getStartMillis(), eventTime), Math.max(range.getEndMillis(), eventTime + 1),DateTimeZone.UTC);
+    }
+
+    /**
+     * @param timeRange
+     *
+     * @return
+     *
+     * @deprecated Moved to
+     * {@link org.sleuthkit.autopsy.timeline.visualization.RangeDivisionInfo#getRangeDivisionInfo}
+     */
+    @Deprecated
+    public static RangeDivisionInfo getRangeDivisionInfo(Interval timeRange) {
+        return RangeDivisionInfo.getRangeDivisionInfo(timeRange);
+    }
+
+    public static DateTime middleOf(Interval interval) {
+        return new DateTime((interval.getStartMillis() + interval.getEndMillis()) / 2);
+    }
+
+    public static Interval getAdjustedInterval(Interval oldInterval, TimeUnits requestedUnit) {
+        return getIntervalAround(middleOf(oldInterval), requestedUnit.getPeriod());
+    }
+
+    static public Interval getIntervalAround(DateTime aroundInstant, ReadablePeriod period) {
+        DateTime start = aroundInstant.minus(period);
+        DateTime end = aroundInstant.plus(period);
+        Interval range = new Interval(start, end);
+        DateTime middleOf = IntervalUtils.middleOf(range);
+        long halfRange = range.toDurationMillis() / 4;
+        final Interval newInterval = new Interval(middleOf.minus(halfRange), middleOf.plus(halfRange));
+        return newInterval;
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/utils/ObservableStack.java b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/ObservableStack.java
new file mode 100644
index 0000000000000000000000000000000000000000..142434d5ac428ade4eb11954c8173501a0d528c5
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/ObservableStack.java
@@ -0,0 +1,59 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.utils;
+
+import javafx.beans.property.SimpleListProperty;
+import javafx.collections.FXCollections;
+
+/**
+ *
+ */
+public class ObservableStack<T> extends SimpleListProperty<T> {
+
+    public ObservableStack() {
+        super(FXCollections.synchronizedObservableList(FXCollections.observableArrayList()));
+    }
+
+    public void push(T item) {
+        synchronized (this) {
+            add(0, item);
+        }
+    }
+
+    public T pop() {
+        synchronized (this) {
+            if (isEmpty()) {
+                return null;
+            } else {
+                return remove(0);
+            }
+        }
+    }
+
+    public T peek() {
+        synchronized (this) {
+            if (isEmpty()) {
+                return null;
+            } else {
+                return get(0);
+            }
+        }
+    }
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/utils/RangeDivisionInfo.java b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/RangeDivisionInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..5be09e376dc1b9dafa1f059ff1459bb8f61002ce
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/RangeDivisionInfo.java
@@ -0,0 +1,151 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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.utils;
+
+import javax.annotation.concurrent.Immutable;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeFieldType;
+import org.joda.time.Days;
+import org.joda.time.Hours;
+import org.joda.time.Interval;
+import org.joda.time.Minutes;
+import org.joda.time.Months;
+import org.joda.time.Seconds;
+import org.joda.time.Years;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.zooming.TimeUnits;
+
+/** * bundles up the results of analyzing a time range for the appropriate
+ * {@link TimeUnits} to use to visualize it. Partly, this class exists
+ * so I don't have to have more member variables in other places , and partly
+ * because I can only return a single value from a function. This might only be
+ * a temporary design but is working well for now. */
+@Immutable
+public class RangeDivisionInfo {
+
+    /** the size of the periods we should divide the interval into */
+    private final TimeUnits blockSize;
+
+    /** The number of Blocks we are going to divide the interval into. */
+    private final int numberOfBlocks;
+
+    /** a {@link DateTimeFormatter} corresponding to the block size for the
+     * tick marks on the date axis of the graph */
+    private final DateTimeFormatter tickFormatter;
+
+    /** an adjusted lower bound for the range such that is lines up with a block
+     * boundary before or at the start of the timerange */
+    private final long lowerBound;
+
+    /** an adjusted upper bound for the range such that is lines up with a block
+     * boundary at or after the end of the timerange */
+    private final long upperBound;
+
+    /** the time range this {@link RangeDivisionInfo} describes */
+    private final Interval timeRange;
+
+    public Interval getTimeRange() {
+        return timeRange;
+    }
+
+    private RangeDivisionInfo(Interval timeRange, int periodsInRange, TimeUnits periodSize, DateTimeFormatter tickformatter, long lowerBound, long upperBound) {
+        super();
+        this.numberOfBlocks = periodsInRange;
+        this.blockSize = periodSize;
+        this.tickFormatter = tickformatter;
+
+        this.lowerBound = lowerBound;
+        this.upperBound = upperBound;
+        this.timeRange = timeRange;
+    }
+
+    /**
+     * Static factory method.
+     *
+     * Determine the period size, number of periods, whole period bounds, and
+     * formatters to use to visualize the given timerange.
+     *
+     * @param timeRange
+     *
+     * @return
+     */
+    public static RangeDivisionInfo getRangeDivisionInfo(Interval timeRange) {
+        //Check from largest to smallest unit
+
+        //TODO: make this more generic... reduce code duplication -jm
+        DateTimeFieldType timeUnit;
+        final DateTime startWithZone = timeRange.getStart().withZone(TimeLineController.getJodaTimeZone());
+        final DateTime endWithZone = timeRange.getEnd().withZone(TimeLineController.getJodaTimeZone());
+
+        if (Years.yearsIn(timeRange).isGreaterThan(Years.THREE)) {
+            timeUnit = DateTimeFieldType.year();
+            long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis();
+            long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis();
+            return new RangeDivisionInfo(timeRange, Years.yearsIn(timeRange).get(timeUnit.getDurationType()) + 1, TimeUnits.YEARS, ISODateTimeFormat.year(), lower, upper);
+        } else if (Months.monthsIn(timeRange).isGreaterThan(Months.THREE)) {
+            timeUnit = DateTimeFieldType.monthOfYear();
+            long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis();
+            long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis();
+            return new RangeDivisionInfo(timeRange, Months.monthsIn(timeRange).getMonths() + 1, TimeUnits.MONTHS, DateTimeFormat.forPattern("YYYY'-'MMMM"), lower, upper);
+        } else if (Days.daysIn(timeRange).isGreaterThan(Days.THREE)) {
+            timeUnit = DateTimeFieldType.dayOfMonth();
+            long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis();
+            long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis();
+            return new RangeDivisionInfo(timeRange, Days.daysIn(timeRange).getDays() + 1, TimeUnits.DAYS, DateTimeFormat.forPattern("YYYY'-'MMMM'-'dd"), lower, upper);
+        } else if (Hours.hoursIn(timeRange).isGreaterThan(Hours.THREE)) {
+            timeUnit = DateTimeFieldType.hourOfDay();
+            long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis();
+            long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis();
+            return new RangeDivisionInfo(timeRange, Hours.hoursIn(timeRange).getHours() + 1, TimeUnits.HOURS, DateTimeFormat.forPattern("YYYY'-'MMMM'-'dd HH"), lower, upper);
+        } else if (Minutes.minutesIn(timeRange).isGreaterThan(Minutes.THREE)) {
+            timeUnit = DateTimeFieldType.minuteOfHour();
+            long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis();
+            long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis();
+            return new RangeDivisionInfo(timeRange, Minutes.minutesIn(timeRange).getMinutes() + 1, TimeUnits.MINUTES, DateTimeFormat.forPattern("YYYY'-'MMMM'-'dd HH':'mm"), lower, upper);
+        } else {
+            timeUnit = DateTimeFieldType.secondOfMinute();
+            long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis();
+            long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis();
+            return new RangeDivisionInfo(timeRange, Seconds.secondsIn(timeRange).getSeconds() + 1, TimeUnits.SECONDS, DateTimeFormat.forPattern("YYYY'-'MMMM'-'dd HH':'mm':'ss"), lower, upper);
+        }
+    }
+
+    public DateTimeFormatter getTickFormatter() {
+        return tickFormatter;
+    }
+
+    public int getPeriodsInRange() {
+        return numberOfBlocks;
+    }
+
+    public TimeUnits getPeriodSize() {
+        return blockSize;
+    }
+
+    public long getUpperBound() {
+        return upperBound;
+    }
+
+    public long getLowerBound() {
+        return lowerBound;
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java
new file mode 100644
index 0000000000000000000000000000000000000000..be12be9825b3c33642debcd2645bf9f47bdd9046
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java
@@ -0,0 +1,37 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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.zooming;
+
+/**
+ *
+ */
+public enum DescriptionLOD {
+
+    SHORT("Short"), MEDIUM("Medium"), FULL("Full");
+
+    private final String displayName;
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    private DescriptionLOD(String displayName) {
+        this.displayName = displayName;
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/EventTypeZoomLevel.java b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/EventTypeZoomLevel.java
new file mode 100644
index 0000000000000000000000000000000000000000..854f7d73abd7daac3c7a3eb069283d4eead97961
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/EventTypeZoomLevel.java
@@ -0,0 +1,38 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.zooming;
+
+/**
+ *
+ *
+ */
+public enum EventTypeZoomLevel {
+
+    ROOT_TYPE("Root Type"), BASE_TYPE("Base Type"), SUB_TYPE("Sub Type");
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    private final String displayName;
+
+    private EventTypeZoomLevel(String displayName) {
+        this.displayName = displayName;
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/TimeUnits.java b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/TimeUnits.java
new file mode 100644
index 0000000000000000000000000000000000000000..91c97cc981570e5c41fa1e298755fffcc16a8a38
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/TimeUnits.java
@@ -0,0 +1,88 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.zooming;
+
+import java.time.temporal.ChronoUnit;
+import org.joda.time.Days;
+import org.joda.time.Hours;
+import org.joda.time.Minutes;
+import org.joda.time.Months;
+import org.joda.time.Period;
+import org.joda.time.Seconds;
+import org.joda.time.Years;
+
+/** predefined units of time for use in choosing axis labels and sub intervals. */
+public enum TimeUnits {
+
+    FOREVER(null, ChronoUnit.FOREVER),
+    YEARS(Years.ONE.toPeriod(), ChronoUnit.YEARS),
+    MONTHS(Months.ONE.toPeriod(), ChronoUnit.MONTHS),
+    DAYS(Days.ONE.toPeriod(), ChronoUnit.DAYS),
+    HOURS(Hours.ONE.toPeriod(), ChronoUnit.HOURS),
+    MINUTES(Minutes.ONE.toPeriod(), ChronoUnit.MINUTES),
+    SECONDS(Seconds.ONE.toPeriod(), ChronoUnit.SECONDS);
+
+    public static TimeUnits fromChronoUnit(ChronoUnit chronoUnit) {
+        switch (chronoUnit) {
+
+            case FOREVER:
+                return FOREVER;
+            case ERAS:
+            case MILLENNIA:
+            case CENTURIES:
+            case DECADES:
+            case YEARS:
+                return YEARS;
+            case MONTHS:
+                return MONTHS;
+            case WEEKS:
+            case DAYS:
+                return DAYS;
+            case HOURS:
+            case HALF_DAYS:
+                return HOURS;
+            case MINUTES:
+                return MINUTES;
+            case SECONDS:
+            case MILLIS:
+            case MICROS:
+            case NANOS:
+                return SECONDS;
+            default:
+                return YEARS;
+        }
+    }
+
+    private final Period p;
+
+    private final ChronoUnit cu;
+
+    public Period getPeriod() {
+        return p;
+    }
+
+    public ChronoUnit getChronoUnit() {
+        return cu;
+    }
+
+    private TimeUnits(Period p, ChronoUnit cu) {
+        this.p = p;
+        this.cu = cu;
+    }
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java
new file mode 100644
index 0000000000000000000000000000000000000000..a0e523769778124b66239fc1f7d9c6018c7a8b05
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java
@@ -0,0 +1,158 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 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.zooming;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Objects;
+import java.util.Set;
+import org.joda.time.Interval;
+import org.sleuthkit.autopsy.timeline.filters.Filter;
+
+/**
+ * This class encapsulates all the zoom(and filter) parameters into one object
+ * for passing around and as a memento of the zoom/filter state.
+ */
+public class ZoomParams {
+
+    private final Interval timeRange;
+
+    private final EventTypeZoomLevel typeZoomLevel;
+
+    private final Filter filter;
+
+    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;
+    }
+
+    public EventTypeZoomLevel getTypeZoomLevel() {
+        return typeZoomLevel;
+    }
+
+    public Filter getFilter() {
+        return filter;
+    }
+
+    public DescriptionLOD getDescrLOD() {
+        return descrLOD;
+    }
+
+    public ZoomParams(Interval timeRange, EventTypeZoomLevel zoomLevel, Filter filter, DescriptionLOD descrLOD) {
+        this.timeRange = timeRange;
+        this.typeZoomLevel = zoomLevel;
+        this.filter = filter;
+        this.descrLOD = descrLOD;
+        changedFields = EnumSet.allOf(Field.class);
+    }
+
+    public ZoomParams(Interval timeRange, EventTypeZoomLevel zoomLevel, Filter 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));
+    }
+
+    public ZoomParams withTypeZoomLevel(EventTypeZoomLevel zoomLevel) {
+        return new ZoomParams(timeRange, zoomLevel, filter, descrLOD, EnumSet.of(Field.EVENT_TYPE_ZOOM));
+    }
+
+    public ZoomParams withTimeRange(Interval timeRange) {
+        return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD, EnumSet.of(Field.TIME));
+    }
+
+    public ZoomParams withDescrLOD(DescriptionLOD descrLOD) {
+        return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD, EnumSet.of(Field.DESCRIPTION_LOD));
+    }
+
+    public ZoomParams withFilter(Filter filter) {
+        return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD, EnumSet.of(Field.FILTER));
+    }
+
+    public boolean hasFilter(Filter filterSet) {
+        return this.filter.equals(filterSet);
+    }
+
+    public boolean hasTypeZoomLevel(EventTypeZoomLevel typeZoom) {
+        return this.typeZoomLevel.equals(typeZoom);
+    }
+
+    public boolean hasTimeRange(Interval timeRange) {
+        return this.timeRange == null ? false : this.timeRange.equals(timeRange);
+    }
+
+    public boolean hasDescrLOD(DescriptionLOD newLOD) {
+        return this.descrLOD.equals(newLOD);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 97 * hash + Objects.hashCode(this.timeRange.getStartMillis());
+        hash = 97 * hash + Objects.hashCode(this.timeRange.getEndMillis());
+        hash = 97 * hash + Objects.hashCode(this.typeZoomLevel);
+        hash = 97 * hash + Objects.hashCode(this.filter.isActive());
+        hash = 97 * hash + Objects.hashCode(this.descrLOD);
+
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final ZoomParams other = (ZoomParams) obj;
+        if (!Objects.equals(this.timeRange, other.timeRange)) {
+            return false;
+        }
+        if (this.typeZoomLevel != other.typeZoomLevel) {
+            return false;
+        }
+        if (this.filter.equals(other.filter) == false) {
+            return false;
+        }
+        if (this.descrLOD != other.descrLOD) {
+            return false;
+        }
+
+        return true;
+    }
+
+}
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..894e35fd4a3590e7c1575a8fbf1c362884d8f610
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.fxml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+<?import javafx.scene.text.*?>
+
+<fx:root alignment="TOP_LEFT" collapsible="false" contentDisplay="RIGHT" minHeight="-Infinity" minWidth="-Infinity" type="TitledPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+    <content>
+        <AnchorPane minHeight="-Infinity" minWidth="-Infinity">
+            <children>
+                <GridPane alignment="CENTER" hgap="5.0" minHeight="-Infinity" minWidth="-Infinity" vgap="5.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="0.0">
+                    <children>
+                        <Label text="Description Detail:" GridPane.rowIndex="2">
+                            <GridPane.margin>
+                                <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+                            </GridPane.margin>
+                        </Label>
+                        <Slider fx:id="descrLODSlider" blockIncrement="1.0" majorTickUnit="1.0" max="10.0" minorTickCount="0" showTickLabels="true" showTickMarks="true" snapToTicks="true" GridPane.columnIndex="1" GridPane.rowIndex="2">
+                            <GridPane.margin>
+                                <Insets />
+                            </GridPane.margin>
+                        </Slider>
+                        <Label text="Event Type: " GridPane.rowIndex="1">
+                            <GridPane.margin>
+                                <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+                            </GridPane.margin>
+                        </Label>
+                        <Slider fx:id="typeZoomSlider" blockIncrement="1.0" majorTickUnit="1.0" max="10.0" minorTickCount="0" showTickLabels="true" showTickMarks="true" snapToTicks="true" GridPane.columnIndex="1" GridPane.rowIndex="1">
+                            <GridPane.margin>
+                                <Insets />
+                            </GridPane.margin>
+                        </Slider>
+                        <Label text="Time Units:" GridPane.valignment="CENTER">
+                            <GridPane.margin>
+                                <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+                            </GridPane.margin>
+                        </Label>
+                        <Slider fx:id="timeUnitSlider" blockIncrement="1.0" majorTickUnit="1.0" max="10.0" minorTickCount="0" showTickLabels="true" showTickMarks="true" snapToTicks="true" GridPane.columnIndex="1">
+                            <GridPane.margin>
+                                <Insets />
+                            </GridPane.margin>
+                        </Slider>
+                    </children>
+                    <columnConstraints>
+                        <ColumnConstraints fillWidth="false" halignment="LEFT" hgrow="NEVER" minWidth="-Infinity" />
+                        <ColumnConstraints halignment="CENTER" hgrow="SOMETIMES" minWidth="-Infinity" />
+                    </columnConstraints>
+                    <rowConstraints>
+                        <RowConstraints minHeight="-Infinity" vgrow="NEVER" />
+                        <RowConstraints minHeight="-Infinity" vgrow="NEVER" />
+                        <RowConstraints minHeight="-Infinity" vgrow="NEVER" />
+                        <RowConstraints />
+                    </rowConstraints>
+                </GridPane>
+            </children>
+            <padding>
+                <Insets right="5.0" />
+            </padding>
+        </AnchorPane>
+    </content>
+    <graphic>
+        <BorderPane>
+            <left>
+                <HBox alignment="CENTER" maxHeight="-Infinity" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-Infinity" spacing="5.0" BorderPane.alignment="CENTER_LEFT">
+                    <children>
+                        <Label text="Zoom ">
+                            <font>
+                                <Font name="System Bold" size="12.0" />
+                            </font>
+                            <graphic>
+                                <ImageView fitHeight="16.0" fitWidth="16.0"  preserveRatio="true">
+                                    <image>
+                                        <Image url="@../images/magnifier-zoom.png" />
+                                    </image>
+                                </ImageView>
+                            </graphic>
+                        </Label>
+                        <Region maxWidth="1.7976931348623157E308" minWidth="60.0" HBox.hgrow="ALWAYS" />
+                        <Label text="History: " />
+                        <Button fx:id="backButton" mnemonicParsing="false" HBox.hgrow="SOMETIMES">
+                            <graphic>
+                                <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+                                    <image>
+                                        <Image url="@../images/arrow-180.png" />
+                                    </image>
+                                </ImageView>
+                            </graphic>
+                        </Button>
+                        <Button fx:id="forwardButton" mnemonicParsing="false" HBox.hgrow="SOMETIMES">
+                            <graphic>
+                                <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+                                    <image>
+                                        <Image url="@../images/arrow.png" />
+                                    </image>
+                                </ImageView>
+                            </graphic>
+                        </Button>
+                    </children>
+                    <BorderPane.margin>
+                        <Insets top="3.0" />
+                    </BorderPane.margin>
+                    <padding>
+                        <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" />
+                    </padding>
+                </HBox>
+            </left>
+        </BorderPane>
+    </graphic>
+</fx:root>
diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java
new file mode 100644
index 0000000000000000000000000000000000000000..f07e113461ce78e50d4544d251d98a3c08a1cd63
--- /dev/null
+++ b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java
@@ -0,0 +1,227 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 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.zooming;
+
+import java.net.URL;
+import java.time.temporal.ChronoUnit;
+import java.util.ResourceBundle;
+import javafx.application.Platform;
+import javafx.beans.InvalidationListener;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.Slider;
+import javafx.scene.control.TitledPane;
+import javafx.scene.control.Tooltip;
+import javafx.util.StringConverter;
+import org.sleuthkit.autopsy.timeline.FXMLConstructor;
+import org.sleuthkit.autopsy.timeline.TimeLineController;
+import org.sleuthkit.autopsy.timeline.TimeLineView;
+import org.sleuthkit.autopsy.timeline.VisualizationMode;
+import org.sleuthkit.autopsy.timeline.actions.Back;
+import org.sleuthkit.autopsy.timeline.actions.Forward;
+import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel;
+import org.sleuthkit.autopsy.timeline.utils.IntervalUtils;
+import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;
+
+/** FXML Controller class for the ZoomSettingsPane.fxml
+ *
+ * has sliders to provide context/control over three axes of zooming (timescale,
+ * event hierarchy, and description detail). */
+public class ZoomSettingsPane extends TitledPane implements TimeLineView {
+
+    @FXML
+    private ResourceBundle resources;
+
+    @FXML
+    private URL location;
+
+    @FXML
+    private Button backButton;
+
+    @FXML
+    private Button forwardButton;
+
+    @FXML
+    private Slider descrLODSlider;
+
+    @FXML
+    private Slider typeZoomSlider;
+
+    @FXML
+    private Slider timeUnitSlider;
+
+    private TimeLineController controller;
+
+    private FilteredEventsModel filteredEvents;
+
+    /**
+     * Initializes the controller class.
+     */
+    public void initialize() {
+
+        timeUnitSlider.setMax(TimeUnits.values().length - 2);
+        timeUnitSlider.setLabelFormatter(new TimeUnitConverter());
+
+        typeZoomSlider.setMin(1);
+        typeZoomSlider.setMax(2);
+        typeZoomSlider.setLabelFormatter(new TypeZoomConverter());
+        descrLODSlider.setMax(DescriptionLOD.values().length - 1);
+        descrLODSlider.setLabelFormatter(new DescrLODConverter());
+    }
+
+    public ZoomSettingsPane() {
+        FXMLConstructor.construct(this, "ZoomSettingsPane.fxml");
+    }
+
+    @Override
+    synchronized public void setController(TimeLineController controller) {
+        this.controller = controller;
+        setModel(controller.getEventsModel());
+        descrLODSlider.disableProperty().bind(controller.getViewMode().isEqualTo(VisualizationMode.COUNTS));
+        Back back = new Back(controller);
+        backButton.disableProperty().bind(back.disabledProperty());
+        backButton.setOnAction(back);
+        backButton.setTooltip(new Tooltip("Back: " + back.getAccelerator().getName()));
+        Forward forward = new Forward(controller);
+        forwardButton.disableProperty().bind(forward.disabledProperty());
+        forwardButton.setOnAction(forward);
+        forwardButton.setTooltip(new Tooltip("Forward: " + forward.getAccelerator().getName()));
+
+    }
+
+    @Override
+    public void setModel(FilteredEventsModel filteredEvents) {
+        this.filteredEvents = filteredEvents;
+
+        initializeSlider(timeUnitSlider,
+                         () -> {
+                             TimeUnits requestedUnit = TimeUnits.values()[new Double(timeUnitSlider.getValue()).intValue()];
+                             if (requestedUnit == TimeUnits.FOREVER) {
+                                 controller.showFullRange();
+                             } else {
+                                 controller.pushTimeRange(IntervalUtils.getIntervalAround(IntervalUtils.middleOf(ZoomSettingsPane.this.filteredEvents.timeRange().get()), requestedUnit.getPeriod()));
+                             }
+                         },
+                         this.filteredEvents.timeRange(),
+                         () -> {
+                             RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(this.filteredEvents.timeRange().get());
+                             ChronoUnit chronoUnit = rangeInfo.getPeriodSize().getChronoUnit();
+
+                             timeUnitSlider.setValue(TimeUnits.fromChronoUnit(chronoUnit).ordinal() - 1);
+                         });
+
+        initializeSlider(descrLODSlider,
+                         () -> {
+                             DescriptionLOD newLOD = DescriptionLOD.values()[Math.round(descrLODSlider.valueProperty().floatValue())];
+                             controller.pushDescrLOD(newLOD);
+                         }, this.filteredEvents.descriptionLOD(),
+                         () -> {
+                             descrLODSlider.setValue(this.filteredEvents.descriptionLOD().get().ordinal());
+                         });
+
+        initializeSlider(typeZoomSlider,
+                         () -> {
+                             EventTypeZoomLevel newZoomLevel = EventTypeZoomLevel.values()[Math.round(typeZoomSlider.valueProperty().floatValue())];
+                             controller.pushEventTypeZoom(newZoomLevel);
+                         },
+                         this.filteredEvents.eventTypeZoom(),
+                         () -> {
+                             typeZoomSlider.setValue(this.filteredEvents.eventTypeZoom().get().ordinal());
+                         });
+    }
+
+    /**
+     * setup a slider that with a listener that is added and removed to avoid
+     * circular updates.
+     *
+     * @param <T>                 the type of the driving property
+     * @param slider              the slider that will have its change handlers
+     *                            setup
+     * @param sliderChangeHandler the runnable that will be executed whenever
+     *                            the slider value has changed and is not
+     *                            currently changing
+     * @param driver              the property that drives updates to this
+     *                            slider
+     * @param driverChangHandler  the code to update the slider bases on the
+     *                            value of the driving property. This will be wrapped in a
+     *                            remove/add-listener pair to prevent circular updates.
+     */
+    private <T> void initializeSlider(Slider slider, Runnable sliderChangeHandler, ReadOnlyObjectProperty<T> driver, Runnable driverChangHandler) {
+        final InvalidationListener sliderListener = observable -> {
+            if (slider.isValueChanging() == false) {
+                sliderChangeHandler.run();
+            }
+        };
+        slider.valueProperty().addListener(sliderListener);
+        slider.valueChangingProperty().addListener(sliderListener);
+
+        Platform.runLater(driverChangHandler);
+
+        driver.addListener(observable -> {
+            slider.valueProperty().removeListener(sliderListener);
+            slider.valueChangingProperty().removeListener(sliderListener);
+
+            driverChangHandler.run();
+
+            slider.valueProperty().addListener(sliderListener);
+            slider.valueChangingProperty().addListener(sliderListener);
+        });
+    }
+
+    //Can these be abstracted to a sort of Enum converter for use in a potential enumslider
+    private static class TimeUnitConverter extends StringConverter<Double> {
+
+        @Override
+        public String toString(Double object) {
+            return TimeUnits.values()[Math.min(TimeUnits.values().length - 1, object.intValue() + 1)].toString();
+        }
+
+        @Override
+        public Double fromString(String string) {
+            return new Integer(TimeUnits.valueOf(string).ordinal()).doubleValue();
+        }
+    }
+
+    private static class TypeZoomConverter extends StringConverter<Double> {
+
+        @Override
+        public String toString(Double object) {
+            return EventTypeZoomLevel.values()[object.intValue()].getDisplayName();
+        }
+
+        @Override
+        public Double fromString(String string) {
+            return new Integer(EventTypeZoomLevel.valueOf(string).ordinal()).doubleValue();
+        }
+    }
+
+    private static class DescrLODConverter extends StringConverter<Double> {
+
+        @Override
+        public String toString(Double object) {
+            return DescriptionLOD.values()[object.intValue()].getDisplayName();
+        }
+
+        @Override
+        public Double fromString(String string) {
+            return new Integer(DescriptionLOD.valueOf(string).ordinal()).doubleValue();
+        }
+    }
+}
diff --git a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties
index 92f657c0b37ed56045a468ec3da39fc9bad4166f..df82a8680af661b7869ad26904949b5bd2cc359c 100644
--- a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties
+++ b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties
@@ -1,5 +1,5 @@
 #Updated by build script
-#Tue, 05 Aug 2014 14:10:24 -0400
+#Mon, 28 Apr 2014 01:45:18 -0400
 LBL_splash_window_title=Starting Autopsy
 SPLASH_HEIGHT=288
 SPLASH_WIDTH=538
@@ -8,4 +8,4 @@ SplashRunningTextBounds=5,266,530,17
 SplashRunningTextColor=0x0
 SplashRunningTextFontSize=18
 
-currentVersion=Autopsy 3.1.0_Beta
+currentVersion=Autopsy 3.1.0
diff --git a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties
index c9b7f9be63e51c9b1b23b7d62be081c323302f70..91c0d156ef0f725db9d322ab14350824da90fdb8 100644
--- a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties
+++ b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties
@@ -1,5 +1,5 @@
 #Updated by build script
 #Tue, 05 Aug 2014 14:10:24 -0400
 
-CTL_MainWindow_Title=Autopsy 3.1.0_Beta
-CTL_MainWindow_Title_No_Project=Autopsy 3.1.0_Beta
+CTL_MainWindow_Title=Autopsy 3.1.0
+CTL_MainWindow_Title_No_Project=Autopsy 3.1.0
diff --git a/nbproject/project.properties b/nbproject/project.properties
index 55eff9f604bf9f88edff15c28f2699b181efdd95..b25757db069cf992e91231f783b23de9a649cc42 100644
--- a/nbproject/project.properties
+++ b/nbproject/project.properties
@@ -10,6 +10,7 @@ app.version=3.1.0
 #build.type=RELEASE
 build.type=DEVELOPMENT
 project.org.sleuthkit.autopsy.imageanalyzer=ImageAnalyzer
+project.org.sleuthkit.autopsy.advancedtimeline=Timeline
 update_versions=false
 #custom JVM options
 #Note: can be higher on 64 bit systems, should be in sync with build.xml
@@ -29,7 +30,8 @@ modules=\
     ${project.org.sleuthkit.autopsy.core}:\
     ${project.org.sleuthkit.autopsy.corelibs}:\
     ${project.org.sleuthkit.autopsy.scalpel}:\
-    ${project.org.sleuthkit.autopsy.imageanalyzer}
+    ${project.org.sleuthkit.autopsy.imageanalyzer}:\
+    ${project.org.sleuthkit.autopsy.advancedtimeline}
 project.org.sleuthkit.autopsy.core=Core
 project.org.sleuthkit.autopsy.corelibs=CoreLibs
 project.org.sleuthkit.autopsy.keywordsearch=KeywordSearch