Timestrip plan Inspection (#3863)

* Stub in static HTML for Timestrip Activity Inspection
- Added static markup with placeholder values and display logic;
- Refined approach for Links;
* Refactor duration formatting
* Display activity name when it's available
* Don't use indices for keys
* Don't show properties with no labels
This commit is contained in:
Charles Hacskaylo 2021-06-28 10:04:54 -07:00 committed by GitHub
parent 2a1e322230
commit 96dd581a67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 616 additions and 45 deletions

View File

@ -1,3 +1,25 @@
<!--
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.
-->
<template>
<div ref="plan"
class="c-plan c-timeline-holder"
@ -28,7 +50,6 @@ import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
import { getValidatedPlan } from "./util";
import Vue from "vue";
//TODO: UI direction needed for the following property values
const PADDING = 1;
const OUTER_TEXT_PADDING = 12;
const INNER_TEXT_PADDING = 17;
@ -281,7 +302,9 @@ export default {
exceeds: {
start: this.xScale(this.viewBounds.start) > this.xScale(activity.start),
end: this.xScale(this.viewBounds.end) < this.xScale(activity.end)
}
},
start: activity.start,
end: activity.end
},
textLines: textLines,
textStart: textStart,
@ -339,6 +362,9 @@ export default {
components: {
SwimLane
},
provide: {
openmct: this.openmct
},
data() {
return {
heading,
@ -376,7 +402,6 @@ export default {
activityRows.forEach((row) => {
const items = activitiesByRow[row];
items.forEach(item => {
//TODO: Don't draw the left-border of the rectangle if the activity started before viewBounds.start
this.plotActivity(item, parseInt(row, 10), groupSVG);
});
});
@ -399,6 +424,9 @@ export default {
element.setAttributeNS(null, key, attributes[key]);
});
},
getNSAttributesForElement(element, attribute) {
return element.getAttributeNS(null, attribute);
},
// Experimental for now - unused
addForeignElement(svgElement, label, x, y) {
let foreign = document.createElementNS('http://www.w3.org/2000/svg', "foreignObject");
@ -443,6 +471,10 @@ export default {
fill: activity.color
});
rectElement.addEventListener('click', (event) => {
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
});
svgElement.appendChild(rectElement);
item.textLines.forEach((line, index) => {
@ -456,6 +488,9 @@ export default {
const textNode = document.createTextNode(line);
textElement.appendChild(textNode);
textElement.addEventListener('click', (event) => {
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
});
svgElement.appendChild(textElement);
});
// this.addForeignElement(svgElement, activity.name, item.textStart, item.textY - LINE_HEIGHT);
@ -482,6 +517,22 @@ export default {
const cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000;
return cBrightness > cThreshold ? "#000000" : "#ffffff";
},
setSelectionForActivity(element, activity, multiSelect) {
this.openmct.selection.select([{
element: element,
context: {
type: 'activity',
activity: activity
}
}, {
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: this.domainObject,
supportsMultiSelect: true
}
}], multiSelect);
event.stopPropagation();
}
}
};

View File

@ -0,0 +1,52 @@
<!--
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.
-->
<template>
<li class="c-inspect-properties__row">
<div class="c-inspect-properties__label">
{{ label }}
</div>
<div class="c-inspect-properties__value">
{{ value }}
</div>
</li>
</template>
<script>
export default {
props: {
label: {
type: String,
default() {
return '';
}
},
value: {
type: String,
default() {
return '';
}
}
}
};
</script>

View File

