diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java index e2b8771ad2..e41623cbd2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -88,6 +88,7 @@ import org.sleuthkit.autopsy.timeline.filters.TypeFilter; import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; 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.datamodel.Content; @@ -433,9 +434,9 @@ public class TimeLineController { /** * Show the entire range of the timeline. */ - public void showFullRange() { + public boolean showFullRange() { synchronized (filteredEvents) { - pushTimeRange(filteredEvents.getSpanningInterval()); + return pushTimeRange(filteredEvents.getSpanningInterval()); } } @@ -652,6 +653,22 @@ public class TimeLineController { } } + /** + * Change the view by setting a new time range that is the length of + * timeUnit and centered at the current center. + * + * @param timeUnit The unit of time to view + * + * @return true if the view actually changed. + */ + synchronized public boolean pushTimeUnit(TimeUnits timeUnit) { + if (timeUnit == TimeUnits.FOREVER) { + return showFullRange(); + } else { + return pushTimeRange(IntervalUtils.getIntervalAroundMiddle(filteredEvents.getTimeRange(), timeUnit.getPeriod())); + } + } + @NbBundle.Messages({"# {0} - the number of events", "Timeline.pushDescrLOD.confdlg.msg=You are about to show details for {0} events. This might be very slow or even crash Autopsy.\n\nDo you want to continue?", "Timeline.pushDescrLOD.confdlg.title=Change description level of detail?"}) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java index d53316ef4d..b9d4f85447 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java @@ -167,8 +167,9 @@ public final class FilteredEventsModel { } /** - * @return a read only view of the time range requested via - * {@link #requestTimeRange(org.joda.time.Interval)} + * Get a read only view of the time range currently in view. + * + * @return A read only view of the time range currently in view. */ synchronized public ReadOnlyObjectProperty timeRangeProperty() { if (requestedTimeRange.get() == null) { @@ -189,6 +190,15 @@ public final class FilteredEventsModel { return requestedTypeZoom.getReadOnlyProperty(); } + /** + * The time range currently in view. + * + * @return The time range currently in view. + */ + synchronized public Interval getTimeRange() { + return timeRangeProperty().get(); + } + synchronized public DescriptionLoD getDescriptionLOD() { return requestedLOD.get(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/utils/IntervalUtils.java b/Core/src/org/sleuthkit/autopsy/timeline/utils/IntervalUtils.java index c53e57e7a9..7cc8205c65 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/utils/IntervalUtils.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/utils/IntervalUtils.java @@ -67,4 +67,19 @@ public class IntervalUtils { final Interval newInterval = new Interval(middleOf.minus(halfRange), middleOf.plus(halfRange)); return newInterval; } + + /** + * Get an interval the length of the given period, centered around the + * center of the given interval. + * + * @param interval The interval whose center will be the center of the new + * interval. + * @param period The length of the new interval + * + * @return An interval the length of the given period, centered around the + * center of the given interval. + */ + static public Interval getIntervalAroundMiddle(Interval interval, ReadablePeriod period) { + return getIntervalAround(middleOf(interval), period); + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/zooming/Bundle.properties index 4e6e8c45bf..432a15dc7c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/Bundle.properties @@ -1,6 +1,4 @@ DescriptionLOD.short=Short DescriptionLOD.medium=Medium DescriptionLOD.full=Full -EventTypeZoomLevel.rootType=Root Type -EventTypeZoomLevel.baseType=Base Type -EventTypeZoomLevel.subType=Sub Type + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLoD.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLoD.java index 50d612b210..b9fb9116ca 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLoD.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLoD.java @@ -23,7 +23,7 @@ import org.openide.util.NbBundle; /** * Enumeration of all description levels of detail. */ -public enum DescriptionLoD { +public enum DescriptionLoD implements DisplayNameProvider { SHORT(NbBundle.getMessage(DescriptionLoD.class, "DescriptionLOD.short")), MEDIUM(NbBundle.getMessage(DescriptionLoD.class, "DescriptionLOD.medium")), diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/DisplayNameProvider.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DisplayNameProvider.java new file mode 100644 index 0000000000..1dc7654ac6 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DisplayNameProvider.java @@ -0,0 +1,32 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2016 Basis Technology Corp. + * Contact: carrier sleuthkit 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; + +/** + * An interface for objects with a display name + */ +interface DisplayNameProvider { + + /** + * Get the display name of this object + * + * @return The display name. + */ + String getDisplayName(); +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/EventTypeZoomLevel.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/EventTypeZoomLevel.java index bbe14e2031..0f4b1aeb6b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/EventTypeZoomLevel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/EventTypeZoomLevel.java @@ -21,15 +21,20 @@ package org.sleuthkit.autopsy.timeline.zooming; import org.openide.util.NbBundle; /** - * + * Enum of event type zoom levels * */ -public enum EventTypeZoomLevel { +@NbBundle.Messages({ + "EventTypeZoomLevel.rootType=Root Type", + "EventTypeZoomLevel.baseType=Base Type", + "EventTypeZoomLevel.subType=Sub Type"}) +public enum EventTypeZoomLevel implements DisplayNameProvider { - ROOT_TYPE(NbBundle.getMessage(EventTypeZoomLevel.class, "EventTypeZoomLevel.rootType")), BASE_TYPE( - NbBundle.getMessage(EventTypeZoomLevel.class, "EventTypeZoomLevel.baseType")), SUB_TYPE( - NbBundle.getMessage(EventTypeZoomLevel.class, "EventTypeZoomLevel.subType")); + ROOT_TYPE(Bundle.EventTypeZoomLevel_rootType()), + BASE_TYPE(Bundle.EventTypeZoomLevel_baseType()), + SUB_TYPE(Bundle.EventTypeZoomLevel_subType()); + @Override public String getDisplayName() { return displayName; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/TimeUnits.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/TimeUnits.java index 8ac7ba69f4..a40a32837c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/TimeUnits.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/TimeUnits.java @@ -30,7 +30,7 @@ import org.joda.time.Years; /** * predefined units of time for use in choosing axis labels and sub intervals. */ -public enum TimeUnits { +public enum TimeUnits implements DisplayNameProvider { FOREVER(null, ChronoUnit.FOREVER), YEARS(Years.ONE.toPeriod(), ChronoUnit.YEARS), @@ -88,7 +88,8 @@ public enum TimeUnits { this.cu = cu; } - String getDisplayName() { + @Override + public String getDisplayName() { return toString(); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java index a8f5b77e4c..59e74248f3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,8 @@ */ package org.sleuthkit.autopsy.timeline.zooming; -import java.time.temporal.ChronoUnit; +import java.util.function.Consumer; +import java.util.function.Function; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.property.ReadOnlyObjectProperty; @@ -32,37 +33,33 @@ import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.VisualizationMode; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; /** * A Panel that acts as a view for a given * TimeLineController/FilteredEventsModel. It has sliders to provide - * context/control over three axes of zooming (timescale, event hierarchy, and - * description detail). + * context/control over three axes of zooming (timescale, event hierarchy level, + * and description level of detail). */ public class ZoomSettingsPane extends TitledPane { + @FXML + private Label zoomLabel; + + @FXML + private Label descrLODLabel; @FXML private Slider descrLODSlider; + @FXML + private Label typeZoomLabel; @FXML private Slider typeZoomSlider; - @FXML - private Slider timeUnitSlider; - - @FXML - private Label descrLODLabel; - - @FXML - private Label typeZoomLabel; - @FXML private Label timeUnitLabel; - @FXML - private Label zoomLabel; + private Slider timeUnitSlider; private final TimeLineController controller; private final FilteredEventsModel filteredEvents; @@ -84,126 +81,169 @@ public class ZoomSettingsPane extends TitledPane { "ZoomSettingsPane.timeUnitLabel.text=Time Units:", "ZoomSettingsPane.zoomLabel.text=Zoom"}) 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()); - descrLODLabel.setText(Bundle.ZoomSettingsPane_descrLODLabel_text()); - typeZoomLabel.setText(Bundle.ZoomSettingsPane_typeZoomLabel_text()); - timeUnitLabel.setText(Bundle.ZoomSettingsPane_timeUnitLabel_text()); zoomLabel.setText(Bundle.ZoomSettingsPane_zoomLabel_text()); - 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.timeRangeProperty().get()), requestedUnit.getPeriod())); - } - }, - this.filteredEvents.timeRangeProperty(), - () -> { - RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(this.filteredEvents.timeRangeProperty().get()); - ChronoUnit chronoUnit = rangeInfo.getPeriodSize().getChronoUnit(); - timeUnitSlider.setValue(TimeUnits.fromChronoUnit(chronoUnit).ordinal() - 1); - }); - - initializeSlider(descrLODSlider, - () -> controller.pushDescrLOD(DescriptionLoD.values()[Math.round(descrLODSlider.valueProperty().floatValue())]), - this.filteredEvents.descriptionLODProperty(), () -> { - descrLODSlider.setValue(this.filteredEvents.descriptionLODProperty().get().ordinal()); - }); - - initializeSlider(typeZoomSlider, - () -> controller.pushEventTypeZoom(EventTypeZoomLevel.values()[Math.round(typeZoomSlider.valueProperty().floatValue())]), - this.filteredEvents.eventTypeZoomProperty(), - () -> typeZoomSlider.setValue(this.filteredEvents.eventTypeZoomProperty().get().ordinal())); + typeZoomSlider.setMin(1); //don't show ROOT_TYPE + typeZoomSlider.setMax(EventTypeZoomLevel.values().length - 1); + configureSliderListeners(typeZoomSlider, + controller::pushEventTypeZoom, + filteredEvents.eventTypeZoomProperty(), + EventTypeZoomLevel.class, + EventTypeZoomLevel::ordinal, + Function.identity()); + typeZoomLabel.setText(Bundle.ZoomSettingsPane_typeZoomLabel_text()); + descrLODSlider.setMax(DescriptionLoD.values().length - 1); + configureSliderListeners(descrLODSlider, + controller::pushDescrLOD, + filteredEvents.descriptionLODProperty(), + DescriptionLoD.class, + DescriptionLoD::ordinal, + Function.identity()); + descrLODLabel.setText(Bundle.ZoomSettingsPane_descrLODLabel_text()); + //the description slider is only usefull in the detail view descrLODSlider.disableProperty().bind(controller.viewModeProperty().isEqualTo(VisualizationMode.COUNTS)); + + /** + * In order for the selected value in the time unit slider to correspond + * to the amount of time used as units along the x-axis of the + * visualization, and since we don't want to show "forever" as a time + * unit, the range of the slider is restricted, and there is an offset + * of 1 between the "real" value, and what is shown in the slider + * labels. + */ + timeUnitSlider.setMax(TimeUnits.values().length - 2); + configureSliderListeners(timeUnitSlider, + controller::pushTimeUnit, + filteredEvents.timeRangeProperty(), + TimeUnits.class, + //for the purposes of this slider we want the TimeUnit one bigger than RangeDivisionInfo indicates + modelTimeRange -> RangeDivisionInfo.getRangeDivisionInfo(modelTimeRange).getPeriodSize().ordinal() - 1, + index -> index + 1); //compensate for the -1 above when mapping to the Enum whose displayName will be shown at index + timeUnitLabel.setText(Bundle.ZoomSettingsPane_timeUnitLabel_text()); } /** - * setup a slider that with a listener that is added and removed to avoid - * circular updates. + * Configure the listeners that keep the given slider in sync with model + * property changes, and that handle user input on the slider. The listener + * attached to the slider is added and removed to avoid circular updates. * - * @param 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. + * Because Sliders work in terms of Doubles but represent ordered Enums that + * are indexed by Integers, and because the model properties may not be of + * the same type as the Enum(timeUnitSlider relates to an Interval in the + * filteredEvents model, rather than the TimeUnits shown on the Slider), a + * mapper is needed to convert between DriverType and Integer + * indices(driverValueMapper). Another mapper is used to modifiy the mapping + * from Integer index to Enum value displayed as the slider tick + * label(labelIndexMapper). + * + * @param The type of the driving model property. + * @param The type of the Enum that is represented along + * the slider. + * + * + * @param slider The slider that we are configuring. + * + * @param sliderValueConsumer The consumer that will get passed the newly + * selected slider value (mapped to EnumType + * automatically). + * + * @param modelProperty The readonly model property that this slider + * should be synced to. + * + * @param enumClass A type token for EnumType, ie value of type + * Class + * + * @param driverValueMapper A Function that maps from driver values of + * type DriverType to Integers representing the + * index of the corresponding EnumType. + * + * @param labelIndexMapper A Function that maps from Integer (narrowed + * slider value) to Integers representing the + * index of the corresponding EnumType. Used to + * compensate for slider values that do not + * lineup exactly with the Enum value indices to + * use as tick Labels. */ - private void initializeSlider(Slider slider, Runnable sliderChangeHandler, ReadOnlyObjectProperty driver, Runnable driverChangHandler) { + private static & DisplayNameProvider> void configureSliderListeners( + Slider slider, + Consumer sliderValueConsumer, + ReadOnlyObjectProperty modelProperty, + Class enumClass, + Function driverValueMapper, + Function labelIndexMapper) { + + //set the tick labels to the enum displayNames + slider.setLabelFormatter(new EnumSliderLabelFormatter<>(enumClass, labelIndexMapper)); + + //make a listener to responds to slider value changes (by updating the visualization) final InvalidationListener sliderListener = observable -> { + //only process event if the slider value is not changing (user has released slider thumb) if (slider.isValueChanging() == false) { - sliderChangeHandler.run(); + //convert slider value to EnumType and pass to consumer + EnumType sliderValueAsEnum = enumClass.getEnumConstants()[Math.round((float) slider.getValue())]; + sliderValueConsumer.accept(sliderValueAsEnum); } }; + //attach listener slider.valueProperty().addListener(sliderListener); slider.valueChangingProperty().addListener(sliderListener); - Platform.runLater(driverChangHandler); - - driver.addListener(observable -> { - slider.valueProperty().removeListener(sliderListener); - slider.valueChangingProperty().removeListener(sliderListener); + //set intial value of slider + slider.setValue(driverValueMapper.apply(modelProperty.get())); + modelProperty.addListener(modelProp -> { + //handle changes in the model property Platform.runLater(() -> { - driverChangHandler.run(); + //remove listener to avoid circular updates + slider.valueProperty().removeListener(sliderListener); + slider.valueChangingProperty().removeListener(sliderListener); + + //sync value of slider to model property value + slider.setValue(driverValueMapper.apply(modelProperty.get())); + + //reattach listener 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 { + /** + * StringConverter for the tick Labels of a Slider that is "backed" by an + * Enum that extends DisplayNameProvider. Narrows the Slider's Double value + * to an Integer and then uses that as the index of the Enum value whose + * displayName will be shown as the tick Label + * + * @param The type of Enum that this converter works with. + */ + static private class EnumSliderLabelFormatter & DisplayNameProvider> extends StringConverter { + + /** + * A Type token for the class of Enum that this converter works with. + */ + private final Class clazz; + /** + * + * A Function that can be used to adjust the narrowed slider value if it + * doesn't correspond exactly to the Enum value index. + */ + private final Function indexAdjsuter; + + EnumSliderLabelFormatter(Class clazz, Function indexMapper) { + this.clazz = clazz; + this.indexAdjsuter = indexMapper; + } @Override - public String toString(Double object) { - return TimeUnits.values()[Math.min(TimeUnits.values().length - 1, object.intValue() + 1)].getDisplayName(); + public String toString(Double dbl) { + //get the displayName of the EnumType whose index is the given dbl after it has been narrowed and then adjusted + return clazz.getEnumConstants()[indexAdjsuter.apply(dbl.intValue())].getDisplayName(); } @Override public Double fromString(String string) { - return new Integer(TimeUnits.valueOf(string).ordinal()).doubleValue(); - } - } - - private static class TypeZoomConverter extends StringConverter { - - @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 { - - @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(); + throw new UnsupportedOperationException("This method should not be used. This EnumSliderLabelFormatter is being used in an unintended way."); } } }