weltermann17

it's just a blog.

jit + gwt: a low-cost solution July 2, 2009

Filed under: jit with gwt — weltermann17 @ 21:37
Tags: ,

jit (JavaScript InfoVis Toolkit) and gwt (Google Web Toolkit) are both powerful and elegant libraries to create browser based user interfaces. jit offers a number of really cool graph visualizations that can be easily customized with Javascript and CSS. gwt is a dream for Java and J2EE developers facing the RIA challange come true. So why not combine both?

According to the jit discussion group there is not yet a plug-and-play solution for this. A development of a gwt extension gwt-graph has just started. This looks very promising as it aims at fully wrapping the functionality of jit in gwt.

Because I needed to find a way to embed one or more Spacetrees into a gwt ui quickly in the last couple of days I came up with a very low-cost solution for jit + gwt that I would like to outline here.

Main goals for my solution were:

  • write as little code as possbile
  • only wrap the jit objects where necessary to treat them like gwt objects where possible
  • make the jit objects act like gwt widgets so they can be dropped anywhere in a gwt ui
  • make it possible to drop more than one jit visualization into a gwt ui
  • let the jit objects react on resize automatically
  • let the jit objects easily interact with the gwt event system
  • concentrate on the jit Spacetree, but make it easy to implement the others as well

These few classes implement the solution:

  • interface ClickLabelHandler extends EventHandler (onClickLabel/onRightClickLabel)
  • class ClickLabelEvent extends DomEvent<ClickLabelHandler>
  • interface HasClickLabelHandlers extends HasHandlers
  • abstract class JitWidget extends Widget implements ResizeHandler, HasClickLabelHandlers
  • class SpaceTree extends JitWidget

