Compare commits
	
		
			20 Commits
		
	
	
		
			imagery-te
			...
			plot-vee-t
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 681ffef1d3 | ||
|   | e15f58c1c6 | ||
|   | 785226cc5a | ||
|   | e597c47171 | ||
|   | fb7b46fcaa | ||
|   | 0435e1b667 | ||
|   | 0de4192345 | ||
|   | 3bdbf2aa56 | ||
|   | baee0870d3 | ||
|   | b4d0786369 | ||
|   | eb69e02ce3 | ||
|   | 056b3f61ce | ||
|   | a0dc3da8fb | ||
|   | 48f345a46b | ||
|   | 889a5c6ea9 | ||
|   | 5502009127 | ||
|   | cb41be7922 | ||
|   | 52b8720d37 | ||
|   | bb8c8a75ab | ||
|   | b40494ac95 | 
| @@ -11,7 +11,7 @@ | ||||
|     "platform/telemetry", | ||||
|     "platform/features/layout", | ||||
|     "platform/features/pages", | ||||
|     "platform/features/plot", | ||||
|     "platform/features/plot-reborn", | ||||
|     "platform/features/scrolling", | ||||
|     "platform/forms", | ||||
|     "platform/persistence/queue", | ||||
|   | ||||
							
								
								
									
										57
									
								
								platform/features/plot-reborn/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								platform/features/plot-reborn/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| # plot-reborn | ||||
