Merge branch 'master' into mct588comm

This commit is contained in:
Alex M 2016-09-14 19:50:19 +03:00
commit d8dc3c8445
24 changed files with 667 additions and 206 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
*.gzip *.gzip
*.tgz *.tgz
*.DS_Store *.DS_Store
*.swp
# Compiled CSS, unless directly added # Compiled CSS, unless directly added
*.sass-cache *.sass-cache

View File

@ -13,11 +13,13 @@
"moment-duration-format": "^1.3.0", "moment-duration-format": "^1.3.0",
"requirejs": "~2.1.22", "requirejs": "~2.1.22",
"text": "requirejs-text#^2.0.14", "text": "requirejs-text#^2.0.14",
"es6-promise": "^3.0.2", "es6-promise": "^3.3.0",
"screenfull": "^3.0.0", "screenfull": "^3.0.0",
"node-uuid": "^1.4.7", "node-uuid": "^1.4.7",
"comma-separated-values": "^3.6.4", "comma-separated-values": "^3.6.4",
"FileSaver.js": "^0.0.2", "FileSaver.js": "^0.0.2",
"zepto": "^1.1.6" "zepto": "^1.1.6",
"html2canvas": "^0.4.1",
"jspdf": "^1.2.61"
} }
} }

View File

@ -933,7 +933,7 @@ Note that `templateUrl` is not supported for `containers`.
Controls provide options for the `mct-control` directive. Controls provide options for the `mct-control` directive.
Six standard control types are included in the forms bundle: Ten standard control types are included in the forms bundle:
* `textfield`: An area to enter plain text. * `textfield`: An area to enter plain text.
* `select`: A drop-down list of options. * `select`: A drop-down list of options.
@ -942,6 +942,12 @@ Six standard control types are included in the forms bundle:
* `button`: A button. * `button`: A button.
* `datetime`: An input for UTC date/time entry; gives result as a UNIX * `datetime`: An input for UTC date/time entry; gives result as a UNIX
timestamp, in milliseconds since start of 1970, UTC. timestamp, in milliseconds since start of 1970, UTC.
* `composite`: A control parenting an array of other controls.
* `menu-button`: A drop-down list of items supporting custom behavior
on click.
* `dialog-button`: A button which opens a dialog allowing a single property
to be edited.
* `radio`: A radio button.
New controls may be added as extensions of the controls category. Extensions of New controls may be added as extensions of the controls category. Extensions of
this category have two properties: this category have two properties:

View File