You find the code for the classes further down, but let’s first look at how to use them:

    public void onModuleLoad() {
	Window.addResizeHandler(this);
	Window.enableScrolling(false);
	Window.setMargin("0px");
	History.addValueChangeHandler(this);
        // ... layout your gwt ui

        scrollPanel = new ScrollPanel();
	scrollPanel.setSize("100%", "100%");
	int w = scrollPanel.getOffsetWidth();
	int h = scrollPanel.getOffsetHeight();
	simplePanel = new SimplePanel();
	simplePanel.setPixelSize((int) (w * 1.33), (int) (h * 1.33));
	scrollPanel.add(simplePanel);
	scrollPanel.setPixelSize(w, h);

        // simplePanel will be the container for our SpaceTree, it will fill it up 100%
	spacetree = new SpaceTree("myspacetree", config("myspacetree"));
	spacetree.addClickLabelHandler(this);
	spacetree.addMouseOverLabelHandler(this);
	spacetree.addMouseOutLabelHandler(this);
	simplePanel.add(spacetree);
	DeferredCommand.addCommand(new Command() {
	    public void execute() {
		loadAndDisplay("myspacetree", dataset());
	    }
	});
    }

    private native JavaScriptObject config(String name) /*-{
        return {
        orientation: 'left',
        duration: 333,
        fps: 25,
        levelDistance: 32,
        levelsToShow: 5,
        Node: {
            width: 160,
            height: 20,
            type: 'rectangle',
            color: '#7aa',
            overridable: true },
        Edge: {
            type: 'bezier',
            overridable: true },
        onCreateLabel: function(label, node) {
            label.id = node.id;
            if (0 < node.data.childcount && !node.selected) {
            	label.innerHTML = node.name + "  " + node.data.childcount;
            	node.data.$color = "#aa7";
            } else {
            	label.innerHTML = node.name;
            	delete node.data.$color;
            }
            var forwardEvent = function(event) {
                jitWrapperForwardEvent(name, event || $wnd.event, label, node); };
            label.onclick = function(event) {
                jitWrappedObject(name).onClick(node.id);
                forwardEvent(event); };
            label.oncontextmenu = forwardEvent;
            label.onmouseover = forwardEvent;
            label.onmouseout = forwardEvent;
            var style = label.style;
            style.display = 'block';
            style.width = '160px';
            style.height ='20px';
            style.cursor = 'pointer';
            style.color = '#222';
            style.fontSize = '0.7em';
            style.textAlign= 'left';
            style.paddingTop = '2px';
            style.paddingLeft = '2px';
        },
        onBeforePlotNode: function(node){
            if (node.selected) {
                node.data.$color = "#ff7";
            } else {
                delete node.data.$color;
            }
        },
        onBeforePlotLine: function(adj){
            if (adj.nodeFrom.selected && adj.nodeTo.selected) {
                adj.data.$color = "#44d";
                adj.data.$lineWidth = 1.2;
            } else {
                delete adj.data.$color;
                delete adj.data.$lineWidth;
            }
        }
        }
    }-*/;

    private native void loadAndDisplay(String name, JavaScriptObject json)  /*-{
        var jit = jitWrappedObject(name);
        jit.loadJSON(json);
        jit.compute();
        jit.onClick(jit.root);
        jitWrapperOnClick(name, null, json);
    }-*/;

    @Override
    public void onClickLabel(ClickLabelEvent event) {
	JitWidget jit = (JitWidget) event.getSource();
	dump("node", jit.getClickedNode());
    }

    @Override
    public void onRightClickLabel(ClickLabelEvent event) {
	JitWidget jit = (JitWidget) event.getSource();
	dump("label", jit.getClickedLabel());
    }

    @Override
    public void onMouseOverLabel(MouseOverLabelEvent event) {
	JitWidget jit = (JitWidget) event.getSource();
	JSONObject json = new JSONObject(jit.getClickedNode());
	PopupPanel popup= new DecoratedPopupPanel();
	StringBuilder b = new StringBuilder();
	for (String k : json.keySet()) {
	    b.append(k);
	    b.append(" : ");
	    b.append(json.get(k).isString());
	    b.append("");
	}
	popup.setWidth("200px");
	popup.setWidget(new HTML(b.toString()));
	int x = event.getNativeEvent().getClientX();
	int y = event.getNativeEvent().getClientY();
	tooltip = new ToolTip(2000, popup, x, y);
    }

    @Override
    public void onMouseOutLabel(MouseOutLabelEvent event) {
	tooltip.cancel();
    }

    @Override
    public void onValueChange(ValueChangeEvent<String> event) {
	String token = event.getValue();
	String name = token.substring(0, token.indexOf(";"));
	String nodeid = token.substring(token.indexOf(";") + 1);
	clickNode(name, nodeid);
    };

    private native void clickNode(String name, String nodeid) /*-{
        var jit = jitWrappedObject(name);
        jit.onClick(nodeid);
        jit.controller.onAfterCompute();
    }-*/;

Some comments:

  • a SpaceTree is simply constructed with new SpaceTree(<name>, <method returning the config as JavaScriptObject>)
  • <name> is very important as it is the reference in JavaScript to retrieve either the jit object or the gwt wrapper of type JitWidget
  • <config method> will almost always be a native JSNI method that will look very familiar to jit users
  • <onCreateLabel> (as always) is the key controller function: label.onclick and label.oncontextmenu must be implemented in order to provide a link back from the interactive jit visualization into the gwt event system
  • jitWrappedObject(name) returns the jit visualization created with new SpaceTree() (in this case an ST); it can then be used to call onClick(), for instance
  • jitWrapperOnClick(name, label, node) and jitWrapperOnRightClick(…) propagate the event “label was clicked” back into gwt’s event system which calls all ClickLabelHandlers registered with this JitWidget. The new gwt event system (1.6) is really neat.
  • JitWiget provides the methods getClickedNode and getClickedLabel to get at their content in the event handlers. This makes it easy to update other ui objects according to the clicked node or create a very specific context menu based on the current node and/or label.
  • Almost everything else you do with a JitWiget you need to write in JavaScript (e. g. loadAndDisplay()). The trick is to retrieve the jit object first using jitWrappedObject(name) and then use it the way you want. This way it is not necessary to wrap the entire functionality of jit with gwt and Java. It should also make this solution robust against enhancements in either jit or gwt.

