19package org.sleuthkit.autopsy.timeline.ui.countsview;
21import com.google.common.collect.ImmutableList;
22import com.google.common.collect.Lists;
25import java.util.function.Function;
26import javafx.application.Platform;
27import javafx.beans.Observable;
28import javafx.beans.binding.BooleanBinding;
29import javafx.beans.property.SimpleObjectProperty;
30import javafx.collections.FXCollections;
31import javafx.concurrent.Task;
32import javafx.fxml.FXML;
33import javafx.geometry.Insets;
34import javafx.scene.Cursor;
35import javafx.scene.Node;
36import javafx.scene.chart.CategoryAxis;
37import javafx.scene.chart.NumberAxis;
38import javafx.scene.chart.XYChart;
39import javafx.scene.control.Label;
40import javafx.scene.control.RadioButton;
41import javafx.scene.control.ToggleGroup;
42import javafx.scene.control.Tooltip;
43import javafx.scene.image.Image;
44import javafx.scene.image.ImageView;
45import javafx.scene.layout.BorderPane;
46import javafx.scene.layout.HBox;
47import javafx.scene.layout.Pane;
48import javafx.scene.text.Font;
49import javafx.scene.text.FontPosture;
50import javafx.scene.text.FontWeight;
51import javafx.scene.text.Text;
52import javafx.scene.text.TextFlow;
53import org.controlsfx.control.PopOver;
54import org.joda.time.Interval;
55import org.openide.util.NbBundle;
56import org.sleuthkit.autopsy.coreutils.Logger;
57import org.sleuthkit.autopsy.coreutils.ThreadConfined;
58import org.sleuthkit.autopsy.timeline.FXMLConstructor;
59import org.sleuthkit.autopsy.timeline.EventsModel;
60import org.sleuthkit.autopsy.timeline.TimeLineController;
61import org.sleuthkit.autopsy.timeline.ViewMode;
62import org.sleuthkit.autopsy.timeline.ui.AbstractTimelineChart;
63import org.sleuthkit.autopsy.timeline.utils.RangeDivision;
64import org.sleuthkit.datamodel.TimelineEventType;
86 private final NumberAxis
countAxis =
new NumberAxis();
87 private final CategoryAxis
dateAxis =
new CategoryAxis(FXCollections.<String>observableArrayList());
93 return labelValueString;
98 return dataSeries.stream().flatMap(series -> series.getData().stream())
99 .anyMatch(data -> data.getXValue().equals(value) && data.getYValue().intValue() > 0);
113 "# {0} - scale name",
114 "CountsViewPane.numberOfEvents=Number of Events ({0})"})
128 countAxis.tickLabelsVisibleProperty().bind(scaleIsLinear);
129 countAxis.tickMarkVisibleProperty().bind(scaleIsLinear);
130 countAxis.minorTickVisibleProperty().bind(scaleIsLinear);
150 return dateAxis.getCategorySpacing();
155 c1.setEffect(applied ?
getChart().getSelectionEffect() :
null);
161 for (XYChart.Series<String, Number> series :
dataSeries) {
162 series.getData().clear();
186 return ImmutableList.of();
194 countAxis.setLabel(Bundle.CountsViewPane_numberOfEvents(
scaleProp.get().getDisplayName()));
201 "ScaleType.Linear=Linear",
202 "ScaleType.Logarithmic=Logarithmic"})
203 private static enum Scale implements Function<Long, Double> {
207 public Double apply(Long inValue) {
208 return inValue.doubleValue();
211 LOGARITHMIC(Bundle.ScaleType_Logarithmic()) {
213 public Double apply(Long inValue) {
214 return Math.log10(inValue) + 1;
248 private class CountsViewSettingsPane
extends HBox {
267 "CountsViewPane.logRadio.text=Logarithmic",
268 "CountsViewPane.scaleLabel.text=Scale:",
269 "CountsViewPane.scaleHelp.label.text=Scales: ",
270 "CountsViewPane.linearRadio.text=Linear",
271 "CountsViewPane.scaleHelpLinear=The linear scale is good for many use cases. When this scale is selected, the height of the bars represents the counts in a linear, one-to-one fashion, and the y-axis is labeled with values. When the range of values is very large, time periods with low counts may have a bar that is too small to see. To help the user detect this, the labels for date ranges with events are bold. To see bars that are too small, there are three options: adjust the window size so that the timeline has more vertical space, adjust the time range shown so that time periods with larger bars are excluded, or adjust the scale setting to logarithmic.",
272 "CountsViewPane.scaleHelpLog=The logarithmic scale represents the number of events in a non-linear way that compresses the difference between large and small numbers. Note that even with the logarithmic scale, an extremely large difference in counts may still produce bars too small to see. In this case the only option may be to filter events to reduce the difference in counts. NOTE: Because the logarithmic scale is applied to each event type separately, the meaning of the height of the combined bar is not intuitive, and to emphasize this, no labels are shown on the y-axis with the logarithmic scale. The logarithmic scale should be used to quickly compare the counts ",
273 "CountsViewPane.scaleHelpLog2=across time within a type, or across types for one time period, but not both.",
274 "CountsViewPane.scaleHelpLog3= The actual counts (available in tooltips or the result viewer) should be used for absolute comparisons. Use the logarithmic scale with care."})
276 assert
logRadio != null :
"fx:id=\"logRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'.";
277 assert
linearRadio != null :
"fx:id=\"linearRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'.";
278 scaleLabel.setText(Bundle.CountsViewPane_scaleLabel_text());
279 linearRadio.setText(Bundle.CountsViewPane_linearRadio_text());
280 logRadio.setText(Bundle.CountsViewPane_logRadio_text());
282 scaleGroup.selectedToggleProperty().addListener((observable, oldToggle, newToggle) -> {
294 Text text =
new Text(Bundle.CountsViewPane_scaleHelpLog());
295 Text text2 =
new Text(Bundle.CountsViewPane_scaleHelpLog2());
296 Font baseFont = text.getFont();
297 text2.setFont(Font.font(baseFont.getFamily(), FontWeight.BOLD, FontPosture.ITALIC, baseFont.getSize()));
298 Text text3 =
new Text(Bundle.CountsViewPane_scaleHelpLog3());
300 Bundle.CountsViewPane_logRadio_text(),
302 new TextFlow(text, text2, text3));
308 Text text =
new Text(Bundle.CountsViewPane_scaleHelpLinear());
309 text.setWrappingWidth(480);
311 Bundle.CountsViewPane_linearRadio_text(),
319 @SuppressWarnings(
"this-escape")
320 CountsViewSettingsPane() {
321 FXMLConstructor.construct(
this,
"CountsViewSettingsPane.fxml");
338 private static void showPopoverHelp(
final Node owner,
final String headerText,
final Image headerImage,
final Node content) {
339 Pane borderPane =
new BorderPane(
null,
null,
new ImageView(headerImage),
341 new Label(headerText));
342 borderPane.setPadding(
new Insets(10));
343 borderPane.setPrefWidth(500);
345 PopOver popOver =
new PopOver(borderPane);
346 popOver.setDetachable(
false);
347 popOver.setArrowLocation(PopOver.ArrowLocation.TOP_CENTER);
358 "CountsViewPane.loggedTask.name=Updating Counts View",
359 "CountsViewPane.loggedTask.updatingCounts=Populating view"})
363 super(Bundle.CountsViewPane_loggedTask_name(),
true);
373 protected Boolean
call() throws Exception {
387 updateMessage(Bundle.CountsViewPane_loggedTask_updatingCounts());
389 int numIntervals = intervals.size();
398 for (
int i = 0; i < numIntervals; i++) {
402 updateProgress(i, numIntervals);
403 final Interval interval = intervals.get(i);
404 int maxPerInterval = 0;
407 Map<TimelineEventType, Long> eventCounts = eventsModel.
getEventCounts(interval);
410 for (
final TimelineEventType eventType : eventCounts.keySet()) {
415 final Long count = eventCounts.get(eventType);
417 final String intervalCategory = interval.getStart().toString(rangeInfo.
getTickFormatter());
418 final double adjustedCount = activeScale.apply(count);
420 final XYChart.Data<String, Number> dataItem
421 =
new XYChart.Data<>(intervalCategory, adjustedCount,
422 new EventCountsChart.ExtraData(interval, eventType, count));
423 Platform.runLater(() ->
getSeries(eventType).getData().add(dataItem));
424 maxPerInterval += adjustedCount;
427 chartMax = Math.max(chartMax, maxPerInterval);
431 double countAxisUpperbound = 1 + chartMax * 1.2;
433 ? Math.pow(10, Math.max(0, Math.floor(Math.log10(chartMax)) - 1))
436 Platform.runLater(() -> {
438 countAxis.setUpperBound(countAxisUpperbound);
446 dateAxis.getCategories().setAll(categories);
synchronized static Logger getLogger(String name)
Map< TimelineEventType, Long > getEventCounts(Interval timeRange)
synchronized Interval getTimeRange()
static DateTimeZone getJodaTimeZone()
ViewRefreshTask(String taskName, boolean logStateChanges)
void resetView(AxisValuesType axisValues)
EventsModel getEventsModel()
final TimeLineController controller
final synchronized void refresh()
final ObservableList< XYChart.Series< X, Y > > dataSeries
ObservableList< NodeType > getSelectedNodes()
final void createSeries()
static Tooltip getDefaultTooltip()
AbstractTimelineChart(TimeLineController controller)
synchronized void layoutDateLabels()
void setChart(ChartType chart)
final Map< TimelineEventType, XYChart.Series< X, Y > > eventTypeToSeriesMap
final XYChart.Series< X, Y > getSeries(final TimelineEventType eventType)
void setDateValues(List< String > categories)
ImageView linearImageView
final NumberAxis countAxis
boolean hasCustomTimeNavigationControls()
void syncAxisScaleLabel()
static final Logger logger
final ViewMode getViewMode()
final CategoryAxis getXAxis()
String getTickMarkLabel(String labelValueString)
final SimpleObjectProperty< Scale > scaleProp
static void showPopoverHelp(final Node owner, final String headerText, final Image headerImage, final Node content)
void applySelectionEffect(Node c1, Boolean applied)
Task< Boolean > getNewUpdateTask()
final NumberAxis getYAxis()
Boolean isTickBold(String value)
final CategoryAxis dateAxis
ImmutableList< Node > getTimeNavigationControls()
CountsViewPane(TimeLineController controller)
ImmutableList< Node > getSettingsControls()
synchronized List< Interval > getIntervals(DateTimeZone tz)
DateTimeFormatter getTickFormatter()
static RangeDivision getRangeDivision(Interval timeRange, DateTimeZone timeZone)
Scale(String displayName)