@ -0,0 +1,206 @@
<!--
Open MCT, Copyright (c) 2014-2021, 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.
-->
<template>
<div class="c-inspector__properties c-inspect-properties">
<plan-activity-view v-for="activity in activities"
:key="activity.id"
:activity="activity"
:heading="heading"
/>
</div>
</template>
<script>
import PlanActivityView from "./PlanActivityView.vue";
import { getPreciseDuration } from "utils/duration";
import uuid from 'uuid';
const propertyLabels = {
'start': 'Start DateTime',
'end': 'End DateTime',
'duration': 'Duration',
'earliestStart': 'Earliest Start',
'latestEnd': 'Latest End',
'gap': 'Gap',
'overlap': 'Overlap',
'totalTime': 'Total Time'
};
export default {
components: {
PlanActivityView
},
inject: ['openmct', 'selection'],
data() {
return {
name: '',
activities: [],
heading: ''
};
},
mounted() {
this.setFormatters();
this.getPlanData(this.selection);
this.getActivities();
this.openmct.selection.on('change', this.updateSelection);
this.openmct.time.on('timeSystem', this.setFormatters);
},
beforeDestroy() {
this.openmct.selection.off('change', this.updateSelection);
this.openmct.time.off('timeSystem', this.setFormatters);
},
methods: {
setFormatters() {
let timeSystem = this.openmct.time.timeSystem();
this.timeFormatter = this.openmct.telemetry.getValueFormatter({
format: timeSystem.timeFormat
}).formatter;
},
updateSelection(newSelection) {
this.getPlanData(newSelection);
this.getActivities();
},
getPlanData(selection) {
this.selectedActivities = [];
selection.forEach((selectionItem) => {
if (selectionItem[0].context.type === 'activity') {
const activity = selectionItem[0].context.activity;
if (activity) {
this.selectedActivities.push(activity);
}
}
});
},
getActivities() {
if (this.selectedActivities.length <= 1) {
this.heading = 'Time';
this.setSingleActivityProperties();
} else {
this.heading = 'Convex Hull';
this.setMultipleActivityProperties();
}
},
setSingleActivityProperties() {
this.activities.splice(0);
this.selectedActivities.forEach((selectedActivity, index) => {
const activity = {
id: uuid(),
start: {
label: propertyLabels.start,
value: this.formatTime(selectedActivity.start)
},
end: {
label: propertyLabels.end,
value: this.formatTime(selectedActivity.end)
},
duration: {
label: propertyLabels.duration,
value: this.formatDuration(selectedActivity.end - selectedActivity.start)
}
};
this.$set(this.activities, index, activity);
});
},
sortFn(a, b) {
const numA = parseInt(a.start, 10);
const numB = parseInt(b.start, 10);
if (numA > numB) {
return 1;
}
if (numA < numB) {
return -1;
}
return 0;
},
setMultipleActivityProperties() {
this.activities.splice(0);
let earliestStart;
let latestEnd;
let gap;
let overlap;
//Sort by start time
let selectedActivities = this.selectedActivities.sort(this.sortFn);
selectedActivities.forEach((selectedActivity, index) => {
if (selectedActivities.length === 2 && index > 0) {
const previous = selectedActivities[index - 1];
//they're on different rows so there must be overlap
if (previous.end > selectedActivity.start) {
overlap = previous.end - selectedActivity.start;
} else if (previous.end < selectedActivity.start) {
gap = selectedActivity.start - previous.end;
}
}
if (index > 0) {
earliestStart = Math.min(earliestStart, selectedActivity.start);
latestEnd = Math.max(latestEnd, selectedActivity.end);
} else {
earliestStart = selectedActivity.start;
latestEnd = selectedActivity.end;
}
});
let totalTime = latestEnd - earliestStart;
const activity = {
id: uuid(),
'earliestStart': {
label: propertyLabels.earliestStart,
value: this.formatTime(earliestStart)
},
'latestEnd': {
label: propertyLabels.latestEnd,
value: this.formatTime(latestEnd)
}
};
if (gap) {
activity.gap = {
label: propertyLabels.gap,
value: this.formatDuration(gap)
};
} else if (overlap) {
activity.overlap = {
label: propertyLabels.overlap,
value: this.formatDuration(overlap)
};
}
activity.totalTime = {
label: propertyLabels.totalTime,
value: this.formatDuration(totalTime)
};
this.$set(this.activities, 0, activity);
},
formatDuration(duration) {
return getPreciseDuration(duration);
},
formatTime(time) {
return this.timeFormatter.format(time);
}
}
};
</script>

