mirror of
https://github.com/nasa/openmct.git
synced 2025-06-26 11:09:22 +00:00
Compare commits
13 Commits
vue-hack
...
imagery-te
Author | SHA1 | Date | |
---|---|---|---|
b2dadaeb45 | |||
a98cf17b58 | |||
a874e906a0 | |||
d1f1707893 | |||
b7ec3605a7 | |||
8d7d7d8211 | |||
9697c20dc1 | |||
26266ab831 | |||
949f45b31c | |||
690c0e7466 | |||
cce6fe0f31 | |||
4c5a3362a0 | |||
fc87b3afec |
258
src/plugins/imagery/pluginSpec.js
Normal file
258
src/plugins/imagery/pluginSpec.js
Normal file
@ -0,0 +1,258 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT 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 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.
|
||||
*****************************************************************************/
|
||||
import ImageryPlugin from './plugin.js';
|
||||
import Vue from 'vue';
|
||||
import {
|
||||
createOpenMct,
|
||||
resetApplicationState
|
||||
} from 'utils/testing';
|
||||
|
||||
const ONE_MINUTE = 1000 * 60;
|
||||
const TEN_MINUTES = ONE_MINUTE * 10;
|
||||
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
|
||||
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
|
||||
const REFRESH_CSS_MS = 500;
|
||||
|
||||
function getImageInfo(doc) {
|
||||
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
|
||||
let timestamp = imageElement.dataset.openmctImageTimestamp;
|
||||
let identifier = imageElement.dataset.openmctObjectKeystring;
|
||||
let url = imageElement.style.backgroundImage;
|
||||
|
||||
return {
|
||||
timestamp,
|
||||
identifier,
|
||||
url
|
||||
};
|
||||
}
|
||||
|
||||
function isNew(doc) {
|
||||
let newIcon = doc.querySelectorAll(NEW_IMAGE_CLASS);
|
||||
|
||||
return newIcon.length !== 0;
|
||||
}
|
||||
|
||||
function generateTelemetry(start, count) {
|
||||
let telemetry = [];
|
||||
|
||||
for (let i = 1, l = count + 1; i < l; i++) {
|
||||
let stringRep = i + 'minute';
|
||||
let logo = 'images/logo-openmct.svg';
|
||||
|
||||
telemetry.push({
|
||||
"name": stringRep + " Imagery",
|
||||
"utc": start + (i * ONE_MINUTE),
|
||||
"url": location.host + '/' + logo + '?time=' + stringRep,
|
||||
"timeId": stringRep
|
||||
});
|
||||
}
|
||||
|
||||
return telemetry;
|
||||
}
|
||||
|
||||
describe("The Imagery View Layout", () => {
|
||||
const imageryKey = 'example.imagery';
|
||||
const START = Date.now();
|
||||
const COUNT = 10;
|
||||
|
||||
let openmct;
|
||||
let imageryPlugin;
|
||||
let parent;
|
||||
let child;
|
||||
let timeFormat = 'utc';
|
||||
let bounds = {
|
||||
start: START - TEN_MINUTES,
|
||||
end: START
|
||||
};
|
||||
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
|
||||
let imageryObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "imageryId"
|
||||
},
|
||||
name: "Example Imagery",
|
||||
type: "example.imagery",
|
||||
location: "parentId",
|
||||
modified: 0,
|
||||
persisted: 0,
|
||||
telemetry: {
|
||||
values: [
|
||||
{
|
||||
"name": "Image",
|
||||
"key": "url",
|
||||
"format": "image",
|
||||
"hints": {
|
||||
"image": 1,
|
||||
"priority": 3
|
||||
},
|
||||
"source": "url"
|
||||
},
|
||||
{
|
||||
"name": "Name",
|
||||
"key": "name",
|
||||
"source": "name",
|
||||
"hints": {
|
||||
"priority": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Time",
|
||||
"key": "utc",
|
||||
"format": "utc",
|
||||
"hints": {
|
||||
"domain": 2,
|
||||
"priority": 1
|
||||
},
|
||||
"source": "utc"
|
||||
},
|
||||
{
|
||||
"name": "Local Time",
|
||||
"key": "local",
|
||||
"format": "local-format",
|
||||
"hints": {
|
||||
"domain": 1,
|
||||
"priority": 2
|
||||
},
|
||||
"source": "local"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// this setups up the app
|
||||
beforeEach((done) => {
|
||||
const appHolder = document.createElement('div');
|
||||
appHolder.style.width = '640px';
|
||||
appHolder.style.height = '480px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
|
||||
parent = document.createElement('div');
|
||||
child = document.createElement('div');
|
||||
parent.appendChild(child);
|
||||
|
||||
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
|
||||
|
||||
imageryPlugin = new ImageryPlugin();
|
||||
openmct.install(imageryPlugin);
|
||||
|
||||
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
|
||||
|
||||
openmct.time.timeSystem(timeFormat, {
|
||||
start: 0,
|
||||
end: 4
|
||||
});
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless(appHolder);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it("should provide an imagery view only for imagery producing objects", () => {
|
||||
let applicableViews = openmct.objectViews.get(imageryObject);
|
||||
let imageryView = applicableViews.find(
|
||||
viewProvider => viewProvider.key === imageryKey
|
||||
);
|
||||
|
||||
expect(imageryView).toBeDefined();
|
||||
});
|
||||
|
||||
describe("imagery view", () => {
|
||||
let applicableViews;
|
||||
let imageryViewProvider;
|
||||
let imageryView;
|
||||
|
||||
beforeEach(async (done) => {
|
||||
let telemetryRequestResolve;
|
||||
let telemetryRequestPromise = new Promise((resolve) => {
|
||||
telemetryRequestResolve = resolve;
|
||||
});
|
||||
|
||||
openmct.telemetry.request.and.callFake(() => {
|
||||
telemetryRequestResolve(imageTelemetry);
|
||||
|
||||
return telemetryRequestPromise;
|
||||
});
|
||||
|
||||
openmct.time.clock('local', {
|
||||
start: bounds.start,
|
||||
end: bounds.end + 100
|
||||
});
|
||||
|
||||
applicableViews = openmct.objectViews.get(imageryObject);
|
||||
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
|
||||
imageryView = imageryViewProvider.view(imageryObject);
|
||||
imageryView.show(child);
|
||||
|
||||
await telemetryRequestPromise;
|
||||
await Vue.nextTick();
|
||||
|
||||
return done();
|
||||
});
|
||||
|
||||
it("on mount should show the the most recent image", () => {
|
||||
const imageInfo = getImageInfo(parent);
|
||||
|
||||
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should show the clicked thumbnail as the main image", async () => {
|
||||
const target = imageTelemetry[5].url;
|
||||
parent.querySelectorAll(`img[src='${target}']`)[0].click();
|
||||
await Vue.nextTick();
|
||||
const imageInfo = getImageInfo(parent);
|
||||
|
||||
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should show that an image is new", async (done) => {
|
||||
await Vue.nextTick();
|
||||
|
||||
// used in code, need to wait to the 500ms here too
|
||||
setTimeout(() => {
|
||||
const imageIsNew = isNew(parent);
|
||||
|
||||
expect(imageIsNew).toBeTrue();
|
||||
done();
|
||||
}, REFRESH_CSS_MS);
|
||||
});
|
||||
|
||||
it("should show that an image is not new", async (done) => {
|
||||
const target = imageTelemetry[2].url;
|
||||
parent.querySelectorAll(`img[src='${target}']`)[0].click();
|
||||
|
||||
await Vue.nextTick();
|
||||
|
||||
// used in code, need to wait to the 500ms here too
|
||||
setTimeout(() => {
|
||||
const imageIsNew = isNew(parent);
|
||||
|
||||
expect(imageIsNew).toBeFalse();
|
||||
done();
|
||||
}, REFRESH_CSS_MS);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@ -73,6 +73,15 @@
|
||||
>
|
||||
<!-- RT start -->
|
||||
<div class="c-direction-indicator icon-minus"></div>
|
||||
<time-popup
|
||||
v-if="showTCInputStart"
|
||||
class="pr-tc-input-menu--start"
|
||||
:type="'start'"
|
||||
:offset="offsets.start"
|
||||
@focus.native="$event.target.select()"
|
||||
@hide="hideAllTimePopups"
|
||||
@update="timePopUpdate"
|
||||
/>
|
||||
<input
|
||||
ref="startOffset"
|
||||
v-model="offsets.start"
|
||||
@ -81,6 +90,7 @@
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
@change="validateAllOffsets(); submitForm()"
|
||||
@click="showTimePopupStart"
|
||||
>
|
||||
</div>
|
||||
|
||||
@ -97,7 +107,7 @@
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
:disabled="!isFixed"
|
||||
@change="validateAllBounds('endDate'); submitForm()"
|
||||
@change="validateAllBounds('endDate'); submitForm ()"
|
||||
>
|
||||
<date-picker
|
||||
v-if="isFixed && isUTCBased"
|
||||
@ -114,6 +124,15 @@
|
||||
>
|
||||
<!-- RT end -->
|
||||
<div class="c-direction-indicator icon-plus"></div>
|
||||
<time-popup
|
||||
v-if="showTCInputEnd"
|
||||
class="pr-tc-input-menu--end"
|
||||
:type="'end'"
|
||||
:offset="offsets.end"
|
||||
@focus.native="$event.target.select()"
|
||||
@hide="hideAllTimePopups"
|
||||
@update="timePopUpdate"
|
||||
/>
|
||||
<input
|
||||
ref="endOffset"
|
||||
v-model="offsets.end"
|
||||
@ -122,6 +141,7 @@
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
@change="validateAllOffsets(); submitForm()"
|
||||
@click="showTimePopupEnd"
|
||||
>
|
||||
</div>
|
||||
|
||||
@ -163,6 +183,7 @@ import DatePicker from './DatePicker.vue';
|
||||
import ConductorAxis from './ConductorAxis.vue';
|
||||
import ConductorModeIcon from './ConductorModeIcon.vue';
|
||||
import ConductorHistory from './ConductorHistory.vue';
|
||||
import TimePopup from './timePopup.vue';
|
||||
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
|
||||
@ -174,7 +195,8 @@ export default {
|
||||
DatePicker,
|
||||
ConductorAxis,
|
||||
ConductorModeIcon,
|
||||
ConductorHistory
|
||||
ConductorHistory,
|
||||
TimePopup
|
||||
},
|
||||
data() {
|
||||
let bounds = this.openmct.time.bounds();
|
||||
@ -208,7 +230,9 @@ export default {
|
||||
showDatePicker: false,
|
||||
altPressed: false,
|
||||
isPanning: false,
|
||||
isZooming: false
|
||||
isZooming: false,
|
||||
showTCInputStart: false,
|
||||
showTCInputEnd: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -457,6 +481,25 @@ export default {
|
||||
this.formattedBounds.end = this.timeFormatter.format(date);
|
||||
this.validateAllBounds('endDate');
|
||||
this.submitForm();
|
||||
},
|
||||
hideAllTimePopups() {
|
||||
this.showTCInputStart = false;
|
||||
this.showTCInputEnd = false;
|
||||
},
|
||||
showTimePopupStart() {
|
||||
this.hideAllTimePopups();
|
||||
this.showTCInputStart = !this.showTCInputStart;
|
||||
},
|
||||
showTimePopupEnd() {
|
||||
this.hideAllTimePopups();
|
||||
this.showTCInputEnd = !this.showTCInputEnd;
|
||||
},
|
||||
timePopUpdate(opts) {
|
||||
let { type, hours, minutes, seconds } = opts;
|
||||
|
||||
this.offsets[type] = [hours, minutes, seconds].join(':');
|
||||
this.setOffsetsFromView();
|
||||
this.hideAllTimePopups();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -207,7 +207,7 @@
|
||||
}
|
||||
|
||||
.is-realtime-mode {
|
||||
button {
|
||||
.c-conductor__controls button {
|
||||
@include themedButton($colorTimeBg);
|
||||
color: $colorTimeFg;
|
||||
|
||||
@ -236,3 +236,77 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prototype
|
||||
[class^='pr-tc-input-menu'] {
|
||||
background: $colorBodyBg;
|
||||
border-radius: $controlCr;
|
||||
filter: brightness(1.4);
|
||||
box-shadow: $shdwMenu;
|
||||
padding: $interiorMargin;
|
||||
position: absolute;
|
||||
width: 170px;
|
||||
height: 90px;
|
||||
bottom: 20px;
|
||||
z-index: 99;
|
||||
|
||||
&[class*='--start'] {
|
||||
left: -25px;
|
||||
}
|
||||
|
||||
&[class*='--end'] {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
[class^='pr-tim'] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
> * + * {
|
||||
//margin-left: $interiorMarginSm;
|
||||
}
|
||||
&[class*='labels'] {
|
||||
font-size: 0.8em;
|
||||
|
||||
[class*='__'] {
|
||||
opacity: 0.6;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
[class*='_hrs'] {
|
||||
width: 67px;
|
||||
}
|
||||
}
|
||||
|
||||
&[class*='inputs'] {
|
||||
//background: deeppink;
|
||||
border: 1px solid rgba($colorBodyFg, 0.2);
|
||||
border-radius: $controlCr;
|
||||
padding: 2px;
|
||||
|
||||
input {
|
||||
font-size: 1.25em;
|
||||
width: 42px;
|
||||
}
|
||||
|
||||
[class*='_hrs'] {
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
[class*="colon"] {
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&[class*='__buttons'] {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
}
|
||||
|
127
src/plugins/timeConductor/timePopup.vue
Normal file
127
src/plugins/timeConductor/timePopup.vue
Normal file
@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div
|
||||
class="pr-tc-input-menu"
|
||||
@keydown.enter.prevent
|
||||
@keyup.enter.prevent="submit"
|
||||
@click.stop
|
||||
>
|
||||
<div class="pr-tim-labels">
|
||||
<div class="pr-time-label__hrs">Hrs</div>
|
||||
<div class="pr-time-label__mins">Mins</div>
|
||||
<div class="pr-time-label__secs">Secs</div>
|
||||
</div>
|
||||
<div class="pr-tim-inputs">
|
||||
<input
|
||||
ref="inputHrs"
|
||||
v-model="inputHrs"
|
||||
class="pr-time-input__hrs"
|
||||
step="1"
|
||||
type="number"
|
||||
min="0"
|
||||
max="999"
|
||||
@focusin="selectAll($event)"
|
||||
@focusout="format('inputHrs')"
|
||||
@wheel="increment($event, 'inputHrs')"
|
||||
>
|
||||
<span class="pr-tim-colon">:</span>
|
||||
<input
|
||||
ref="inputMins"
|
||||
v-model="inputMins"
|
||||
type="number"
|
||||
class="pr-time-input__mins"
|
||||
min="0"
|
||||
max="59"
|
||||
step="1"
|
||||
@focusin="selectAll($event)"
|
||||
@focusout="format('inputMins')"
|
||||
@wheel="increment($event, 'inputMins')"
|
||||
>
|
||||
<span class="pr-tim-colon">:</span>
|
||||
<input
|
||||
ref="inputSecs"
|
||||
v-model="inputSecs"
|
||||
type="number"
|
||||
class="pr-time-input__secs"
|
||||
min="0"
|
||||
max="59"
|
||||
step="1"
|
||||
@focusin="selectAll($event)"
|
||||
@focusout="format('inputSecs')"
|
||||
@wheel="increment($event, 'inputSecs')"
|
||||
>
|
||||
</div>
|
||||
<div class="pr-tim__buttons c-button-set c-button-set--strip-h">
|
||||
<button class="c-button icon-check"
|
||||
@click.prevent="submit"
|
||||
></button>
|
||||
<button class="c-button icon-x"
|
||||
@click.prevent="hide"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
offset: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputHrs: '000',
|
||||
inputMins: '00',
|
||||
inputSecs: '00'
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.setOffset();
|
||||
document.addEventListener('click', this.hide);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('click', this.hide);
|
||||
},
|
||||
methods: {
|
||||
format(ref) {
|
||||
const curVal = this[ref];
|
||||
const padAmt = (ref === 'inputHrs') ? 3 : 2;
|
||||
this[ref] = curVal.padStart(padAmt, '0');
|
||||
},
|
||||
submit() {
|
||||
this.$emit('update', {
|
||||
type: this.type,
|
||||
hours: this.inputHrs,
|
||||
minutes: this.inputMins,
|
||||
seconds: this.inputSecs
|
||||
});
|
||||
},
|
||||
hide() {
|
||||
this.$emit('hide');
|
||||
},
|
||||
increment($ev, ref) {
|
||||
$ev.preventDefault();
|
||||
const padAmt = (ref === 'inputHrs') ? 3 : 2;
|
||||
const step = (ref === 'inputHrs') ? 1 : 5;
|
||||
const maxVal = (ref === 'inputHrs') ? 999 : 59;
|
||||
let cv = Math.round(parseInt(this[ref], 10) / step) * step;
|
||||
cv = Math.min(maxVal, Math.max(0, ($ev.deltaY < 0) ? cv + step : cv - step));
|
||||
this[ref] = cv.toString().padStart(padAmt, '0');
|
||||
},
|
||||
setOffset() {
|
||||
[this.inputHrs, this.inputMins, this.inputSecs] = this.offset.split(':');
|
||||
this.inputHrs = this.inputHrs.padStart(3, '0');
|
||||
this.$refs.inputHrs.focus();
|
||||
},
|
||||
selectAll($ev) {
|
||||
$ev.target.select();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
Reference in New Issue
Block a user