@ -19,16 +19,15 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<!DOCTYPE html> <!doctype html>
<html> <html lang="en">
<head lang="en"> <head>
<meta charset="UTF-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title></title> <title></title>
<script type="text/javascript" <script src="bower_components/requirejs/require.js">
src="bower_components/requirejs/require.js">
</script> </script>
<script type="text/javascript"> <script>
require(['main'], function (mct) { require(['main'], function (mct) {
require([ require([
'./example/imagery/bundle', './example/imagery/bundle',
@ -39,10 +38,10 @@
</script> </script>
<link rel="stylesheet" href="platform/commonUI/general/res/css/startup-base.css"> <link rel="stylesheet" href="platform/commonUI/general/res/css/startup-base.css">
<link rel="stylesheet" href="platform/commonUI/general/res/css/openmct.css"> <link rel="stylesheet" href="platform/commonUI/general/res/css/openmct.css">
<link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-32x32.png" sizes="32x32"> <link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-96x96.png" sizes="96x96"> <link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-16x16.png" sizes="16x16"> <link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-16x16.png" sizes="16x16">
<link rel="shortcut icon" href="platform/commonUI/general/res/images/favicons/favicon.ico"> <link rel="shortcut icon" href="platform/commonUI/general/res/images/favicons/favicon.ico">
</head> </head>
<body class="user-environ"> <body class="user-environ">
<div class="l-splash-holder s-splash-holder"> <div class="l-splash-holder s-splash-holder">

10
main.js
View File

@ -27,7 +27,9 @@ requirejs.config({
"angular": "bower_components/angular/angular.min", "angular": "bower_components/angular/angular.min",
"angular-route": "bower_components/angular-route/angular-route.min", "angular-route": "bower_components/angular-route/angular-route.min",
"csv": "bower_components/comma-separated-values/csv.min", "csv": "bower_components/comma-separated-values/csv.min",
"es6-promise": "bower_components/es6-promise/promise.min", "es6-promise": "bower_components/es6-promise/es6-promise.min",
"html2canvas": "bower_components/html2canvas/build/html2canvas.min",
"jsPDF": "bower_components/jspdf/dist/jspdf.min",
"moment": "bower_components/moment/moment", "moment": "bower_components/moment/moment",
"moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format", "moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format",
"saveAs": "bower_components/FileSaver.js/FileSaver.min", "saveAs": "bower_components/FileSaver.js/FileSaver.min",
@ -43,6 +45,12 @@ requirejs.config({
"angular-route": { "angular-route": {
"deps": ["angular"] "deps": ["angular"]
}, },
"html2canvas": {
"exports": "html2canvas"
},
"jsPDF": {
"exports": "jsPDF"
},
"moment-duration-format": { "moment-duration-format": {
"deps": ["moment"] "deps": ["moment"]
}, },

View File

@ -40,7 +40,7 @@
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"moment": "^2.11.1", "moment": "^2.11.1",
"node-bourbon": "^4.2.3", "node-bourbon": "^4.2.3",
"phantomjs-prebuilt": "^2.1.0", "phantomjs-prebuilt": "2.1.11 || >2.1.12 <3.0.0",
"requirejs": "2.1.x", "requirejs": "2.1.x",
"split": "^1.0.0" "split": "^1.0.0"
}, },

View File

@ -387,7 +387,7 @@ define([
"constants": [ "constants": [
{ {
"key": "editModeBlacklist", "key": "editModeBlacklist",
"value": ["copy", "follow", "window", "link", "locate"] "value": ["copy", "follow", "link", "locate"]
}, },
{ {
"key": "nonEditContextBlacklist", "key": "nonEditContextBlacklist",

View File

@ -126,6 +126,7 @@ $menuLineH: 1.5rem;
$menuLineHPx: 24px; $menuLineHPx: 24px;
$btnStdH: 25px; $btnStdH: 25px;
$btnToolbarH: $btnStdH; $btnToolbarH: $btnStdH;
$controlBarH: $btnStdH;
$btnFrameH: 16px; $btnFrameH: 16px;
/************************** PATHS */ /************************** PATHS */

View File

@ -1,8 +1,11 @@
/* Styles for sub-dividing views generically */ /* Styles for sub-dividing views generically */
.l-control-bar {
// Element that can be placed above l-view-section, holds controls, buttons, etc.
height: $controlBarH;
}
.l-view-section { .l-view-section {
@include absPosDefault(0); @include absPosDefault(0);
font-size: 0.8rem;
h2 { h2 {
color: #fff; color: #fff;
margin-bottom: $interiorMargin; margin-bottom: $interiorMargin;
@ -16,3 +19,35 @@
display: inline-block; display: inline-block;
} }
} }
.has-control-bar {
.l-view-section {
top: $controlBarH + $interiorMargin;
}
}
.child-frame {
.has-control-bar {
$btnExportH: $btnFrameH;
.l-control-bar {
@include trans-prop-nice(opacity, $dur: 50ms);
opacity: 0;
}
.l-view-section {
@include trans-prop-nice(top, $dur: 150ms, $delay: 50ms);
top: 0;
}
&:hover {
.l-control-bar {
@include trans-prop-nice(opacity, 150ms, 100ms);
opacity: 1;
}
.l-view-section {
@include trans-prop-nice(top, $dur: 150ms);
top: $btnExportH + $interiorMargin;
}
}
}
}

View File

@ -160,39 +160,3 @@ table {
} }
} }
} }
/********************************************************** SPECIFIC TABULAR VIEWS */
.tabular-holder {
&.t-exportable {
$btnExportH: 25px;
.l-view-section {
top: $btnExportH + $interiorMargin;
}
}
}
.child-frame {
.tabular-holder {
&.t-exportable {
$btnExportH: $btnFrameH;
.s-button.t-export {
@include trans-prop-nice(opacity, $dur: 50ms);
opacity: 0;
}
.l-view-section {
@include trans-prop-nice(top, $dur: 150ms, $delay: 50ms);
top: 0;
}
&:hover {
.s-button.t-export {
@include trans-prop-nice(opacity, 150ms, 100ms);
opacity: 1;
}
.l-view-section {
@include trans-prop-nice(top, $dur: 150ms);
top: $btnExportH + $interiorMargin;
}
}
}
}
}

View File

@ -25,6 +25,7 @@ define([
"./src/PlotController", "./src/PlotController",
"./src/policies/PlotViewPolicy", "./src/policies/PlotViewPolicy",
"./src/PlotOptionsController", "./src/PlotOptionsController",
"./src/services/ExportImageService",
"text!./res/templates/plot.html", "text!./res/templates/plot.html",
"text!./res/templates/plot-options-browse.html", "text!./res/templates/plot-options-browse.html",
'legacyRegistry' 'legacyRegistry'
@ -33,6 +34,7 @@ define([
PlotController, PlotController,
PlotViewPolicy, PlotViewPolicy,
PlotOptionsController, PlotOptionsController,
exportImageService,
plotTemplate, plotTemplate,
plotOptionsBrowseTemplate, plotOptionsBrowseTemplate,
legacyRegistry legacyRegistry
@ -70,6 +72,8 @@ define([
"implementation": PlotController, "implementation": PlotController,
"depends": [ "depends": [
"$scope", "$scope",
"$element",
"exportImageService",
"telemetryFormatter", "telemetryFormatter",
"telemetryHandler", "telemetryHandler",
"throttle", "throttle",
@ -84,12 +88,30 @@ define([
] ]
} }
], ],
"services": [
{
"key": "exportImageService",
"implementation": exportImageService,
"depends": [
"$q",
"$timeout",
"$log",
"EXPORT_IMAGE_TIMEOUT"
]
}
],
"constants": [ "constants": [
{ {
"key": "PLOT_FIXED_DURATION", "key": "PLOT_FIXED_DURATION",
"value": 900000, "value": 900000,
"priority": "fallback", "priority": "fallback",
"comment": "Fifteen minutes." "comment": "Fifteen minutes."
},
{
"key": "EXPORT_IMAGE_TIMEOUT",
"value": 500,
"priority": "fallback"
} }
], ],
"policies": [ "policies": [
@ -103,6 +125,38 @@ define([
"key": "plot-options-browse", "key": "plot-options-browse",
"template": plotOptionsBrowseTemplate "template": plotOptionsBrowseTemplate
} }
],
"licenses": [
{
"name": "FileSaver.js",
"version": "0.0.2",
"author": "Eli Grey",
"description": "File download initiator (for file exports)",
"website": "https://github.com/eligrey/FileSaver.js/",
"copyright": "Copyright © 2015 Eli Grey.",
"license": "license-mit",
"link": "https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md"
},
{
"name": "html2canvas",
"version": "0.4.1",
"author": "Niklas von Hertzen",
"description": "JavaScript HTML renderer",
"website": "https://github.com/niklasvh/html2canvas",
"copyright": "Copyright © 2012 Niklas von Hertzen.",
"license": "license-mit",
"link": "https://github.com/niklasvh/html2canvas/blob/master/LICENSE"
},
{
"name": "jsPDF",
"version": "1.2.61",
"author": "James Hall",
"description": "JavaScript HTML renderer",
"website": "https://github.com/MrRio/jsPDF",
"copyright": "Copyright © 2010-2016 James Hall",
"license": "license-mit",
"link": "https://github.com/MrRio/jsPDF/blob/master/MIT-LICENSE.txt"
}
] ]
} }
}); });

View File

@ -20,120 +20,141 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<span ng-controller="PlotController as plot" <span ng-controller="PlotController as plot"
class="abs holder holder-plot"> class="abs holder holder-plot has-control-bar">
<div class="gl-plot" <div class="l-control-bar" ng-show="!plot.hideExportButtons">
ng-style="{ height: 100 / plot.getSubPlots().length + '%'}" <span class="l-btn-set">
ng-repeat="subplot in plot.getSubPlots()"> <a class="s-button t-export icon-download labeled first"
<div class="gl-plot-legend"> ng-click="plot.exportPDF()"
<!-- ng-class is temporarily hard-coded in next element --> title="Export This View's Data as PDF">
PDF
</a>
<a class="s-button t-export labeled"
ng-click="plot.exportPNG()"
title="Export This View's Data as PNG">
PNG
</a>
<a class="s-button t-export labeled last"
ng-click="plot.exportJPG()"
title="Export This View's Data as JPG">
JPG
</a>
</span>
</div>
<div class="l-view-section">
<div class="gl-plot"
ng-style="{ height: 100 / plot.getSubPlots().length + '%'}"
ng-repeat="subplot in plot.getSubPlots()">
<div class="gl-plot-legend">
<!-- ng-class is temporarily hard-coded in next element -->
<span <span
class='plot-legend-item' class='plot-legend-item'
ng-repeat="telemetryObject in subplot.getTelemetryObjects()" ng-repeat="telemetryObject in subplot.getTelemetryObjects()"
ng-class="plot.getLegendClass(telemetryObject)"> ng-class="plot.getLegendClass(telemetryObject)">
<span class='plot-color-swatch' <span class='plot-color-swatch'
ng-style="{ 'background-color': plot.getColor($index) }"> ng-style="{ 'background-color': plot.getColor($index) }">
</span> </span>
<span class='title-label'>{{telemetryObject.getModel().name}}</span> <span class='title-label'>{{telemetryObject.getModel().name}}</span>
</span> </span>
</div>
<div class="gl-plot-coords"
ng-if="subplot.isHovering() && subplot.getHoverCoordinates()">
{{subplot.getHoverCoordinates()}}
</div>
<div class="gl-plot-axis-area gl-plot-y">
<div class="gl-plot-label gl-plot-y-label">
{{axes[1].active.name}}
</div> </div>
<div ng-repeat="tick in subplot.getRangeTicks()" <div class="gl-plot-coords"
class="gl-plot-tick gl-plot-y-tick-label" ng-if="subplot.isHovering() && subplot.getHoverCoordinates()">
ng-style="{ bottom: (100 * $index / (subplot.getRangeTicks().length - 1)) + '%' }"> {{subplot.getHoverCoordinates()}}
{{tick.label | reverse}}
</div> </div>
<div class="gl-plot-y-options gl-plot-local-controls" <div class="gl-plot-axis-area gl-plot-y">
ng-if="axes[1].options.length > 1"> <div class="gl-plot-label gl-plot-y-label">
<div class='form-control shell select'> {{axes[1].active.name}}
<select class="form-control input shell"
ng-model="axes[1].active"
ng-options="option.name for option in axes[1].options">
</select>
</div> </div>
</div> <div ng-repeat="tick in subplot.getRangeTicks()"
</div> class="gl-plot-tick gl-plot-y-tick-label"
<div class="gl-plot-display-area" ng-style="{ bottom: (100 * $index / (subplot.getRangeTicks().length - 1)) + '%' }">
ng-mouseenter="subplot.isHovering(true);" {{tick.label | reverse}}
ng-mouseleave="subplot.isHovering(false)" </div>
ng-class="{ loading: plot.isRequestPending() }"> <div class="gl-plot-y-options gl-plot-local-controls"
<!-- Out-of-bounds data indicators --> ng-if="axes[1].options.length > 1">
<!-- ng-show is temporarily hard-coded in next element --> <div class='form-control shell select'>
<div ng-show="false" class="l-oob-data l-oob-data-up"></div> <select class="form-control input shell"
<div ng-show="false" class="l-oob-data l-oob-data-dwn"></div> ng-model="axes[1].active"
<div class="gl-plot-hash hash-v" ng-options="option.name for option in axes[1].options">
ng-repeat="tick in subplot.getDomainTicks()" </select>
ng-style="{ left: (100 * $index / (subplot.getDomainTicks().length - 1)) + '%', height: '100%' }"
ng-show="$index > 0 && $index < (subplot.getDomainTicks().length - 1)">
</div>
<div class="gl-plot-hash hash-h"
ng-repeat="tick in subplot.getRangeTicks()"
ng-style="{ bottom: (100 * $index / (subplot.getRangeTicks().length - 1)) + '%', width: '100%' }"
ng-show="$index > 0 && $index < (subplot.getRangeTicks().length - 1)">
</div>
<mct-chart draw="subplot.getDrawingObject()"
ng-if="subplot.getTelemetryObjects().length > 0"
ng-mousemove="subplot.hover($event)"
mct-drag="subplot.continueDrag($event)"
mct-drag-down="subplot.startDrag($event)"
mct-drag-up="subplot.endDrag($event); plot.update()">
</mct-chart>
<!-- TODO: Move into correct position; make part of group; infer from set of actions -->
<div class="l-local-controls gl-plot-local-controls t-plot-display-controls"
ng-if="$first">
<a class="s-button icon-arrow-left"
ng-click="plot.stepBackPanZoom()"
ng-show="plot.isZoomed()"
title="Restore previous pan/zoom">
</a>
<a class="s-button icon-arrows-out"
ng-click="plot.unzoom()"
ng-show="plot.isZoomed()"
title="Reset pan/zoom">
</a>
<div class="menu-element s-menu-button menus-to-left {{plot.getMode().cssclass}}"
ng-if="plot.getModeOptions().length > 1"
ng-controller="ClickAwayController as toggle">
<span class="l-click-area" ng-click="toggle.toggle()"></span>
<span>{{plot.getMode().name}}</span>
<div class="menu" ng-show="toggle.isActive()">
<ul>
<li ng-repeat="option in plot.getModeOptions()"
ng-click="plot.setMode(option); toggle.setState(false)"
class="{{option.cssclass}}">
{{option.name}}
</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="gl-plot-display-area"
<div ng-if="$last" class="gl-plot-axis-area gl-plot-x"> ng-mouseenter="subplot.isHovering(true);"
<div ng-repeat="tick in subplot.getDomainTicks()" ng-mouseleave="subplot.isHovering(false)"
class="gl-plot-tick gl-plot-x-tick-label" ng-class="{ loading: plot.isRequestPending() }">
ng-show="$index > 0 && $index < (subplot.getDomainTicks().length - 1)" <!-- Out-of-bounds data indicators -->
ng-style="{ left: (100 * $index / (subplot.getDomainTicks().length - 1)) + '%' }"> <!-- ng-show is temporarily hard-coded in next element -->
{{tick.label | reverse}} <div ng-show="false" class="l-oob-data l-oob-data-up"></div>
</div> <div ng-show="false" class="l-oob-data l-oob-data-dwn"></div>
<div class="gl-plot-label gl-plot-x-label"> <div class="gl-plot-hash hash-v"
{{axes[0].active.name}} ng-repeat="tick in subplot.getDomainTicks()"
</div> ng-style="{ left: (100 * $index / (subplot.getDomainTicks().length - 1)) + '%', height: '100%' }"
<div class="gl-plot-x-options gl-plot-local-controls" ng-show="$index > 0 && $index < (subplot.getDomainTicks().length - 1)">
ng-if="axes[0].options.length > 1"> </div>
<div class='form-control shell select'> <div class="gl-plot-hash hash-h"
<select class="form-control input shell" ng-repeat="tick in subplot.getRangeTicks()"
ng-model="axes[0].active" ng-style="{ bottom: (100 * $index / (subplot.getRangeTicks().length - 1)) + '%', width: '100%' }"
ng-options="option.name for option in axes[0].options"> ng-show="$index > 0 && $index < (subplot.getRangeTicks().length - 1)">
</select> </div>
<mct-chart draw="subplot.getDrawingObject()"
ng-if="subplot.getTelemetryObjects().length > 0"
ng-mousemove="subplot.hover($event)"
mct-drag="subplot.continueDrag($event)"
mct-drag-down="subplot.startDrag($event)"
mct-drag-up="subplot.endDrag($event); plot.update()">
</mct-chart>
<!-- TODO: Move into correct position; make part of group; infer from set of actions -->
<div class="l-local-controls gl-plot-local-controls t-plot-display-controls"
ng-if="$first">
<a class="s-button icon-arrow-left"
ng-click="plot.stepBackPanZoom()"
ng-show="plot.isZoomed()"
title="Restore previous pan/zoom">
</a>
<a class="s-button icon-arrows-out"
ng-click="plot.unzoom()"
ng-show="plot.isZoomed()"
title="Reset pan/zoom">
</a>
<div class="menu-element s-menu-button menus-to-left {{plot.getMode().cssclass}}"
ng-if="plot.getModeOptions().length > 1"
ng-controller="ClickAwayController as toggle">
<span class="l-click-area" ng-click="toggle.toggle()"></span>
<span>{{plot.getMode().name}}</span>
<div class="menu" ng-show="toggle.isActive()">
<ul>
<li ng-repeat="option in plot.getModeOptions()"
ng-click="plot.setMode(option); toggle.setState(false)"
class="{{option.cssclass}}">
{{option.name}}
</li>
</ul>
</div>
</div>
</div> </div>
</div> </div>
<div ng-if="$last" class="gl-plot-axis-area gl-plot-x">
<div ng-repeat="tick in subplot.getDomainTicks()"
class="gl-plot-tick gl-plot-x-tick-label"
ng-show="$index > 0 && $index < (subplot.getDomainTicks().length - 1)"
ng-style="{ left: (100 * $index / (subplot.getDomainTicks().length - 1)) + '%' }">
{{tick.label | reverse}}
</div>
<div class="gl-plot-label gl-plot-x-label">
{{axes[0].active.name}}
</div>
<div class="gl-plot-x-options gl-plot-local-controls"
ng-if="axes[0].options.length > 1">
<div class='form-control shell select'>
<select class="form-control input shell"
ng-model="axes[0].active"
ng-options="option.name for option in axes[0].options">
</select>
</div>
</div>
</div>
</div> </div>
</div> </div>
</span> </span>

View File

@ -54,7 +54,8 @@ define(
* @throws {Error} an error is thrown if WebGL is unavailable. * @throws {Error} an error is thrown if WebGL is unavailable.
*/ */
function GLChart(canvas) { function GLChart(canvas) {
var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"), var gl = canvas.getContext("webgl", { preserveDrawingBuffer: true }) ||
canvas.getContext("experimental-webgl", { preserveDrawingBuffer: true }),
vertexShader, vertexShader,
fragmentShader, fragmentShader,
program, program,

View File

@ -63,6 +63,8 @@ define(
*/ */
function PlotController( function PlotController(
$scope, $scope,
$element,
exportImageService,
telemetryFormatter, telemetryFormatter,
telemetryHandler, telemetryHandler,
throttle, throttle,
@ -246,6 +248,8 @@ define(
}); });
self.pending = true; self.pending = true;
self.$element = $element;
self.exportImageService = exportImageService;
// Initialize axes; will get repopulated when telemetry // Initialize axes; will get repopulated when telemetry
// metadata becomes available. // metadata becomes available.
@ -364,6 +368,39 @@ define(
return this.pending; return this.pending;
}; };
/**
* Export the plot to PDF
*/
PlotController.prototype.exportPDF = function () {
var self = this;
self.hideExportButtons = true;
self.exportImageService.exportPDF(self.$element[0], "plot.pdf").finally(function () {
self.hideExportButtons = false;
});
};
/**
* Export the plot to PNG
*/
PlotController.prototype.exportPNG = function () {
var self = this;
self.hideExportButtons = true;
self.exportImageService.exportPNG(self.$element[0], "plot.png").finally(function () {
self.hideExportButtons = false;
});
};
/**
* Export the plot to JPG
*/
PlotController.prototype.exportJPG = function () {
var self = this;
self.hideExportButtons = true;
self.exportImageService.exportJPG(self.$element[0], "plot.jpg").finally(function () {
self.hideExportButtons = false;
});
};
return PlotController; return PlotController;
} }
); );

View File

@ -0,0 +1,176 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining ExportImageService. Created by hudsonfoo on 09/02/16
*/
define(
[
"html2canvas",
"jsPDF",
"saveAs"
],
function (
html2canvas,
jsPDF,
saveAs
) {
var self = this;
/**
* The export image service will export any HTML node to
* PDF, JPG, or PNG.
* @param {object} $q
* @param {object} $timeout
* @param {object} $log
* @param {constant} EXPORT_IMAGE_TIMEOUT time in milliseconds before a timeout error is returned
* @constructor
*/
function ExportImageService($q, $timeout, $log, EXPORT_IMAGE_TIMEOUT, injHtml2Canvas, injJsPDF, injSaveAs, injFileReader) {
self.$q = $q;
self.$timeout = $timeout;
self.$log = $log;
self.EXPORT_IMAGE_TIMEOUT = EXPORT_IMAGE_TIMEOUT;
self.html2canvas = injHtml2Canvas || html2canvas;
self.jsPDF = injJsPDF || jsPDF;
self.saveAs = injSaveAs || saveAs;
self.reader = injFileReader || new FileReader();
}
/**
* Renders an HTML element into a base64 encoded image
* as a BLOB, PNG, or JPG.
* @param {node} element that will be converted to an image
* @param {string} type of image to convert the element to
* @returns {promise}
*/
function renderElement(element, type) {
var defer = self.$q.defer(),
validTypes = ["png", "jpg", "jpeg"],
renderTimeout;
if (validTypes.indexOf(type) === -1) {
self.$log.error("Invalid type requested. Try: (" + validTypes.join(",") + ")");
return;
}
renderTimeout = self.$timeout(function () {
defer.reject("html2canvas timed out");
self.$log.warn("html2canvas timed out");
}, self.EXPORT_IMAGE_TIMEOUT);
try {
self.html2canvas(element, {
onrendered: function (canvas) {
switch (type.toLowerCase()) {
case "png":
canvas.toBlob(defer.resolve, "image/png");
break;
default:
case "jpg":
case "jpeg":
canvas.toBlob(defer.resolve, "image/jpeg");
break;
}
}
});
} catch (e) {
defer.reject(e);
self.$log.warn("html2canvas failed with error: " + e);
}
defer.promise.finally(renderTimeout.cancel);
return defer.promise;
}
/**
* canvas.toBlob() not supported in IE < 10, Opera, and Safari. This polyfill
* implements the method in browsers that would not otherwise support it.
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
*/
function polyfillToBlob() {
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, "toBlob", {
value: function (callback, type, quality) {
var binStr = atob(this.toDataURL(type, quality).split(',')[1]),
len = binStr.length,
arr = new Uint8Array(len);
for (var i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
callback(new Blob([arr], {type: type || "image/png"}));
}
});
}
}
/**
* Takes a screenshot of a DOM node and exports to PDF.
* @param {node} element to be exported
* @param {string} filename the exported image
* @returns {promise}
*/
ExportImageService.prototype.exportPDF = function (element, filename) {
return renderElement(element, "jpeg").then(function (img) {
self.reader.readAsDataURL(img);
self.reader.onloadend = function () {
var pdf = new self.jsPDF("l", "px", [element.offsetHeight, element.offsetWidth]);
pdf.addImage(self.reader.result, "JPEG", 0, 0, element.offsetWidth, element.offsetHeight);
pdf.save(filename);
};
});
};
/**
* Takes a screenshot of a DOM node and exports to JPG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @returns {promise}
*/
ExportImageService.prototype.exportJPG = function (element, filename) {
return renderElement(element, "jpeg").then(function (img) {
self.saveAs(img, filename);
});
};
/**
* Takes a screenshot of a DOM node and exports to PNG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @returns {promise}
*/
ExportImageService.prototype.exportPNG = function (element, filename) {
return renderElement(element, "png").then(function (img) {
self.saveAs(img, filename);
});
};
polyfillToBlob();
return ExportImageService;
}
);

View File

@ -1,3 +1,5 @@
/*global angular*/
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government * Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
@ -29,6 +31,8 @@ define(
describe("The plot controller", function () { describe("The plot controller", function () {
var mockScope, var mockScope,
mockElement,
mockExportImageService,
mockFormatter, mockFormatter,
mockHandler, mockHandler,
mockThrottle, mockThrottle,
@ -65,6 +69,11 @@ define(
"$scope", "$scope",
["$watch", "$on", "$emit"] ["$watch", "$on", "$emit"]
); );
mockElement = angular.element('<div />');
mockExportImageService = jasmine.createSpyObj(
"ExportImageService",
["exportJPG", "exportPNG", "exportPDF"]
);
mockFormatter = jasmine.createSpyObj( mockFormatter = jasmine.createSpyObj(
"formatter", "formatter",
["formatDomainValue", "formatRangeValue"] ["formatDomainValue", "formatRangeValue"]
@ -107,6 +116,8 @@ define(
controller = new PlotController( controller = new PlotController(
mockScope, mockScope,
mockElement,
mockExportImageService,
mockFormatter, mockFormatter,
mockHandler, mockHandler,
mockThrottle mockThrottle

View File

@ -0,0 +1,141 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* ExportImageServiceSpec. Created by hudsonfoo on 09/03/16.
*/
define(
["../../src/services/ExportImageService"],
function (ExportImageService) {
var mockQ,
mockDeferred,
mockPromise,
mockTimeout,
mockLog,
mockHtml2Canvas,
mockCanvas,
mockJsPDF,
mockJsPDFSave,
mockSaveAs,
mockFileReader,
mockExportTimeoutConstant,
testElement,
exportImageService;
describe("ExportImageService", function () {
beforeEach(function () {
mockDeferred = jasmine.createSpyObj(
"deferred",
["reject", "resolve"]
);
mockPromise = jasmine.createSpyObj(
"promise",
["then", "finally"]
);
mockPromise.then = function (callback) {
callback();
};
mockQ = {
"defer": function () {
return {
"resolve": mockDeferred.resolve,
"reject": mockDeferred.reject,
"promise": mockPromise
};
}
};
mockTimeout = function (fn, time) {
return {
"cancel": function () {}
};
};
mockLog = jasmine.createSpyObj(
"$log",
["warn"]
);
mockHtml2Canvas = jasmine.createSpy("html2canvas").andCallFake(function (element, opts) {
opts.onrendered(mockCanvas);
});
mockCanvas = jasmine.createSpyObj(
"canvas",
["toBlob"]
);
mockJsPDFSave = jasmine.createSpy("jsPDFSave");
mockJsPDF = function () {
return {
"addImage": function () {},
"save": mockJsPDFSave
};
};
mockSaveAs = jasmine.createSpy("saveAs");
mockFileReader = jasmine.createSpyObj(
"FileReader",
["readAsDataURL", "onloadend"]
);
mockExportTimeoutConstant = 0;
testElement = {};
exportImageService = new ExportImageService(
mockQ,
mockTimeout,
mockLog,
mockExportTimeoutConstant,
mockHtml2Canvas,
mockJsPDF,
mockSaveAs,
mockFileReader
);
});
it("runs html2canvas and tries to save a pdf", function () {
exportImageService.exportPDF(testElement, "plot.pdf");
mockFileReader.onloadend();
expect(mockHtml2Canvas).toHaveBeenCalledWith(testElement, { onrendered: jasmine.any(Function) });
expect(mockCanvas.toBlob).toHaveBeenCalledWith(mockDeferred.resolve, "image/jpeg");
expect(mockDeferred.reject).not.toHaveBeenCalled();
expect(mockJsPDFSave).toHaveBeenCalled();
expect(mockPromise.finally).toHaveBeenCalled();
});
it("runs html2canvas and tries to save a png", function () {
exportImageService.exportPNG(testElement, "plot.png");
expect(mockHtml2Canvas).toHaveBeenCalledWith(testElement, { onrendered: jasmine.any(Function) });
expect(mockCanvas.toBlob).toHaveBeenCalledWith(mockDeferred.resolve, "image/png");
expect(mockDeferred.reject).not.toHaveBeenCalled();
expect(mockSaveAs).toHaveBeenCalled();
expect(mockPromise.finally).toHaveBeenCalled();
});
it("runs html2canvas and tries to save a jpg", function () {
exportImageService.exportJPG(testElement, "plot.png");
expect(mockHtml2Canvas).toHaveBeenCalledWith(testElement, { onrendered: jasmine.any(Function) });
expect(mockCanvas.toBlob).toHaveBeenCalledWith(mockDeferred.resolve, "image/jpeg");
expect(mockDeferred.reject).not.toHaveBeenCalled();
expect(mockSaveAs).toHaveBeenCalled();
expect(mockPromise.finally).toHaveBeenCalled();
});
});
}
);

View File

@ -4,6 +4,6 @@
rows="rows" rows="rows"
enableFilter="true" enableFilter="true"
enableSort="true" enableSort="true"
class="tabular-holder t-exportable"> class="tabular-holder has-control-bar">
</mct-table> </mct-table>
</div> </div>

View File

@ -1,8 +1,10 @@
<a class="s-button t-export icon-download labeled" <div class="l-control-bar">
ng-click="exportAsCSV()" <a class="s-button t-export icon-download labeled"
title="Export This View's Data"> ng-click="exportAsCSV()"
Export title="Export This View's Data">
</a> Export
</a>
</div>
<div class="l-view-section scrolling" style="overflow: auto;" mct-resize="resize()"> <div class="l-view-section scrolling" style="overflow: auto;" mct-resize="resize()">
<table class="sizing-table"> <table class="sizing-table">
<tbody> <tbody>

View File

@ -4,7 +4,7 @@
rows="rows" rows="rows"
enableFilter="true" enableFilter="true"
enableSort="true" enableSort="true"
class="tabular-holder t-exportable" class="tabular-holder has-control-bar"
auto-scroll="true"> auto-scroll="true">
</mct-table> </mct-table>
</div> </div>

View File

@ -36,18 +36,18 @@
ng-controller="ColorController as colors" ng-controller="ColorController as colors"
ng-show="toggle.isActive()"> ng-show="toggle.isActive()">
<div <div
class="l-palette-row l-option-row" class="l-palette-row l-option-row"
ng-if="!structure.mandatory"> ng-if="!structure.mandatory">
<div class="l-palette-item s-palette-item {{ngModel[field] === 'transparent' ? 'icon-check' : '' }}" <div class="l-palette-item s-palette-item {{ngModel[field] === 'transparent' ? 'icon-check' : '' }}"
ng-click="ngModel[field] = 'transparent'"> ng-click="ngModel[field] = 'transparent'">
</div> </div>
<span class="l-palette-item-label">None</span> <span class="l-palette-item-label">None</span>
</div> </div>
<div <div
class="l-palette-row" class="l-palette-row"
ng-repeat="group in colors.groups()"> ng-repeat="group in colors.groups()">
<div class="l-palette-item s-palette-item {{ngModel[field] === color ? 'icon-check' : '' }}" <div class="l-palette-item s-palette-item {{ngModel[field] === color ? 'icon-check' : '' }}"
ng-repeat="color in group" ng-repeat="color in group"
ng-style="{ background: color }" ng-style="{ background: color }"
ng-click="ngModel[field] = color"> ng-click="ngModel[field] = color">
</div> </div>

View File

@ -21,18 +21,18 @@
--> -->
<span ng-controller="CompositeController as compositeCtrl"> <span ng-controller="CompositeController as compositeCtrl">
<ng-form name="mctFormItem" ng-repeat="item in structure.items"> <ng-form name="mctFormItem" ng-repeat="item in structure.items">
<div class="l-composite-control l-{{item.control}} {{item.cssclass}}"> <div class="l-composite-control l-{{item.control}} {{item.cssclass}}">
<mct-control key="item.control" <mct-control key="item.control"
ng-model="ngModel[field]" ng-model="ngModel[field]"
ng-required="ngRequired || compositeCtrl.isNonEmpty(ngModel[field])" ng-required="ngRequired || compositeCtrl.isNonEmpty(ngModel[field])"
ng-pattern="ngPattern" ng-pattern="ngPattern"
options="item.options" options="item.options"
structure="row" structure="row"
field="$index"> field="$index">
</mct-control> </mct-control>
<span class="composite-control-label"> <span class="composite-control-label">
{{item.name}} {{item.name}}
</span> </span>
</div> </div>
</ng-form> </ng-form>
</span> </span>

View File

@ -20,31 +20,31 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<form novalidate> <form novalidate>
<div class="tool-bar btn-bar contents abs"> <div class="tool-bar btn-bar contents abs">
<span ng-repeat="section in structure.sections" <span ng-repeat="section in structure.sections"
class="l-control-group" class="l-control-group"
ng-if="!section.hidden" ng-if="!section.hidden"
title="{{section.description}}"> title="{{section.description}}">
<ng-form ng-repeat="item in section.items" <ng-form ng-repeat="item in section.items"
ng-class="{ 'input-labeled': item.name }" ng-class="{ 'input-labeled': item.name }"
ng-hide="item.hidden" ng-hide="item.hidden"
class="inline" class="inline"
title="{{item.description}}" title="{{item.description}}"
name="mctFormInner"> name="mctFormInner">
<label ng-if="item.name"> <label ng-if="item.name">
{{item.name}}: {{item.name}}:
</label> </label>
<mct-control key="item.control" <mct-control key="item.control"
ng-class="{ disabled: item.disabled }" ng-class="{ disabled: item.disabled }"
ng-model="ngModel" ng-model="ngModel"
ng-required="item.required" ng-required="item.required"
ng-pattern="getRegExp(item.pattern)" ng-pattern="getRegExp(item.pattern)"
options="item.options" options="item.options"
structure="item" structure="item"
field="item.key"> field="item.key">
</mct-control> </mct-control>
</ng-form> </ng-form>
</span> </span>
</div> </div>
</form> </form>

View File

@ -53,7 +53,9 @@ requirejs.config({
"angular": "bower_components/angular/angular.min", "angular": "bower_components/angular/angular.min",
"angular-route": "bower_components/angular-route/angular-route.min", "angular-route": "bower_components/angular-route/angular-route.min",
"csv": "bower_components/comma-separated-values/csv.min", "csv": "bower_components/comma-separated-values/csv.min",
"es6-promise": "bower_components/es6-promise/promise.min", "es6-promise": "bower_components/es6-promise/es6-promise.min",
"html2canvas": "bower_components/html2canvas/build/html2canvas.min",
"jsPDF": "bower_components/jspdf/dist/jspdf.min",
"moment": "bower_components/moment/moment", "moment": "bower_components/moment/moment",
"moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format", "moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format",
"saveAs": "bower_components/FileSaver.js/FileSaver.min", "saveAs": "bower_components/FileSaver.js/FileSaver.min",