|  | ||||
| The `plot-reborn` bundle provides directives for composing plot based views. | ||||
| It also exposes domain objects for plotting telemetry points. | ||||
|  | ||||
| ## Views | ||||
|  | ||||
| * OverlayPlot: can be used on any domain object that has or delegates a  | ||||
|     telemetry capability. | ||||
|  | ||||
| * StackedPlot: can be used on any domain object that delegates telemetry or  | ||||
|     delegates composition of elements that have telemetry. | ||||
|  | ||||
| ## Directives | ||||
|  | ||||
| * `mct-chart`: an element that takes `series`, `viewport`, and | ||||
|     `rectangles` and plots the data.  Adding points to a series after it has  | ||||
|     been initially plotted can be done either by recreating the series object | ||||
|     or by broadcasting "series:data:add" with arguments `event`, `seriesIndex`,  | ||||
|     `points`.  This will append `points` to the `series` at index `seriesIndex`. | ||||
|  | ||||
| * `mct-plot`: A directive that wraps a mct-chart and handles user interactions | ||||
|     with that plot.  It emits events that a parent view can use for coordinating | ||||
|     functionality: | ||||
|     * emits a `user:viewport:change:start` event when the viewport begins being | ||||
|         changed by a user, to allow any parent controller to prevent viewport  | ||||
|         modifications while the user is interacting with the plot. | ||||
|     * emits a `user:viewport:change:end` event when the user has finished | ||||
|         changing the viewport.  This allows a controller on a parent scope to | ||||
|         track viewport history and provide any necessary functionality | ||||
|         around viewport changes, e.g. viewport history. | ||||
|  | ||||
| * `mct-overlay-plot`: A directive that takes `domainObject` and plots either a | ||||
|     single series of data (in the case of a single telemetry object) or multiple | ||||
|     series of data (in the case of a object which delegates telemetry). | ||||
|  | ||||
| ## Controllers | ||||
|  | ||||
| NOTE: this section not accurate.  Essentially, these controllers format data for | ||||
| the mct-chart directive.  They also handle live viewport updating, as well as | ||||
| managing all transformations from domain objects to views. | ||||
|  | ||||
| * StackPlotController: Uses the composition capability of a StackPlot domain  | ||||
|     object to retrieve SubPlots and render them with individual PlotControllers. | ||||
| * PlotController: Uses either a domain object that delegates telemetry or a  | ||||
|     domain object with telemetry to and feeds that data to the mct-chart  | ||||
|     directive. | ||||
|  | ||||
| ## TODOS: | ||||
|  | ||||
| * [ ] Re-implement history stack. | ||||
| * [ ] Re-implement plot pallette. | ||||
| * [ ] Re-implement stacked plot viewport synchronization (share viewport object) | ||||
| * [ ] Other things? | ||||
| * [ ] Handle edge cases with marquee zoom/panning. | ||||
| * [ ] Tidy code. | ||||
|  | ||||
							
								
								
									
										98
									
								
								platform/features/plot-reborn/bundle.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								platform/features/plot-reborn/bundle.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| { | ||||
|     "name": "Plot view for telemetry, reborn", | ||||
|     "extensions": { | ||||
|         "views": [ | ||||
|             { | ||||
|                 "name": "Plot", | ||||
|                 "key": "plot-single", | ||||
|                 "glyph": "6", | ||||
|                 "templateUrl": "templates/plot.html", | ||||
|                 "needs": ["telemetry"], | ||||
|                 "uses": ["composition"], | ||||
|                 "delegation": false | ||||
|             }, | ||||
|             { | ||||
|                 "name": "Overlay Plot", | ||||
|                 "key": "plot", | ||||
|                 "glyph": "6", | ||||
|                 "type": "telemetry.plot.overlay", | ||||
|                 "templateUrl": "templates/plot.html", | ||||
|                 "needs": ["telemetry", "composition"], | ||||
|                 "uses": ["composition"], | ||||
|                 "delegation": true | ||||
|             }, | ||||
|             { | ||||
|                 "name": "Stacked Plot", | ||||
|                 "key": "stackedPlot", | ||||
|                 "glyph": "6", | ||||
|                 "type": "telemetry.plot.stacked", | ||||
|                 "templateUrl": "templates/stacked-plot.html", | ||||
|                 "needs": ["composition", "delegation"], | ||||
|                 "uses": ["composition"], | ||||
|                 "gestures": [ "drop" ], | ||||
|                 "delegation": true | ||||
|             } | ||||
|         ], | ||||
|         "directives": [ | ||||
|             { | ||||
|                 "key": "mctChart", | ||||
|                 "implementation": "directives/MCTChart.js", | ||||
|                 "depends": [ "$interval", "$log" ] | ||||
|             }, | ||||
|             { | ||||
|                 "key": "mctPlot", | ||||
|                 "implementation": "directives/MCTPlot.js", | ||||
|                 "depends": [], | ||||
|                 "templateUrl": "templates/mct-plot.html" | ||||
|             }, | ||||
|             { | ||||
|                 "key": "mctOverlayPlot", | ||||
|                 "implementation": "directives/MCTOverlayPlot.js", | ||||
|                 "depends": [] | ||||
|             } | ||||
|         ], | ||||
|         "controllers": [ | ||||
|             { | ||||
|                 "key": "PlotController", | ||||
|                 "implementation": "controllers/PlotController.js", | ||||
|                 "depends": [ "$scope", "$q", "colorService"] | ||||
|             }, | ||||
|             { | ||||
|                 "key": "StackedPlotController", | ||||
|                 "implementation": "controllers/StackedPlotController.js", | ||||
|                 "depends": [ "$scope" ] | ||||
|             } | ||||
|         ], | ||||
|         "types": [ | ||||
|             { | ||||
|                 "key": "telemetry.plot.overlay", | ||||
|                 "name": "Overlay Plot", | ||||
|                 "glyph": "t", | ||||
|                 "description": "A plot containing one or more telemetry elements.", | ||||
|                 "delegates": ["telemetry"], | ||||
|                 "features": "creation", | ||||
|                 "contains": [{"has": "telemetry"}], | ||||
|                 "model": {"composition": []}, | ||||
|                 "properties": [] | ||||
|             }, | ||||
|             { | ||||
|                 "key": "telemetry.plot.stacked", | ||||
|                 "name": "Stacked Plot", | ||||
|                 "glyph": "t", | ||||
|                 "description": "A stacked plot of overlay plots.", | ||||
|                 "delegates": ["delegation"], | ||||
|                 "features": "creation", | ||||
|                 "contains": ["telemetry.plot.overlay", {"has": "telemetry"}], | ||||
|                 "model": {"composition": []}, | ||||
|                 "properties": [] | ||||
|             } | ||||
|         ], | ||||
|         "services": [ | ||||
|             { | ||||
|                 "key": "colorService", | ||||
|                 "implementation": "services/ColorService.js", | ||||
|                 "description": "Provides objects for working with colors." | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
| } | ||||
							
								
								
									
										76
									
								
								platform/features/plot-reborn/res/templates/mct-plot.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								platform/features/plot-reborn/res/templates/mct-plot.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| <!--TODO: Don't require plotcontroller here. --> | ||||
| <div class="gl-plot"> | ||||
|     <div class="gl-plot-legend"> | ||||
|         <span class="plot-legend-item" | ||||
|               ng-repeat="series in series track by $index"> | ||||
|             <span class="plot-color-swatch" | ||||
|                   ng-style="{ 'background-color': series.color.asHexString() }"> | ||||
|             </span> | ||||
|             <span class="title-label">{{ series.name }}</span> | ||||
|         </span> | ||||
|     </div> | ||||
|  | ||||
|     <div class="gl-plot-coords" | ||||
|          ng-if="mouseCoordinates"> | ||||
|         {{ displayableDomain(mouseCoordinates.positionAsPlotPoint.domain)  }}, | ||||
|         {{ displayableRange(mouseCoordinates.positionAsPlotPoint.range) }} | ||||
|  | ||||
|     </div> | ||||
|  | ||||
|     <div class="gl-plot-axis-area gl-plot-y"> | ||||
|  | ||||
|         <div class="gl-plot-label gl-plot-y-label"> | ||||
|             {{ axes.range.label}} | ||||
|         </div> | ||||
|  | ||||
|         <div ng-repeat="tick in axes.range.ticks track by $index" | ||||
|              class="gl-plot-tick gl-plot-y-tick-label" | ||||
|              ng-style="{ top: (100 * $index / (axes.range.ticks.length - 1)) + '%' }" | ||||
|              style="margin-top: -0.50em;"> | ||||
|             {{ displayableRange(tick) }} | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="gl-plot-display-area"> | ||||
|  | ||||
|         <div class="gl-plot-hash hash-v" | ||||
|              ng-repeat="tick in axes.domain.ticks track by $index" | ||||
|              ng-style="{ left: (100 * $index / (axes.domain.ticks.length - 1)) + '%', height: '100%' }" | ||||
|              ng-show="$index > 0 && $index < (axes.domain.ticks.length - 1)"> | ||||
|             <!--TODO: Show/hide using CSS? --> | ||||
|         </div> | ||||
|  | ||||
|         <div class="gl-plot-hash hash-h" | ||||
|              ng-repeat="tick in axes.range.ticks track by $index" | ||||
|              ng-style="{ bottom: (100 * $index / (axes.range.ticks.length - 1)) + '%', width: '100%' }" | ||||
|              ng-show="$index > 0 && $index < (axes.range.ticks.length - 1)"> | ||||
|              <!--TODO: Show/hide using CSS? --> | ||||
|         </div> | ||||
|  | ||||
|         <mct-chart series="series" | ||||
|                    viewport="viewport" | ||||
|                    rectangles="rectangles" | ||||
|                    ng-mousemove="plot.trackMousePosition($event)" | ||||
|                    ng-mouseleave="plot.untrackMousePosition()" | ||||
|                    ng-mousedown="plot.startMarquee()" | ||||
|                    ng-mouseup="plot.endMarquee()"> | ||||
|         </mct-chart> | ||||
|  | ||||
|         <span class="t-wait-spinner loading" ng-show="plot.isRequestPending()"> | ||||
|         </span> | ||||
|  | ||||
|     </div> | ||||
|  | ||||
|     <div class="gl-plot-axis-area gl-plot-x"> | ||||
|         <div ng-repeat="tick in axes.domain.ticks track by $index" | ||||
|              class="gl-plot-tick gl-plot-x-tick-label" | ||||
|              ng-show="$index > 0 && $index < (axes.domain.ticks.length - 1)" | ||||
|              ng-style="{ left: (100 * $index / (axes.domain.ticks.length - 1)) + '%' }"> | ||||
|             {{ displayableDomain(tick) }} | ||||
|         </div> | ||||
|  | ||||
|         <div class="gl-plot-label gl-plot-x-label"> | ||||
|             {{ axes.domain.label }} | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
							
								
								
									
										9
									
								
								platform/features/plot-reborn/res/templates/plot.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								platform/features/plot-reborn/res/templates/plot.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| <span ng-controller="PlotController as controller"> | ||||
|     <mct-plot series="series" | ||||
|               viewport="viewport" | ||||
|               rectangles="rectangles" | ||||
|               axes="axes" | ||||
|               displayable-range="displayableRange" | ||||
|               displayable-domain="displayableDomain"> | ||||
|               </mct-plot> | ||||
| </span> | ||||
| @@ -0,0 +1,8 @@ | ||||
| <span ng-controller="StackedPlotController as stackedPlot"> | ||||
|     <div class="gl-plot" | ||||
|         ng-style="{ height: 100 / telemetryObjects.length + '%'}" | ||||
|         ng-repeat="telemetryObject in telemetryObjects"> | ||||
|  | ||||
|         <mct-overlay-plot domain-object="telemetryObject"></mct-overlay-plot> | ||||
|     </div> | ||||
| </span> | ||||
							
								
								
									
										248
									
								
								platform/features/plot-reborn/src/controllers/PlotController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								platform/features/plot-reborn/src/controllers/PlotController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,248 @@ | ||||
| /*global define,requestAnimationFrame*/ | ||||
|  | ||||
| define( | ||||
|     [], | ||||
|     function () { | ||||
|         "use strict"; | ||||
|  | ||||
|         // TODO: Store this in more accessible locations / retrieve from | ||||
|         // domainObject metadata. | ||||
|         var DOMAIN_INTERVAL = 2 * 60 * 1000; // Two minutes. | ||||
|  | ||||
|         function PlotController($scope, $q, colorService) { | ||||
|             var plotHistory = [], | ||||
|                 isLive = true, | ||||
|                 extrema = {}, | ||||
|                 unsubscribes = [], | ||||
|                 palette = new colorService.ColorPalette(), | ||||
|                 pendingUpdate, | ||||
|                 selectedDomain; | ||||
|  | ||||
|  | ||||
|             function setToDefaultViewport() { | ||||
|                 // TODO: We shouldn't set the viewport until we have received data or something has given us a reasonable viewport. | ||||
|                 if (!extrema.domain || !extrema.range) { | ||||
|                     return; | ||||
|                 } | ||||
|                 $scope.viewport = { | ||||
|                     topLeft: { | ||||
|                         domain: extrema.domain.min, | ||||
|                         range: extrema.range.max | ||||
|                     }, | ||||
|                     bottomRight: { | ||||
|                         domain: extrema.domain.max, | ||||
|                         range: extrema.range.min | ||||
|                     } | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             setToDefaultViewport(); | ||||
|  | ||||
|             $scope.displayableRange = function (rangeValue) { | ||||
|                 // TODO: Call format function provided by domain object. | ||||
|                 return rangeValue; | ||||
|             }; | ||||
|             $scope.displayableDomain = function (domainValue) { | ||||
|                 // TODO: Call format function provided by domain object. | ||||
|                 return new Date(domainValue).toUTCString(); | ||||
|             }; | ||||
|  | ||||
|             $scope.series = []; | ||||
|  | ||||
|             $scope.rectangles = []; | ||||
|  | ||||
|             function addPointToSeries(series, seriesIndex, point) { | ||||
|                 if (!extrema.domain) { | ||||
|                     extrema.domain = {}; | ||||
|                     extrema.domain.max = extrema.domain.min = point.domain; | ||||
|                 } else { | ||||
|                     extrema.domain.max = Math.max( | ||||
|                         extrema.domain.max, point.domain | ||||
|                     ); | ||||
|                     extrema.domain.min = Math.min( | ||||
|                         extrema.domain.min, point.domain | ||||
|                     ); | ||||
|                 } | ||||
|                 if (!extrema.range) { | ||||
|                     extrema.range = {}; | ||||
|                     extrema.range.max = extrema.range.min = point.range; | ||||
|                 } else { | ||||
|                     extrema.range.max = Math.max( | ||||
|                         extrema.range.max, point.range | ||||
|                     ); | ||||
|                     extrema.range.min = Math.min( | ||||
|                         extrema.range.min, point.range | ||||
|                     ); | ||||
|                 } | ||||
|                 series.data.push(point); | ||||
|                 $scope.$broadcast('series:data:add', seriesIndex, [point]); | ||||
|             } | ||||
|  | ||||
|             function addTelemetrySeriesToPlotSeries(series, seriesIndex) { | ||||
|                 return function (telemSeries) { | ||||
|                     var i = 0, | ||||
|                         len = telemSeries.getPointCount(); | ||||
|  | ||||
|                     for (; i < len; i++) { | ||||
|                         addPointToSeries(series, seriesIndex, { | ||||
|                             domain: telemSeries.getDomainValue(i, selectedDomain), | ||||
|                             range: telemSeries.getRangeValue(i) | ||||
|                         }); | ||||
|                     } | ||||
|                     return; | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             function startRealTimeFeed(series, seriesIndex, telemetryCapability) { | ||||
|                 return function () { | ||||
|                     var updater = addTelemetrySeriesToPlotSeries( | ||||
|                         series, | ||||
|                         seriesIndex | ||||
|                     ); | ||||
|                     unsubscribes.push(telemetryCapability.subscribe(updater)); | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             function subscribeToDomainObject(domainObject) { | ||||
|                 var telemetryCapability = domainObject.getCapability('telemetry'), | ||||
|                     model = domainObject.getModel(), | ||||
|                     series, | ||||
|                     seriesIndex; | ||||
|  | ||||
|                 series = { | ||||
|                     name: model.name, | ||||
|                     // TODO: Bring back PlotPalette. | ||||
|                     color: palette.getColor($scope.series.length), | ||||
|                     data: [] | ||||
|                 }; | ||||
|  | ||||
|                 $scope.series.push(series); | ||||
|                 seriesIndex = $scope.series.indexOf(series); | ||||
|  | ||||
|                 return telemetryCapability | ||||
|                     .requestData({}) | ||||
|                     .then(addTelemetrySeriesToPlotSeries( | ||||
|                         series, | ||||
|                         seriesIndex | ||||
|                     )) | ||||
|                     .then(startRealTimeFeed( | ||||
|                         series, | ||||
|                         seriesIndex, | ||||
|                         telemetryCapability | ||||
|                     )); | ||||
|  | ||||
|             } | ||||
|  | ||||
|             function unlinkDomainObject() { | ||||
|                 $scope.series = []; | ||||
|                 extrema = {}; | ||||
|                 unsubscribes.forEach(function(unsubscribe) { | ||||
|                     unsubscribe(); | ||||
|                 }); | ||||
|                 unsubscribes = []; | ||||
|             } | ||||
|  | ||||
|  | ||||
|             function linkDomainObject() { | ||||
|                 var domainObject = $scope.domainObject; | ||||
|                 unlinkDomainObject(); | ||||
|                 if (!domainObject || !domainObject.hasCapability) { | ||||
|                     return; | ||||
|                 } | ||||
|                 if (domainObject.hasCapability('telemetry')) { | ||||
|                     subscribeToDomainObject(domainObject); | ||||
|                 } else if (domainObject.hasCapability('delegation')) { | ||||
|                     // Makes no sense that we have to use a subscription to get domain objects associated with delegates (and their names).  We can map the same series generation code to telemetry delegates; Let's do that ourselves. | ||||
|                     var subscribeToDelegates = function(delegates) { | ||||
|                         return $q.all(delegates.map(subscribeToDomainObject)); | ||||
|                         // TODO: Should return a promise. | ||||
|                     }; | ||||
|                     domainObject | ||||
|                         .getCapability('delegation') | ||||
|                         .getDelegates('telemetry') | ||||
|                         .then(subscribeToDelegates); | ||||
|                         // TODO: should have a catch. | ||||
|                 } else { | ||||
|                     throw new Error('Domain object type not supported.'); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|  | ||||
|             function onUserViewportChangeStart() { | ||||
|                 // TODO: this is a great time to track a history entry. | ||||
|                 // Disable live mode so they have full control of viewport. | ||||
|                 plotHistory.push($scope.viewport); | ||||
|                 isLive = false; | ||||
|             } | ||||
|  | ||||
|             function onUserViewportChangeEnd(event, viewport) { | ||||
|                 // If the new viewport is "close enough" to the maxDomain then | ||||
|                 // enable live mode.  Set empirically to 10% of the domain | ||||
|                 // interval. | ||||
|                 // TODO: Better UX pattern for this. | ||||
|  | ||||
|                 // if (Math.abs(maxDomain - viewport.bottomRight.domain) < (DOMAIN_INTERVAL/10)) { | ||||
|                 //     isLive = true; | ||||
|                 //     $scope.viewport.bottomRight.domain = maxDomain; | ||||
|                 // } else { | ||||
|                 //     isLive = false; | ||||
|                 // } | ||||
|                 plotHistory.push(viewport); | ||||
|             } | ||||
|  | ||||
|              function viewportForExtrema() { | ||||
|                  return { | ||||
|                     topLeft: { | ||||
|                         domain: extrema.domain.min, | ||||
|                         range: extrema.range.max | ||||
|                     }, | ||||
|                     bottomRight: { | ||||
|                         domain: extrema.domain.max, | ||||
|                         range: extrema.range.min | ||||
|                     } | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             function followDataIfLive() { | ||||
|                 if (pendingUpdate) { | ||||
|                     return; | ||||
|                 } | ||||
|                 requestAnimationFrame(function () { | ||||
|                     if (isLive) { | ||||
|                         $scope.viewport = viewportForExtrema(); | ||||
|                     } | ||||
|                     pendingUpdate = false; | ||||
|                 }); | ||||
|                 pendingUpdate = true; | ||||
|             } | ||||
|  | ||||
|             $scope.$on('series:data:add', followDataIfLive); | ||||
|             $scope.$on('user:viewport:change:end', onUserViewportChangeEnd); | ||||
|             $scope.$on('user:viewport:change:start', onUserViewportChangeStart); | ||||
|  | ||||
|             $scope.$on( | ||||
|                 'telemetry:display:bounds', | ||||
|                 function (event, bounds) { | ||||
|                     selectedDomain = bounds.domain; | ||||
|                     linkDomainObject(); | ||||
|                 } | ||||
|             ); | ||||
|             $scope.$watch('domainObject', linkDomainObject); | ||||
|  | ||||
|             return { | ||||
|                 historyBack: function() { | ||||
|                     // TODO: Step History Back. | ||||
|                 }, | ||||
|                 historyForward: function() { | ||||
|                     // TODO: Step History Forward. | ||||
|                 }, | ||||
|                 resetZoom: function() { | ||||
|                     // TODO: Reset view to defaults.  Keep history stack alive? | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return PlotController; | ||||
|  | ||||
|     } | ||||
| ); | ||||
| @@ -0,0 +1,38 @@ | ||||
| /*global define */ | ||||
|  | ||||
| define( | ||||
|     function () { | ||||
|         "use strict"; | ||||
|  | ||||
|         function StackedPlotController($scope) { | ||||
|  | ||||
|             $scope.telemetryObjects = []; | ||||
|  | ||||
|             var linkDomainObject = function(domainObject) { | ||||
|                 $scope.telemetryObjects = []; | ||||
|                 if (domainObject.hasCapability('telemetry')) { | ||||
|                     $scope.telemetryObjects = [domainObject]; | ||||
|                 } else if (domainObject.hasCapability('delegation')) { | ||||
|  | ||||
|                     var addObjectsIfCompatible = function(objects) { | ||||
|                         objects.forEach(function(object) { | ||||
|                             if (object.hasCapability('telemetry')) { | ||||
|                                 $scope.telemetryObjects.push(object); | ||||
|                             } else if (object.hasCapability('delegation')) { | ||||
|                                 $scope.telemetryObjects.push(object); | ||||
|                             } | ||||
|                         }); | ||||
|                     }; | ||||
|                     domainObject | ||||
|                         .useCapability('composition') | ||||
|                         .then(addObjectsIfCompatible); | ||||
|                         // TODO: should have a catch. | ||||
|                 } else { | ||||
|                     throw new Error('Domain object type not supported.'); | ||||
|                 } | ||||
|             }; | ||||
|             $scope.$watch('domainObject', linkDomainObject); | ||||
|         } | ||||
|         return StackedPlotController; | ||||
|     } | ||||
| ); | ||||
							
								
								
									
										250
									
								
								platform/features/plot-reborn/src/directives/MCTChart.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								platform/features/plot-reborn/src/directives/MCTChart.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,250 @@ | ||||
| /*global define,requestAnimationFrame,Float32Array*/ | ||||
|  | ||||
| /** | ||||
|  * Module defining MCTChart. Created by vwoeltje on 11/12/14. | ||||
|  */ | ||||
| define( | ||||
|     ["../draw/DrawLoader"], | ||||
|     function (DrawLoader) { | ||||
|         "use strict"; | ||||
|  | ||||
|         var TEMPLATE = "<canvas style='position: absolute; background: none; width: 100%; height: 100%;'></canvas>"; | ||||
|  | ||||
|         /** | ||||
|          * Offsetter adjusts domain and range values by a fixed amount, | ||||
|          * generally increasing the precision of the 32 bit float representation | ||||
|          * required for plotting. | ||||
|          * | ||||
|          * @constructor | ||||
|          */ | ||||
|         function Offsetter(domainOffset, rangeOffset) { | ||||
|             this.domainOffset = domainOffset; | ||||
|             this.rangeOffset = rangeOffset; | ||||
|         } | ||||
|  | ||||
|         Offsetter.prototype.domain = function(dataDomain) { | ||||
|             return dataDomain - this.domainOffset; | ||||
|         }; | ||||
|  | ||||
|         Offsetter.prototype.range = function(dataRange) { | ||||
|             return dataRange - this.rangeOffset; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * MCTChart draws charts utilizing a drawAPI. | ||||
|          * | ||||
|          * @constructor | ||||
|          */ | ||||
|         function MCTChart($interval) { | ||||
|  | ||||
|             function linkChart($scope, $element) { | ||||
|                 var canvas = $element.find("canvas")[0], | ||||
|                     isDestroyed = false, | ||||
|                     activeInterval, | ||||
|                     drawAPI, | ||||
|                     lines = [], | ||||
|                     offset; | ||||
|  | ||||
|                 drawAPI = DrawLoader.getDrawAPI(canvas); | ||||
|  | ||||
|                 if (!drawAPI) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 function ensureOffset() { | ||||
|                     if (offset) { | ||||
|                         return; | ||||
|                     } | ||||
|                     if ($scope.series && | ||||
|                         $scope.series.length && | ||||
|                         $scope.series[0].data && | ||||
|                         $scope.series[0].data.length) { | ||||
|  | ||||
|                         // Take offset from series. | ||||
|                         var point = $scope.series[0].data[0]; | ||||
|                         offset = new Offsetter( | ||||
|                             point.domain, | ||||
|                             point.range | ||||
|                         ); | ||||
|                         return; | ||||
|                     } | ||||
|                     // TODO: Fallback and get offset from viewport. | ||||
|                     // TODO: Fallback and get offset from rectangles. | ||||
|                 } | ||||
|  | ||||
|                 function lineFromSeries(series) { | ||||
|                     // TODO: handle when lines get longer than 10,000 points. | ||||
|                     // Each line allocates 10,000 points.  This should be more | ||||
|                     // that we ever need, but we have to decide how to handle | ||||
|                     // this at the higher level.  I imagine the plot controller | ||||
|                     // should watch it's series and when they get huge, slice | ||||
|                     // them in half and delete the oldest half. | ||||
|                     // | ||||
|                     // As long as the controller replaces $scope.series with a | ||||
|                     // new series object, then this directive will | ||||
|                     // automatically generate new arrays for those lines. | ||||
|                     // In practice, the overhead of regenerating these lines | ||||
|                     // appears minimal. | ||||
|                     var lineBuffer = new Float32Array(20000), | ||||
|                         i = 0; | ||||
|                     ensureOffset(); | ||||
|                     for (i = 0; i < series.data.length; i++) { | ||||
|                         lineBuffer[2*i] = offset.domain(series.data[i].domain); | ||||
|                         lineBuffer[2*i+1] = offset.range(series.data[i].range); | ||||
|                     } | ||||
|                     return { | ||||
|                         color: series.color, | ||||
|                         buffer: lineBuffer, | ||||
|                         pointCount: series.data.length | ||||
|                     }; | ||||
|                 } | ||||
|  | ||||
|                 function drawSeries() { | ||||
|                     if (!lines && !lines.length) { | ||||
|                         return; | ||||
|                     } | ||||
|                     lines.forEach(function(line) { | ||||
|                         drawAPI.drawLine( | ||||
|                             line.buffer, | ||||
|                             line.color.asRGBAArray(), | ||||
|                             line.pointCount | ||||
|                         ); | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 function drawRectangles() { | ||||
|                     if ($scope.rectangles) { | ||||
|                         $scope.rectangles.forEach(function(rect) { | ||||
|                             drawAPI.drawSquare( | ||||
|                                 [ | ||||
|                                     offset.domain(rect.start.domain), | ||||
|                                     offset.range(rect.start.range) | ||||
|                                 ], | ||||
|                                 [ | ||||
|                                     offset.domain(rect.end.domain), | ||||
|                                     offset.range(rect.end.range) | ||||
|                                 ], | ||||
|                                 rect.color | ||||
|                             ); | ||||
|                         }); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 function updateViewport() { | ||||
|                     var dimensions, | ||||
|                         origin; | ||||
|  | ||||
|                     dimensions = [ | ||||
|                         Math.abs( | ||||
|                             offset.domain($scope.viewport.topLeft.domain) - | ||||
|                             offset.domain($scope.viewport.bottomRight.domain) | ||||
|                         ), | ||||
|                         Math.abs( | ||||
|                             offset.range($scope.viewport.topLeft.range) - | ||||
|                             offset.range($scope.viewport.bottomRight.range) | ||||
|                         ) | ||||
|                     ]; | ||||
|  | ||||
|                     origin = [ | ||||
|                         offset.domain( | ||||
|                             $scope.viewport.topLeft.domain | ||||
|                         ), | ||||
|                         offset.range( | ||||
|                             $scope.viewport.bottomRight.range | ||||
|                         ) | ||||
|                     ]; | ||||
|  | ||||
|                     drawAPI.setDimensions( | ||||
|                         dimensions, | ||||
|                         origin | ||||
|                     ); | ||||
|                 } | ||||
|  | ||||
|                 function onSeriesDataAdd(event, seriesIndex, points) { | ||||
|                     var line = lines[seriesIndex]; | ||||
|                     if (!line) { return; } | ||||
|                     ensureOffset(); | ||||
|                     points.forEach(function (point) { | ||||
|                         line.buffer[2*line.pointCount] = offset.domain(point.domain); | ||||
|                         line.buffer[2*line.pointCount+1] = offset.range(point.range); | ||||
|                         line.pointCount += 1; | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|  | ||||
|  | ||||
|                 function redraw() { | ||||
|                     if (isDestroyed) { | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
|                     requestAnimationFrame(redraw); | ||||
|                     canvas.width = canvas.offsetWidth; | ||||
|                     canvas.height = canvas.offsetHeight; | ||||
|                     drawAPI.clear(); | ||||
|                     ensureOffset(); | ||||
|                     if (!offset) { | ||||
|                         return; | ||||
|                     } | ||||
|                     updateViewport(); | ||||
|                     drawSeries(); | ||||
|                     drawRectangles(); | ||||
|                 } | ||||
|  | ||||
|  | ||||
|                 function drawIfResized() { | ||||
|                     if (canvas.width !== canvas.offsetWidth || | ||||
|                             canvas.height !== canvas.offsetHeight) { | ||||
|                         redraw(); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 function destroyChart() { | ||||
|                     isDestroyed = true; | ||||
|                     if (activeInterval) { | ||||
|                         $interval.cancel(activeInterval); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 function recreateLines() { | ||||
|                     offset = undefined; | ||||
|                     if ($scope.series && $scope.series.length) { | ||||
|                         lines = $scope.series.map(lineFromSeries); | ||||
|                     } | ||||
|                     ensureOffset(); | ||||
|                 } | ||||
|  | ||||
|                 // Check for resize, on a timer | ||||
|                 activeInterval = $interval(drawIfResized, 1000); | ||||
|  | ||||
|                 $scope.$on('series:data:add', onSeriesDataAdd); | ||||
|                 $scope.$watchCollection('series', recreateLines); | ||||
|                 redraw(); | ||||
|  | ||||
|                 // Stop checking for resize when $scope is destroyed | ||||
|                 $scope.$on("$destroy", destroyChart); | ||||
|             } | ||||
|  | ||||
|             return { | ||||
|                 // Apply directive only to $elements | ||||
|                 restrict: "E", | ||||
|  | ||||
|                 // Template to use (a canvas $element) | ||||
|                 template: TEMPLATE, | ||||
|  | ||||
|                 // Link function; set up $scope | ||||
|                 link: linkChart, | ||||
|  | ||||
|                 // Initial, isolate $scope for the directive | ||||
|                 scope: { | ||||
|                     draw: "=" , | ||||
|                     rectangles: "=", | ||||
|                     series: "=", | ||||
|                     viewport: "=" | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return MCTChart; | ||||
|     } | ||||
| ); | ||||
| @@ -0,0 +1,14 @@ | ||||
| /*global define*/ | ||||
|  | ||||
| define(function () { | ||||
|         return function MCTOverlayPlot() { | ||||
|             return { | ||||
|                 restrict: "E", | ||||
|                 templateUrl: 'platform/features/plot-reborn/res/templates/plot.html', | ||||
|                 scope: { | ||||
|                     domainObject: "=" | ||||
|                 } | ||||
|             }; | ||||
|         }; | ||||
|     } | ||||
| ); | ||||
							
								
								
									
										272
									
								
								platform/features/plot-reborn/src/directives/MCTPlot.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										272
									
								
								platform/features/plot-reborn/src/directives/MCTPlot.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,272 @@ | ||||
| /*global define,window*/ | ||||
|  | ||||
| define( | ||||
|     [ | ||||
|         '../lib/utils' | ||||
|     ], | ||||
|     function (utils) { | ||||
|         "use strict"; | ||||
|  | ||||
|         var RANGE_TICK_COUNT = 7, | ||||
|             DOMAIN_TICK_COUNT = 5; | ||||
|  | ||||
|         function MCTPlot() { | ||||
|  | ||||
|             function link($scope, $element) { | ||||
|                 // Now that we're here, let's handle some scope management that the controller would otherwise handle. | ||||
|  | ||||
|                 if (typeof $scope.rectangles === "undefined") { | ||||
|                     $scope.rectangles = []; | ||||
|                 } | ||||
|                 if (typeof $scope.displayableRange === "undefined") { | ||||
|                     $scope.displayableRange = function (x) { return x; }; | ||||
|                 } | ||||
|                 if (typeof $scope.displayableDomain === "undefined") { | ||||
|                     $scope.displayableDomain = function (x) { return x; }; | ||||
|                 } | ||||
|                 if (typeof $scope.axes === "undefined") { | ||||
|                     $scope.axes = { | ||||
|                         domain: { | ||||
|                             label: "Time", | ||||
|                             tickCount: DOMAIN_TICK_COUNT, | ||||
|                             ticks: [] | ||||
|                         }, | ||||
|                         range: { | ||||
|                             label: "Value", | ||||
|                             tickCount: RANGE_TICK_COUNT, | ||||
|                             ticks: [] | ||||
|                         } | ||||
|                     }; | ||||
|                 } | ||||
|  | ||||
|  | ||||
|                 var dragStart, | ||||
|                     marqueeBox = {}, | ||||
|                     marqueeRect, // Set when exists. | ||||
|                     chartElementBounds, | ||||
|                     $canvas = $element.find('canvas'); | ||||
|  | ||||
|                 function updateAxesForCurrentViewport() { | ||||
|                     // Update axes definitions for current viewport. | ||||
|                     if (!$scope.axes || !$scope.viewport) { | ||||
|                         return; | ||||
|                     } | ||||
|                     ['domain', 'range'].forEach(function(axisName) { | ||||
|                         var axis = $scope.axes[axisName], | ||||
|                             firstTick = $scope.viewport.topLeft[axisName], | ||||
|                             lastTick = $scope.viewport.bottomRight[axisName], | ||||
|                             axisSize = firstTick - lastTick, | ||||
|                             denominator = axis.tickCount - 1, | ||||
|                             tickNumber, | ||||
|                             tickIncrement, | ||||
|                             tickValue; | ||||
|                         // Yes, ticksize is negative for domain and positive for range. | ||||
|                         // It's because ticks are generated/displayed top to bottom and left to right. | ||||
|                         axis.ticks = []; | ||||
|                         for (tickNumber = 0; tickNumber < axis.tickCount; tickNumber++) { | ||||
|                             tickIncrement = (axisSize * (tickNumber / denominator)); | ||||
|                             tickValue = firstTick - tickIncrement; | ||||
|                             axis.ticks.push( | ||||
|                                 tickValue | ||||
|                             ); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 function drawMarquee() { | ||||
|                     // Create rectangle for Marquee if it should be set. | ||||
|                     if (marqueeBox && marqueeBox.start && marqueeBox.end) { | ||||
|                         if (!marqueeRect) { | ||||
|                             marqueeRect = {}; | ||||
|                             $scope.rectangles.push(marqueeRect); | ||||
|                         } | ||||
|                         marqueeRect.start = marqueeBox.start; | ||||
|                         marqueeRect.end = marqueeBox.end; | ||||
|                         marqueeRect.color = [1, 1, 1, 0.5]; | ||||
|                         marqueeRect.layer = 'top'; // TODO: implement this. | ||||
|                         $scope.$broadcast('rectangle-change'); | ||||
|                     } else if (marqueeRect && $scope.rectangles.indexOf(marqueeRect) !== -1) { | ||||
|                         $scope.rectangles.splice($scope.rectangles.indexOf(marqueeRect)); | ||||
|                         marqueeRect = undefined; | ||||
|                         $scope.$broadcast('rectangle-change'); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 function untrackMousePosition() { | ||||
|                     $scope.mouseCoordinates = undefined; | ||||
|                 } | ||||
|                 function updateMarquee() { | ||||
|                     // Update the marquee box in progress. | ||||
|                     marqueeBox.end = $scope.mouseCoordinates.positionAsPlotPoint; | ||||
|                     drawMarquee(); | ||||
|                 } | ||||
|                 function startMarquee() { | ||||
|                     marqueeBox.start = $scope.mouseCoordinates.positionAsPlotPoint; | ||||
|                 } | ||||
|                 function endMarquee() { | ||||
|                     // marqueeBox start/end are opposite corners but we need | ||||
|                     // topLeft and bottomRight. | ||||
|                     var boxPoints = utils.boxPointsFromOppositeCorners(marqueeBox.start, marqueeBox.end), | ||||
|                         newViewport = utils.oppositeCornersFromBoxPoints(boxPoints); | ||||
|  | ||||
|                     marqueeBox = {}; | ||||
|                     drawMarquee(); | ||||
|                     $scope.$emit('user:viewport:change:end', newViewport); | ||||
|                     $scope.viewport = newViewport; | ||||
|                 } | ||||
|  | ||||
|                 function startDrag($event) { | ||||
|                     $scope.$emit('user:viewport:change:start'); | ||||
|                     if (!$scope.mouseCoordinates) { | ||||
|                         return; | ||||
|                     } | ||||
|                     $event.preventDefault(); | ||||
|                     // Track drag location relative to position over element | ||||
|                     // not domain, as chart viewport will change as we drag. | ||||
|                     dragStart = $scope.mouseCoordinates.positionAsPlotPoint; | ||||
|                     // Tell controller that we're starting to navigate. | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 function updateDrag() { | ||||
|                     // calculate offset between points.  Apply that offset to viewport. | ||||
|                     var newPosition = $scope.mouseCoordinates.positionAsPlotPoint, | ||||
|                         dDomain = dragStart.domain - newPosition.domain, | ||||
|                         dRange = dragStart.range - newPosition.range; | ||||
|  | ||||
|                     $scope.viewport = { | ||||
|                         topLeft: { | ||||
|                             domain: $scope.viewport.topLeft.domain + dDomain, | ||||
|                             range: $scope.viewport.topLeft.range + dRange | ||||
|                         }, | ||||
|                         bottomRight: { | ||||
|                             domain: $scope.viewport.bottomRight.domain + dDomain, | ||||
|                             range: $scope.viewport.bottomRight.range + dRange | ||||
|                         } | ||||
|                     }; | ||||
|                 } | ||||
|  | ||||
|                 function endDrag() { | ||||
|                     dragStart = undefined; | ||||
|                     $scope.$emit('user:viewport:change:end', $scope.viewport); | ||||
|                 } | ||||
|  | ||||
|                 function trackMousePosition($event) { | ||||
|                     if (!$scope.viewport) { return; } | ||||
|                     // Calculate coordinates of mouse related to canvas and as | ||||
|                     // domain, range value and make available in scope for display. | ||||
|  | ||||
|                     var bounds = $event.target.getBoundingClientRect(), | ||||
|                         positionOverElement, | ||||
|                         positionAsPlotPoint; | ||||
|  | ||||
|                     chartElementBounds = bounds; | ||||
|  | ||||
|                     positionOverElement = { | ||||
|                         x: $event.clientX - bounds.left, | ||||
|                         y: $event.clientY - bounds.top | ||||
|                     }; | ||||
|  | ||||
|                     positionAsPlotPoint = utils.elementPositionAsPlotPosition( | ||||
|                         positionOverElement, | ||||
|                         bounds, | ||||
|                         $scope.viewport | ||||
|                     ); | ||||
|  | ||||
|                     $scope.mouseCoordinates = { | ||||
|                         positionOverElement: positionOverElement, | ||||
|                         positionAsPlotPoint: positionAsPlotPoint | ||||
|                     }; | ||||
|  | ||||
|                     if (marqueeBox && marqueeBox.start) { | ||||
|                         updateMarquee(); | ||||
|                     } | ||||
|  | ||||
|                     if (dragStart) { | ||||
|                         updateDrag(); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 function watchForMarquee() { | ||||
|                     $canvas.removeClass('plot-drag'); | ||||
|                     $canvas.addClass('plot-marquee'); | ||||
|                     $canvas.on('mousedown', startMarquee); | ||||
|                     $canvas.on('mouseup', endMarquee); | ||||
|                     $canvas.off('mousedown', startDrag); | ||||
|                     $canvas.off('mouseup', endDrag); | ||||
|                 } | ||||
|  | ||||
|                 function watchForDrag() { | ||||
|                     $canvas.addClass('plot-drag'); | ||||
|                     $canvas.removeClass('plot-marquee'); | ||||
|                     $canvas.on('mousedown', startDrag); | ||||
|                     $canvas.on('mouseup', endDrag); | ||||
|                     $canvas.off('mousedown', startMarquee); | ||||
|                     $canvas.off('mouseup', endMarquee); | ||||
|                 } | ||||
|  | ||||
|                 function toggleInteractionMode(event) { | ||||
|                     if (event.keyCode === 18) { // control key. | ||||
|                         watchForDrag(); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 function resetInteractionMode(event) { | ||||
|                     if (event.keyCode === 18) { | ||||
|                         watchForMarquee(); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 function stopWatching() { | ||||
|                     $canvas.off('mousedown', startDrag); | ||||
|                     $canvas.off('mouseup', endDrag); | ||||
|                     $canvas.off('mousedown', startMarquee); | ||||
|                     $canvas.off('mouseup', endMarquee); | ||||
|                     window.removeEventListener('keydown', toggleInteractionMode); | ||||
|                     window.removeEventListener('keyup', resetInteractionMode); | ||||
|                 } | ||||
|  | ||||
|                 $canvas.on('mousemove', trackMousePosition); | ||||
|                 $canvas.on('mouseleave', untrackMousePosition); | ||||
|                 watchForMarquee(); | ||||
|  | ||||
|                 window.addEventListener('keydown', toggleInteractionMode); | ||||
|                 window.addEventListener('keyup', resetInteractionMode); | ||||
|  | ||||
|                 function onViewportChange() { | ||||
|                     if ($scope.mouseCoordinates && chartElementBounds) { | ||||
|                         $scope.mouseCoordinates.positionAsPlotPoint = | ||||
|                             utils.elementPositionAsPlotPosition( | ||||
|                                 $scope.mouseCoordinates.positionOverElement, | ||||
|                                 chartElementBounds, | ||||
|                                 $scope.viewport | ||||
|                             ); | ||||
|                     } | ||||
|                     // TODO: Discuss whether marqueeBox start should be fixed to data or fixed to canvas element, especially when "isLive is true". | ||||
|                     updateAxesForCurrentViewport(); | ||||
|                 } | ||||
|  | ||||
|                 $scope.$watchCollection('viewport', onViewportChange); | ||||
|  | ||||
|                 $scope.$on('$destroy', stopWatching); | ||||
|  | ||||
|             } | ||||
|  | ||||
|             return { | ||||
|                 restrict: "E", | ||||
|                 templateUrl: 'platform/features/plot-reborn/res/templates/mct-plot.html', | ||||
|                 link: link, | ||||
|                 scope: { | ||||
|                     viewport: "=", | ||||
|                     series: "=", | ||||
|                     rectangles: "=?", | ||||
|                     axes: "=?", | ||||
|                     displayableRange: "=?", | ||||
|                     displayableDomain: "=?" | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return MCTPlot; | ||||
|     } | ||||
| ); | ||||
							
								
								
									
										120
									
								
								platform/features/plot-reborn/src/draw/Draw2D.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								platform/features/plot-reborn/src/draw/Draw2D.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| /*global define*/ | ||||
|  | ||||
| define( | ||||
|     [], | ||||
|     function () { | ||||
|         "use strict"; | ||||
|  | ||||
|         /** | ||||
|          * Create a new draw API utilizing the Canvas's 2D API for rendering. | ||||
|          * | ||||
|          * @constructor | ||||
|          * @param {CanvasElement} canvas the canvas object to render upon | ||||
|          * @throws {Error} an error is thrown if Canvas's 2D API is unavailable. | ||||
|          */ | ||||
|         function Draw2D(canvas) { | ||||
|             var c2d = canvas.getContext('2d'), | ||||
|                 width = canvas.width, | ||||
|                 height = canvas.height, | ||||
|                 dimensions = [ width, height ], | ||||
|                 origin = [ 0, 0 ]; | ||||
|  | ||||
|             // Convert from logical to physical x coordinates | ||||
|             function x(v) { | ||||
|                 return ((v - origin[0]) / dimensions[0]) * width; | ||||
|             } | ||||
|  | ||||
|             // Convert from logical to physical y coordinates | ||||
|             function y(v) { | ||||
|                 return height - ((v - origin[1]) / dimensions[1]) * height; | ||||
|             } | ||||
|  | ||||
|             // Set the color to be used for drawing operations | ||||
|             function setColor(color) { | ||||
|                 var mappedColor = color.map(function (c, i) { | ||||
|                         return i < 3 ? Math.floor(c * 255) : (c); | ||||
|                     }).join(','); | ||||
|                 c2d.strokeStyle = "rgba(" + mappedColor + ")"; | ||||
|                 c2d.fillStyle = "rgba(" + mappedColor + ")"; | ||||
|             } | ||||
|  | ||||
|             if (!c2d) { | ||||
|                 throw new Error("Canvas 2d API unavailable."); | ||||
|             } | ||||
|  | ||||
|             return { | ||||
|                 /** | ||||
|                  * Clear the chart. | ||||
|                  */ | ||||
|                 clear: function () { | ||||
|                     width = canvas.width; | ||||
|                     height = canvas.height; | ||||
|                     c2d.clearRect(0, 0, width, height); | ||||
|                 }, | ||||
|                 /** | ||||
|                  * Set the logical boundaries of the chart. | ||||
|                  * @param {number[]} dimensions the horizontal and | ||||
|                  *        vertical dimensions of the chart | ||||
|                  * @param {number[]} origin the horizontal/vertical | ||||
|                  *        origin of the chart | ||||
|                  */ | ||||
|                 setDimensions: function (newDimensions, newOrigin) { | ||||
|                     dimensions = newDimensions; | ||||
|                     origin = newOrigin; | ||||
|                 }, | ||||
|                 /** | ||||
|                  * Draw the supplied buffer as a line strip (a sequence | ||||
|                  * of line segments), in the chosen color. | ||||
|                  * @param {Float32Array} buf the line strip to draw, | ||||
|                  *        in alternating x/y positions | ||||
|                  * @param {number[]} color the color to use when drawing | ||||
|                  *        the line, as an RGBA color where each element | ||||
|                  *        is in the range of 0.0-1.0 | ||||
|                  * @param {number} points the number of points to draw | ||||
|                  */ | ||||
|                 drawLine: function (buf, color, points) { | ||||
|                     var i; | ||||
|  | ||||
|                     setColor(color); | ||||
|  | ||||
|                     // Configure context to draw two-pixel-thick lines | ||||
|                     c2d.lineWidth = 2; | ||||
|  | ||||
|                     // Start a new path... | ||||
|                     if (buf.length > 1) { | ||||
|                         c2d.beginPath(); | ||||
|                         c2d.moveTo(x(buf[0]), y(buf[1])); | ||||
|                     } | ||||
|  | ||||
|                     // ...and add points to it... | ||||
|                     for (i = 2; i < points * 2; i = i + 2) { | ||||
|                         c2d.lineTo(x(buf[i]), y(buf[i + 1])); | ||||
|                     } | ||||
|  | ||||
|                     // ...before finally drawing it. | ||||
|                     c2d.stroke(); | ||||
|                 }, | ||||
|                 /** | ||||
|                  * Draw a rectangle extending from one corner to another, | ||||
|                  * in the chosen color. | ||||
|                  * @param {number[]} min the first corner of the rectangle | ||||
|                  * @param {number[]} max the opposite corner | ||||
|                  * @param {number[]} color the color to use when drawing | ||||
|                  *        the rectangle, as an RGBA color where each element | ||||
|                  *        is in the range of 0.0-1.0 | ||||
|                  */ | ||||
|                 drawSquare: function (min, max, color) { | ||||
|                     var x1 = x(min[0]), | ||||
|                         y1 = y(min[1]), | ||||
|                         w = x(max[0]) - x1, | ||||
|                         h = y(max[1]) - y1; | ||||
|  | ||||
|                     setColor(color); | ||||
|                     c2d.fillRect(x1, y1, w, h); | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return Draw2D; | ||||
|     } | ||||
| ); | ||||
							
								
								
									
										46
									
								
								platform/features/plot-reborn/src/draw/DrawLoader.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								platform/features/plot-reborn/src/draw/DrawLoader.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| /*global define,$log */ | ||||
|  | ||||
| define( | ||||
|     [ | ||||
|         './DrawWebGL', | ||||
|         './Draw2D' | ||||
|     ], | ||||
|     function (DrawWebGL, Draw2D) { | ||||
|  | ||||
|         var CHARTS = [ | ||||
|             DrawWebGL, | ||||
|             Draw2D | ||||
|         ]; | ||||
|  | ||||
|         /** | ||||
|          * Draw loader attaches a draw API to a canvas element and returns the | ||||
|          * draw API. | ||||
|          */ | ||||
|         return { | ||||
|             /** | ||||
|              * Return the first draw API available.  Returns | ||||
|              * `undefined` if a draw API could not be constructed. | ||||
|              *. | ||||
|              * @param {CanvasElement} canvas - The canvas eelement to attach | ||||
|                       the draw API to. | ||||
|              */ | ||||
|             getDrawAPI: function (canvas) { | ||||
|                 var i; | ||||
|                 for (i = 0; i < CHARTS.length; i++) { | ||||
|                     try { | ||||
|                         return new CHARTS[i](canvas); | ||||
|                     } catch (e) { | ||||
|                         $log.warn([ | ||||
|                             "Could not instantiate chart", | ||||
|                             CHARTS[i].name, | ||||
|                             ";", | ||||
|                             e.message | ||||
|                         ].join(" ")); | ||||
|                     } | ||||
|                 } | ||||
|                 $log.warn("Cannot initialize mct-chart."); | ||||
|                 return undefined; | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| ); | ||||
							
								
								
									
										152
									
								
								platform/features/plot-reborn/src/draw/DrawWebGL.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								platform/features/plot-reborn/src/draw/DrawWebGL.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | ||||
| /*global define,Float32Array*/ | ||||
|  | ||||
| define( | ||||
|     [], | ||||
|     function () { | ||||
|         "use strict"; | ||||
|  | ||||
|         // WebGL shader sources (for drawing plain colors) | ||||
|         var FRAGMENT_SHADER = [ | ||||
|                 "precision mediump float;", | ||||
|                 "uniform vec4 uColor;", | ||||
|                 "void main(void) {", | ||||
|                 "gl_FragColor = uColor;", | ||||
|                 "}" | ||||
|             ].join('\n'), | ||||
|             VERTEX_SHADER = [ | ||||
|                 "attribute vec2 aVertexPosition;", | ||||
|                 "uniform vec2 uDimensions;", | ||||
|                 "uniform vec2 uOrigin;", | ||||
|                 "void main(void) {", | ||||
|                 "gl_Position = vec4(2.0 * ((aVertexPosition - uOrigin) / uDimensions) - vec2(1,1), 0, 1);", | ||||
|                 "}" | ||||
|             ].join('\n'); | ||||
|  | ||||
|         /** | ||||
|          * Create a draw api utilizing WebGL. | ||||
|          * | ||||
|          * @constructor | ||||
|          * @param {CanvasElement} canvas the canvas object to render upon | ||||
|          * @throws {Error} an error is thrown if WebGL is unavailable. | ||||
|          */ | ||||
|         function DrawWebGL(canvas) { | ||||
|             var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"), | ||||
|                 vertexShader, | ||||
|                 fragmentShader, | ||||
|                 program, | ||||
|                 aVertexPosition, | ||||
|                 uColor, | ||||
|                 uDimensions, | ||||
|                 uOrigin, | ||||
|                 buffer; | ||||
|  | ||||
|             // Ensure a context was actually available before proceeding | ||||
|             if (!gl) { | ||||
|                 throw new Error("WebGL unavailable."); | ||||
|             } | ||||
|  | ||||
|             // Initialize shaders | ||||
|             vertexShader = gl.createShader(gl.VERTEX_SHADER); | ||||
|             gl.shaderSource(vertexShader, VERTEX_SHADER); | ||||
|             gl.compileShader(vertexShader); | ||||
|             fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); | ||||
|             gl.shaderSource(fragmentShader, FRAGMENT_SHADER); | ||||
|             gl.compileShader(fragmentShader); | ||||
|  | ||||
|             // Assemble vertex/fragment shaders into programs | ||||
|             program = gl.createProgram(); | ||||
|             gl.attachShader(program, vertexShader); | ||||
|             gl.attachShader(program, fragmentShader); | ||||
|             gl.linkProgram(program); | ||||
|             gl.useProgram(program); | ||||
|  | ||||
|             // Get locations for attribs/uniforms from the | ||||
|             // shader programs (to pass values into shaders at draw-time) | ||||
|             aVertexPosition = gl.getAttribLocation(program, "aVertexPosition"); | ||||
|             uColor = gl.getUniformLocation(program, "uColor"); | ||||
|             uDimensions = gl.getUniformLocation(program, "uDimensions"); | ||||
|             uOrigin = gl.getUniformLocation(program, "uOrigin"); | ||||
|             gl.enableVertexAttribArray(aVertexPosition); | ||||
|  | ||||
|             // Create a buffer to holds points which will be drawn | ||||
|             buffer = gl.createBuffer(); | ||||
|  | ||||
|             // Use a line width of 2.0 for legibility | ||||
|             gl.lineWidth(2.0); | ||||
|  | ||||
|             // Enable blending, for smoothness | ||||
|             gl.enable(gl.BLEND); | ||||
|             gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); | ||||
|  | ||||
|             // Utility function to handle drawing of a buffer; | ||||
|             // drawType will determine whether this is a box, line, etc. | ||||
|             function doDraw(drawType, buf, color, points) { | ||||
|                 gl.bindBuffer(gl.ARRAY_BUFFER, buffer); | ||||
|                 gl.bufferData(gl.ARRAY_BUFFER, buf, gl.DYNAMIC_DRAW); | ||||
|                 gl.vertexAttribPointer(aVertexPosition, 2, gl.FLOAT, false, 0, 0); | ||||
|                 gl.uniform4fv(uColor, color); | ||||
|                 gl.drawArrays(drawType, 0, points); | ||||
|             } | ||||
|  | ||||
|             return { | ||||
|                 /** | ||||
|                  * Clear the chart. | ||||
|                  */ | ||||
|                 clear: function () { | ||||
|                     // Set the viewport size; note that we use the width/height | ||||
|                     // that our WebGL context reports, which may be lower | ||||
|                     // resolution than the canvas we requested. | ||||
|                     gl.viewport( | ||||
|                         0, | ||||
|                         0, | ||||
|                         gl.drawingBufferWidth, | ||||
|                         gl.drawingBufferHeight | ||||
|                     ); | ||||
|                     gl.clear(gl.COLOR_BUFFER_BIT + gl.DEPTH_BUFFER_BIT); | ||||
|                 }, | ||||
|                 /** | ||||
|                  * Set the logical boundaries of the chart. | ||||
|                  * @param {number[]} dimensions the horizontal and | ||||
|                  *        vertical dimensions of the chart | ||||
|                  * @param {number[]} origin the horizontal/vertical | ||||
|                  *        origin of the chart | ||||
|                  */ | ||||
|                 setDimensions: function (dimensions, origin) { | ||||
|                     if (dimensions && dimensions.length > 0 && | ||||
|                             origin && origin.length > 0) { | ||||
|                         gl.uniform2fv(uDimensions, dimensions); | ||||
|                         gl.uniform2fv(uOrigin, origin); | ||||
|                     } | ||||
|                 }, | ||||
|                 /** | ||||
|                  * Draw the supplied buffer as a line strip (a sequence | ||||
|                  * of line segments), in the chosen color. | ||||
|                  * @param {Float32Array} buf the line strip to draw, | ||||
|                  *        in alternating x/y positions | ||||
|                  * @param {number[]} color the color to use when drawing | ||||
|                  *        the line, as an RGBA color where each element | ||||
|                  *        is in the range of 0.0-1.0 | ||||
|                  * @param {number} points the number of points to draw | ||||
|                  */ | ||||
|                 drawLine: function (buf, color, points) { | ||||
|                     doDraw(gl.LINE_STRIP, buf, color, points); | ||||
|                 }, | ||||
|                 /** | ||||
|                  * Draw a rectangle extending from one corner to another, | ||||
|                  * in the chosen color. | ||||
|                  * @param {number[]} min the first corner of the rectangle | ||||
|                  * @param {number[]} max the opposite corner | ||||
|                  * @param {number[]} color the color to use when drawing | ||||
|                  *        the rectangle, as an RGBA color where each element | ||||
|                  *        is in the range of 0.0-1.0 | ||||
|                  */ | ||||
|                 drawSquare: function (min, max, color) { | ||||
|                     doDraw(gl.TRIANGLE_FAN, new Float32Array( | ||||
|                         min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]]) | ||||
|                     ), color, 4); | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|         return DrawWebGL; | ||||
|     } | ||||
| ); | ||||
							
								
								
									
										75
									
								
								platform/features/plot-reborn/src/lib/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								platform/features/plot-reborn/src/lib/utils.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| /*global define*/ | ||||
|  | ||||
| define(function() { | ||||
|     "use strict"; | ||||
|  | ||||
|     var utils = {}; | ||||
|  | ||||
|     utils.boxPointsFromOppositeCorners = function(start, end) { | ||||
|         // Given two points defining opposite corners of a square, | ||||
|         // return an array of points containing all of the boxes' rectangles. | ||||
|         return [ | ||||
|             start, | ||||
|             {domain: start.domain, range: end.range}, | ||||
|             end, | ||||
|             {domain: end.domain, range: start.range} | ||||
|         ]; | ||||
|     }; | ||||
|  | ||||
|     utils.oppositeCornersFromBoxPoints = function(boxPoints) { | ||||
|         // Given an array of box points, return the topLeft and bottomRight points of the box. | ||||
|         var topLeft = boxPoints.reduce(function(topLeft, currentPoint) { | ||||
|             if (!topLeft) { | ||||
|                 return currentPoint; | ||||
|             } | ||||
|             if (currentPoint.domain <= topLeft.domain && | ||||
|                     currentPoint.range >= topLeft.range) { | ||||
|                 return currentPoint; | ||||
|             } | ||||
|             return topLeft; | ||||
|         }); | ||||
|  | ||||
|         var bottomRight = boxPoints.reduce(function(bottomRight, currentPoint) { | ||||
|             if (!bottomRight) { | ||||
|                 return currentPoint; | ||||
|             } | ||||
|             if (currentPoint.domain >= bottomRight.domain && | ||||
|                     currentPoint.range <= bottomRight.range) { | ||||
|                 return currentPoint; | ||||
|             } | ||||
|             return bottomRight; | ||||
|         }); | ||||
|  | ||||
|         return { | ||||
|             topLeft: topLeft, | ||||
|             bottomRight: bottomRight | ||||
|         }; | ||||
|     }; | ||||
|  | ||||
|     utils.elementPositionAsPlotPosition = function(elementPosition, elementBounds, viewport) { | ||||
|         // Convert an (x, y) pair in element space to a | ||||
|         // (domain, range) pair viewport. | ||||
|  | ||||
|         // Element space has (0,0) as the topLeft corner, With x | ||||
|         // increasing to the right and y increasing to the bottom. | ||||
|  | ||||
|         var maxDomain = viewport.bottomRight.domain; | ||||
|         var minDomain = viewport.topLeft.domain; | ||||
|         var domainDenominator = maxDomain - minDomain; | ||||
|  | ||||
|         var maxRange = viewport.topLeft.range; | ||||
|         var minRange = viewport.bottomRight.range; | ||||
|         var rangeDenominator = maxRange - minRange; | ||||
|  | ||||
|         var xFraction = elementPosition.x / elementBounds.width; | ||||
|         var yFraction = elementPosition.y / elementBounds.height; | ||||
|  | ||||
|         return { | ||||
|             domain: minDomain + domainDenominator * xFraction, | ||||
|             range: maxRange - rangeDenominator * yFraction | ||||
|         }; | ||||
|     }; | ||||
|  | ||||
|  | ||||
|     return utils; | ||||
| }); | ||||
							
								
								
									
										154
									
								
								platform/features/plot-reborn/src/services/ColorService.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								platform/features/plot-reborn/src/services/ColorService.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /*global define*/ | ||||
| define( | ||||
|     function () { | ||||
|         'use strict'; | ||||
|  | ||||
|         var COLOR_PALETTE = [ | ||||
|             [ 0x20, 0xB2, 0xAA ], | ||||
|             [ 0x9A, 0xCD, 0x32 ], | ||||
|             [ 0xFF, 0x8C, 0x00 ], | ||||
|             [ 0xD2, 0xB4, 0x8C ], | ||||
|             [ 0x40, 0xE0, 0xD0 ], | ||||
|             [ 0x41, 0x69, 0xFF ], | ||||
|             [ 0xFF, 0xD7, 0x00 ], | ||||
|             [ 0x6A, 0x5A, 0xCD ], | ||||
|             [ 0xEE, 0x82, 0xEE ], | ||||
|             [ 0xCC, 0x99, 0x66 ], | ||||
|             [ 0x99, 0xCC, 0xCC ], | ||||
|             [ 0x66, 0xCC, 0x33 ], | ||||
|             [ 0xFF, 0xCC, 0x00 ], | ||||
|             [ 0xFF, 0x66, 0x33 ], | ||||
|             [ 0xCC, 0x66, 0xFF ], | ||||
|             [ 0xFF, 0x00, 0x66 ], | ||||
|             [ 0xFF, 0xFF, 0x00 ], | ||||
|             [ 0x80, 0x00, 0x80 ], | ||||
|             [ 0x00, 0x86, 0x8B ], | ||||
|             [ 0x00, 0x8A, 0x00 ], | ||||
|             [ 0xFF, 0x00, 0x00 ], | ||||
|             [ 0x00, 0x00, 0xFF ], | ||||
|             [ 0xF5, 0xDE, 0xB3 ], | ||||
|             [ 0xBC, 0x8F, 0x8F ], | ||||
|             [ 0x46, 0x82, 0xB4 ], | ||||
|             [ 0xFF, 0xAF, 0xAF ], | ||||
|             [ 0x43, 0xCD, 0x80 ], | ||||
|             [ 0xCD, 0xC1, 0xC5 ], | ||||
|             [ 0xA0, 0x52, 0x2D ], | ||||
|             [ 0x64, 0x95, 0xED ] | ||||
|         ]; | ||||
|  | ||||
|         /** | ||||
|          * A representation of a color that allows conversions between different | ||||
|          * formats. | ||||
|          * | ||||
|          * @constructor | ||||
|          */ | ||||
|         function Color(integerArray) { | ||||
|             this.integerArray = integerArray; | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Return color as a three element array of RGB values, where each value | ||||
|          * is a integer in the range of 0-255. | ||||
|          * | ||||
|          * @return {number[]} the color, as integer RGB values | ||||
|          */ | ||||
|         Color.prototype.asIntegerArray = function () { | ||||
|             return this.integerArray.map(function (c) { | ||||
|                 return c; | ||||
|             }); | ||||
|         }; | ||||
|  | ||||
|        /** | ||||
|         * Return color as a string using #-prefixed six-digit RGB hex notation | ||||
|         * (e.g. #FF0000).  See http://www.w3.org/TR/css3-color/#rgb-color. | ||||
|         * | ||||
|         * @return {string} the color, as a style-friendly string | ||||
|         */ | ||||
|  | ||||
|         Color.prototype.asHexString = function () { | ||||
|             return '#' + this.integerArray.map(function (c) { | ||||
|                 return (c < 16 ? '0' : '') + c.toString(16); | ||||
|             }).join(''); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Return color as a RGBA float array. | ||||
|          * | ||||
|          * This format is present specifically to support use with | ||||
|          * WebGL, which expects colors of that form. | ||||
|          * | ||||
|          * @return {number[]} the color, as floating-point RGBA values | ||||
|          */ | ||||
|         Color.prototype.asRGBAArray = function () { | ||||
|             return this.integerArray.map(function (c) { | ||||
|                 return c / 255.0; | ||||
|             }).concat([1]); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * A color palette stores a set of colors and allows for different | ||||
|          * methods of color allocation. | ||||
|          * | ||||
|          * @constructor | ||||
|          */ | ||||
|         function ColorPalette() { | ||||
|             this.nextColor = 0; | ||||
|             this.colors = COLOR_PALETTE.map(function (color) { | ||||
|                 return new Color(color); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * @returns {Color} the next unused color in the palette.  If all colors | ||||
|          * have been allocated, it will wrap around. | ||||
|          */ | ||||
|         ColorPalette.prototype.getNextColor = function () { | ||||
|             var color = this.colors[this.nextColor % this.colors.length]; | ||||
|             this.nextColor++; | ||||
|             return color; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * @param {number} index the index of the color to return.  An index | ||||
|          * value larger than the size of the index will wrap around. | ||||
|          * @returns {Color} | ||||
|         */ | ||||
|         ColorPalette.prototype.getColor = function (index) { | ||||
|             return this.colors[index % this.colors.length]; | ||||
|         }; | ||||
|  | ||||
|  | ||||
|         function ColorService() {} | ||||
|  | ||||
|         ColorService.prototype.ColorPalette = ColorPalette; | ||||
|         ColorService.prototype.Color = Color; | ||||
|  | ||||
|         function getColorService() { | ||||
|             return new ColorService(); | ||||
|         } | ||||
|  | ||||
|         return getColorService; | ||||
|     } | ||||
| ); | ||||
		Reference in New Issue
	
	Block a user