nasa / openmct

A web based mission control framework.
https://nasa.github.io/openmct/
Other
12.09k stars 1.26k forks source link

Export a plot as a PDF, JPG or PNG #967

Closed charlesh88 closed 8 years ago

charlesh88 commented 8 years ago

Assigned to @simistern

Multiple user requests; related to https://github.jpl.nasa.gov/MissionControl/vista/issues/203

Allow a view to export itself as a PDF, JPG or PNG. Mainly intended for use with plot views, but my be applicable to other view types.

If someone is interested in helping with this, we need to discuss details before any work is started. Thanks!

akhenry commented 8 years ago

Could be tricky without rewriting the plots. The content of canvases can be exported as images quite easily, but our ticks and axes are HTML. There are libraries for screen grabbing arbitrary dom elements, but none of them are that great. Agree that making the view printable (esp. for plots and tables) might be the best short term solution.

larkin commented 8 years ago

No need for a short term solution here-- just need this tracked somewhere. This might be something that a contributor can work on-- we don't want to specify the solution exactly, we want something that works well without compromising our flexibility. Given that we might have various plot plugins in the future, I think we need a generic-- not something specific to our plot object.

simistern commented 8 years ago

I'd like to take a crack at this. I recently worked on a large scale project that involved a lot of canvas rendering elements as well as phantomjs for screen grabbing and rasterizing dom elements. I will work on a solution that will be as reusable as possible for future implementations.

akhenry commented 8 years ago

@simistern great! Your experience will definitely come in handy here. At this stage Open MCT is a browser-side component only. Specific implementations will of course have a server-side component, but they're not part of the open source project at this stage, so in the short to medium term server-side solutions are out. I will reach out to you separately regarding some specifics about contributing to this project. Thanks again for your interest.

simistern commented 8 years ago

Awesome, happy to be involved! My experience has been with implementing phantomjs rendering via server-side API, so I am a bit unsure of the best way to deliver a client-side solution, but I am more than happy to figure it out 👍

akhenry commented 8 years ago

@simistern Have you had any luck with this? Curious if there's a way to achieve this client side.

simistern commented 8 years ago

Hey Andrew,

I've been a bit under the gun work-wise and have not been able to put as much time as I'd like into it :( I have been thinking of ways around the client-side limitation though, I'm not sure thats possible with phantomjs because the binaries need to be installed in the environment its used....I don't think that'd be possible given the size of the software (65mb).

Two other options I considered were a dedicated nodejs microservice with shrinkwrapped dependencies that you guys could maybe validate, or using AWS lambda for serverless image generation.

I am happy to discuss any other suggestions or ideas as well :)

On Thu, Jul 14, 2016 at 12:49 PM, Andrew Henry notifications@github.com wrote:

@simistern https://github.com/simistern Have you had any luck with this? Curious if there's a way to achieve this client side.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/nasa/openmct/issues/967#issuecomment-232723847, or mute the thread https://github.com/notifications/unsubscribe/ADSKDtBuXb7Pe1w0CY5J0XJfOHFuvmS1ks5qVmiWgaJpZM4InyIn .

Kind Regards, Simon

VWoeltjen commented 8 years ago

I was going to joke that we could use emscripten to cross-compile WebKit, and render the content we wanted to snapshot that way. Of course, that's been done, because why wouldn't you?

Instead of having an explicit "export image" function, would it be more practical to simply pop out a separate tab/window with just the view content that users want to export, then allow users to resize that and take a screenshot, or print to PDF? We can basically do that now (collapse left/right panes, print to PDF) and the results are not awful, and could be made much better with print-specific CSS:

A sine wave generator.pdf

larkin commented 8 years ago

@VWoeltjen I think a print specific stylesheet (or media query) is a great option here-- we don't even need to open in a new tab.

When the user tries to print, we invoke the browser print menu. the browser will apply all the styles for the print media query, which we can set up to hide the inspector/tree and other unnecessary content, and allows the main view to scroll instead of forcing it within a frame of a specific size. That way, a long scrollable layout would print over multiple pages.