Here is the code for all classes:

// ----------------------------------------------------------------------

package yourpackage;

import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.event.shared.HasHandlers;

public interface HasClickLabelHandlers extends HasHandlers {

    HandlerRegistration addClickLabelHandler(ClickLabelHandler handler);

}

// ----------------------------------------------------------------------

import com.google.gwt.event.shared.GwtEvent;

public class ClickLabelEvent extends GwtEvent<ClickLabelHandler> {

    protected ClickLabelEvent(boolean rightclick) {
	this.rightclick = rightclick;
    }

    public static Type getType() {
	return TYPE;
    }

    public static void fire(HasClickLabelHandlers source, boolean rightclick) {
	ClickLabelEvent event = new ClickLabelEvent(rightclick);
	source.fireEvent(event);
    }

    @Override
    protected void dispatch(ClickLabelHandler handler) {
	if (rightclick) {
	    handler.onRightClickLabel(this);
	} else {
	    handler.onClickLabel(this);
	}
    }

    @Override
    public final Type getAssociatedType() {
	return TYPE;
    }

    private final boolean rightclick;
    private static Type<ClickLabelHandler> TYPE = new Type<ClickLabelHandler>();

}

// ----------------------------------------------------------------------

import java.util.LinkedHashMap;
import java.util.Map;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Widget;

