[Time Conductor] add history and select range features (#2932)

* basic brush prototype visible

* require alt pressed for grab handle. display only

* pan and zoom now co-exist

* revert selection to times

* make LocalTimeSystem UTCBased (Earth based)

* add LocalTimeSystem

* make isTimeFixed check reusable

* linting

* zoom axis sets start and end times

* pass isFixed as props so we can watch for change from parent

* disable cursor for local time and enable for fixed time

* linting

* resize brush on window resize

* just use d3-brush instead of entire d3 package

* WIP prototyping conductor history

* set global bounds before emitting change event

* WIP conductor history

* WIP save history to and pull history from local storage

* WIP persistence works

* reset axis height after prototyping

* conductor history functionality complete

* clean up refactoring

* add presets

code cleanup

* axis visual tuning

* remove unused function calls

* change tick to timespan to avoid confusion

* fix bounds to use for timespans on pan axis

* linting

* linting

* more linting

* linting

* change realtime end bound to 30 secondes

* add max duration validation

* Tweaks to Time Conductor History menu

- Enhanced styles for `.c-menu`;
- Added hint messaging and separator;
- Reversed displayed history array so that latest entry is always first;

* refactor to use browser mouse events instead of d3brush

* Styling Time Conductor axis area

- Styles for `is-zooming` state and brush;
- Styles for `is-alt-key-down` for panning;
- Styles for hover modified;

* resolve merge conflicts

* Styling Time Conductor axis and inputs

- Moved panning and zooming styles up into `conductor.scss`;
- Stubbed in :class names in Conductor.vue;
- New theme constants;

* fix merge conflict

* move zoom/pan styling up to conductor

* WIP almost there

* fix zoom

* move altPressed up to parent

* handle no drag on pan

* rename inMode vars for clarity

* Styling for Time Conductor zoom and pan

- Minor fix to hover cursor for alt-pressed panning;

* add configurable bounds limit to time conductor

* add presets and records

* fixes for history

* remove lodash

* add default configurables for examples

* do not install local time system

* cleanup

* fix indentation

remove logging

* remove comments

* section-hint without section-separator styling

* provide reasonable defaults for conductor configuration

* specify input to check validation on

* improve validation

* first check both inputs for valid formats

* clear each valid input on new entry

* tear down listeners

* add user instructions

* allow preset bounds to be declared as callback function

* set this.left on resize

code refactoring

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
This commit is contained in:
David Tsay
2020-06-30 12:10:35 -07:00
committed by GitHub
parent 16677c99c9
commit 4ecd264d93
12 changed files with 624 additions and 106 deletions

View File

@@ -22,7 +22,12 @@
<template>
<div
class="c-conductor"
:class="[isFixed ? 'is-fixed-mode' : 'is-realtime-mode']"
:class="[
{ 'is-zooming': isZooming },
{ 'is-panning': isPanning },
{ 'alt-pressed': altPressed },
isFixed ? 'is-fixed-mode' : 'is-realtime-mode'
]"
>
<form
ref="conductorForm"
@@ -52,7 +57,7 @@
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds(); submitForm()"
@change="validateAllBounds('startDate'); submitForm()"
>
<date-picker
v-if="isFixed && isUTCBased"
@@ -92,7 +97,7 @@
autocorrect="off"
spellcheck="false"
:disabled="!isFixed"
@change="validateAllBounds(); submitForm()"
@change="validateAllBounds('endDate'); submitForm()"
>
<date-picker
v-if="isFixed && isUTCBased"
@@ -122,14 +127,25 @@
<conductor-axis
class="c-conductor__ticks"
:bounds="rawBounds"
@panAxis="setViewFromBounds"
:view-bounds="viewBounds"
:is-fixed="isFixed"
:alt-pressed="altPressed"
@endPan="endPan"
@endZoom="endZoom"
@panAxis="pan"
@zoomAxis="zoom"
/>
</div>
<div class="c-conductor__controls">
<!-- Mode, time system menu buttons and duration slider -->
<ConductorMode class="c-conductor__mode-select" />
<ConductorTimeSystem class="c-conductor__time-system-select" />
<ConductorHistory
v-if="isFixed"
class="c-conductor__history-select"
:bounds="openmct.time.bounds()"
:time-system="timeSystem"
/>
</div>
<input
type="submit"
@@ -145,6 +161,7 @@ import ConductorTimeSystem from './ConductorTimeSystem.vue';
import DatePicker from './DatePicker.vue';
import ConductorAxis from './ConductorAxis.vue';
import ConductorModeIcon from './ConductorModeIcon.vue';
import ConductorHistory from './ConductorHistory.vue'
const DEFAULT_DURATION_FORMATTER = 'duration';
@@ -155,7 +172,8 @@ export default {
ConductorTimeSystem,
DatePicker,
ConductorAxis,
ConductorModeIcon
ConductorModeIcon,
ConductorHistory
},
data() {
let bounds = this.openmct.time.bounds();
@@ -165,6 +183,7 @@ export default {
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
return {
timeSystem: timeSystem,
timeFormatter: timeFormatter,
durationFormatter: durationFormatter,
offsets: {
@@ -175,29 +194,68 @@ export default {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
},
rawBounds: {
viewBounds: {
start: bounds.start,
end: bounds.end
},
isFixed: this.openmct.time.clock() === undefined,
isUTCBased: timeSystem.isUTCBased,
showDatePicker: false
showDatePicker: false,
altPressed: false,
isPanning: false,
isZooming: false
}
},
mounted() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('bounds', this.setViewFromBounds);
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
this.openmct.time.on('clockOffsets', this.setViewFromOffsets)
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
},
methods: {
handleKeyDown(event) {
if (event.key === 'Alt') {
this.altPressed = true;
}
},
handleKeyUp(event) {
if (event.key === 'Alt') {
this.altPressed = false;
}
},
pan(bounds) {
this.isPanning = true;
this.setViewFromBounds(bounds);
},
endPan(bounds) {
this.isPanning = false;
if (bounds) {
this.openmct.time.bounds(bounds);
}
},
zoom(bounds) {
this.isZooming = true;
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
},
endZoom(bounds) {
const _bounds = bounds ? bounds : this.openmct.time.bounds();
this.isZooming = false;
this.openmct.time.bounds(_bounds);
},
setTimeSystem(timeSystem) {
this.timeSystem = timeSystem
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.isUTCBased = timeSystem.isUTCBased;
},
setOffsetsFromView($event) {
@@ -237,8 +295,8 @@ export default {
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
this.rawBounds.start = bounds.start;
this.rawBounds.end = bounds.end;
this.viewBounds.start = bounds.start;
this.viewBounds.end = bounds.end;
},
setViewFromOffsets(offsets) {
this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start));
@@ -251,6 +309,15 @@ export default {
this.setOffsetsFromView();
}
},
getBoundsLimit() {
const configuration = this.configuration.menuOptions
.filter(option => option.timeSystem === this.timeSystem.key)
.find(option => option.limit);
const limit = configuration ? configuration.limit : undefined;
return limit;
},
clearAllValidation() {
if (this.isFixed) {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
@@ -262,36 +329,52 @@ export default {
input.setCustomValidity('');
input.title = '';
},
validateAllBounds() {
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let validationResult = true;
let formattedDate;
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
return false;
}
if (input === this.$refs.startDate) {
formattedDate = this.formattedBounds.start;
let validationResult = true;
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
const limit = this.getBoundsLimit();
if (
this.timeSystem.isUTCBased
&& limit
&& boundsValues.end - boundsValues.start > limit
) {
if (input === currentInput) {
validationResult = "Start and end difference exceeds allowable limit";
}
} else {
formattedDate = this.formattedBounds.end;
if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
}
return this.handleValidationResults(input, validationResult);
});
},
areBoundsFormatsValid() {
let validationResult = true;
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
const formattedDate = input === this.$refs.startDate
? this.formattedBounds.start
: this.formattedBounds.end
;
if (!this.timeFormatter.validate(formattedDate)) {
validationResult = 'Invalid date';
} else {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
validationResult = this.openmct.time.validateBounds(boundsValues);
}
if (validationResult !== true) {
input.setCustomValidity(validationResult);
input.title = validationResult;
return false;
} else {
input.setCustomValidity('');
input.title = '';
return true;
}
return this.handleValidationResults(input, validationResult);
});
},
validateAllOffsets(event) {
@@ -315,17 +398,20 @@ export default {
validationResult = this.openmct.time.validateOffsets(offsetValues);
}
if (validationResult !== true) {
input.setCustomValidity(validationResult);
input.title = validationResult;
return false;
} else {
input.setCustomValidity('');
input.title = '';
return true;
}
return this.handleValidationResults(input, validationResult);
});
},
handleValidationResults(input, validationResult) {
if (validationResult !== true) {
input.setCustomValidity(validationResult);
input.title = validationResult;
return false;
} else {
input.setCustomValidity('');
input.title = '';
return true;
}
},
submitForm() {
// Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click)
@@ -338,12 +424,12 @@ export default {
},
startDateSelected(date) {
this.formattedBounds.start = this.timeFormatter.format(date);
this.validateAllBounds();
this.validateAllBounds('startDate');
this.submitForm();
},
endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date);
this.validateAllBounds();
this.validateAllBounds('endDate');
this.submitForm();
}
}