Package org.sleuthkit.autopsy.timeline.ui.countsview

Source Code of org.sleuthkit.autopsy.timeline.ui.countsview.CountsViewPane$CountsViewSettingsPane

/*
* 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.ArrayList;
import java.util.Arrays;
import java.util.Map;
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 javax.swing.JOptionPane;
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.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.ColorUtilities;
import org.sleuthkit.autopsy.coreutils.LoggedTask;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.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.events.type.EventType;
import org.sleuthkit.autopsy.timeline.events.type.RootEventType;
import org.sleuthkit.autopsy.timeline.ui.AbstractVisualization;
import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo;

/**
* 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.LOGARITHMIC);

    //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);
                } else {

                    int showConfirmDialog = JOptionPane.showConfirmDialog(null,
                            NbBundle.getMessage(CountsViewPane.class, "CountsViewPane.detailSwitchMessage"),
                            NbBundle.getMessage(CountsViewPane.class, "CountsViewPane.detailSwitchTitle"), JOptionPane.YES_NO_OPTION);
                    if (showConfirmDialog == JOptionPane.YES_OPTION) {
                        controller.setViewMode(VisualizationMode.DETAIL);
                    }

                    /* //I would like to use the JAvafx dialog, but it doesn't
                     * block the ui (because it is embeded in a TopComponent)
                     * -jm
                     *
                     * final Dialogs.CommandLink yes = new
                     * Dialogs.CommandLink("Yes", "switch to Details view");
                     * final Dialogs.CommandLink no = new
                     * Dialogs.CommandLink("No", "return to Counts view with a
                     * resolution of Seconds");
                     * Action choice = Dialogs.create()
                     * .title("Switch to Details View?")
                     * .masthead("There is no temporal resolution smaller than
                     * Seconds.")
                     * .message("Would you like to switch to the Details view
                     * instead?")
                     * .showCommandLinks(Arrays.asList(yes, no));
                     *
                     * if (choice == yes) {
                     * controller.setViewMode(VisualizationMode.DETAIL);
                     * } */
                }
            }
        }
    }

    private class CountsViewSettingsPane extends HBox {

        @FXML
        private RadioButton logRadio;

        @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 linearRadio != null : "fx:id=\"linearRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'.";
            logRadio.setSelected(true);
            scaleGroup.selectedToggleProperty().addListener(observable -> {
                if (scaleGroup.getSelectedToggle() == linearRadio) {
                    scale.set(ScaleType.LINEAR);
                }
                if (scaleGroup.getSelectedToggle() == logRadio) {
                    scale.set(ScaleType.LOGARITHMIC);
                }
            });
        }

        public CountsViewSettingsPane() {
            FXMLConstructor.construct(this, "CountsViewSettingsPane.fxml");
        }
    }

    private static enum ScaleType {

        LINEAR(t -> t.doubleValue()),
        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);
        }
    }
}
TOP

Related Classes of org.sleuthkit.autopsy.timeline.ui.countsview.CountsViewPane$CountsViewSettingsPane

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.