public abstract class JitWidget extends Widget implements ResizeHandler, HasClickLabelHandlers,
	HasMouseOverLabelHandlers, HasMouseOutLabelHandlers {

    public JitWidget(String name, JavaScriptObject config) {
	this.name = name;
	this.config = config;
	final Element domelement = DOM.createElement("div");
	DOM.setElementProperty(domelement, "id", name);
	setElement(domelement);
	backgroundColor = 0 < DOM.getStyleAttribute(domelement, "backgroundColor").length() ? DOM
		.getStyleAttribute(getElement(), "backgroundColor") : defaultBackgroundColor;
	DOM.setStyleAttribute(domelement, "backgroundColor", backgroundColor);
	jitgwtwrappers.put(name, this);
    }

    public String getName() {
	return name;
    }

    public JavaScriptObject getClickedNode() {
	return node;
    }

    public JavaScriptObject getClickedLabel() {
	return label;
    }

    @Override
    protected void onAttach() {
	setSize("100%", "100%");
	JavaScriptObject canvas = initCanvas(name, getOffsetWidth(), getOffsetHeight(), backgroundColor);
	JavaScriptObject jit = init(canvas, config);
	jitwrappedobjects.put(name, jit);
	super.onAttach();
    }

    @Override
    public void onResize(ResizeEvent event) {
	double width = event.getWidth();
	double height = event.getHeight();
	double percentagex = width / oldWidth;
	double percentagey = height / oldHeight;
	double w = getOffsetWidth() * percentagex;
	double h = getOffsetHeight() * percentagey;
	setPixelSize((int) w, (int) h);
	resize(name, w, h);
    }

    protected abstract JavaScriptObject init(JavaScriptObject canvas, JavaScriptObject config);

    private final native JavaScriptObject initCanvas(String name, double w, double h, String bcolor) /*-{
        return new $wnd.Canvas(name + '_canvas', {
           'injectInto': name,
           'width': w,
           'height': h,
           'backgroundColor': bcolor});
    }-*/;

    private final native void resize(String name, double w, double h) /*-{
        var jit = jitWrappedObject(name);
        jit.canvas.resize(w, h);
        jit.refresh();
        jit.controller.onAfterCompute();
    }-*/;

    public final static void resizeAll(ResizeEvent event) {
	for (JitWidget jit : jitgwtwrappers.values()) {
	    jit.onResize(event);
	}
	oldWidth = event.getWidth();
	oldHeight = event.getHeight();
    }

    @Override
    public HandlerRegistration addClickLabelHandler(ClickLabelHandler handler) {
	return addHandler(handler, ClickLabelEvent.getType());
    }

    @Override
    public HandlerRegistration addMouseOverLabelHandler(MouseOverLabelHandler handler) {
	return addHandler(handler, MouseOverLabelEvent.getType());
    }

    @Override
    public HandlerRegistration addMouseOutLabelHandler(MouseOutLabelHandler handler) {
	return addHandler(handler, MouseOutLabelEvent.getType());
    }

    private final void setClickedNode(JavaScriptObject node) {
	this.node = node;
    }

    private final void setClickedLabel(JavaScriptObject label) {
	this.label = label;
    }

    @SuppressWarnings("unused")
    private final static JavaScriptObject getWrappedObject(String name) {
	return jitwrappedobjects.get(name);
    }

    @SuppressWarnings("unused")
    private final static void forwardEvent(String name, JavaScriptObject event, JavaScriptObject label,
	    JavaScriptObject node) {
	NativeEvent received = (NativeEvent) (null == event ? null : event.cast());
	Document document = Document.get();
	JitWidget jit = jitgwtwrappers.get(name);
	jit.setClickedLabel(label);
	jit.setClickedNode(node);
	String type = null == received ? "click" : received.getType();
	if ("click".equals(type)) {
	    JSONObject nodeobject = new JSONObject(node);
	    String nodeid = nodeobject.get("id").toString();
	    nodeid = nodeid.substring(1, nodeid.length() - 1);
	    History.newItem(name + ";" + nodeid);
	    ClickLabelEvent.fire(jit, false);
	} else if ("contextmenu".equals(type)) {
	    ClickLabelEvent.fire(jit, true);
	} else if ("mouseover".equals(type)) {
	    MouseOverLabelEvent.fire(jit, received);
	} else if ("mouseout".equals(type)) {
	    MouseOutLabelEvent.fire(jit, received);
	} else {
	    GWT.log("not handled: " + type, null);
	}
    }

    private final static native void exportToJavaScript() /*-{
        jitWrappedObject = @yourpackage.JitWidget::getWrappedObject(Ljava/lang/String;);
        jitWrapperForwardEvent = @yourpackage.JitWidget::forwardEvent(Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;);
    }-*/;

    static {
	exportToJavaScript();
	oldWidth = Window.getClientWidth();
	oldHeight = Window.getClientHeight();
    }

    private final String name;
    private final JavaScriptObject config;
    private final String backgroundColor;
    private JavaScriptObject node;
    private JavaScriptObject label;
    private static final String defaultBackgroundColor = "#fff";
    private static final Map jitwrappedobjects = new LinkedHashMap();
    private static final Map jitgwtwrappers = new LinkedHashMap();
    private static double oldWidth;
    private static double oldHeight;

}

// ----------------------------------------------------------------------

import com.google.gwt.core.client.JavaScriptObject;

public class SpaceTree extends JitWidget {

    public SpaceTree(String name, JavaScriptObject config) {
	super(name, config);
    }

    @Override
    protected native JavaScriptObject init(JavaScriptObject canvas, JavaScriptObject config) /*-{
        return new $wnd.ST(canvas, config);
    }-*/;

}

In every ui library I know automatic resizing is a pain. With jit+gwt there is no exception. As this is not yet working 100% I will describe the resize mechanism in a follow-up post.

Advertisement
 

One Response to “jit + gwt: a low-cost solution”

  1. [...] July 7, 2009 Filed under: jit with gwt — weltermann17 @ 19:52 I said in my recent post jit + gwt: a low-cost solution that the new event system of gwt 1.6 is “neat”. Well, I wasn’t neat enough for [...]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

 
Follow

Get every new post delivered to your Inbox.