diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.java index 18fd2c6202..337fa49f0b 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.java @@ -22,6 +22,7 @@ import java.awt.Dimension; import java.awt.Toolkit; import java.io.File; import java.io.IOException; +import java.nio.file.Paths; import java.util.logging.Level; import javax.swing.JFileChooser; import javax.swing.JFrame; @@ -31,6 +32,7 @@ import org.apache.commons.io.FilenameUtils; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ModuleSettings; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.modules.hashdatabase.HashDbManager.HashDb; import org.sleuthkit.autopsy.modules.hashdatabase.HashDbManager.HashDb.KnownFilesType; import org.sleuthkit.autopsy.modules.hashdatabase.HashDbManager.HashDbManagerException; @@ -249,9 +251,16 @@ final class HashDbImportDatabaseDialog extends javax.swing.JDialog { }// //GEN-END:initComponents private void openButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_openButtonActionPerformed + String lastBaseDirectory = Paths.get(PlatformUtil.getUserConfigDirectory(), "HashDatabases").toString(); if (ModuleSettings.settingExists(ModuleSettings.MAIN_SETTINGS, LAST_FILE_PATH_KEY)) { - fileChooser.setCurrentDirectory(new File(ModuleSettings.getConfigSetting(ModuleSettings.MAIN_SETTINGS, LAST_FILE_PATH_KEY))); + lastBaseDirectory = ModuleSettings.getConfigSetting(ModuleSettings.MAIN_SETTINGS, LAST_FILE_PATH_KEY); } + File hashDbFolder = new File(lastBaseDirectory); + // create the folder if it doesn't exist + if (!hashDbFolder.exists()) { + hashDbFolder.mkdir(); + } + fileChooser.setCurrentDirectory(hashDbFolder); if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { File databaseFile = fileChooser.getSelectedFile(); try { diff --git a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties index ba477498c3..6e920ad631 100644 --- a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties @@ -225,7 +225,8 @@ ReportHTML.writeSum.noCaseNum=No case number ReportBodyFile.generateReport.srcModuleName.text=TSK Body File ReportExcel.endReport.srcModuleName.text=Excel Report ReportHTML.writeIndex.srcModuleName.text=HTML Report -ReportKML.genReport.srcModuleName.text=KML Report +ReportKML.genReport.srcModuleName.text=Geospatial Data +ReportKML.genReport.reportName=KML Report ReportGenerator.artTableColHdr.extension.text=Extension ReportGenerator.artTableColHdr.mimeType.text=MIME Type ReportGenerator.artTableColHdr.processorArchitecture.text=Processor Architecture diff --git a/Core/src/org/sleuthkit/autopsy/report/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/report/Bundle_ja.properties index 6ff657b68a..a877c52899 100644 --- a/Core/src/org/sleuthkit/autopsy/report/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/report/Bundle_ja.properties @@ -188,7 +188,7 @@ ReportExcel.endReport.srcModuleName.text=Excel\u30ec\u30dd\u30fc\u30c8 ReportGenerator.artTableColHdr.extension.text=\u62e1\u5f35\u5b50 ReportGenerator.artTableColHdr.mimeType.text=MIME\u30bf\u30a4\u30d7 ReportHTML.writeIndex.srcModuleName.text=HTML\u30ec\u30dd\u30fc\u30c8 -ReportKML.genReport.srcModuleName.text=KML\u30ec\u30dd\u30fc\u30c8 +ReportKML.genReport.reportName=KML\u30ec\u30dd\u30fc\u30c8 ReportGenerator.artTableColHdr.associatedArtifact=\u95a2\u4fc2\u3059\u308b\u30a2\u30fc\u30c6\u30a3\u30d5\u30a1\u30af\u30c8 ReportGenerator.artTableColHdr.count=\u30ab\u30a6\u30f3\u30c8 ReportGenerator.artTableColHdr.devMake=\u6a5f\u5668\u578b\u540d diff --git a/Core/src/org/sleuthkit/autopsy/report/ReportKML.java b/Core/src/org/sleuthkit/autopsy/report/ReportKML.java index abdd7dee96..44185bfc94 100644 --- a/Core/src/org/sleuthkit/autopsy/report/ReportKML.java +++ b/Core/src/org/sleuthkit/autopsy/report/ReportKML.java @@ -2,7 +2,7 @@ * * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-2016 Basis Technology Corp. * contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,33 +27,56 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.*; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.datamodel.BlackboardArtifact; -import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; import java.util.logging.Level; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.Namespace; import org.jdom2.output.Format; import org.jdom2.output.XMLOutputter; -import org.apache.commons.lang.StringEscapeUtils; +import org.jdom2.CDATA; +import org.openide.filesystems.FileUtil; /** - * Generates a KML file based on geo coordinates store in blackboard. + * Generates a KML file based on geospatial information from the BlackBoard. */ class ReportKML implements GeneralReportModule { private static final Logger logger = Logger.getLogger(ReportKML.class.getName()); + private static final String KML_STYLE_FILE = "style.kml"; + private static final String REPORT_KML = "ReportKML.kml"; + private static final String STYLESHEETS_PATH = "/org/sleuthkit/autopsy/report/stylesheets/"; private static ReportKML instance = null; private Case currentCase; private SleuthkitCase skCase; - private String reportPath; + private final SimpleDateFormat kmlDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"); + private Namespace ns; + private final String SEP = "
"; + + private enum FeatureColor { + RED("style.kml#redFeature"), + GREEN("style.kml#greenFeature"), + BLUE("style.kml#blueFeature"), + PURPLE("style.kml#purpleFeature"), + WHITE("style.kml#whiteFeature"), + YELLOW("style.kml#yellowFeature"); + private String color; + + FeatureColor(String color) { + this.color = color; + } + + String getColor() { + return this.color; + } + } // Hidden constructor for the report private ReportKML() { @@ -80,279 +103,684 @@ class ReportKML implements GeneralReportModule { progressPanel.setIndeterminate(false); progressPanel.start(); progressPanel.updateStatusLabel(NbBundle.getMessage(this.getClass(), "ReportKML.progress.querying")); - reportPath = baseReportDir + "ReportKML.kml"; //NON-NLS - String reportPath2 = baseReportDir + "ReportKML.txt"; //NON-NLS + String kmlFileFullPath = baseReportDir + REPORT_KML; //NON-NLS currentCase = Case.getCurrentCase(); skCase = currentCase.getSleuthkitCase(); progressPanel.updateStatusLabel(NbBundle.getMessage(this.getClass(), "ReportKML.progress.loading")); - // Check if ingest has finished - String ingestwarning = ""; - if (IngestManager.getInstance().isIngestRunning()) { - ingestwarning = NbBundle.getMessage(this.getClass(), "ReportBodyFile.ingestWarning.text"); - } + progressPanel.setMaximumProgress(5); progressPanel.increment(); - // @@@ BC: I don't get why we do this in two passes. - // Why not just print the coordinates as we find them and make some utility methods to do the printing? - // Should pull out time values for all of these points and store in TimeSpan element + ns = Namespace.getNamespace("", "http://www.opengis.net/kml/2.2"); //NON-NLS + + Element kml = new Element("kml", ns); //NON-NLS + kml.addNamespaceDeclaration(Namespace.getNamespace("gx", "http://www.google.com/kml/ext/2.2")); //NON-NLS + kml.addNamespaceDeclaration(Namespace.getNamespace("kml", "http://www.opengis.net/kml/2.2")); //NON-NLS + kml.addNamespaceDeclaration(Namespace.getNamespace("atom", "http://www.w3.org/2005/Atom")); //NON-NLS + Document kmlDocument = new Document(kml); + + Element document = new Element("Document", ns); //NON-NLS + kml.addContent(document); + + Element name = new Element("name", ns); //NON-NLS + ReportBranding rb = new ReportBranding(); + name.setText(rb.getReportTitle() + " KML"); //NON-NLS + document.addContent(name); + + // Check if ingest has finished + if (IngestManager.getInstance().isIngestRunning()) { + Element ingestwarning = new Element("snippet", ns); //NON-NLS + ingestwarning.addContent(NbBundle.getMessage(this.getClass(), "ReportBodyFile.ingestWarning.text")); //NON-NLS + document.addContent(ingestwarning); + } + + // Create folder structure + Element gpsExifMetadataFolder = new Element("Folder", ns); //NON-NLS + CDATA cdataExifMetadataFolder = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/camera-icon-16.png"); //NON-NLS + Element hrefExifMetadata = new Element("href", ns).addContent(cdataExifMetadataFolder); //NON-NLS + gpsExifMetadataFolder.addContent(new Element("Icon", ns).addContent(hrefExifMetadata)); //NON-NLS + + Element gpsBookmarksFolder = new Element("Folder", ns); //NON-NLS + CDATA cdataBookmarks = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gpsfav.png"); //NON-NLS + Element hrefBookmarks = new Element("href", ns).addContent(cdataBookmarks); //NON-NLS + gpsBookmarksFolder.addContent(new Element("Icon", ns).addContent(hrefBookmarks)); //NON-NLS + + Element gpsLastKnownLocationFolder = new Element("Folder", ns); //NON-NLS + CDATA cdataLastKnownLocation = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gps-lastlocation.png"); //NON-NLS + Element hrefLastKnownLocation = new Element("href", ns).addContent(cdataLastKnownLocation); //NON-NLS + gpsLastKnownLocationFolder.addContent(new Element("Icon", ns).addContent(hrefLastKnownLocation)); //NON-NLS + + Element gpsRouteFolder = new Element("Folder", ns); //NON-NLS + CDATA cdataRoute = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gps-trackpoint.png"); //NON-NLS + Element hrefRoute = new Element("href", ns).addContent(cdataRoute); //NON-NLS + gpsRouteFolder.addContent(new Element("Icon", ns).addContent(hrefRoute)); //NON-NLS + + Element gpsSearchesFolder = new Element("Folder", ns); //NON-NLS + CDATA cdataSearches = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gps-search.png"); //NON-NLS + Element hrefSearches = new Element("href", ns).addContent(cdataSearches); //NON-NLS + gpsSearchesFolder.addContent(new Element("Icon", ns).addContent(hrefSearches)); //NON-NLS + + Element gpsTrackpointsFolder = new Element("Folder", ns); //NON-NLS + CDATA cdataTrackpoints = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gps-trackpoint.png"); //NON-NLS + Element hrefTrackpoints = new Element("href", ns).addContent(cdataTrackpoints); //NON-NLS + gpsTrackpointsFolder.addContent(new Element("Icon", ns).addContent(hrefTrackpoints)); //NON-NLS + + gpsExifMetadataFolder.addContent(new Element("name", ns).addContent("EXIF Metadata")); //NON-NLS + gpsBookmarksFolder.addContent(new Element("name", ns).addContent("GPS Bookmarks")); //NON-NLS + gpsLastKnownLocationFolder.addContent(new Element("name", ns).addContent("GPS Last Known Location")); //NON-NLS + gpsRouteFolder.addContent(new Element("name", ns).addContent("GPS Routes")); //NON-NLS + gpsSearchesFolder.addContent(new Element("name", ns).addContent("GPS Searches")); //NON-NLS + gpsTrackpointsFolder.addContent(new Element("name", ns).addContent("GPS Trackpoints")); //NON-NLS + + document.addContent(gpsExifMetadataFolder); + document.addContent(gpsBookmarksFolder); + document.addContent(gpsLastKnownLocationFolder); + document.addContent(gpsRouteFolder); + document.addContent(gpsSearchesFolder); + document.addContent(gpsTrackpointsFolder); + + /** + * In the following code, nulls are okay, and are handled when we go to + * write out the KML feature. Nulls are expected to be returned from any + * method where the artifact is not found and is handled in the + * individual feature creation methods. This is done because we don't + * know beforehand which attributes will be included for which artifact, + * as anyone could write a module that adds additional attributes to an + * artifact. + * + */ try { - try (BufferedWriter out = new BufferedWriter(new FileWriter(reportPath2))) { - - double lat = 0; // temp latitude - double lon = 0; //temp longitude - AbstractFile aFile; - String geoPath = ""; // will hold values of images to put in kml - String imageName = ""; - - File f; - for (BlackboardArtifact artifact : skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF)) { - lat = 0; - lon = 0; - geoPath = ""; - String extractedToPath; - for (BlackboardAttribute attribute : artifact.getAttributes()) { - if (attribute.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE.getTypeID()) //latitude - { - - lat = attribute.getValueDouble(); - } - if (attribute.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE.getTypeID()) //longitude - { - lon = attribute.getValueDouble(); - } - } - if (lon != 0 && lat != 0) { - aFile = artifact.getSleuthkitCase().getAbstractFileById(artifact.getObjectID()); - - if (aFile != null) { - extractedToPath = reportPath + aFile.getName(); - geoPath = extractedToPath; - f = new File(extractedToPath); - f.createNewFile(); - copyFileUsingStream(aFile, f); - imageName = aFile.getName(); - } - out.write(String.valueOf(lat)); - out.write(";"); - out.write(String.valueOf(lon)); - out.write(";"); - out.write(String.valueOf(geoPath)); - out.write(";"); - out.write(String.valueOf(imageName)); - out.write("\n"); - // lat lon path name + for (BlackboardArtifact artifact : skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF)) { + Long timestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED); + String desc = getDescriptionFromArtifact(artifact, "EXIF Metadata With Locations"); //NON-NLS + Double lat = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE); + Double lon = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE); + Element point = makePoint(lat, lon, getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE)); + Path destination = null; + if (lat != null && lat != 0.0 && lon != null && lon != 0.0) { + AbstractFile abstractFile = artifact.getSleuthkitCase().getAbstractFileById(artifact.getObjectID()); + if (abstractFile != null) { + destination = Paths.get(baseReportDir, abstractFile.getName()); + copyFileUsingStream(abstractFile, destination.toFile()); } + String formattedCoordinates = String.format("%.2f, %.2f", lat, lon); + gpsExifMetadataFolder.addContent(makePlacemarkWithPicture(abstractFile.getName(), FeatureColor.RED, desc, timestamp, point, destination, formattedCoordinates)); } - for (BlackboardArtifact artifact : skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACKPOINT)) { - lat = 0; - lon = 0; - for (BlackboardAttribute attribute : artifact.getAttributes()) { - if (attribute.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE.getTypeID()) //latitude - { - lat = attribute.getValueDouble(); - } - if (attribute.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE.getTypeID()) //longitude - { - lon = attribute.getValueDouble(); - } - } - if (lon != 0 && lat != 0) { - out.write(lat + ";" + lon + "\n"); - } - } - - for (BlackboardArtifact artifact : skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE)) { - lat = 0; - lon = 0; - double destlat = 0; - double destlon = 0; - String name = ""; - String location = ""; - for (BlackboardAttribute attribute : artifact.getAttributes()) { - if (attribute.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START.getTypeID()) //latitude - { - lat = attribute.getValueDouble(); - } else if (attribute.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END.getTypeID()) //longitude - { - destlat = attribute.getValueDouble(); - } else if (attribute.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_START.getTypeID()) //longitude - { - lon = attribute.getValueDouble(); - } else if (attribute.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END.getTypeID()) //longitude - { - destlon = attribute.getValueDouble(); - } else if (attribute.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME.getTypeID()) //longitude - { - name = attribute.getValueString(); - } else if (attribute.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_LOCATION.getTypeID()) //longitude - { - location = attribute.getValueString(); - } - } - - // @@@ Should do something more fancy with these in KML and store them as a single point. - String display = name; - if (display.isEmpty()) { - display = location; - } - - if (lon != 0 && lat != 0) { - out.write(NbBundle.getMessage(this.getClass(), "ReportKML.latLongStartPoint", lat, lon, display)); - } - if (destlat != 0 && destlon != 0) { - out.write(NbBundle.getMessage(this.getClass(), "ReportKML.latLongEndPoint", destlat, destlon, - display)); - } - } - - out.flush(); - - progressPanel.increment(); - /* - * Step 1: generate XML stub - */ - Namespace ns = Namespace.getNamespace("", "http://earth.google.com/kml/2.2"); //NON-NLS - // kml - Element kml = new Element("kml", ns); //NON-NLS - Document kmlDocument = new Document(kml); - - // Document - Element document = new Element("Document", ns); //NON-NLS - kml.addContent(document); - - // name - Element name = new Element("name", ns); //NON-NLS - name.setText("Java Generated KML Document"); //NON-NLS - document.addContent(name); - - /* - * Step 2: add in Style elements - */ - // Style - Element style = new Element("Style", ns); //NON-NLS - style.setAttribute("id", "redIcon"); //NON-NLS - document.addContent(style); - - // IconStyle - Element iconStyle = new Element("IconStyle", ns); //NON-NLS - style.addContent(iconStyle); - - // color - Element color = new Element("color", ns); //NON-NLS - color.setText("990000ff"); //NON-NLS - iconStyle.addContent(color); - - // Icon - Element icon = new Element("Icon", ns); //NON-NLS - iconStyle.addContent(icon); - - // href - Element href = new Element("href", ns); //NON-NLS - href.setText("http://www.cs.mun.ca/~hoeber/teaching/cs4767/notes/02.1-kml/circle.png"); //NON-NLS - icon.addContent(href); - progressPanel.increment(); - /* - * Step 3: read data from source location and add in a Placemark - * for each data element - */ - - File file = new File(reportPath2); - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - String line = reader.readLine(); - while (line != null) { - String[] lineParts = line.split(";"); - if (lineParts.length > 1) { - String coordinates = lineParts[1].trim() + "," + lineParts[0].trim(); //lat,lon - // Placemark - Element placemark = new Element("Placemark", ns); //NON-NLS - document.addContent(placemark); - - if (lineParts.length == 4) { - // name - Element pmName = new Element("name", ns); //NON-NLS - pmName.setText(lineParts[3].trim()); - placemark.addContent(pmName); - - String savedPath = lineParts[2].trim(); - if (savedPath.isEmpty() == false) { - // Path - Element pmPath = new Element("Path", ns); //NON-NLS - pmPath.setText(savedPath); - placemark.addContent(pmPath); - - // description - Element pmDescription = new Element("description", ns); //NON-NLS - String xml = "
" + featureType + ""); //NON-NLS + + String name = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME); + if (name != null && !name.isEmpty()) { + result.append("Name: ").append(name).append(SEP); //NON-NLS + } + + String location = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_LOCATION); + if (location != null && !location.isEmpty()) { + result.append("Location: ").append(location).append(SEP); //NON-NLS + } + + Long timestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME); + if (timestamp != null) { + result.append("Timestamp: ").append(getTimeStamp(timestamp)).append(SEP); //NON-NLS + result.append("Unix timestamp: ").append(timestamp).append(SEP); //NON-NLS + } + + Long startingTimestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_START); + if (startingTimestamp != null) { + result.append("Starting Timestamp: ").append(getTimeStamp(startingTimestamp)).append(SEP); //NON-NLS + result.append("Starting Unix timestamp: ").append(startingTimestamp).append(SEP); //NON-NLS + } + + Long endingTimestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_END); + if (endingTimestamp != null) { + result.append("Ending Timestamp: ").append(getTimeStamp(endingTimestamp)).append(SEP); //NON-NLS + result.append("Ending Unix timestamp: ").append(endingTimestamp).append(SEP); //NON-NLS + } + + Long createdTimestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED); + if (createdTimestamp != null) { + result.append("Created Timestamp: ").append(getTimeStamp(createdTimestamp)).append(SEP); //NON-NLS + result.append("Created Unix timestamp: ").append(createdTimestamp).append(SEP); //NON-NLS + } + + Double latitude = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE); + if (latitude != null) { + result.append("Latitude: ").append(latitude).append(SEP); //NON-NLS + } + + Double longitude = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE); + if (longitude != null) { + result.append("Longitude: ").append(longitude).append(SEP); //NON-NLS + } + + Double latitudeStart = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START); + if (latitudeStart != null) { + result.append("Latitude Start: ").append(latitudeStart).append(SEP); //NON-NLS + } + + Double longitudeStart = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_START); + if (longitudeStart != null) { + result.append("Longitude Start: ").append(longitudeStart).append(SEP); //NON-NLS + } + + Double latitudeEnd = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END); + if (latitudeEnd != null) { + result.append("Latitude End: ").append(latitudeEnd).append(SEP); //NON-NLS + } + + Double longitudeEnd = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END); + if (longitudeEnd != null) { + result.append("Longitude End: ").append(longitudeEnd).append(SEP); //NON-NLS + } + + Double velocity = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_VELOCITY); + if (velocity != null) { + result.append("Velocity: ").append(velocity).append(SEP); //NON-NLS + } + + Double altitude = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE); + if (altitude != null) { + result.append("Altitude: ").append(altitude).append(SEP); //NON-NLS + } + + Double bearing = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_BEARING); + if (bearing != null) { + result.append("Bearing: ").append(bearing).append(SEP); //NON-NLS + } + + Integer hPrecision = getInteger(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_HPRECISION); + if (hPrecision != null) { + result.append("Horizontal Precision Figure of Merit: ").append(hPrecision).append(SEP); //NON-NLS + } + + Integer vPrecision = getInteger(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_VPRECISION); + if (vPrecision != null) { + result.append("Vertical Precision Figure of Merit: ").append(vPrecision).append(SEP); //NON-NLS + } + + String mapDatum = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_MAPDATUM); + if (mapDatum != null && !mapDatum.isEmpty()) { + result.append("Map Datum: ").append(mapDatum).append(SEP); //NON-NLS + } + + String programName = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME); + if (programName != null && !programName.isEmpty()) { + result.append("Reported by: ").append(programName).append(SEP); //NON-NLS + } + + String flag = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FLAG); + if (flag != null && !flag.isEmpty()) { + result.append("Flag: ").append(flag).append(SEP); //NON-NLS + } + + String pathSource = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_SOURCE); + if (pathSource != null && !pathSource.isEmpty()) { + result.append("Source: ").append(pathSource).append(SEP); //NON-NLS + } + + String deviceMake = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MAKE); + if (deviceMake != null && !deviceMake.isEmpty()) { + result.append("Device Make: ").append(deviceMake).append(SEP); //NON-NLS + } + + String deviceModel = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MODEL); + if (deviceModel != null && !deviceModel.isEmpty()) { + result.append("Device Model: ").append(deviceModel).append(SEP); //NON-NLS + } + + return result.toString(); + } + + private String getTimeStamp(long timeStamp) { + return kmlDateFormat.format(new java.util.Date(timeStamp * 1000)); + } + + /** + * Create a Point for use in a Placemark. Note in this method altitude is + * ignored, as Google Earth apparently has trouble using altitudes for + * LineStrings, though the parameters are still in the call. Also note that + * any null value passed in will be set to 0.0, under the idea that it is + * better to show some data with gaps, than to show nothing at all. + * + * @param latitude point latitude + * @param longitude point longitude + * @param altitude point altitude. Currently ignored. + * + * @return the Point as an Element + */ + private Element makePoint(Double latitude, Double longitude, Double altitude) { + if (latitude == null) { + latitude = 0.0; + } + if (longitude == null) { + longitude = 0.0; + } + if (altitude == null) { + altitude = 0.0; + } + Element point = new Element("Point", ns); //NON-NLS + + // KML uses lon, lat. Deliberately reversed. + Element coordinates = new Element("coordinates", ns).addContent(longitude + "," + latitude + "," + altitude); //NON-NLS + + if (altitude != 0) { + /* + Though we are including a non-zero altitude, clamp it to the + ground because inaccuracies from the GPS data can cause the terrain + to occlude points when zoomed in otherwise. Show the altitude, but + keep the point clamped to the ground. We may change this later for + flying GPS sensors. + */ + Element altitudeMode = new Element("altitudeMode", ns).addContent("clampToGround"); //NON-NLS + point.addContent(altitudeMode); + } + point.addContent(coordinates); + + return point; + } + + /** + * Create a LineString for use in a Placemark. Note in this method, start + * and stop altitudes get ignored, as Google Earth apparently has trouble + * using altitudes for LineStrings, though the parameters are still in the + * call. Also note that any null value passed in will be set to 0.0, under + * the idea that it is better to show some data with gaps, than to show + * nothing at all. + * + * @param startLatitude Starting latitude + * @param startLongitude Starting longitude + * @param startAltitude Starting altitude. Currently ignored. + * @param stopLatitude Ending latitude + * @param stopLongitude Ending longitude + * @param stopAltitude Ending altitude. Currently ignored. + * + * @return the Line as an Element + */ + private Element makeLineString(Double startLatitude, Double startLongitude, Double startAltitude, Double stopLatitude, Double stopLongitude, Double stopAltitude) { + if (startLatitude == null) { + startLatitude = 0.0; + } + if (startLongitude == null) { + startLongitude = 0.0; + } + if (startAltitude == null) { + startAltitude = 0.0; + } + if (stopLatitude == null) { + stopLatitude = 0.0; + } + if (stopLongitude == null) { + stopLongitude = 0.0; + } + if (stopAltitude == null) { + stopAltitude = 0.0; + } + + Element lineString = new Element("LineString", ns); //NON-NLS + lineString.addContent(new Element("extrude", ns).addContent("1")); //NON-NLS + lineString.addContent(new Element("tessellate", ns).addContent("1")); //NON-NLS + lineString.addContent(new Element("altitudeMode", ns).addContent("clampToGround")); //NON-NLS + // KML uses lon, lat. Deliberately reversed. + lineString.addContent(new Element("coordinates", ns).addContent( + startLongitude + "," + startLatitude + ",0.0," + + stopLongitude + "," + stopLatitude + ",0.0")); //NON-NLS + return lineString; + } + + /** + * Make a Placemark for use in displaying features. Takes a + * coordinate-bearing feature (Point, LineString, etc) and places it in the + * Placemark element. + * + * @param name Placemark name + * @param color Placemark color + * @param description Description for the info bubble on the map + * @param timestamp Placemark timestamp + * @param feature The feature to show. Could be Point, LineString, etc. + * @param coordinates The coordinates to display in the list view snippet + * + * @return the entire KML placemark + */ + private Element makePlacemark(String name, FeatureColor color, String description, Long timestamp, Element feature, String coordinates) { + Element placemark = new Element("Placemark", ns); //NON-NLS + if (name != null && !name.isEmpty()) { + placemark.addContent(new Element("name", ns).addContent(name)); //NON-NLS + } else if (timestamp != null) { + placemark.addContent(new Element("name", ns).addContent(getTimeStamp(timestamp))); //NON-NLS + } else { + placemark.addContent(new Element("name", ns).addContent("")); //NON-NLS + } + placemark.addContent(new Element("styleUrl", ns).addContent(color.getColor())); //NON-NLS + placemark.addContent(new Element("description", ns).addContent(description)); //NON-NLS + if (timestamp != null) { + Element time = new Element("TimeStamp", ns); //NON-NLS + time.addContent(new Element("when", ns).addContent(getTimeStamp(timestamp))); //NON-NLS + placemark.addContent(time); + } + placemark.addContent(feature); + if (coordinates != null && !coordinates.isEmpty()) { + placemark.addContent(new Element("snippet", ns).addContent(coordinates)); //NON-NLS + } + return placemark; + } + + /** + * Make a Placemark for use in displaying features. Takes a + * coordinate-bearing feature (Point, LineString, etc) and places it in the + * Placemark element. + * + * @param name Placemark name + * @param color Placemark color + * @param description Description for the info bubble on the map + * @param timestamp Placemark timestamp + * @param feature The feature to show. Could be Point, LineString, etc. + * @param path The path to the file on disk + * @param coordinates The coordinates to display in the list view snippet + * + * @return the entire KML Placemark, including a picture. + */ + private Element makePlacemarkWithPicture(String name, FeatureColor color, String description, Long timestamp, Element feature, Path path, String coordinates) { + Element placemark = new Element("Placemark", ns); //NON-NLS + if (name != null && !name.isEmpty()) { + placemark.addContent(new Element("name", ns).addContent(name)); //NON-NLS + } + + placemark.addContent(new Element("styleUrl", ns).addContent(color.getColor())); //NON-NLS + Element desc = new Element("description", ns); //NON-NLS + + if (path != null) { + String pathAsString = path.toString(); + if (pathAsString != null && !pathAsString.isEmpty()) { + String image = ""; //NON-NLS + desc.addContent(image); + } + } + + desc.addContent(description + "File Path: " + path.toString()); + placemark.addContent(desc); + if (timestamp != null) { + Element time = new Element("TimeStamp", ns); //NON-NLS + time.addContent(new Element("when", ns).addContent(getTimeStamp(timestamp))); //NON-NLS + placemark.addContent(time); + } + placemark.addContent(feature); + if (coordinates != null && !coordinates.isEmpty()) { + placemark.addContent(new Element("snippet", ns).addContent(coordinates)); //NON-NLS + } + return placemark; + } + + /** + * Extracts the file to the output folder. + * + * @param inputFile The input AbstractFile to copy + * @param outputFile the output file + * + * @throws IOException + */ + private void copyFileUsingStream(AbstractFile inputFile, File outputFile) throws IOException { + byte[] buffer = new byte[65536]; + int length; + outputFile.createNewFile(); + try (InputStream is = new ReadContentInputStream(inputFile); + OutputStream os = new FileOutputStream(outputFile)) { while ((length = is.read(buffer)) != -1) { os.write(buffer, 0, length); } - - } finally { - is.close(); - os.close(); } } diff --git a/Core/src/org/sleuthkit/autopsy/report/stylesheets/style.kml b/Core/src/org/sleuthkit/autopsy/report/stylesheets/style.kml new file mode 100755 index 0000000000..aeb3992df1 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/report/stylesheets/style.kml @@ -0,0 +1,299 @@ + + + + + + normal + #n_YellowPushpin + + + highlight + #h_YellowPushpin + + + + + + + normal + #n_bluePushpin + + + highlight + #h_bluePushpin + + + + + + + normal + #n_redPushpin + + + highlight + #h_redPushpin + + + + + + + normal + #n_greenPushpin + + + highlight + #h_greenPushpin + + + + + + + normal + #n_purplePushpin + + + highlight + #h_purplePushpin + + + + + + + normal + #n_whitePushpin + + + highlight + #h_whitePushpin + + + + + + \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.form b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.form index ea5a68ccdc..a05e7505ec 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.form +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.form @@ -54,7 +54,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java index 2c97d775f4..1ca594fa14 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java @@ -61,7 +61,7 @@ import org.sleuthkit.autopsy.timeline.explorernodes.EventRootNode; import org.sleuthkit.autopsy.timeline.ui.HistoryToolBar; import org.sleuthkit.autopsy.timeline.ui.StatusBar; import org.sleuthkit.autopsy.timeline.ui.TimeZonePanel; -import org.sleuthkit.autopsy.timeline.ui.VisualizationPanel; +import org.sleuthkit.autopsy.timeline.ui.ViewFrame; import org.sleuthkit.autopsy.timeline.ui.detailview.tree.EventsTree; import org.sleuthkit.autopsy.timeline.ui.filtering.FilterSetPanel; import org.sleuthkit.autopsy.timeline.zooming.ZoomSettingsPane; @@ -137,7 +137,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer LOGGER.log(Level.SEVERE, "Failed to lookup Sleuthkit object backing a SingleEvent.", ex); // NON-NLS Platform.runLater(() -> { Notifications.create() - .owner(jFXVizPanel.getScene().getWindow()) + .owner(jFXViewPanel.getScene().getWindow()) .text(Bundle.TimelineTopComponent_selectedEventListener_errorMsg()) .showError(); }); @@ -260,8 +260,8 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer final VBox leftVBox = new VBox(5, timeZonePanel, historyToolBar, zoomSettingsPane, leftTabPane); SplitPane.setResizableWithParent(leftVBox, Boolean.FALSE); - final VisualizationPanel visualizationPanel = new VisualizationPanel(controller, eventsTree); - final SplitPane mainSplitPane = new SplitPane(leftVBox, visualizationPanel); + final ViewFrame viewFrame = new ViewFrame(controller, eventsTree); + final SplitPane mainSplitPane = new SplitPane(leftVBox, viewFrame); mainSplitPane.setDividerPositions(0); final Scene scene = new Scene(mainSplitPane); @@ -278,7 +278,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer }); //add ui componenets to JFXPanels - jFXVizPanel.setScene(scene); + jFXViewPanel.setScene(scene); jFXstatusPanel.setScene(new Scene(new StatusBar(controller))); } @@ -297,7 +297,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer jFXstatusPanel = new javafx.embed.swing.JFXPanel(); splitYPane = new javax.swing.JSplitPane(); - jFXVizPanel = new javafx.embed.swing.JFXPanel(); + jFXViewPanel = new javafx.embed.swing.JFXPanel(); horizontalSplitPane = new javax.swing.JSplitPane(); leftFillerPanel = new javax.swing.JPanel(); rightfillerPanel = new javax.swing.JPanel(); @@ -308,7 +308,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer splitYPane.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT); splitYPane.setResizeWeight(0.9); splitYPane.setPreferredSize(new java.awt.Dimension(1024, 400)); - splitYPane.setLeftComponent(jFXVizPanel); + splitYPane.setLeftComponent(jFXViewPanel); horizontalSplitPane.setDividerLocation(600); horizontalSplitPane.setResizeWeight(0.5); @@ -361,7 +361,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JSplitPane horizontalSplitPane; - private javafx.embed.swing.JFXPanel jFXVizPanel; + private javafx.embed.swing.JFXPanel jFXViewPanel; private javafx.embed.swing.JFXPanel jFXstatusPanel; private javax.swing.JPanel leftFillerPanel; private javax.swing.JPanel rightfillerPanel; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java index e50e1909a8..7649b35705 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java @@ -71,7 +71,7 @@ public class SaveSnapshotAsReport extends Action { "Timeline.ModuleName=Timeline", "SaveSnapShotAsReport.action.dialogs.title=Timeline", "SaveSnapShotAsReport.action.name.text=Snapshot Report", - "SaveSnapShotAsReport.action.longText=Save a screen capture of the visualization as a report.", + "SaveSnapShotAsReport.action.longText=Save a screen capture of the current view of the timeline as a report.", "# {0} - report file path", "SaveSnapShotAsReport.ReportSavedAt=Report saved at [{0}]", "SaveSnapShotAsReport.Success=Success", diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/CombinedEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/CombinedEvent.java index 3fa3265f17..52a0488d9f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/CombinedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/CombinedEvent.java @@ -21,6 +21,7 @@ package org.sleuthkit.autopsy.timeline.datamodel; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Set; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; @@ -43,8 +44,7 @@ public class CombinedEvent { * epoch. * @param description The full description shared by all the combined events * @param fileID The ID of the file all the combined events are for. - * @param eventMap A map from EventType to the ID of the event for the - * given file ID with that type. + * @param eventMap A map from EventType to event ID. */ public CombinedEvent(long epochMillis, String description, long fileID, Map eventMap) { this.epochMillis = epochMillis; @@ -105,7 +105,45 @@ public class CombinedEvent { * * @return An arbitrary representative event ID for the combined events. */ - public Long getRepresentitiveEventID() { + public Long getRepresentativeEventID() { return eventTypeMap.values().stream().findFirst().get(); } + + @Override + public int hashCode() { + int hash = 3; + hash = 53 * hash + (int) (this.fileID ^ (this.fileID >>> 32)); + hash = 53 * hash + (int) (this.epochMillis ^ (this.epochMillis >>> 32)); + hash = 53 * hash + Objects.hashCode(this.description); + hash = 53 * hash + Objects.hashCode(this.eventTypeMap); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final CombinedEvent other = (CombinedEvent) obj; + if (this.fileID != other.fileID) { + return false; + } + if (this.epochMillis != other.epochMillis) { + return false; + } + if (!Objects.equals(this.description, other.description)) { + return false; + } + if (!Objects.equals(this.eventTypeMap, other.eventTypeMap)) { + return false; + } + return true; + } + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/RefreshRequestedEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/events/RefreshRequestedEvent.java index 47b04eb479..903cb55178 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/events/RefreshRequestedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/events/RefreshRequestedEvent.java @@ -20,7 +20,7 @@ package org.sleuthkit.autopsy.timeline.events; /** * A "local" event published by filteredEventsModel to indicate that the user - * requested that the current visualization be refreshed with out changing any + * requested that the current view be refreshed with out changing any * of the parameters ( to include more up to date tag data for example.) * * This event is not intended for use out side of the Timeline module. diff --git a/Core/src/org/sleuthkit/autopsy/timeline/snapshot/SnapShotReportWriter.java b/Core/src/org/sleuthkit/autopsy/timeline/snapshot/SnapShotReportWriter.java index 75377ff5bb..917d2a0881 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/snapshot/SnapShotReportWriter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/snapshot/SnapShotReportWriter.java @@ -70,7 +70,7 @@ public class SnapShotReportWriter { * @param zoomParams The ZoomParams in effect when the snapshot was * taken. * @param generationDate The generation Date of the report. - * @param snapshot A snapshot of the visualization to include in the + * @param snapshot A snapshot of the view to include in the * report. */ public SnapShotReportWriter(Case currentCase, Path reportFolderPath, String reportName, ZoomParams zoomParams, Date generationDate, BufferedImage snapshot) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimeLineView.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimeLineView.java index faf55aef15..86fa7ec132 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimeLineView.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimeLineView.java @@ -136,7 +136,7 @@ public abstract class AbstractTimeLineView extends BorderPane { /** * Refresh this view based on current state of zoom / filters. Primarily - * this invokes the background VisualizationUpdateTask returned by + * this invokes the background ViewRefreshTask returned by * getUpdateTask(), which derived classes must implement. * * TODO: replace this logic with a javafx Service ? -jm @@ -291,11 +291,11 @@ public abstract class AbstractTimeLineView extends BorderPane { * @throws Exception If there is an unhandled exception during the * background operation */ - @NbBundle.Messages(value = {"VisualizationUpdateTask.preparing=Analyzing zoom and filter settings"}) + @NbBundle.Messages(value = {"ViewRefreshTask.preparing=Analyzing zoom and filter settings"}) @Override protected Boolean call() throws Exception { updateProgress(-1, 1); - updateMessage(Bundle.VisualizationUpdateTask_preparing()); + updateMessage(Bundle.ViewRefreshTask_preparing()); Platform.runLater(() -> { MaskerPane maskerPane = new MaskerPane(); maskerPane.textProperty().bind(messageProperty()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimelineChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimelineChart.java index fc2ee5ca63..38f05a9bfc 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimelineChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimelineChart.java @@ -57,7 +57,7 @@ import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; /** - * Abstract base class for TimeLineChart based visualizations. + * Abstract base class for TimeLineChart based views. * * @param The type of data plotted along the x axis * @param The type of data plotted along the y axis @@ -74,12 +74,12 @@ public abstract class AbstractTimelineChart of the nodes that are selected in - * this visualization. + * this view. */ protected ObservableList getSelectedNodes() { return selectedNodes; @@ -127,18 +127,18 @@ public abstract class AbstractTimelineChart getXAxis(); /** - * Get the Y-Axis of this Visualization's chart + * Get the Y-Axis of this view's chart * - * @return The vertical axis used by this Visualization's chart + * @return The vertical axis used by this view's chart */ abstract protected Axis getYAxis(); @@ -252,7 +252,7 @@ public abstract class AbstractTimelineChart -