Sunday, June 25, 2006

Coding SVG With GWT

If you have been reading my prior posts you will know that I have a strong interest in GWT, and have been doing a lot of work with it, even in production systems. GWT is the acronym for the Google Web Toolkit. It allows a developer to write code in Java, and have it compiled to JavaScript that can run in any modern browser. For more details check out the GWT site, and check out my prior posts.

Introduction

The Scalable Vector Graphics (SVG) specification 1.1 has been a W3C Recommendation for over three years now. Firefox 1.5 introduced a built-in SVG rendering engine, and Adobe has an SVG plug-in available for Internet Explorer. This article explains how you can use the SVG support contained in GWT Widget Library o.o.5 to render SVG elements, and how you can make your page compatible with both Firefox and Internet Explorer 6.

SVGPanel Widget

The first widget you always need to create to render SVG graphics is the SVGPanel widget. When you create this widget you need to specify the width and height or the drawing area.

SVGPanel sp = new SVGPanel(500, 300);

This panel, once created, becomes a factory for other components. This works similar to the XML DOM. In this initial release you can create circles, rectangles, ellipses, and paths.

SVGRectangle rect = p.createRectangle(x, y, width, height);
SVGCircle circle = p.createCircle(cx, cy, radius);
SVGEllipse ellipse = p.createEllipse(cx, cy, rx, ry);
SVGPath path = p.createPath(x, y);

Creating a widget this was does not add it to the drawing. You still need to add the SVGPanel via the add() method.

SVGRectangle, SVGCircle, and SVGEllipse

Once created you can set additional attributes to these objects. For all of these you may set the fill color, stroke color, fill opacity, stroke opacity, and stroke width. Each of these objects will also have attributes specific to the object type. For example, you can change the radius of a circle after it's creation, and you may extend a path by adding additional points.

Each method that sets the attribute of an object will return the object itself. This allows you to chain the setting of attributes. The only trick is that some methods belong to the super class SVGBasicShape, which will return an instance of SVGBasicShape, and not the specific subclass. So you just need to be sure to set your widget specific attributes first, followed by the attributes that belong to the super class.

Below are some concrete examples, which show off the various attributes.

p.add(p.createRectangle(0, 0, 500, 300)
.setStroke(Color.DARK_GRAY)
.setStrokeWidth(2)
.setFill(Color.LIGHT_GRAY));

p.add(p.createEllipse(250, 225, 150, 70)
.setStroke(Color.RED)
.setStrokeWidth(1)
.setFill(Color.BLUE));

p.add(p.createCircle(420, 225, 60)
.setFill(Color.BLACK)
.setStrokeWidth(15)
.setStroke(Color.WHITE));

SVGPath Widget

The path widget deserves special attention. You create a path widget by specifying the initial point on the path. From this initial point you can add additional points by drawing either straight lines or curves. You may also move the current point to include multiple lines, for example the inner and outter circles of a doughnut. You may leave the path open, or close it to create a shape. The curves may be cubic Bezier, quadratic Bezier, or elliptical arcs. Each line or curve may be specified using either absolute coordinates, or coordinates relative to the current point. I suggest reading through the path portion of the SVG specification for details on using each of the curves.

The example below draws a hexagon using relative lineTo commands, coloring the shape with red translucent fill, and an orange border.

p.add(p.createPath(150, 190)
.relLineTo(10, 0)
.relLineTo(4, 8)
.relLineTo(-4, 8)
.relLineTo(-10, 0)
.relLineTo(-4, -8)
.closePath()
.setFill(Color.RED)
.setFillOpacity(50)
.setStroke(Color.ORANGE)
.setStrokeWidth(1));

Here is another path that creates an upside-down tear-drop shape, with a translucent yellow fill.

p.add(p.createPath(250, 225)
.relMoveTo(-25, -25)
.relCurveToC(0, -25, 50, -25, 50, 0)
.relLineTo(-25, 50)
.closePath()
.setFill(Color.YELLOW)
.setFillOpacity(50)
.setStroke(Color.BLACK)
.setStrokeWidth(1));

Gradients

You may also create linear gradients that can be used to fill a widget, or in place of a stroke color. Gradients consist of one or more "stops". Each stop contains a color, and optionally an opacity . For each stop you specify the color value, and the SVG engine will transition the color/opacity between stops.

