19 package org.sleuthkit.autopsy.timeline.ui.countsview;
 
   21 import com.google.common.collect.ImmutableList;
 
   22 import com.google.common.collect.Lists;
 
   23 import java.util.List;
 
   25 import java.util.function.Function;
 
   26 import javafx.application.Platform;
 
   27 import javafx.beans.Observable;
 
   28 import javafx.beans.binding.BooleanBinding;
 
   29 import javafx.beans.property.SimpleObjectProperty;
 
   30 import javafx.collections.FXCollections;
 
   31 import javafx.concurrent.Task;
 
   32 import javafx.fxml.FXML;
 
   33 import javafx.geometry.Insets;
 
   34 import javafx.scene.Cursor;
 
   35 import javafx.scene.Node;
 
   36 import javafx.scene.chart.CategoryAxis;
 
   37 import javafx.scene.chart.NumberAxis;
 
   38 import javafx.scene.chart.XYChart;
 
   39 import javafx.scene.control.Label;
 
   40 import javafx.scene.control.RadioButton;
 
   41 import javafx.scene.control.ToggleGroup;
 
   42 import javafx.scene.control.Tooltip;
 
   43 import javafx.scene.image.Image;
 
   44 import javafx.scene.image.ImageView;
 
   45 import javafx.scene.layout.BorderPane;
 
   46 import javafx.scene.layout.HBox;
 
   47 import javafx.scene.layout.Pane;
 
   48 import javafx.scene.text.Font;
 
   49 import javafx.scene.text.FontPosture;
 
   50 import javafx.scene.text.FontWeight;
 
   51 import javafx.scene.text.Text;
 
   52 import javafx.scene.text.TextFlow;
 
   53 import org.controlsfx.control.PopOver;
 
   54 import org.joda.time.Interval;
 
   55 import org.openide.util.NbBundle;
 
   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})"})
 
  122         dateAxis.getTickMarks().addListener((Observable tickMarks) -> 
layoutDateLabels());
 
  123         dateAxis.categorySpacingProperty().addListener((Observable spacing) -> 
layoutDateLabels());
 
  124         dateAxis.getCategories().addListener((Observable categories) -> 
layoutDateLabels());
 
  127         BooleanBinding scaleIsLinear = scaleProp.isEqualTo(
Scale.
LINEAR);
 
  128         countAxis.tickLabelsVisibleProperty().bind(scaleIsLinear);
 
  129         countAxis.tickMarkVisibleProperty().bind(scaleIsLinear);
 
  130         countAxis.minorTickVisibleProperty().bind(scaleIsLinear);
 
  131         scaleProp.addListener(scale -> {
 
  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> {
 
  205         LINEAR(Bundle.ScaleType_Linear()) {
 
  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;
 
  226             this.displayName = displayName;
 
  241         return dateAxis.getStartMargin() + dateAxis.getEndMargin();
 
  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) -> {
 
  283                 if (newToggle == linearRadio) {
 
  285                 } 
else if (newToggle == logRadio) {
 
  286                     scaleProp.set(Scale.LOGARITHMIC);
 
  289             logRadio.setSelected(
true);
 
  292             logImageView.setCursor(Cursor.HAND);
 
  293             logImageView.setOnMouseClicked(clicked -> {
 
  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(),
 
  301                         logImageView.getImage(),
 
  302                         new TextFlow(text, text2, text3));
 
  306             linearImageView.setCursor(Cursor.HAND);
 
  307             linearImageView.setOnMouseClicked(clicked -> {
 
  308                 Text text = 
new Text(Bundle.CountsViewPane_scaleHelpLinear());
 
  309                 text.setWrappingWidth(480);  
 
  311                         Bundle.CountsViewPane_linearRadio_text(),
 
  312                         linearImageView.getImage(), text);
 
  319         CountsViewSettingsPane() {
 
  320             FXMLConstructor.construct(
this, 
"CountsViewSettingsPane.fxml"); 
 
  337     private static void showPopoverHelp(
final Node owner, 
final String headerText, 
final Image headerImage, 
final Node content) {
 
  338         Pane borderPane = 
new BorderPane(null, null, 
new ImageView(headerImage),
 
  340                 new Label(headerText));
 
  341         borderPane.setPadding(
new Insets(10));
 
  342         borderPane.setPrefWidth(500);
 
  344         PopOver popOver = 
new PopOver(borderPane);
 
  345         popOver.setDetachable(
false);
 
  346         popOver.setArrowLocation(PopOver.ArrowLocation.TOP_CENTER);
 
  357         "CountsViewPane.loggedTask.name=Updating Counts View",
 
  358         "CountsViewPane.loggedTask.updatingCounts=Populating view"})
 
  362             super(Bundle.CountsViewPane_loggedTask_name(), 
true);
 
  372         protected Boolean 
call() throws Exception {
 
  384             resetView(Lists.transform(intervals, interval -> interval.getStart().toString(rangeInfo.
getTickFormatter())));
 
  386             updateMessage(Bundle.CountsViewPane_loggedTask_updatingCounts());
 
  388             int numIntervals = intervals.size();
 
  389             Scale activeScale = scaleProp.get();
 
  397             for (
int i = 0; i < numIntervals; i++) {
 
  401                 updateProgress(i, numIntervals);
 
  402                 final Interval interval = intervals.get(i);
 
  403                 int maxPerInterval = 0;
 
  406                 Map<TimelineEventType, Long> eventCounts = eventsModel.
getEventCounts(interval);
 
  414                     final Long count = eventCounts.get(eventType);
 
  416                         final String intervalCategory = interval.getStart().toString(rangeInfo.
getTickFormatter());
 
  417                         final double adjustedCount = activeScale.apply(count);
 
  419                         final XYChart.Data<String, Number> dataItem
 
  420                                 = 
new XYChart.Data<>(intervalCategory, adjustedCount,
 
  421                                         new EventCountsChart.ExtraData(interval, eventType, count));
 
  422                         Platform.runLater(() -> 
getSeries(eventType).getData().add(dataItem));
 
  423                         maxPerInterval += adjustedCount;
 
  426                 chartMax = Math.max(chartMax, maxPerInterval);
 
  430             double countAxisUpperbound = 1 + chartMax * 1.2;
 
  432                     ? Math.pow(10, Math.max(0, Math.floor(Math.log10(chartMax)) - 1))
 
  435             Platform.runLater(() -> {
 
  436                 countAxis.setTickUnit(tickUnit);
 
  437                 countAxis.setUpperBound(countAxisUpperbound);
 
  445             dateAxis.getCategories().setAll(categories);
 
Task< Boolean > getNewUpdateTask()
final Map< TimelineEventType, XYChart.Series< X, Y > > eventTypeToSeriesMap
DateTimeFormatter getTickFormatter()
final CategoryAxis dateAxis
void syncAxisScaleLabel()
boolean hasCustomTimeNavigationControls()
EventsModel getEventsModel()
static Tooltip getDefaultTooltip()
final synchronized void refresh()
final NumberAxis countAxis
ImmutableList< Node > getSettingsControls()
final ViewMode getViewMode()
final XYChart.Series< X, Y > getSeries(final TimelineEventType eventType)
ObservableList< NodeType > getSelectedNodes()
ImmutableList< Node > getTimeNavigationControls()
final ObservableList< XYChart.Series< X, Y > > dataSeries
final SimpleObjectProperty< Scale > scaleProp
Boolean isTickBold(String value)
String getTickMarkLabel(String labelValueString)
final CategoryAxis getXAxis()
final NumberAxis getYAxis()
final TimeLineController controller
static DateTimeZone getJodaTimeZone()
synchronized List< Interval > getIntervals(DateTimeZone tz)
static final Logger logger
static void showPopoverHelp(final Node owner, final String headerText, final Image headerImage, final Node content)
void setDateValues(List< String > categories)
synchronized static Logger getLogger(String name)
Scale(String displayName)
CountsViewPane(TimeLineController controller)
final void createSeries()
ImageView linearImageView
void applySelectionEffect(Node c1, Boolean applied)
synchronized void layoutDateLabels()
void setChart(ChartType chart)
synchronized Interval getTimeRange()
static RangeDivision getRangeDivision(Interval timeRange, DateTimeZone timeZone)
Map< TimelineEventType, Long > getEventCounts(Interval timeRange)