From baa8078d23b7c304156280e7657b2e0254678cc4 Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Fri, 2 Oct 2020 11:13:04 -0700 Subject: [PATCH] Plan view to display activities (#3413) * (WIP) Adds Plan view and visualization of activities on different rows * Updates to show activities in the right rows * Improve algorithm to get activityRow for next activity * When activities have names that are longer than their width, show the name outside the activity rectangle * Remove Activity component as we don't need it right now * Use canvas to draw activities instead of svg for performance * Retain SVG version if needed * Include text when calculating overlap * Fix padding, text positioning * Add colors for activities * Fixed bug - Rectangle was shrinking as time passed Draw using SVG * Adds performance activities * [WIP] Refactoring code to be more readable * Fix issues with activity layout * Adds draft for groups * Adds x-offset for groups * Draw a "now" marker for the canvas * Fix formatting for the timeline * Adds now line for the timeline * Add ability to upload a plan json file. * Add tests for the Plan view * Fix issue with File Type checking add resizing for timeline view plans * Refactor code to be more readable * Fix tests that are failing on circleCI * Fix icon for timeline view --- index.html | 1 + platform/forms/src/FileInputService.js | 7 +- platform/forms/src/MCTFileInput.js | 2 +- src/plugins/plugins.js | 7 +- src/plugins/timeline/Plan.vue | 437 +++++++++++++++++++ src/plugins/timeline/TimelineViewLayout.vue | 45 ++ src/plugins/timeline/TimelineViewProvider.js | 64 +++ src/plugins/timeline/activities.json | 38 ++ src/plugins/timeline/plugin.js | 49 +++ src/plugins/timeline/pluginSpec.js | 205 +++++++++ src/plugins/timeline/timeline-axis.scss | 57 +++ src/styles/vue-styles.scss | 1 + 12 files changed, 908 insertions(+), 5 deletions(-) create mode 100644 src/plugins/timeline/Plan.vue create mode 100644 src/plugins/timeline/TimelineViewLayout.vue create mode 100644 src/plugins/timeline/TimelineViewProvider.js create mode 100644 src/plugins/timeline/activities.json create mode 100644 src/plugins/timeline/plugin.js create mode 100644 src/plugins/timeline/pluginSpec.js create mode 100644 src/plugins/timeline/timeline-axis.scss diff --git a/index.html b/index.html index 3911020002..4bb9f9f33d 100644 --- a/index.html +++ b/index.html @@ -48,6 +48,7 @@ openmct.install(openmct.plugins.MyItems()); openmct.install(openmct.plugins.Generator()); openmct.install(openmct.plugins.ExampleImagery()); + openmct.install(openmct.plugins.Timeline()); openmct.install(openmct.plugins.UTCTimeSystem()); openmct.install(openmct.plugins.AutoflowView({ type: "telemetry.panel" diff --git a/platform/forms/src/FileInputService.js b/platform/forms/src/FileInputService.js index fae0f4ef37..ea26c9a2ec 100644 --- a/platform/forms/src/FileInputService.js +++ b/platform/forms/src/FileInputService.js @@ -29,7 +29,6 @@ define(["zepto"], function ($) { * @memberof platform/forms */ function FileInputService() { - } /** @@ -38,7 +37,7 @@ define(["zepto"], function ($) { * * @returns {Promise} promise for an object containing file meta-data */ - FileInputService.prototype.getInput = function () { + FileInputService.prototype.getInput = function (fileType) { var input = this.newInput(); var read = this.readFile; var fileInfo = {}; @@ -51,6 +50,10 @@ define(["zepto"], function ($) { file = this.files[0]; input.remove(); if (file) { + if (fileType && (!file.type || (file.type !== fileType))) { + reject("Incompatible file type"); + } + read(file) .then(function (contents) { fileInfo.name = file.name; diff --git a/platform/forms/src/MCTFileInput.js b/platform/forms/src/MCTFileInput.js index 1726753b68..f908592def 100644 --- a/platform/forms/src/MCTFileInput.js +++ b/platform/forms/src/MCTFileInput.js @@ -40,7 +40,7 @@ define( } function handleClick() { - fileInputService.getInput().then(function (result) { + fileInputService.getInput(scope.structure.type).then(function (result) { setText(result.name); scope.ngModel[scope.field] = result; control.$setValidity("file-input", true); diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index 6c3f7b2401..d49e0eb801 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -57,7 +57,8 @@ define([ './notificationIndicator/plugin', './newFolderAction/plugin', './persistence/couch/plugin', - './defaultRootName/plugin' + './defaultRootName/plugin', + './timeline/plugin' ], function ( _, UTCTimeSystem, @@ -95,7 +96,8 @@ define([ NotificationIndicator, NewFolderAction, CouchDBPlugin, - DefaultRootName + DefaultRootName, + Timeline ) { const bundleMap = { LocalStorage: 'platform/persistence/local', @@ -188,6 +190,7 @@ define([ plugins.NewFolderAction = NewFolderAction.default; plugins.ISOTimeFormat = ISOTimeFormat.default; plugins.DefaultRootName = DefaultRootName.default; + plugins.Timeline = Timeline.default; return plugins; }); diff --git a/src/plugins/timeline/Plan.vue b/src/plugins/timeline/Plan.vue new file mode 100644 index 0000000000..54036ec4e9 --- /dev/null +++ b/src/plugins/timeline/Plan.vue @@ -0,0 +1,437 @@ + + + diff --git a/src/plugins/timeline/TimelineViewLayout.vue b/src/plugins/timeline/TimelineViewLayout.vue new file mode 100644 index 0000000000..c61214c833 --- /dev/null +++ b/src/plugins/timeline/TimelineViewLayout.vue @@ -0,0 +1,45 @@ +/***************************************************************************** +* 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. +*****************************************************************************/ + + + + diff --git a/src/plugins/timeline/TimelineViewProvider.js b/src/plugins/timeline/TimelineViewProvider.js new file mode 100644 index 0000000000..7d968f8f9e --- /dev/null +++ b/src/plugins/timeline/TimelineViewProvider.js @@ -0,0 +1,64 @@ +/***************************************************************************** + * 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 TimelineViewLayout from './TimelineViewLayout.vue'; +import Vue from 'vue'; + +export default function TimelineViewProvider(openmct) { + + return { + key: 'timeline.view', + name: 'Timeline', + cssClass: 'icon-clock', + canView(domainObject) { + return domainObject.type === 'plan'; + }, + + canEdit(domainObject) { + return domainObject.type === 'plan'; + }, + + view: function (domainObject) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + TimelineViewLayout + }, + provide: { + openmct, + domainObject + }, + template: '' + }); + }, + destroy: function () { + component.$destroy(); + component = undefined; + } + }; + } + }; +} diff --git a/src/plugins/timeline/activities.json b/src/plugins/timeline/activities.json new file mode 100644 index 0000000000..35878c3fc8 --- /dev/null +++ b/src/plugins/timeline/activities.json @@ -0,0 +1,38 @@ +{ + "ROVER": [ + { + "name": "Activity 1", + "start": 1597170002854, + "end": 1597171032854, + "type": "ROVER", + "color": "fuchsia", + "textColor": "black" + }, + { + "name": "Activity 2", + "start": 1597171132854, + "end": 1597171232854, + "type": "ROVER", + "color": "fuchsia", + "textColor": "black" + }, + { + "name": "Activity 4", + "start": 1597171132854, + "end": 1597171232854, + "type": "ROVER", + "color": "fuchsia", + "textColor": "black" + } + ], + "VIPER": [ + { + "name": "Activity 3", + "start": 1597170132854, + "end": 1597171202854, + "type": "VIPER", + "color": "fuchsia", + "textColor": "black" + } + ] +} diff --git a/src/plugins/timeline/plugin.js b/src/plugins/timeline/plugin.js new file mode 100644 index 0000000000..8d39f7b26a --- /dev/null +++ b/src/plugins/timeline/plugin.js @@ -0,0 +1,49 @@ +/***************************************************************************** + * 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 TimelineViewProvider from './TimelineViewProvider'; + +export default function () { + return function install(openmct) { + openmct.types.addType('plan', { + name: 'Plan', + key: 'plan', + description: 'An activity timeline', + creatable: true, + cssClass: 'icon-timeline', + form: [ + { + name: 'Upload Plan (JSON File)', + key: 'selectFile', + control: 'file-input', + required: true, + text: 'Select File', + type: 'application/json' + } + ], + initialize: function (domainObject) { + } + }); + openmct.objectViews.addProvider(new TimelineViewProvider(openmct)); + }; +} + diff --git a/src/plugins/timeline/pluginSpec.js b/src/plugins/timeline/pluginSpec.js new file mode 100644 index 0000000000..c14e496c23 --- /dev/null +++ b/src/plugins/timeline/pluginSpec.js @@ -0,0 +1,205 @@ +/***************************************************************************** + * 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 { createOpenMct, resetApplicationState } from "utils/testing"; +import TimelinePlugin from "./plugin"; +import Vue from 'vue'; +import TimelineViewLayout from "./TimelineViewLayout.vue"; + +describe('the plugin', function () { + let planDefinition; + let element; + let child; + let openmct; + + beforeEach((done) => { + const appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; + + openmct = createOpenMct(); + openmct.install(new TimelinePlugin()); + + planDefinition = openmct.types.get('plan').definition; + + element = document.createElement('div'); + element.style.width = '640px'; + element.style.height = '480px'; + child = document.createElement('div'); + child.style.width = '640px'; + child.style.height = '480px'; + element.appendChild(child); + + openmct.time.bounds({ + start: 1597160002854, + end: 1597181232854 + }); + + openmct.on('start', done); + openmct.startHeadless(appHolder); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + let mockPlanObject = { + name: 'Plan', + key: 'plan', + creatable: true + }; + + it('defines a plan object type with the correct key', () => { + expect(planDefinition.key).toEqual(mockPlanObject.key); + }); + + describe('the plan object', () => { + + it('is creatable', () => { + expect(planDefinition.creatable).toEqual(mockPlanObject.creatable); + }); + + it('provides a timeline view', () => { + const testViewObject = { + id: "test-object", + type: "plan" + }; + + const applicableViews = openmct.objectViews.get(testViewObject); + let timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'timeline.view'); + expect(timelineView).toBeDefined(); + }); + + }); + + describe('the timeline view displays activities', () => { + let planDomainObject; + let component; + let planViewComponent; + + beforeEach((done) => { + planDomainObject = { + type: 'plan', + id: "test-object", + selectFile: { + body: JSON.stringify({ + "TEST-GROUP": [ + { + "name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua", + "start": 1597170002854, + "end": 1597171032854, + "type": "TEST-GROUP", + "color": "fuchsia", + "textColor": "black" + }, + { + "name": "Sed ut perspiciatis", + "start": 1597171132854, + "end": 1597171232854, + "type": "TEST-GROUP", + "color": "fuchsia", + "textColor": "black" + } + ] + }) + } + }; + + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + provide: { + openmct: openmct, + domainObject: planDomainObject + }, + el: viewContainer, + components: { + TimelineViewLayout + }, + template: '' + }); + + return Vue.nextTick().then(() => { + planViewComponent = component.$root.$children[0].$children[0]; + setTimeout(() => { + clearInterval(planViewComponent.resizeTimer); + //TODO: this is a hack to ensure the canvas has a width - maybe there's a better way to set the width of the plan div + planViewComponent.width = 1200; + planViewComponent.setScaleAndPlotActivities(); + done(); + }, 300); + }); + }); + + it('loads activities into the view', () => { + expect(planViewComponent.json).toBeDefined(); + expect(planViewComponent.json["TEST-GROUP"].length).toEqual(2); + }); + + it('loads a time axis into the view', () => { + let ticks = planViewComponent.axisElement.node().querySelectorAll('g.tick'); + expect(ticks.length).toEqual(11); + }); + + it('calculates the activity layout', () => { + const expectedActivitiesByRow = { + "0": [ + { + "heading": "TEST-GROUP", + "activity": { + "color": "fuchsia", + "textColor": "black" + }, + "textLines": [ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, ", + "sed sed do eiusmod tempor incididunt ut labore et " + ], + "textStart": -47.51342439943476, + "textY": 12, + "start": -47.51625058878945, + "end": 204.97315120113046, + "rectWidth": -4.9971738106453145 + } + ], + "42": [ + { + "heading": "", + "activity": { + "color": "fuchsia", + "textColor": "black" + }, + "textLines": [ + "Sed ut perspiciatis " + ], + "textStart": -48.483749411210546, + "textY": 54, + "start": -52.99858690532266, + "end": 9.032501177578908, + "rectWidth": -0.48516250588788523 + } + ] + }; + expect(Object.keys(planViewComponent.activitiesByRow)).toEqual(Object.keys(expectedActivitiesByRow)); + }); + }); + +}); diff --git a/src/plugins/timeline/timeline-axis.scss b/src/plugins/timeline/timeline-axis.scss new file mode 100644 index 0000000000..efdf7de7c7 --- /dev/null +++ b/src/plugins/timeline/timeline-axis.scss @@ -0,0 +1,57 @@ +.c-timeline { + $h: 18px; + $tickYPos: ($h / 2) + 12px + 10px; + $tickXPos: 100px; + + height: 100%; + + svg { + text-rendering: geometricPrecision; + width: 100%; + height: 100%; + > g.axis { + // Overall Tick holder + transform: translateY($tickYPos) translateX($tickXPos); + + g { + //Each tick. These move on drag. + line { + // Line beneath ticks + display: none; + } + } + } + + text:not(.activity) { + // Tick labels + fill: $colorBodyFg; + font-size: 1em; + paint-order: stroke; + font-weight: bold; + stroke: $colorBodyBg; + stroke-linecap: butt; + stroke-linejoin: bevel; + stroke-width: 6px; + } + + text.activity { + stroke: none; + } + } + + + + .nowMarker { + width: 2px; + position: absolute; + z-index: 10; + background: gray; + + & .icon-arrow-down { + font-size: large; + position: absolute; + top: -8px; + left: -8px; + } + } +} diff --git a/src/styles/vue-styles.scss b/src/styles/vue-styles.scss index 3d9e24960d..1713be5504 100644 --- a/src/styles/vue-styles.scss +++ b/src/styles/vue-styles.scss @@ -26,6 +26,7 @@ @import "../plugins/timeConductor/conductor-mode.scss"; @import "../plugins/timeConductor/conductor-mode-icon.scss"; @import "../plugins/timeConductor/date-picker.scss"; +@import "../plugins/timeline/timeline-axis.scss"; @import "../ui/components/object-frame.scss"; @import "../ui/components/object-label.scss"; @import "../ui/components/progress-bar.scss";