The following gradient will color a widget from red to blue. The first value of each stop is the percentage across the widget. 0% being the left-most point on the widget, to 100% being the right-most point.

SVGLinearGradient grad = p.createLinearGradient()
.addStop(0, Color.RED)
.addStop(100, Color.BLUE);

You may also change the vector of the gradient to something other then left ro right. For example, you might want to color a widget from the top to the bottom. Here is the same gradient which will color from top to bottom instead of left to right. The values are percentages of the objects width and height. Specifically this vector definition starts at 0%,0%(x,y) to 0%,100%(x.y).

SVGLinearGradient grad = p.createLinearGradient()
.addStop(0, Color.RED)
.addStop(100, Color.BLUE)
.setVector(0, 0, 0, 100);

Gradients are not widgets, so you do not add them to the SVGPanel, you just use them. Also notice that each method returns the object instance allowing you to chain method calls.

Working in the Google Hosted Browser

Good luck, it doesn't seem to work. If you are able to get this to work, I would be very interested in the solution. For development I have been compiling the Java code to JavaScript, then testing in Firefox and IE.

Setting up your HTML and Deployment

One of the trickest parts I found was trying to get the SVG content to render in both Firefox 1.5 and Internet Explorer 6. If you are using Iinternet Explorer you will need to first download and install the SVG Viewer plug-in from Adobe. I also strongly suggest reading the Inline SVG page on the SVG Wiki, it will explain in detail how to get everything working across browsers.

Below is the HTML template that I have been using.

<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:svg="http://www.w3.org/2000/svg">
<head>
<title>SVG Component Example</title>
<meta name="'gwt:module'"
content="'org.gwtwidgets.examples.svg.Project'">

<object id="AdobeSVG"
classid="clsid:78156a80-c6a1-4bbf-8e6a-3cd390eeb4e2">
</object>
<?import namespace="svg" implementation="#AdobeSVG"?>

</head>
<body>
<script type="text/javascript" src="gwt.js">
</script>
</body>
</html>

Firefox requires that the page be either XML or XHTML, requiring the XHTML namespace definition. This is because Firefox uses a different parser for XML and HTML, and the XML parser is the one that understands SVG.

Internet Explorer requires the svg namespace declaration, as well as the tag and instruction. The issue is that IE6 doesn't know anything about namespaces, so these are just instructions to the Adove SVG Viewer plug-in.

When viewing the file locally, the file extension must be .xhtml to view in Firefox, and .html to view in Internet Explorer. This of course presents a problem when you try to deploy a single file to work in both browsers. The Inline SVG page on the SVG Wiki details how you can configure your web server to get this to work.

If you do not have the ability to change the server configuration you may try placing the commands in a .htaccess file, and putting this file in the same directory as the pages using SVG. This worked flawlessly on the SourceForge hosted web site.

Conclusion

I hope that this article covers most of the questions you might have about using and deploying an SVGPanel, but it is likely that there will be additional questions. Please feel free to ask questions and to comment on the API. I will do my best to respond to them.

Thursday, June 01, 2006

Trivial GWT Example

Editor's note 5/24/07: As you probably know, Adam and I just finished writing GWT in Action, a Manning title. Last week we were asked two pick two chapters that would be released for free. One of those, chapter 10, covers the GWT-RPC mechanism in some detail. Seeing that this entry is the most popular on the site, that was probably a good choice. So read the entry first, and if you still need to know more, go download chapter 10 from the GWT in Action book page. As always, feedback is welcome.

I was asked for a trivial RPC example for GWT by Plasante, and here it is.

File Structure


./org/hanson/gwt/MyApplication.gwt.xml
./org/hanson/gwt/public/MyApplication.html
./org/hanson/gwt/client/MyApplication.java
./org/hanson/gwt/client/MyService.java
./org/hanson/gwt/client/MyServiceAsync.java
./org/hanson/gwt/server/MyServiceImpl.java

At the root of the project is MyApplication.gwt.xml, which is the config file that defines any GWT project. I don't want to get into what each tag is used for, as I leave that to the GWT documentation. I do want to point out though that we defined a <servlet>, which is the server-side class for our RPC service.


<module>
<inherits name='com.google.gwt.user.User'/>
<entry-point class='org.hanson.gwt.client.MyApplication'/>
<servlet path="/myService" class="org.hanson.gwt.server.MyServiceImpl"/>
</module>

