Compare commits
	
		
			11 Commits
		
	
	
		
			multi-user
			...
			style-time
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 31720d5a05 | ||
|   | 23151d83b9 | ||
|   | c37092e625 | ||
|   | 6306f680a8 | ||
|   | aa3b32c337 | ||
|   | 9313eefdee | ||
|   | 4a6765146f | ||
|   | ad506c56fd | ||
|   | fa0a22c4a0 | ||
|   | 0320c77c02 | ||
|   | f4d5743924 | 
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
| <div ref="plan" | ||||
|      class="c-plan" | ||||
|      class="c-plan c-timeline-holder" | ||||
| > | ||||
|     <template v-if="viewBounds && !options.compact"> | ||||
|         <swim-lane> | ||||
| @@ -22,10 +22,10 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import * as d3Selection from 'd3-selection'; | ||||
| import * as d3Scale from 'd3-scale'; | ||||
| import TimelineAxis from "../../ui/components/TimeSystemAxis.vue"; | ||||
| 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 | ||||
| @@ -38,9 +38,8 @@ const RESIZE_POLL_INTERVAL = 200; | ||||
| const ROW_HEIGHT = 25; | ||||
| const LINE_HEIGHT = 12; | ||||
| const MAX_TEXT_WIDTH = 300; | ||||
| const EDGE_ROUNDING = 10; | ||||
| const DEFAULT_COLOR = 'yellow'; | ||||
| const DEFAULT_TEXT_COLOR = 'white'; | ||||
| const EDGE_ROUNDING = 5; | ||||
| const DEFAULT_COLOR = '#cc9922'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
| @@ -72,7 +71,7 @@ export default { | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.validateJSON(this.domainObject.selectFile.body); | ||||
|         this.getPlanData(this.domainObject); | ||||
|  | ||||
|         this.canvas = this.$refs.plan.appendChild(document.createElement('canvas')); | ||||
|         this.canvas.height = 0; | ||||
| @@ -118,14 +117,8 @@ export default { | ||||
|  | ||||
|             return clientWidth - 200; | ||||
|         }, | ||||
|         validateJSON(jsonString) { | ||||
|             try { | ||||
|                 this.json = JSON.parse(jsonString); | ||||
|             } catch (e) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|         getPlanData(domainObject) { | ||||
|             this.planData = getValidatedPlan(domainObject); | ||||
|         }, | ||||
|         updateViewBounds() { | ||||
|             this.viewBounds = this.openmct.time.bounds(); | ||||
| @@ -148,7 +141,8 @@ export default { | ||||
|             } | ||||
|         }, | ||||
|         clearPreviousActivities() { | ||||
|             d3Selection.selectAll(".c-plan__contents > div").remove(); | ||||
|             let activities = this.$el.querySelectorAll(".c-plan__contents > div"); | ||||
|             activities.forEach(activity => activity.remove()); | ||||
|         }, | ||||
|         setDimensions() { | ||||
|             const planHolder = this.$refs.plan; | ||||
| @@ -231,14 +225,14 @@ export default { | ||||
|             return (currentRow || 0); | ||||
|         }, | ||||
|         calculatePlanLayout() { | ||||
|             let groups = Object.keys(this.json); | ||||
|             let groups = Object.keys(this.planData); | ||||
|             this.groupActivities = {}; | ||||
|  | ||||
|             groups.forEach((key, index) => { | ||||
|                 let activitiesByRow = {}; | ||||
|                 let currentRow = 0; | ||||
|  | ||||
|                 let activities = this.json[key]; | ||||
|                 let activities = this.planData[key]; | ||||
|                 activities.forEach((activity) => { | ||||
|                     if (this.isActivityInBounds(activity)) { | ||||
|                         const currentStart = Math.max(this.viewBounds.start, activity.start); | ||||
| @@ -251,6 +245,13 @@ export default { | ||||
|                         //TODO: Fix bug for SVG where the rectWidth is not proportional to the canvas measuredWidth of the text | ||||
|                         const activityNameFitsRect = (rectWidth >= activityNameWidth); | ||||
|                         const textStart = (activityNameFitsRect ? rectX : rectY) + TEXT_LEFT_PADDING; | ||||
|                         const color = activity.color || DEFAULT_COLOR; | ||||
|                         let textColor = ''; | ||||
|                         if (activity.textColor) { | ||||
|                             textColor = activity.textColor; | ||||
|                         } else if (activityNameFitsRect) { | ||||
|                             textColor = this.getContrastingColor(color); | ||||
|                         } | ||||
|  | ||||
|                         let textLines = this.getActivityDisplayText(this.canvasContext, activity.name, activityNameFitsRect); | ||||
|                         const textWidth = textStart + this.getTextWidth(textLines[0]) + TEXT_LEFT_PADDING; | ||||
| @@ -269,8 +270,8 @@ export default { | ||||
|  | ||||
|                         activitiesByRow[currentRow].push({ | ||||
|                             activity: { | ||||
|                                 color: activity.color || DEFAULT_COLOR, | ||||
|                                 textColor: activity.textColor || DEFAULT_TEXT_COLOR, | ||||
|                                 color: color, | ||||
|                                 textColor: textColor, | ||||
|                                 name: activity.name, | ||||
|                                 exceeds: { | ||||
|                                     start: this.xScale(this.viewBounds.start) > this.xScale(activity.start), | ||||
| @@ -279,6 +280,7 @@ export default { | ||||
|                             }, | ||||
|                             textLines: textLines, | ||||
|                             textStart: textStart, | ||||
|                             textClass: activityNameFitsRect ? "" : "activity-label--outside-rect", | ||||
|                             textY: textY, | ||||
|                             start: rectX, | ||||
|                             end: activityNameFitsRect ? rectY : textStart + textWidth, | ||||
| @@ -423,11 +425,14 @@ export default { | ||||
|                 width = width + EDGE_ROUNDING; | ||||
|             } | ||||
|  | ||||
|             width = Math.max(width, 1); // Set width to a minimum of 1 | ||||
|  | ||||
|             // rx: don't round corners if the width of the rect is smaller than the rounding radius | ||||
|             this.setNSAttributesForElement(rectElement, { | ||||
|                 class: 'activity-bounds', | ||||
|                 x: item.activity.exceeds.start ? item.start - EDGE_ROUNDING : item.start, | ||||
|                 y: row, | ||||
|                 rx: EDGE_ROUNDING, | ||||
|                 rx: (width < EDGE_ROUNDING * 2) ? 0 : EDGE_ROUNDING, | ||||
|                 width: width, | ||||
|                 height: String(ROW_HEIGHT), | ||||
|                 fill: activity.color | ||||
| @@ -438,7 +443,7 @@ export default { | ||||
|             item.textLines.forEach((line, index) => { | ||||
|                 let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text'); | ||||
|                 this.setNSAttributesForElement(textElement, { | ||||
|                     class: 'activity-label', | ||||
|                     class: `activity-label ${item.textClass}`, | ||||
|                     x: item.textStart, | ||||
|                     y: item.textY + (index * LINE_HEIGHT), | ||||
|                     fill: activity.textColor | ||||
| @@ -449,6 +454,29 @@ export default { | ||||
|                 svgElement.appendChild(textElement); | ||||
|             }); | ||||
|             // this.addForeignElement(svgElement, activity.name, item.textStart, item.textY - LINE_HEIGHT); | ||||
|         }, | ||||
|         cutHex(h, start, end) { | ||||
|             const hStr = (h.charAt(0) === '#') ? h.substring(1, 7) : h; | ||||
|  | ||||
|             return parseInt(hStr.substring(start, end), 16); | ||||
|         }, | ||||
|         getContrastingColor(hexColor) { | ||||
|             // https://codepen.io/davidhalford/pen/ywEva/ | ||||
|             // TODO: move this into a general utility function? | ||||
|             const cThreshold = 130; | ||||
|  | ||||
|             if (hexColor.indexOf('#') === -1) { | ||||
|                 // We weren't given a hex color | ||||
|                 return "#ff0000"; | ||||
|             } | ||||
|  | ||||
|             const hR = this.cutHex(hexColor, 0, 2); | ||||
|             const hG = this.cutHex(hexColor, 2, 4); | ||||
|             const hB = this.cutHex(hexColor, 4, 6); | ||||
|  | ||||
|             const cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000; | ||||
|  | ||||
|             return cBrightness > cThreshold ? "#000000" : "#ffffff"; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -1,21 +1,16 @@ | ||||
| .c-plan { | ||||
|  | ||||
|   @include abs(); | ||||
|  | ||||
|   svg { | ||||
|     text-rendering: geometricPrecision; | ||||
|  | ||||
|     .activity-label, .no-activities { | ||||
|     text { | ||||
|       stroke: none; | ||||
|     } | ||||
|  | ||||
|     .no-activities { | ||||
|       fill: #383838; | ||||
|     } | ||||
|  | ||||
|     .activity-bounds { | ||||
|       fill-opacity: 0.5; | ||||
|     } | ||||
|       .activity-label { | ||||
|           &--outside-rect { | ||||
|               fill: $colorBodyFg !important; | ||||
|           } | ||||
|       } | ||||
|   } | ||||
|  | ||||
|   canvas { | ||||
|   | ||||
							
								
								
									
										11
									
								
								src/plugins/plan/util.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/plugins/plan/util.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| export function getValidatedPlan(domainObject) { | ||||
|     let jsonString = domainObject.selectFile.body; | ||||
|     let json = {}; | ||||
|     try { | ||||
|         json = JSON.parse(jsonString); | ||||
|     } catch (e) { | ||||
|         return json; | ||||
|     } | ||||
|  | ||||
|     return json; | ||||
| } | ||||
| @@ -55,7 +55,7 @@ | ||||
|                 <swim-lane :icon-class="item.type.definition.cssClass" | ||||
|                            :min-height="item.height" | ||||
|                            :show-ucontents="item.domainObject.type === 'plan'" | ||||
|                            :span-rows="item.domainObject.type === 'plan'" | ||||
|                            :span-rows-count="item.rowCount" | ||||
|                 > | ||||
|                     <template slot="label"> | ||||
|                         {{ item.domainObject.name }} | ||||
| @@ -78,6 +78,7 @@ | ||||
| import ObjectView from '@/ui/components/ObjectView.vue'; | ||||
| import TimelineAxis from '../../ui/components/TimeSystemAxis.vue'; | ||||
| import SwimLane from "@/ui/components/swim-lane/SwimLane.vue"; | ||||
| import { getValidatedPlan } from "../plan/util"; | ||||
|  | ||||
| const unknownObjectType = { | ||||
|     definition: { | ||||
| @@ -116,6 +117,8 @@ export default { | ||||
|         this.composition.off('add', this.addItem); | ||||
|         this.composition.off('remove', this.removeItem); | ||||
|         this.composition.off('reorder', this.reorder); | ||||
|         this.openmct.time.off("bounds", this.updateViewBounds); | ||||
|  | ||||
|     }, | ||||
|     mounted() { | ||||
|         if (this.composition) { | ||||
| @@ -126,6 +129,7 @@ export default { | ||||
|         } | ||||
|  | ||||
|         this.getTimeSystems(); | ||||
|         this.openmct.time.on("bounds", this.updateViewBounds); | ||||
|     }, | ||||
|     methods: { | ||||
|         addItem(domainObject) { | ||||
| @@ -133,6 +137,10 @@ export default { | ||||
|             let keyString = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|             let objectPath = [domainObject].concat(this.objectPath.slice()); | ||||
|             let viewKey = getViewKey(domainObject, this.openmct); | ||||
|             let rowCount = 0; | ||||
|             if (domainObject.type === 'plan') { | ||||
|                 rowCount = Object.keys(getValidatedPlan(domainObject)).length; | ||||
|             } | ||||
|  | ||||
|             let height = domainObject.type === 'telemetry.plot.stacked' ? `${domainObject.composition.length * 100}px` : '100px'; | ||||
|             let item = { | ||||
| @@ -141,6 +149,7 @@ export default { | ||||
|                 type, | ||||
|                 keyString, | ||||
|                 viewKey, | ||||
|                 rowCount, | ||||
|                 height | ||||
|             }; | ||||
|  | ||||
| @@ -174,6 +183,12 @@ export default { | ||||
|  | ||||
|             //TODO: Some kind of translation via an offset? of current bounds to target timeSystem | ||||
|             return currentBounds; | ||||
|         }, | ||||
|         updateViewBounds(bounds) { | ||||
|             let currentTimeSystem = this.timeSystems.find(item => item.timeSystem.key === this.openmct.time.timeSystem().key); | ||||
|             if (currentTimeSystem) { | ||||
|                 currentTimeSystem.bounds = bounds; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -22,6 +22,7 @@ | ||||
|  | ||||
| import { createOpenMct, resetApplicationState } from "utils/testing"; | ||||
| import TimelinePlugin from "./plugin"; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| describe('the plugin', function () { | ||||
|     let objectDef; | ||||
| @@ -47,7 +48,7 @@ describe('the plugin', function () { | ||||
|         child.style.height = '480px'; | ||||
|         element.appendChild(child); | ||||
|  | ||||
|         openmct.time.bounds({ | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: 1597160002854, | ||||
|             end: 1597181232854 | ||||
|         }); | ||||
| @@ -75,18 +76,32 @@ describe('the plugin', function () { | ||||
|         it('is creatable', () => { | ||||
|             expect(objectDef.creatable).toEqual(mockObject.creatable); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|         it('provides a timeline view', () => { | ||||
|     describe('the view', () => { | ||||
|         let timelineView; | ||||
|  | ||||
|         beforeEach((done) => { | ||||
|             const testViewObject = { | ||||
|                 id: "test-object", | ||||
|                 type: "time-strip" | ||||
|             }; | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(testViewObject); | ||||
|             let timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); | ||||
|             timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); | ||||
|             let view = timelineView.view(testViewObject, element); | ||||
|             view.show(child, true); | ||||
|             Vue.nextTick(done); | ||||
|         }); | ||||
|  | ||||
|         it('provides a view', () => { | ||||
|             expect(timelineView).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it('displays a time axis', () => { | ||||
|             const el = element.querySelector('.c-timesystem-axis'); | ||||
|             expect(el).toBeDefined(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|   | ||||
| @@ -1,7 +1,4 @@ | ||||
| .c-timeline-holder { | ||||
|   @include abs(); | ||||
| } | ||||
|  | ||||
| .c-timeline { | ||||
|  | ||||
| } | ||||
|     @include abs(); | ||||
|     overflow-x: hidden; | ||||
| } | ||||
| @@ -34,7 +34,7 @@ | ||||
| @import "../ui/components/object-label.scss"; | ||||
| @import "../ui/components/progress-bar.scss"; | ||||
| @import "../ui/components/search.scss"; | ||||
| @import "../ui/components/swim-lane/swim-lane.scss"; | ||||
| @import "../ui/components/swim-lane/swimlane.scss"; | ||||
| @import "../ui/components/toggle-switch.scss"; | ||||
| @import "../ui/components/timesystem-axis.scss"; | ||||
| @import "../ui/inspector/elements.scss"; | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| <template> | ||||
| <div class="u-contents" | ||||
|      :class="{'c-swim-lane': !isNested}" | ||||
|      :class="{'c-swimlane': !isNested}" | ||||
| > | ||||
|  | ||||
|     <div class="c-swim-lane__lane-label c-object-label" | ||||
|          :class="{'c-swim-lane__lane-label--span-rows': spanRows, 'c-swim-lane__lane-label--span-cols': (!spanRows && !isNested)}" | ||||
|     <div class="c-swimlane__lane-label c-object-label" | ||||
|          :class="{'c-swimlane__lane-label--span-cols': (!spanRowsCount && !isNested)}" | ||||
|          :style="gridRowSpan" | ||||
|     > | ||||
|         <div v-if="iconClass" | ||||
|              class="c-object-label__type-icon" | ||||
| @@ -17,7 +18,7 @@ | ||||
|         </div> | ||||
|  | ||||
|     </div> | ||||
|     <div class="c-swim-lane__lane-object" | ||||
|     <div class="c-swimlane__lane-object" | ||||
|          :style="{'min-height': minHeight}" | ||||
|          :class="{'u-contents': showUcontents}" | ||||
|          data-selectable | ||||
| @@ -55,10 +56,19 @@ export default { | ||||
|                 return false; | ||||
|             } | ||||
|         }, | ||||
|         spanRows: { | ||||
|             type: Boolean, | ||||
|         spanRowsCount: { | ||||
|             type: Number, | ||||
|             default() { | ||||
|                 return false; | ||||
|                 return 0; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         gridRowSpan() { | ||||
|             if (this.spanRowsCount) { | ||||
|                 return `grid-row: span ${this.spanRowsCount}`; | ||||
|             } else { | ||||
|                 return ''; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -1,27 +0,0 @@ | ||||
| .c-swim-lane { | ||||
|   display: grid; | ||||
|   grid-template-columns: 100px 100px 1fr; | ||||
|   grid-column-gap: 1px; | ||||
|   grid-row-gap: 1px; | ||||
|   width: 100%; | ||||
|  | ||||
|   [class*='__lane-label'] { | ||||
|     background: rgba($colorBodyFg, 0.2); // TODO: convert to theme constant | ||||
|     color: $colorBodyFg; // TODO: convert to theme constant | ||||
|     padding: $interiorMarginSm; | ||||
|   } | ||||
|  | ||||
|   [class*='--span-cols'] { | ||||
|     grid-column: span 2; | ||||
|   } | ||||
|  | ||||
|   [class*='--span-rows'] { | ||||
|     grid-row: span 4; | ||||
|   } | ||||
|  | ||||
|   &__lane-object { | ||||
|     .c-plan { | ||||
|       display: contents; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										30
									
								
								src/ui/components/swim-lane/swimlane.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/ui/components/swim-lane/swimlane.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| .c-swimlane { | ||||
|     display: grid; | ||||
|     grid-template-columns: 100px 100px 1fr; | ||||
|     grid-column-gap: 1px; | ||||
|     grid-row-gap: 1px; | ||||
|     margin-bottom: 1px; | ||||
|     width: 100%; | ||||
|  | ||||
|     [class*='__lane-label'] { | ||||
|         background: rgba($colorBodyFg, 0.2); | ||||
|         color: $colorBodyFg; | ||||
|         padding: $interiorMarginSm; | ||||
|     } | ||||
|  | ||||
|     [class*='--span-cols'] { | ||||
|         grid-column: span 2; | ||||
|     } | ||||
|  | ||||
|     [class*='--span-rows'] { | ||||
|         grid-row: span 4; | ||||
|     } | ||||
|  | ||||
|     &__lane-object { | ||||
|         background: rgba(black, 0.1); | ||||
|  | ||||
|         .c-plan { | ||||
|             display: contents; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,35 +1,42 @@ | ||||
| .c-timesystem-axis { | ||||
|   $h: 30px; | ||||
|   height: $h; | ||||
|     $h: 30px; | ||||
|     height: $h; | ||||
|  | ||||
|   svg { | ||||
|     text-rendering: geometricPrecision; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     svg { | ||||
|         $lineC: rgba($colorBodyFg, 0.3) !important; | ||||
|         text-rendering: geometricPrecision; | ||||
|         width: 100%; | ||||
|         height: 100%; | ||||
|  | ||||
|     text:not(.activity) { | ||||
|       // Tick labels | ||||
|       fill: $colorBodyFg; | ||||
|       paint-order: stroke; | ||||
|       font-weight: bold; | ||||
|       stroke: $colorBodyBg; | ||||
|       stroke-linecap: butt; | ||||
|       stroke-linejoin: bevel; | ||||
|       stroke-width: 6px; | ||||
|         .domain { | ||||
|             stroke: $lineC; | ||||
|         } | ||||
|  | ||||
|         .tick { | ||||
|             line { | ||||
|                 stroke: $lineC; | ||||
|             } | ||||
|  | ||||
|             text { | ||||
|                 // Tick labels | ||||
|                 fill: $colorBodyFg; | ||||
|                 paint-order: stroke; | ||||
|                 font-weight: bold; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .nowMarker { | ||||
|     width: 2px; | ||||
|     position: absolute; | ||||
|     z-index: 10; | ||||
|     background: gray; | ||||
|     .nowMarker { | ||||
|         width: 2px; | ||||
|         position: absolute; | ||||
|         z-index: 10; | ||||
|         background: gray; | ||||
|  | ||||
|     & .icon-arrow-down { | ||||
|       font-size: large; | ||||
|       position: absolute; | ||||
|       top: -8px; | ||||
|       left: -8px; | ||||
|         & .icon-arrow-down { | ||||
|             font-size: large; | ||||
|             position: absolute; | ||||
|             top: -8px; | ||||
|             left: -8px; | ||||
|         } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user