hudsonfoo commented 8 years ago

This can be accomplished by copying HTML nodes to a "scratch" canvas, then copying the plot canvas data over the top of the "scratch" canvas. Once you have both the HTML and the plot drawn, exporting is trivial. I did a prototype using the html2canvas library that worked. I believe that this can be done at any resolution without user interference by negative z-indexing a clone of the requested telemetry module to the size requested by the user. I do intend on working on this at some point, but I wanted to make a note as I'm still deciding what the most fun thing to work on would be and don't want to forget this potential solution.

akhenry commented 8 years ago

@hudsonfoo Sounds promising! We have been considering using something like html2canvas as a solution here, but we were a little unsure of what the results would look like, especially with mixing plot canvases and HTML elements. We haven't prototyped anything let, so if you have a prototype that you'd be ok with sharing with us, we'd love to see it.

hudsonfoo commented 8 years ago

Here we have a reasonably high quality PDF being generated from the plot: http://quick.as/XZwtBgGJ

My prototype is u-g-l-y but it works. One key thing that you should know is that by preserving the draw buffer, capturing the graph and the HTML nodes becomes much easier. See line 57 of GLChart.js:

        function GLChart(canvas) {
+            var gl = canvas.getContext("webgl", { preserveDrawingBuffer: true }) || canvas.getContext("experimental-webgl", { preserveDrawingBuffer: true }),
                vertexShader,

Note that preserveDrawingBuffer: true. This is what makes my prototype possible. Not sure if there's a performance hit here or not, and it's quite possible it can be turned on and off, but for simplicity's sake I just turned it on while I did my test. There's no obvious performance hit on my machine.

hudsonfoo commented 8 years ago

Guys, I really threw this together quickly so please don't judge :) But if you want to see it, here it is: https://github.com/hudsonfoo/openmct/commit/852ac3cd550f3173556c168e81677e8d8d7b262c

Basically it just automatically downloads a PDF screengrab 5 seconds after page load of the first .gl-plot it sees.

This proof-of-concept leads me to believe that completing this issue is very doable. Basically I need to:

VWoeltjen commented 8 years ago

That sounds like a viable approach. html2canvas looks like it might be entirely reliable (they make it clear that full CSS support should not be expected), but the use cases we have for this are specific enough that I think we could work around that if we need to.

Implementing this as an Export Plot action in the context menu would run into some difficulties, as context menu actions don't get any information about where in the DOM they've been fired from. This could be resolved or worked-around, but it is probably simpler to keep the scope of this issue small and just add this as a button in the Plot view. If the export code is split out (e.g. someService.exportPDF(domElement, filename)) then exposing this as a context menu action should be trivial once those other architectural issues are addressed.

hudsonfoo commented 8 years ago

Thoughts on the best method for passing the plot DOM node to the action? I have the button set up, and modified action-button.html to pass $event like so:

<a class="s-button key-{{parameters.action.getMetadata().key}} {{parameters.action.getMetadata().cssclass}}"
   ng-class="{ labeled: parameters.labeled }"
   title="{{parameters.action.getMetadata().description}}"
+   ng-click="parameters.action.perform($event)">

Which allows me to get the srcElement (the action button). Traversing up the DOM to the object holder node is not difficult, but I can't help but feel there's already a method you may use to simplify accessing the DOM representation of the plot. I'm going through all the docs now to see if I'm missing something.

akhenry commented 8 years ago

@hudsonfoo I don't think it's necessary right now to define this as an action. Actions are object-centric, rather than view-centric, and are abstracted from the DOM. Because it's plot-specific at this stage, you could simply implement this as a button in the plot view that invokes a function on scope. Be careful if you end up using a selector with zepto (available as a require dependency) or vanilla js that it's scoped correctly. Bear in mind that there might be multiple plots in a display layout.

There is an 'API' approach to getting the DOM representation of the plot view that would work, but I wouldn't recommend it tbh. You could do it by defining a new representer for plots. Representers are a very blunt instrument though, which are applied to every representation. You could add some logic to only attach it to plot views though. Representers will give you the element and a scope variable. You could then attach a function to scope that is called from ng-click on your button. Representers are not intuitive, and will probably be rendered obsolete by the new API.

hudsonfoo commented 8 years ago

Whoops! I think I misunderstood @VWoeltjen. I took my action out of the context menu and added it as a view-control instead. Sounds like I should actually be inside the plot controller. That helps a lot. Thanks @akhenry!

akhenry commented 8 years ago

Probably worth mentioning mct-table.html and MCTTableController which do something very similar.

hudsonfoo commented 8 years ago

@akhenry BOOM! I think I'm with you now. In action: http://quick.as/3ajio6Jg

VWoeltjen commented 8 years ago

In action: http://quick.as/3ajio6Jg

Fantastic! There are multiple users who are going to be quite happy about this; it is helpful for generating reports/presentations.

@charlesh88 - what are your thoughts based on the video or the PR? Any changes you'd like to see, accounting for the context menu limitations mentioned above?

charlesh88 commented 8 years ago

@VWoeltjen @hudsonfoo The functionality looks great, but I'm pretty concerned about those buttons adding a lot of noise to the UI as they are now. One thing that would be a pretty big issue is if every plot object in a Display Layout showed all these buttons by default, which is what would happen if this is merged as is. As mentioned by @akhenry above, we have very similar functionality to this in place for Historical and Real-time Tables; this should be in parity with that - the buttons are hidden by default and appear with a transition when the user rolls over that element. This should definitely be implemented before we merge this into master.

I have some modified markup for the buttons to make them into a button set; we also need to mod some SASS and the markup in plot.html to make it behave the same as the tabular view. I have the markup ready and know what we need to do in the SASS. What's the best way to integrate?

akhenry commented 8 years ago

@charlesh88 I think we should merge #1167 into master first, then @hudsonfoo can rebase over it and pull in your changes. Alternative is for @hudsonfoo to merge #1167 directly into his branch, but we'll get a cleaner changeset for review with a rebase.

akhenry commented 8 years ago

@hudsonfoo incidentally, this looks really great. Thanks!

It would be great if we could suppress the buttons in the export. If this is not achieved with the new styling, then is there some other way that we can achieve it? Perhaps by filtering out certain DOM elements via a class or something, or adding a step to hide on button click immediately prior to export?

hudsonfoo commented 8 years ago

Thanks @akhenry. I can hide the buttons during export pretty easily. As for @charlesh88's commit ebadad6, I can cherry-pick it so I'm not pulling in his entire branch if you're good with that. I've pushed with that cherry-pick so you can see what it would look like but I can revert if you don't like it.

Current list to completion in no particular order:

@VWoeltjen thanks for the code review! I face-palmed once or twice and got it cleaned up.

hudsonfoo commented 8 years ago

@charlesh88 love the show/hide effect on the buttons. Looks great: http://quick.as/n9yTynp1

akhenry commented 8 years ago

As for @charlesh88's commit ebadad6, I can cherry-pick it so I'm not pulling in his entire branch if you're good with that

I think we will also need to merge in the subsequent commits to be compatible with the generalized approach from #1167.

VWoeltjen commented 8 years ago

Note for testing: Implemented with buttons appearing at the top of a Plot view. In a Layout, these buttons should appear only on roll-over.

VWoeltjen commented 8 years ago

Verified during testathon 2016-09-19 (checked all three formats, both in Layout and with a full screen plot)

charlesh88 commented 8 years ago

Testing notes: Please try exporting in a variety of the available formats and ensure that the output is what you'd expect.

charlesh88 commented 8 years ago

Testathon 2016-09-19: verified. Checked all three formats, both in Layout and with a full screen plot, in both overlay and stacked modes.

larkin commented 8 years ago

re-verified 2016-10-21.