Under the public folder is the HTML file that will be used primarily for testing in "hosted" mode. This is a special mode used for testing GWT applications.


<html>
<head>
<title>Wrapper HTML for MyApplication</title>
<meta name='gwt:module' content='org.hanson.gwt.MyApplication' />
</head>
<body>
<script language="javascript" src="gwt.js"></script>
</body>
</html>

The three files in the "client" package are the files that are converted to JavaScript by GWT. This is the default place to put these files, but you can configure a different package to hold these.

The first file is our "main" application that is executed when the HTML page loads the JavaScript file, and the other two are interfaces (stubs) to the remote code.

Lets look at the interfaces first. The first one, which we called MyService is the interface that is implemented by the server-side code. This is a sample, so I created a very simple interface with only a single method.


// ./org/hanson/gwt/client/MyService.java

package org.hanson.gwt.client;

import com.google.gwt.user.client.rpc.RemoteService;

public interface MyService extends RemoteService
{
public String myMethod (String s);
}

The second interface is used for the client-side code. The name of the interface MUST be the same as the server-side interface with "Async" appended. It must implement all of the methods in the server-side interface, but all of the methods MUST also take an additional parameter, which is an AsyncCallback object.


// ./org/hanson/gwt/client/MyServiceAsync.java

package org.hanson.gwt.client;

import com.google.gwt.user.client.rpc.AsyncCallback;

public interface MyServiceAsync
{
public void myMethod(String s, AsyncCallback callback);
}

Notice that both interfaces are idential except for that additional parameter, and return type. I have highlighted that second part because I usually just copy the code from the first interface to the second, and it is easy to miss this small point.

Now, this is how you use it from the client application. First you create an AsyncCallback object by using the GWT.create() method. Note that this method takes the server-side interface as an argument, and will return an object that uses the client-side interface.

The object returned from the create() call also implements the ServiceDefTarget interface. We cast it to this interface, and set the serviceEntryPoint, which is the URL of the server-side code. We defined this location in the project configuration file with the <servler> tag.

Next we need to create an object instance that implements the AsyncCallback interface. Here we create an anonymous class for this. Calling server-side code happens asynchronously, meaning the code won't wait for a response (i.e. block). That is why we need to create this object. This object will handle the result when it makes it's way back fo the browser. We need to write code to handle both success and error conditions.

The last step is to actually call the server-side code. We do this by using the MyServiceAsync instance that we created, passing it our argument(s) as well as the AsyncCallback object to handle the result.


// ./org/hanson/gwt/client/MyApplication.java

package org.hanson.gwt.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.RootPanel;

/**
* Entry point classes define onModuleLoad().
*/
public class MyApplication implements EntryPoint
{

public void onModuleLoad ()
{
// define the service you want to call
MyServiceAsync svc = (MyServiceAsync) GWT.create(MyService.class);
ServiceDefTarget endpoint = (ServiceDefTarget) svc;
endpoint.setServiceEntryPoint("/myService");

// define a handler for what to do when the
// service returns a result
AsyncCallback callback = new AsyncCallback()
{
public void onSuccess (Object result)
{
RootPanel.get().add(new HTML(result.toString()));
}

public void onFailure (Throwable ex)
{
RootPanel.get().add(new HTML(ex.toString()));
}
};

// execute the service
svc.myMethod("Do Stuff", callback);
}
}

Here is what the server-side code looks like. It just returns a simple String. The two things that are important to note is that the code does NOT live in the "client" package. It can not live there, the "client" package is for client-side only code.

We can use these classes on the server-side though. The difference is that we are actually using the Java versions, not the JavaScript versions of these classes and interfaces. In face we MUST implement the server-side interface, but becuase this interface is references in client-side code it MUST be in the "client" package.

Confusing? I hope not. Here is the short version:

  1. Server side code can go anywhere.
  2. Client side code must be in the "client" package (or sub-package).
  3. All code in the "client" package must be able to be converted to javaScript



// ./org/hanson/gwt/server/MyServiceImpl.java

package org.hanson.gwt.server;

import org.hanson.gwt.client.MyService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class MyServiceImpl extends RemoteServiceServlet implements MyService
{
public String myMethod (String s)
{
return "You sent us '" + s + "'";
}
}

Comments welcome.