mirror of
https://github.com/nasa/openmct.git
synced 2025-03-14 16:26:50 +00:00
Merge pull request #1164 from hudsonfoo/open967
[Enhancement] Export plot as PDF, JPG, or PNG
This commit is contained in:
commit
e73bb4ff4a
@ -18,6 +18,8 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
main.js
8
main.js
@ -28,6 +28,8 @@ requirejs.config({
|
|||||||
"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/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"]
|
||||||
},
|
},
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
176
platform/features/plot/src/services/ExportImageService.js
Normal file
176
platform/features/plot/src/services/ExportImageService.js
Normal 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;
|
||||||
|
}
|
||||||
|
);
|
@ -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
|
||||||
|
141
platform/features/plot/test/services/ExportImageServiceSpec.js
Normal file
141
platform/features/plot/test/services/ExportImageServiceSpec.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
@ -54,6 +54,8 @@ requirejs.config({
|
|||||||
"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/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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user