mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 10:44:21 +00:00
Compare commits
20 Commits
plots-prog
...
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/telemetry",
|
||||||
"platform/features/layout",
|
"platform/features/layout",
|
||||||
"platform/features/pages",
|
"platform/features/pages",
|
||||||
"platform/features/plot",
|
"platform/features/plot-reborn",
|
||||||
"platform/features/scrolling",
|
"platform/features/scrolling",
|
||||||
"platform/forms",
|
"platform/forms",
|
||||||
"platform/persistence/queue",
|
"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