mirror of
https://github.com/nasa/openmct.git
synced 2024-12-18 20:57:53 +00:00
[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:
parent
16677c99c9
commit
4ecd264d93
40
index.html
40
index.html
@ -34,8 +34,8 @@
|
||||
<body>
|
||||
</body>
|
||||
<script>
|
||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||
const THIRTY_MINUTES = 30 * 60 * 1000;
|
||||
const THIRTY_SECONDS = 30 * 1000;
|
||||
const THIRTY_MINUTES = THIRTY_SECONDS * 60;
|
||||
|
||||
[
|
||||
'example/eventGenerator'
|
||||
@ -63,7 +63,39 @@
|
||||
bounds: {
|
||||
start: Date.now() - THIRTY_MINUTES,
|
||||
end: Date.now()
|
||||
}
|
||||
},
|
||||
// commonly used bounds can be stored in history
|
||||
// bounds (start and end) can accept either a milliseconds number
|
||||
// or a callback function returning a milliseconds number
|
||||
// a function is useful for invoking Date.now() at exact moment of preset selection
|
||||
presets: [
|
||||
{
|
||||
label: 'Last Day',
|
||||
bounds: {
|
||||
start: () => Date.now() - 1000 * 60 * 60 * 24,
|
||||
end: () => Date.now()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Last 2 hours',
|
||||
bounds: {
|
||||
start: () => Date.now() - 1000 * 60 * 60 * 2,
|
||||
end: () => Date.now()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Last hour',
|
||||
bounds: {
|
||||
start: () => Date.now() - 1000 * 60 * 60,
|
||||
end: () => Date.now()
|
||||
}
|
||||
}
|
||||
],
|
||||
// maximum recent bounds to retain in conductor history
|
||||
records: 10,
|
||||
// maximum duration between start and end bounds
|
||||
// for utc-based time systems this is in milliseconds
|
||||
limit: 1000 * 60 * 60 * 24
|
||||
},
|
||||
{
|
||||
name: "Realtime",
|
||||
@ -71,7 +103,7 @@
|
||||
clock: 'local',
|
||||
clockOffsets: {
|
||||
start: - THIRTY_MINUTES,
|
||||
end: FIVE_MINUTES
|
||||
end: THIRTY_SECONDS
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -41,7 +41,7 @@ define([], function () {
|
||||
this.timeFormat = 'local-format';
|
||||
this.durationFormat = 'duration';
|
||||
|
||||
this.isUTCBased = false;
|
||||
this.isUTCBased = true;
|
||||
}
|
||||
|
||||
return LocalTimeSystem;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,12 @@
|
||||
ref="axisHolder"
|
||||
class="c-conductor-axis"
|
||||
@mousedown="dragStart($event)"
|
||||
></div>
|
||||
>
|
||||
<div
|
||||
class="c-conductor-axis__zoom-indicator"
|
||||
:style="zoomStyle"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -43,52 +48,81 @@ const PIXELS_PER_TICK_WIDE = 200;
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
bounds: {
|
||||
viewBounds: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isFixed: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
altPressed: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inPanMode: false,
|
||||
dragStartX: undefined,
|
||||
dragX: undefined,
|
||||
zoomStyle: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inZoomMode() {
|
||||
return !this.inPanMode;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
bounds: {
|
||||
handler(bounds) {
|
||||
viewBounds: {
|
||||
handler() {
|
||||
this.setScale();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let axisHolder = this.$refs.axisHolder;
|
||||
let height = axisHolder.offsetHeight;
|
||||
let vis = d3Selection.select(axisHolder)
|
||||
.append("svg:svg")
|
||||
.attr("width", "100%")
|
||||
.attr("height", height);
|
||||
let vis = d3Selection.select(this.$refs.axisHolder).append("svg:svg");
|
||||
|
||||
this.width = this.$refs.axisHolder.clientWidth;
|
||||
this.xAxis = d3Axis.axisTop();
|
||||
this.dragging = false;
|
||||
|
||||
// draw x axis with labels. CSS is used to position them.
|
||||
this.axisElement = vis.append("g");
|
||||
this.axisElement = vis.append("g")
|
||||
.attr("class", "axis");
|
||||
|
||||
this.setViewFromTimeSystem(this.openmct.time.timeSystem());
|
||||
this.setAxisDimensions();
|
||||
this.setScale();
|
||||
|
||||
//Respond to changes in conductor
|
||||
this.openmct.time.on("timeSystem", this.setViewFromTimeSystem);
|
||||
setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
methods: {
|
||||
setAxisDimensions() {
|
||||
const axisHolder = this.$refs.axisHolder;
|
||||
const rect = axisHolder.getBoundingClientRect();
|
||||
|
||||
this.left = Math.round(rect.left);
|
||||
this.width = axisHolder.clientWidth;
|
||||
},
|
||||
setScale() {
|
||||
if (!this.width) {
|
||||
return;
|
||||
}
|
||||
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
let bounds = this.bounds;
|
||||
|
||||
if (timeSystem.isUTCBased) {
|
||||
this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]);
|
||||
this.xScale.domain(
|
||||
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
|
||||
);
|
||||
} else {
|
||||
this.xScale.domain([bounds.start, bounds.end]);
|
||||
this.xScale.domain(
|
||||
[this.viewBounds.start, this.viewBounds.end]
|
||||
);
|
||||
}
|
||||
|
||||
this.xAxis.scale(this.xScale);
|
||||
@ -102,7 +136,7 @@ export default {
|
||||
this.xAxis.ticks(this.width / PIXELS_PER_TICK);
|
||||
}
|
||||
|
||||
this.msPerPixel = (bounds.end - bounds.start) / this.width;
|
||||
this.msPerPixel = (this.viewBounds.end - this.viewBounds.start) / this.width;
|
||||
},
|
||||
setViewFromTimeSystem(timeSystem) {
|
||||
//The D3 scale used depends on the type of time system as d3
|
||||
@ -120,9 +154,8 @@ export default {
|
||||
},
|
||||
getActiveFormatter() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
let isFixed = this.openmct.time.clock() === undefined;
|
||||
|
||||
if (isFixed) {
|
||||
if (this.isFixed) {
|
||||
return this.getFormatter(timeSystem.timeFormat);
|
||||
} else {
|
||||
return this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
@ -134,45 +167,131 @@ export default {
|
||||
}).formatter;
|
||||
},
|
||||
dragStart($event) {
|
||||
let isFixed = this.openmct.time.clock() === undefined;
|
||||
if (isFixed) {
|
||||
if (this.isFixed) {
|
||||
this.dragStartX = $event.clientX;
|
||||
|
||||
if (this.altPressed) {
|
||||
this.inPanMode = true;
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', this.drag);
|
||||
document.addEventListener('mouseup', this.dragEnd, {
|
||||
once: true
|
||||
});
|
||||
|
||||
if (this.inZoomMode) {
|
||||
this.startZoom();
|
||||
}
|
||||
}
|
||||
},
|
||||
drag($event) {
|
||||
if (!this.dragging) {
|
||||
this.dragging = true;
|
||||
requestAnimationFrame(()=>{
|
||||
let deltaX = $event.clientX - this.dragStartX;
|
||||
let percX = deltaX / this.width;
|
||||
let bounds = this.openmct.time.bounds();
|
||||
let deltaTime = bounds.end - bounds.start;
|
||||
let newStart = bounds.start - percX * deltaTime;
|
||||
this.$emit('panAxis',{
|
||||
start: newStart,
|
||||
end: newStart + deltaTime
|
||||
});
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.dragX = $event.clientX;
|
||||
this.inPanMode ? this.pan() : this.zoom();
|
||||
this.dragging = false;
|
||||
})
|
||||
} else {
|
||||
console.log('Rejected drag due to RAF cap');
|
||||
});
|
||||
}
|
||||
},
|
||||
dragEnd() {
|
||||
this.inPanMode ? this.endPan() : this.endZoom();
|
||||
|
||||
document.removeEventListener('mousemove', this.drag);
|
||||
this.openmct.time.bounds({
|
||||
start: this.bounds.start,
|
||||
end: this.bounds.end
|
||||
this.dragStartX = undefined;
|
||||
this.dragX = undefined;
|
||||
},
|
||||
pan() {
|
||||
const panBounds = this.getPanBounds();
|
||||
this.$emit('panAxis', panBounds);
|
||||
},
|
||||
endPan() {
|
||||
const panBounds = this.dragStartX && this.dragX && this.dragStartX !== this.dragX
|
||||
? this.getPanBounds()
|
||||
: undefined;
|
||||
this.$emit('endPan', panBounds);
|
||||
this.inPanMode = false;
|
||||
},
|
||||
getPanBounds() {
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const deltaTime = bounds.end - bounds.start;
|
||||
const deltaX = this.dragX - this.dragStartX;
|
||||
const percX = deltaX / this.width;
|
||||
const panStart = bounds.start - percX * deltaTime;
|
||||
|
||||
return {
|
||||
start: panStart,
|
||||
end: panStart + deltaTime
|
||||
};
|
||||
},
|
||||
startZoom() {
|
||||
const x = this.scaleToBounds(this.dragStartX);
|
||||
|
||||
this.zoomStyle = {
|
||||
left: `${this.dragStartX - this.left}px`
|
||||
};
|
||||
|
||||
this.$emit('zoomAxis', {
|
||||
start: x,
|
||||
end: x
|
||||
});
|
||||
},
|
||||
zoom() {
|
||||
const zoomRange = this.getZoomRange();
|
||||
|
||||
this.zoomStyle = {
|
||||
left: `${zoomRange.start - this.left}px`,
|
||||
width: `${zoomRange.end - zoomRange.start}px`
|
||||
};
|
||||
|
||||
this.$emit('zoomAxis', {
|
||||
start: this.scaleToBounds(zoomRange.start),
|
||||
end: this.scaleToBounds(zoomRange.end)
|
||||
});
|
||||
},
|
||||
endZoom() {
|
||||
const zoomRange = this.dragStartX && this.dragX && this.dragStartX !== this.dragX
|
||||
? this.getZoomRange()
|
||||
: undefined;
|
||||
|
||||
const zoomBounds = zoomRange
|
||||
? {
|
||||
start: this.scaleToBounds(zoomRange.start),
|
||||
end: this.scaleToBounds(zoomRange.end)
|
||||
}
|
||||
: this.openmct.time.bounds();
|
||||
|
||||
this.zoomStyle = {};
|
||||
this.$emit('endZoom', zoomBounds);
|
||||
},
|
||||
getZoomRange() {
|
||||
const leftBound = this.left;
|
||||
const rightBound = this.left + this.width;
|
||||
|
||||
const zoomStart = this.dragX < leftBound
|
||||
? leftBound
|
||||
: Math.min(this.dragX, this.dragStartX);
|
||||
|
||||
const zoomEnd = this.dragX > rightBound
|
||||
? rightBound
|
||||
: Math.max(this.dragX, this.dragStartX);
|
||||
|
||||
return {
|
||||
start: zoomStart,
|
||||
end: zoomEnd
|
||||
};
|
||||
},
|
||||
scaleToBounds(value) {
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const timeDelta = bounds.end - bounds.start;
|
||||
const valueDelta = value - this.left;
|
||||
const offset = valueDelta / this.width * timeDelta;
|
||||
return bounds.start + offset;
|
||||
},
|
||||
resize() {
|
||||
if (this.$refs.axisHolder.clientWidth !== this.width) {
|
||||
this.width = this.$refs.axisHolder.clientWidth;
|
||||
this.setAxisDimensions();
|
||||
this.setScale();
|
||||
}
|
||||
}
|
||||
|
200
src/plugins/timeConductor/ConductorHistory.vue
Normal file
200
src/plugins/timeConductor/ConductorHistory.vue
Normal file
@ -0,0 +1,200 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2018, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is 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.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
<template>
|
||||
<div class="c-ctrl-wrapper c-ctrl-wrapper--menus-up">
|
||||
<button class="c-button--menu c-history-button icon-history"
|
||||
@click.prevent="toggle"
|
||||
>
|
||||
<span class="c-button__label">History</span>
|
||||
</button>
|
||||
<div v-if="open"
|
||||
class="c-menu c-conductor__history-menu"
|
||||
>
|
||||
<ul v-if="hasHistoryPresets">
|
||||
<li
|
||||
v-for="preset in presets"
|
||||
:key="preset.label"
|
||||
class="icon-clock"
|
||||
@click="selectPresetBounds(preset.bounds)"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div
|
||||
v-if="hasHistoryPresets"
|
||||
class="c-menu__section-separator"
|
||||
></div>
|
||||
|
||||
<div class="c-menu__section-hint">
|
||||
Past timeframes, ordered by latest first
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<li
|
||||
v-for="(timespan, index) in historyForCurrentTimeSystem"
|
||||
:key="index"
|
||||
class="icon-history"
|
||||
@click="selectTimespan(timespan)"
|
||||
>
|
||||
{{ formatTime(timespan.start) }} - {{ formatTime(timespan.end) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import toggleMixin from '../../ui/mixins/toggle-mixin';
|
||||
|
||||
const LOCAL_STORAGE_HISTORY_KEY = 'tcHistory';
|
||||
const DEFAULT_RECORDS = 10;
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'configuration'],
|
||||
mixins: [toggleMixin],
|
||||
props: {
|
||||
bounds: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
timeSystem: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
history: {}, // contains arrays of timespans {start, end}, array key is time system key
|
||||
presets: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasHistoryPresets() {
|
||||
return this.timeSystem.isUTCBased && this.presets.length;
|
||||
},
|
||||
historyForCurrentTimeSystem() {
|
||||
const history = this.history[this.timeSystem.key];
|
||||
|
||||
return history;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
bounds: {
|
||||
handler() {
|
||||
this.addTimespan();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
timeSystem: {
|
||||
handler() {
|
||||
this.loadConfiguration();
|
||||
this.addTimespan();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
history: {
|
||||
handler() {
|
||||
this.persistHistoryToLocalStorage();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getHistoryFromLocalStorage();
|
||||
},
|
||||
methods: {
|
||||
getHistoryFromLocalStorage() {
|
||||
if (localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY)) {
|
||||
this.history = JSON.parse(localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY))
|
||||
} else {
|
||||
this.history = {};
|
||||
this.persistHistoryToLocalStorage();
|
||||
}
|
||||
},
|
||||
persistHistoryToLocalStorage() {
|
||||
localStorage.setItem(LOCAL_STORAGE_HISTORY_KEY, JSON.stringify(this.history));
|
||||
},
|
||||
addTimespan() {
|
||||
const key = this.timeSystem.key;
|
||||
let [...currentHistory] = this.history[key] || [];
|
||||
const timespan = {
|
||||
start: this.bounds.start,
|
||||
end: this.bounds.end
|
||||
};
|
||||
|
||||
const isNotEqual = function (entry) {
|
||||
const start = entry.start !== this.start;
|
||||
const end = entry.end !== this.end;
|
||||
|
||||
return start || end;
|
||||
};
|
||||
currentHistory = currentHistory.filter(isNotEqual, timespan);
|
||||
|
||||
while (currentHistory.length >= this.records) {
|
||||
currentHistory.pop();
|
||||
}
|
||||
|
||||
currentHistory.unshift(timespan);
|
||||
this.history[key] = currentHistory;
|
||||
},
|
||||
selectTimespan(timespan) {
|
||||
this.openmct.time.bounds(timespan);
|
||||
},
|
||||
selectPresetBounds(bounds) {
|
||||
const start = typeof bounds.start === 'function' ? bounds.start() : bounds.start;
|
||||
const end = typeof bounds.end === 'function' ? bounds.end() : bounds.end;
|
||||
|
||||
this.selectTimespan({
|
||||
start: start,
|
||||
end: end
|
||||
});
|
||||
},
|
||||
loadConfiguration() {
|
||||
const configurations = this.configuration.menuOptions
|
||||
.filter(option => option.timeSystem === this.timeSystem.key);
|
||||
|
||||
this.presets = this.loadPresets(configurations);
|
||||
this.records = this.loadRecords(configurations);
|
||||
},
|
||||
loadPresets(configurations) {
|
||||
const configuration = configurations.find(option => option.presets);
|
||||
const presets = configuration ? configuration.presets : [];
|
||||
|
||||
return presets;
|
||||
},
|
||||
loadRecords(configurations) {
|
||||
const configuration = configurations.find(option => option.records);
|
||||
const records = configuration ? configuration.records : DEFAULT_RECORDS;
|
||||
|
||||
return records;
|
||||
},
|
||||
formatTime(time) {
|
||||
const formatter = this.openmct.telemetry.getValueFormatter({
|
||||
format: this.timeSystem.timeFormat
|
||||
}).formatter;
|
||||
|
||||
return formatter.format(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -110,7 +110,7 @@ export default {
|
||||
if (clock === undefined) {
|
||||
return {
|
||||
key: 'fixed',
|
||||
name: 'Fixed Timespan Mode',
|
||||
name: 'Fixed Timespan',
|
||||
description: 'Query and explore data that falls between two fixed datetimes.',
|
||||
cssClass: 'icon-tabular'
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
text-rendering: geometricPrecision;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
> g {
|
||||
> g.axis {
|
||||
// Overall Tick holder
|
||||
transform: translateY($tickYPos);
|
||||
path {
|
||||
@ -44,7 +44,6 @@
|
||||
}
|
||||
|
||||
body.desktop .is-fixed-mode & {
|
||||
@include cursorGrab();
|
||||
background-size: 3px 30%;
|
||||
background-color: $colorBodyBgSubtle;
|
||||
box-shadow: inset rgba(black, 0.4) 0 1px 1px;
|
||||
@ -55,17 +54,6 @@
|
||||
stroke: $colorBodyBgSubtle;
|
||||
transition: $transOut;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
$c: $colorKeySubtle;
|
||||
background-color: $c;
|
||||
transition: $transIn;
|
||||
svg text {
|
||||
stroke: $c;
|
||||
transition: $transIn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-realtime-mode & {
|
||||
|
@ -57,6 +57,65 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.is-fixed-mode {
|
||||
.c-conductor-axis {
|
||||
&__zoom-indicator {
|
||||
border: 1px solid transparent;
|
||||
display: none; // Hidden by default
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.is-panning),
|
||||
&:not(.is-zooming) {
|
||||
.c-conductor-axis {
|
||||
&:hover,
|
||||
&:active {
|
||||
cursor: col-resize;
|
||||
filter: $timeConductorAxisHoverFilter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-panning,
|
||||
&.is-zooming {
|
||||
.c-conductor-input input {
|
||||
// Styles for inputs while zooming or panning
|
||||
background: rgba($timeConductorActiveBg, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
&.alt-pressed {
|
||||
.c-conductor-axis:hover {
|
||||
// When alt is being pressed and user is hovering over the axis, set the cursor
|
||||
@include cursorGrab();
|
||||
}
|
||||
}
|
||||
|
||||
&.is-panning {
|
||||
.c-conductor-axis {
|
||||
@include cursorGrab();
|
||||
background-color: $timeConductorActivePanBg;
|
||||
transition: $transIn;
|
||||
|
||||
svg text {
|
||||
stroke: $timeConductorActivePanBg;
|
||||
transition: $transIn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-zooming {
|
||||
.c-conductor-axis__zoom-indicator {
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: rgba($timeConductorActiveBg, 0.4);
|
||||
border-left-color: $timeConductorActiveBg;
|
||||
border-right-color: $timeConductorActiveBg;
|
||||
top: 0; bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-realtime-mode {
|
||||
.c-conductor__time-bounds {
|
||||
grid-template-columns: 20px auto 1fr auto auto;
|
||||
|
@ -142,6 +142,9 @@ $colorTimeHov: pullForward($colorTime, 10%);
|
||||
$colorTimeSubtle: pushBack($colorTime, 20%);
|
||||
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
|
||||
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
|
||||
$timeConductorAxisHoverFilter: brightness(1.2);
|
||||
$timeConductorActiveBg: $colorKey;
|
||||
$timeConductorActivePanBg: #226074;
|
||||
|
||||
/************************************************** BROWSING */
|
||||
$browseFrameColor: pullForward($colorBodyBg, 10%);
|
||||
|
@ -146,6 +146,9 @@ $colorTimeHov: pullForward($colorTime, 10%);
|
||||
$colorTimeSubtle: pushBack($colorTime, 20%);
|
||||
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
|
||||
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
|
||||
$timeConductorAxisHoverFilter: brightness(1.2);
|
||||
$timeConductorActiveBg: $colorKey;
|
||||
$timeConductorActivePanBg: #226074;
|
||||
|
||||
/************************************************** BROWSING */
|
||||
$browseFrameColor: pullForward($colorBodyBg, 10%);
|
||||
|
@ -132,7 +132,7 @@ $colorPausedFg: #fff;
|
||||
// Base variations
|
||||
$colorBodyBgSubtle: pullForward($colorBodyBg, 5%);
|
||||
$colorBodyBgSubtleHov: pushBack($colorKey, 50%);
|
||||
$colorKeySubtle: pushBack($colorKey, 10%);
|
||||
$colorKeySubtle: pushBack($colorKey, 20%);
|
||||
|
||||
// Time Colors
|
||||
$colorTime: #618cff;
|
||||
@ -142,6 +142,9 @@ $colorTimeHov: pushBack($colorTime, 5%);
|
||||
$colorTimeSubtle: pushBack($colorTime, 20%);
|
||||
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
|
||||
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
|
||||
$timeConductorAxisHoverFilter: brightness(0.8);
|
||||
$timeConductorActiveBg: $colorKey;
|
||||
$timeConductorActivePanBg: #A0CDE1;
|
||||
|
||||
/************************************************** BROWSING */
|
||||
$browseFrameColor: pullForward($colorBodyBg, 10%);
|
||||
|
@ -462,9 +462,17 @@ select {
|
||||
text-shadow: $shdwMenuText;
|
||||
padding: $interiorMarginSm;
|
||||
box-shadow: $shdwMenu;
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
//+ * {
|
||||
// margin-top: $interiorMarginSm;
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin menuInner() {
|
||||
@ -502,6 +510,23 @@ select {
|
||||
.c-menu {
|
||||
@include menuOuter();
|
||||
@include menuInner();
|
||||
|
||||
&__section-hint {
|
||||
$m: $interiorMargin;
|
||||
margin: $m 0;
|
||||
padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2);
|
||||
|
||||
opacity: 0.6;
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&__section-separator {
|
||||
$m: $interiorMargin;
|
||||
border-top: 1px solid $colorInteriorBorder;
|
||||
margin: $m 0;
|
||||
padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2);
|
||||
}
|
||||
}
|
||||
|
||||
.c-super-menu {
|
||||
|
Loading…
Reference in New Issue
Block a user