What do you think? Discuss, post comments, or ask questions at the end of this article [More about me]

VAADIN is a fantastic framework for building web applications in Java.  I've been using VAADIN for several years now and have found it largely alleviates a lot of the development burden commonly found in small development teams (read under-resourced) that perhaps are very strong on Java but still might be developing their javascript, html, CSS (or SCSS) skills.

Although VAADIN has a very good directory of web and server-side components, sometimes you might just want to implement a javascript library you prefer (or wrote yourself) into your application.  Good news is that is very possible and straight-forward... but with a few extra things we need to address when working with VAADIN specifically.

This is the situation I found myself in when I wanted to use the excellent viewer.js to allow our spacecraft operators to quickly and easily view images from our satellites and also waterfall plots of the TX/RX signals between our ground stations and satellites.  The details of this are outside the scope of this writeup, but needless to say, the operators needed to be able to quickly and easily pan, zoom and rotate a high-resolution image within the same browser they are using to access our system.  Previously, they would need to download the image, open it up in some image viewer/editor and try to manipulate and zoom etc.

Try out viewer.js by clicking the image below (you can then pan with click-drag and zoom in/out with scrollwheel or up/down keys)

Below we cover the steps needed to implement this for any (single) image in your VAADIN application.

Guide

Client-side callback function to let server know when image has been rendered

So, this is something that's VAADIN specific.  From a VAADIN application, you can execute javascript by sending it to the client with something like:

JavaScript.getCurrent().execute("alert('a message from server-side')");

However, for our implementation we need to make sure the we send and execute our javascript calls once our image is completely rendered client side.

What we're going to do is create a server-side class that will wait until an image has been rendered client-side (the client-side sending a callback) to let the server know it's okay to send the viewer.js javascript command to execute.

For this we'll need two parts: a server-side Java class, and a client-side javascript function (which will do the callback).  Let's start with the server-side. 

The below is largely based off the excellent example by M. Czerwinski found here.

WaitForImageLoadExtension.java
package com.adfaspace.jay.ui.connectors;

import java.util.ArrayList;
import java.util.List;

import com.vaadin.annotations.JavaScript;
import com.vaadin.server.AbstractJavaScriptExtension;
import com.vaadin.ui.Image;
import com.vaadin.ui.JavaScriptFunction;

import elemental.json.JsonArray;

@JavaScript({ "vaadin://connectors/wait-image-load.js" })
public class WaitForImageLoadExtension extends AbstractJavaScriptExtension {

    private List<ImageLoadedListener> imageLoadedListeners = new ArrayList<>();

    public interface ImageLoadedListener {
        void onImageLoaded();
    }

    public void extend(Image image) {
        super.extend(image);
        addFunction("onImageLoaded", new JavaScriptFunction() {

            @Override
            public void call(JsonArray arguments) {
                for (ImageLoadedListener imageLoadedListener : imageLoadedListeners) {
                    if (imageLoadedListener != null) {
                        imageLoadedListener.onImageLoaded();
                    }
                }
            }
        });
    }

    public void addImageLoadedListener(ImageLoadedListener listener) {
        imageLoadedListeners.add(listener);
    }
}

The above essentially takes a VAADIN image object and registers a ImageLoadListener which will execute it's onImageLoaded() method when the image has been rendered client side.  We'll touch on how to use this shortly.

You'll note that we're using the annotation @JavaScript({ "vaadin://connectors/wait-image-load.js" }) which points at a specific file.  The vaadin:// prefix refers to a specific location in our web application:

This file is a simple javascript function that will trigger the server-side method when/if the image has been rendered (completed) or fires a javascript alert with an error message if image fails to render.  See below

wait-image-load.js
window.com_adfaspace_jay_ui_connectors_WaitForImageLoadExtension = function() {
	var connectorId = this.getParentId();
	var img = this.getElement(connectorId);

	if (img.complete) {
		this.onImageLoaded();
	} else {
		img.addEventListener('load', this.onImageLoaded)
		img.addEventListener('error', function() {
			alert('Error: image could be loaded');
		})
	}

}

It's very important that the full class path to the WaitForImageLoadExtension class in Java is fully referenced in the first line above (albeit with the '.' replaced with '_'). So for example, the full class path for my java class is

com.adfaspace.jay.ui.connectors.WaitForImageLoadExtension

so the the javascript file must start with

window.com_adfaspace_jay_ui_connectors_WaitForImageLoadExtension = function() {...}

Using the callback function

Using our new callback function is very straightforward.  We can simply call the extend method on an image and override the onImageLoaded method, something like this

Image myImage = new Image();
// add client-side wait for load extension
WaitForImageLoadExtension ext = new WaitForImageLoadExtension();
ext.extend(image);
ext.addImageLoadedListener(new ImageLoadedListener() {
	@Override
	public void onImageLoaded() {
		System.out.println("oh, the image has been rendered... so what now?");
	}
});

Adding viewer.js to your application