View File

@ -0,0 +1,84 @@
<!--
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.
-->
<template>
<div v-if="timeProperties.length"
class="u-contents"
>
<div class="c-inspect-properties__header">
{{ heading }}
</div>
<ul v-for="timeProperty in timeProperties"
:key="timeProperty.id"
class="c-inspect-properties__section"
>
<activity-property :label="timeProperty.label"
:value="timeProperty.value"
/>
</ul>
</div>
</template>
<script>
import ActivityProperty from './ActivityProperty.vue';
import uuid from 'uuid';
export default {
components: {
ActivityProperty
},
props: {
activity: {
type: Object,
required: true
},
heading: {
type: String,
required: true
}
},
data() {
return {
timeProperties: []
};
},
mounted() {
this.setProperties();
},
methods: {
setProperties() {
Object.keys(this.activity).forEach((key) => {
if (this.activity[key].label) {
const label = this.activity[key].label;
const value = String(this.activity[key].value);
this.$set(this.timeProperties, this.timeProperties.length, {
id: uuid(),
label,
value
});
}
});
}
}
};
</script>

View File

@ -0,0 +1,69 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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 PlanActivitiesView from "./PlanActivitiesView.vue";
import Vue from 'vue';
export default function PlanInspectorViewProvider(openmct) {
return {
key: 'plan-inspector',
name: 'Plan Inspector View',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
}
let context = selection[0][0].context;
return context
&& context.type === 'activity';
},
view: function (selection) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
PlanActivitiesView: PlanActivitiesView
},
provide: {
openmct,
selection: selection
},
template: '<plan-activities-view></plan-activities-view>'
});
},
destroy: function () {
if (component) {
component.$destroy();
component = undefined;
}
}
};
},
priority: function () {
return 1;
}
};
}

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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.
*****************************************************************************/
.c-plan {
svg {
text-rendering: geometricPrecision;

View File

@ -21,6 +21,7 @@
*****************************************************************************/
import PlanViewProvider from './PlanViewProvider';
import PlanInspectorViewProvider from "./inspector/PlanInspectorViewProvider";
export default function () {
return function install(openmct) {
@ -44,6 +45,7 @@ export default function () {
}
});
openmct.objectViews.addProvider(new PlanViewProvider(openmct));
openmct.inspectorViews.addProvider(new PlanInspectorViewProvider(openmct));
};
}

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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.
*****************************************************************************/
export function getValidatedPlan(domainObject) {
let body = domainObject.selectFile.body;
let json = {};

View File

@ -39,9 +39,8 @@ const DEFAULT_DURATION_FORMATTER = 'duration';
const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';
const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
const DEFAULT_RECORDS = 10;
const ONE_MINUTE = 60 * 1000;
const ONE_HOUR = ONE_MINUTE * 60;
const ONE_DAY = ONE_HOUR * 24;
import { getDuration } from "utils/duration";
export default {
inject: ['openmct', 'configuration'],
@ -143,7 +142,7 @@ export default {
let description = `${startTime} - ${this.formatTime(timespan.end)}`;
if (this.timeSystem.isUTCBased && !this.openmct.time.clock()) {
name = `${startTime} ${this.getDuration(timespan.end - timespan.start)}`;
name = `${startTime} ${getDuration(timespan.end - timespan.start)}`;
} else {
name = description;
}
@ -176,41 +175,6 @@ export default {
};
});
},
getDuration(numericDuration) {
let result;
let age;
if (numericDuration > ONE_DAY - 1) {
age = this.normalizeAge((numericDuration / ONE_DAY).toFixed(2));
result = `+ ${age} day`;
if (age !== 1) {
result += 's';
}
} else if (numericDuration > ONE_HOUR - 1) {
age = this.normalizeAge((numericDuration / ONE_HOUR).toFixed(2));
result = `+ ${age} hour`;
if (age !== 1) {
result += 's';
}
} else {
age = this.normalizeAge((numericDuration / ONE_MINUTE).toFixed(2));
result = `+ ${age} min`;
if (age !== 1) {
result += 's';
}
}
return result;
},
normalizeAge(num) {
const hundredtized = num * 100;
const isWhole = hundredtized % 100 === 0;
return isWhole ? hundredtized / 100 : num;
},
getHistoryFromLocalStorage() {
const localStorageHistory = localStorage.getItem(this.storageKey);
const history = localStorageHistory ? JSON.parse(localStorageHistory) : undefined;

View File

@ -20,7 +20,12 @@
<span class="c-object-label__type-icon"
:class="typeCssClass"
></span>
<span class="c-object-label__name">Layout Object</span>
<span v-if="!activity"
class="c-object-label__name"
>Layout Object</span>
<span v-else
class="c-object-label__name"
>{{ activity.name }}</span>
</div>
</div>
<div v-if="multiSelect"
@ -37,6 +42,7 @@ export default {
data() {
return {
domainObject: {},
activity: undefined,
keyString: undefined,
multiSelect: false,
itemsSelected: 0,
@ -51,6 +57,10 @@ export default {
return this.openmct.types.get(this.item.type);
},
typeCssClass() {
if (this.activity) {
return 'icon-activity';
}
if (this.type.definition.cssClass === undefined) {
return 'icon-object';
}
@ -97,7 +107,7 @@ export default {
} else {
this.multiSelect = false;
this.domainObject = selection[0][0].context.item;
this.activity = selection[0][0].context.activity;
if (this.domainObject) {
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.status = this.openmct.status.get(this.keyString);

View File

@ -1,5 +1,7 @@
<template>
<div class="c-inspector__properties c-inspect-properties">
<div v-if="!activity"
class="c-inspector__properties c-inspect-properties"
>
<div class="c-inspect-properties__header">
Details
</div>
@ -81,6 +83,7 @@ export default {
data() {
return {
domainObject: {},
activity: undefined,
multiSelect: false
};
},
@ -157,6 +160,7 @@ export default {
} else {
this.multiSelect = false;
this.domainObject = selection[0][0].context.item;
this.activity = selection[0][0].context.activity;
}
},
formatTime(unixTime) {

85
src/utils/duration.js Normal file
View File

@ -0,0 +1,85 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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.
*****************************************************************************/
const ONE_MINUTE = 60 * 1000;
const ONE_HOUR = ONE_MINUTE * 60;
const ONE_DAY = ONE_HOUR * 24;
function normalizeAge(num) {
const hundredtized = num * 100;
const isWhole = hundredtized % 100 === 0;
return isWhole ? hundredtized / 100 : num;
}
function toDoubleDigits(num) {
if (num >= 10) {
return num;
} else {
return `0${num}`;
}
}
export function getDuration(numericDuration) {
let result;
let age;
if (numericDuration > ONE_DAY - 1) {
age = normalizeAge((numericDuration / ONE_DAY)).toFixed(2);
result = `+ ${age} day`;
if (age !== 1) {
result += 's';
}
} else if (numericDuration > ONE_HOUR - 1) {
age = normalizeAge((numericDuration / ONE_HOUR).toFixed(2));
result = `+ ${age} hour`;
if (age !== 1) {
result += 's';
}
} else {
age = normalizeAge((numericDuration / ONE_MINUTE).toFixed(2));
result = `+ ${age} min`;
if (age !== 1) {
result += 's';
}
}
return result;
}
export function getPreciseDuration(numericDuration) {
let result;
const days = toDoubleDigits(Math.floor((numericDuration) / (24 * 60 * 60 * 1000)));
let remaining = (numericDuration) % (24 * 60 * 60 * 1000);
const hours = toDoubleDigits(Math.floor((remaining) / (60 * 60 * 1000)));
remaining = (remaining) % (60 * 60 * 1000);
const minutes = toDoubleDigits(Math.floor((remaining) / (60 * 1000)));
remaining = (remaining) % (60 * 1000);
const seconds = toDoubleDigits(Math.floor((remaining) / (1000)));
result = `${days}:${hours}:${minutes}:${seconds}`;
return result;
}