mirror of
https://github.com/nasa/openmct.git
synced 2025-04-16 07:26:53 +00:00
Merge remote-tracking branch 'upstream/plot-vee-two' into mobile_gestures_1
This commit is contained in:
commit
17e2da2d2c
@ -14,7 +14,7 @@
|
||||
"platform/features/imagery",
|
||||
"platform/features/layout",
|
||||
"platform/features/pages",
|
||||
"platform/features/plot",
|
||||
"platform/features/plot-reborn",
|
||||
"platform/features/scrolling",
|
||||
"platform/features/events",
|
||||
"platform/forms",
|
||||
|
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.
|
||||
|
96
platform/features/plot-reborn/bundle.json
Normal file
96
platform/features/plot-reborn/bundle.json
Normal file
@ -0,0 +1,96 @@
|
||||
{
|
||||
"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",
|
||||
"templateUrl": "templates/plot.html",
|
||||
"needs": ["telemetry", "composition"],
|
||||
"uses": ["composition"],
|
||||
"delegation": true
|
||||
},
|
||||
{
|
||||
"name": "Stacked Plot",
|
||||
"key": "stackedPlot",
|
||||
"glyph": "6",
|
||||
"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", "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>
|
185
platform/features/plot-reborn/src/controllers/PlotController.js
Normal file
185
platform/features/plot-reborn/src/controllers/PlotController.js
Normal file
@ -0,0 +1,185 @@
|
||||
/*global define*/
|
||||
|
||||
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, colorService) {
|
||||
var plotHistory = [],
|
||||
isLive = true,
|
||||
maxDomain = +new Date(),
|
||||
subscriptions = [],
|
||||
palette = new colorService.ColorPalette();
|
||||
|
||||
|
||||
function setToDefaultViewport() {
|
||||
// TODO: We shouldn't set the viewport until we have received data or something has given us a reasonable viewport.
|
||||
$scope.viewport = {
|
||||
topLeft: {
|
||||
domain: maxDomain - DOMAIN_INTERVAL,
|
||||
range: 1
|
||||
},
|
||||
bottomRight: {
|
||||
domain: maxDomain,
|
||||
range: -1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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 updateSeriesFromTelemetry(series, seriesIndex, telemetry) {
|
||||
var domainValue = telemetry.getDomainValue(
|
||||
telemetry.getPointCount() - 1
|
||||
),
|
||||
rangeValue = telemetry.getRangeValue(
|
||||
telemetry.getPointCount() - 1
|
||||
),
|
||||
newTelemetry;
|
||||
// Track the biggest domain we've seen for sticky-ness.
|
||||
maxDomain = Math.max(maxDomain, domainValue);
|
||||
|
||||
newTelemetry = {
|
||||
domain: domainValue,
|
||||
range: rangeValue
|
||||
};
|
||||
series.data.push(newTelemetry);
|
||||
$scope.$broadcast('series:data:add', seriesIndex, [newTelemetry]);
|
||||
}
|
||||
|
||||
function subscribeToDomainObject(domainObject) {
|
||||
var telemetryCapability = domainObject.getCapability('telemetry'),
|
||||
model = domainObject.getModel(),
|
||||
series,
|
||||
seriesIndex,
|
||||
updater;
|
||||
|
||||
series = {
|
||||
name: model.name,
|
||||
// TODO: Bring back PlotPalette.
|
||||
color: palette.getColor($scope.series.length),
|
||||
data: []
|
||||
};
|
||||
|
||||
$scope.series.push(series);
|
||||
seriesIndex = $scope.series.indexOf(series);
|
||||
|
||||
updater = updateSeriesFromTelemetry.bind(
|
||||
null,
|
||||
series,
|
||||
seriesIndex
|
||||
);
|
||||
subscriptions.push(telemetryCapability.subscribe(updater));
|
||||
}
|
||||
|
||||
function unlinkDomainObject() {
|
||||
subscriptions.forEach(function(subscription) {
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
subscriptions = [];
|
||||
}
|
||||
|
||||
|
||||
function linkDomainObject(domainObject) {
|
||||
unlinkDomainObject();
|
||||
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 delegates.forEach(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 viewportForMaxDomain() {
|
||||
return {
|
||||
topLeft: {
|
||||
range: $scope.viewport.topLeft.range,
|
||||
domain: maxDomain - DOMAIN_INTERVAL
|
||||
},
|
||||
bottomRight: {
|
||||
range: $scope.viewport.bottomRight.range,
|
||||
domain: maxDomain
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function followDataIfLive() {
|
||||
if (isLive) {
|
||||
$scope.viewport = viewportForMaxDomain();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$on('series:data:add', followDataIfLive);
|
||||
$scope.$on('user:viewport:change:end', onUserViewportChangeEnd);
|
||||
$scope.$on('user:viewport:change:start', onUserViewportChangeStart);
|
||||
|
||||
$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,37 @@
|
||||
/*global define */
|
||||
|
||||
define(
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
function StackedPlotController($scope) {
|
||||
|
||||
$scope.telemetryObjects = [];
|
||||
|
||||
var linkDomainObject = function(domainObject) {
|
||||
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;
|
||||
}
|
||||
);
|
234
platform/features/plot-reborn/src/directives/MCTChart.js
Normal file
234
platform/features/plot-reborn/src/directives/MCTChart.js
Normal file
@ -0,0 +1,234 @@
|
||||
/*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 createOffset() {
|
||||
if (offset) {
|
||||
return;
|
||||
}
|
||||
if (!$scope.viewport ||
|
||||
!$scope.viewport.topLeft ||
|
||||
!$scope.viewport.bottomRight) {
|
||||
return;
|
||||
}
|
||||
offset = new Offsetter(
|
||||
$scope.viewport.topLeft.domain,
|
||||
$scope.viewport.topLeft.range
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
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() {
|
||||
// TODO: Don't regenerate lines on each frame.
|
||||
if (!$scope.series || !$scope.series.length) {
|
||||
return;
|
||||
}
|
||||
lines = $scope.series.map(lineFromSeries);
|
||||
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];
|
||||
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();
|
||||
createOffset();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for resize, on a timer
|
||||
activeInterval = $interval(drawIfResized, 1000);
|
||||
|
||||
$scope.$on('series:data:add', onSeriesDataAdd);
|
||||
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: "="
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
268
platform/features/plot-reborn/src/directives/MCTPlot.js
Normal file
268
platform/features/plot-reborn/src/directives/MCTPlot.js
Normal file
@ -0,0 +1,268 @@
|
||||
/*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.
|
||||
['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) {
|
||||
// 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;
|
||||
}
|
||||
);
|
Loading…
x
Reference in New Issue
Block a user