With the above out of the way, we can now add the viewer.js library to our application.  There's a few ways of doing this, but since I'm going to use viewer.js on multiple images throughout the application, I chose to load the library in the main UI of the application.

For this we can simply use the @JavaScript and @StyleSheet annotations on our main UI class, something like below:

@Theme("mytheme")
@SpringUI
@Title("My Application")
@JavaScript({"https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.3.2/viewer.min.js"})
@StyleSheet({"https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.3.2/viewer.min.css"})
public class AppUI extends UI implements BroadcastListener {
	...
}

You'll note that I'm using viewer.js' CDN.  But you could equally download those files (viewer.min.js and viewer.min.css) and reference them in your application like we did previously with our wait-image-load.js file, e.g:

@Theme("mytheme")
@SpringUI
@Title("My Application")
@JavaScript({"vaadin://js/viewer.min.js"})
@StyleSheet({"vaadin://themes/viewer.min.css"})
public class AppUI extends UI implements BroadcastListener {
	...
}

Bringing it all together (almost done)

Right, with our callback method done, and viewer.js added to the application, we're now ready to tackle the final implementation.

We're going to extend com.vaadin.ui.image and automatically take care of our callback function implementation and the callback response which will create the viewer.js viewer on the image.  Enough talk, some code:

ViewerjsImage.java
package com.adfaspace.jay.ui.components;

import java.util.UUID;

import com.adfaspace.jay.ui.connectors.WaitForImageLoadExtension;
import com.adfaspace.jay.ui.connectors.WaitForImageLoadExtension.ImageLoadedListener;
import com.vaadin.ui.JavaScript;
import com.vaadin.ui.Image;

/**
 * Viewer.js enabled image.  Uses the viewer.js javascript library
 * (https://github.com/fengyuanchen/viewerjs) to extend a vaadin image.
 * Constructor creates a client-side 'wait for load' function 
 * and executes viewer.js on said image once it loads client-side.
 * 
 * @author Jay Ta'ala
 *
 */
public class ViewerjsImage extends Image {
	
	/**
	 * Default constructor.
	 * NOTE: this constructor will assign a random UUID based domId to this component.
	 * If you want to set the domId for this component use the ViewerjsImage(domId)
	 * constructor.
	 */
	public ViewerjsImage() {
		this(UUID.randomUUID().toString());
	}
	
	/**
	 * Supply domId of image object to this constructor.
	 * @param domId
	 */
	public ViewerjsImage(String domId) {
		
		setId(domId);
		addStyleName("mouse-zoom-cursor");
		
		// add client-side wait for load extension
		WaitForImageLoadExtension ext = new WaitForImageLoadExtension();
		ext.extend(this);
		ext.addImageLoadedListener(new ImageLoadedListener() {

		    @Override
		    public void onImageLoaded() {
		    	
		    	String js = 
		    			"const viewer = new Viewer(document.getElementById('" + domId + "'), {" + 
		    			"  zIndex: 20000," +
		    			"  navbar: false," +
		    			"  toolbar: {" + 
		    			"    zoomIn: 1," + 
		    			"    zoomOut: 1," +
		    			"    oneToOne: 1," + 
		    			"    reset: 1," + 
		    			"    prev: 0," + 
		    			"    play: 0," +
		    			"    next: 0," + 
		    			"    rotateLeft: 1," + 
		    			"    rotateRight: 1," + 
		    			"    flipHorizontal: 1," + 
		    			"    flipVertical: 1," + 
		    			"  }," +
		    			"  viewed() {" + 
		    			"  }" + 
		    			"});";
		    	
		    	JavaScript.getCurrent().execute(js);
		    }
		});
	}
}

The above class takes care of everything needed, including creating an image object, extending that object with our callback function, and then finally (once the image is rendered) will take care of executing viewer.js when a user clicks on the image.

A few things to note in the class above, the String js defines all the options that we will be using for our viewer.js implementation (including hiding some unneeded buttons etc).  You can change these according do the viewer.js documentation.  The viewed() javascript function is simply a shortcut of the viewed event, so you can add other methods() to call in there if needed/wanted (otherwise leave as is).

Finally, you might have noticed that I'm adding the CSS style mouse-zoom-cursor to the image.  This simply changes the mouse cursor to a magnifying glass when a user hovers the cursor over the image (on browsers that support it) and is defined by

.mouse-zoom-cursor {
	cursor: -moz-zoom-in;
    cursor: -webkit-zoom-in;
	cursor: zoom-in;
}

Done!

We now have fully implemented viewer.js into our application!  To use this, we simply need to do something like the usual

ViewerjsImage image = new ViewerjsImage();

and do what we normally do with Vaadin image objects (i.e. adding a StreamResource as usual as outlined on Vaadin's Image and Other Resources page). 

But now, when a user clicks a ViewjsImage image in our application, it will pop up and allow users to pan, rotate, zoom, mirror, invert, etc.

References

  1. https://stackoverflow.com/questions/43613872/vaadin-run-client-side-javascript-after-image-fully-loaded
  2. https://vaadin.com/docs/v8/framework/application/application-resources.html