Google has quietly put all (most?) of the GWT Conference session videos online now.
Here is my talk on GWT Deferred Binding:
Saturday, December 29, 2007
GWT Conference videos now online
Posted by Timepedia at 11:26 AM 0 comments
Thursday, December 13, 2007
Scala-style Actors in GWT: Architectural details
In the last article, I talked about the targeted API for actors, I'll talk a little about the architecture I am prototyping with.
Store-and-forward messaging
The essential idea for implementing communication between actors is to give them a globally unique address (UUID) first, which is called the ActorEndpoint class. Think of a UUID as the "email address" of an Actor, with Actors themselves being the email client.
Because of limitations in the browser, we can't really use any information about the machine itself in creating these UUIDs, so instead, we create pseudo-version-4 random UUIDs
The Actors.register() registers Actor instances in an internal HashMap so that Actors can be looked up by UUID/Endpoint later.
The only essential difference between a local in-process Actor, and a remote out-of-process Actor, is that the endpoint is a RemoteEndpoint which returns an associated (do nothing) RemoteActor.
So the best way to think of the delivery/receive process is by analog to plain-ole SMTP email queues, except that the "SMTP" server in this case, can only wait for a client to connect, it can't initiate any connections.
Delivery: Local vs Remote
When you invoke Actor.sendTo(anotherActor, msg), it delegates to Actors.addToMailbox(sender, receiver, msg). This code essentially performs a check: if(receiver.getEndpoint() instanceof RemoteEndpoint) it enqueues the message in an envelope addressed to the remote mailbox associated with the UUID of the remote actor.
If the receiver is not a remote actor, and is in the 'started' state, it invokes a method Actor.localReceive() which can process the event immediately. localReceive() delegates to a Generator method (client) or Reflection method (Server) to perform the multiple dispatch.
Transporting the Messages
For outgoing messages, a periodic timer task checks for non-empty outgoing queue, and invokes a GWT RPC call which transmits all enqueued Envelopes, and receives as a result, all messages that had been sitting on the server destined for a local actor in this browser. It then opens the Envelopes received, and uses the UUID of the recipient to lookup the appropriate Actor for local delivery.
For incoming messages, there are two things that can be done: 1) we can poll with RPC for messages. 2) we can use one of the numerous Comet techniques to receive push messages. I'll probably support both in the final library.
The mailbox servlet
All of the communication is orchestrated by a GWT RemoteService that maintains for each UUID, a queue of Envelopes waiting to be delivered. It's currently very simple, transient, and not fail-safe. There is no security for the registrar for globally named actors (e.g. "chatLobby") and no Envelope sequence number to prevent out of order or duplicates, which is sure to happen with Comet + RPC combined channels.
Other cool stuff
Someone has made a JXTA/Scala Actor gateway, which allows messaging across the P2P JXTA network. This could certainly be adapted to GWT Actors. One could also imagine interacting with a JMS Message Queue/EJB MessageBeans, XMPP/Jabber, or Android clients as well. I am trying to take care in making this library 'cloud computing safe' like Chronoscope, so that one day, mobile phone users will be able to play interactive p2p games against Web desktop users. :)
Posted by Timepedia at 11:42 AM 2 comments
Tuesday, December 11, 2007
Playing with feedburner options
I hope this doesn't annoy too many people, or break some subscriber's feeds, but I've been toying around with Feedburner (first time) tonight. Let me know if your feed broke.
-Ray
Posted by Timepedia at 11:57 PM 0 comments
Scala-style Actors in GWT
Another day, another GWT extension.
Recently, on the Chronoscope mailing list, some users have been asking for the ability to stream in updates to the chart, a typical use case being streaming live stock market data. There are issues in Chronoscope blocking this at the moment that require some extensions, but the greater question of how to do live updates of a GWT application got me thinking about how to appropriately model this in GWT.
RPC? Comet? JSON?
So, ordinarily, in the interest of time, I'd take the road most traveled and simply use GWT RPC, some JSNI Comet integration, or some other simple mechanism to receive poll or push data. However, over the last few months, I've really fallen in love with Scala, and I like Scala's abstraction for message passing: Actors.
I like actors for several reasons. First, they are asynchronous by default, hide the underlying implementation (threads, event queues, in-process calls), and well... I just like em because they're cool.
Actors in Java
Unfortunately, Java doesn't support the elegant syntax of Scala, but with a Generator to implement multiple dispatch, it's not too bad. The approach I've taken is to use overloaded receive methods to implement pattern-matching case classes:
public class Pinger extends Actor {
public void receive(Pong p) {
// pong case
sendTo(sender, new Ping());
}
public void receive(Stop s) {
GWT.log("Received a stop message!");
}
}
public class Ponger extends Actor {
int count = 0;
public void receive(Ping p) {
if(count++ < 100) sendTo(sender, new Pong());
else sendTo(sender, new Stop());
}
}
A GWT Generator supplies a compile time dispatch() method which can take dequeued mailbox messages and invoke the appropriate receive() method. Java doesn't have case classes, so you can use Enums or Type-Safe Enum Pattern as messages (as long as they implement org.timepedia.actors.client.Event)
Big deal, Whoop-de-do
What does all this gain us? Well, for one, it abstracts away the RPC mechanism on the client and server. You don't need to deal with RPC interfaces, or AsyncCallback, in fact, RPC can be replaced with Comet for the receive channel if you so desire.
However, the big whoop-de-do is the ability to do peer-to-peer messaging between browsers. Chat? Multiplayer gaming anyone?
Remote Actors
In order to extend actors outside the Javascript environment of your browser, we need to use a server side mailbox server to queue and relay messages between actors in different browsers. In order to publish your actor so that remote actors can retrieve it, all you need to write is:
RemoteActors.register("myId", actor);
and in another peer, you can write:
RemoteActors.select("myId");
public .... extends Actor {
public void receive(ActorEndpoint endpoint) { ... }
}
Wait!? There's no return value? Remote selection is asynchronous (needs to ask mailbox server) so we simply return the result of the select() query as a message using the actor interface!
The returned endpoint represents either a local (in browser), server, or browser peer actor (on someone else's computer).
Chat using Actors
So let's say we want to implement a GTalk chat-like program in GWT, with buddy lists, group chat, and private messages. How would we do this with actors?
public class ChatActor extends Actor {
public void onStart() {
super.onStart();
RemoteActors.select("chatLobby", this);
}
public void receive(ActorEndpoint rae) {
chatLobby = rae.getActor();
sendTo(chatLobby, new JoinLobbyMessage());
}
public void receive(ChatMessage p) {
html.setHTML(html.getHTML()+"
"+p.getMessage());
}
public void onChange(Widget sender) {
sendTo(chatLobby, new ChatMessage(((TextBox)sender).getText()));
}
}
The above code assumes we are using an HTML Widget to store the chat transcript, and a TextBox to handle user input. We start out by selecting the "chatLobby" actor from the server. We don't know where this Actor really resides, it could be someone's PC, but for my test implementation, it resides in a Servlet.
When someone enters text, we send a "ChatMessage" object to the "chatLobby" actor containing the text. What is chatLobby?
public FooServlet extends HttpServlet ... {
public void init() {
ChatLobbyActor lobbyActor = new ChatLobbyActor();
RemoteActors.register("chatLobby", lobbyActor);
}
}
public class ChatLobbyActor extends ReflectionActor {
HashSet<Actor> actors=new HashSet<Actor>;
public void receive(JoinLobbyMessage msg) {
actors.add(sender);
sendTo(sender, new ChatMessage("Welcome to Chat Lobby"));
}
public void receive(ChatMessage msg) {
for(Actor act : actors)
sendTo(act, msg);
}
}
Implementation details
The library isn't ready for release right now, but the mailbox routing servlet uses transient memory arrays to hold per-actor mailboxes (with UUIDs to identify them uniquely). This could be released with transient or persistent JMS queues in some implementations. For communication channels, it uses GWT RPC at the moment. It polls the server periodically for messages, and during the send() operation, it receives all pending messages and sends all pending messages to minimize the number of RPC requests. However, I would like to have a Comet implementation before release that gives you the option of receiving messages pushed on a comet channel.
Holy Grail
I have an itch, a real bad, unproductive itch, to implement a multiplayer graphics game in GWT using Chronoscope's Canvas, Fred Sauer's GWT-Sound library, and this GWT Actors library. And I'm not talking turn based action, but real, predictive physics simulation, such as a 2D space spacewar/omega-race style shooter.
But if I start messing around with this, I'll never finish this blog series, nor fix the Chronoscope bugs, nor launch Timepedia. Argh!
In an upcoming article, I'll talk more about the implementation under the hood, and hopefully release the code.
-Ray
Posted by Timepedia at 4:08 PM 6 comments
Monday, December 10, 2007
Deferred Binding slides online
My GWT Conference presentation on Deferred Binding was a little rushed due to time, so here are the slides for those who wish to review the material:Deferred Binding@Google Docs.
I'll post up a PDF version later.
-Ray
Posted by Timepedia at 2:37 PM 2 comments
Sunday, December 9, 2007
Is Volta's Javascript Interop better than GWT?
In this ZDNet Interview with Volta architect Eric Meijer, Eric says:
"The GWT uses Java native methods to interface to JavaScript where the JavaScript implementation of that native method is defined in a special pragma comment.
In many situations, the compiler can automatically infer the JavaScript implementation from the metadata for the corresponding function declaration in C# (or VB). The Volta toolkit therefore implements a sophisticated convention over configuration heuristic to simplify writing foreign function interfaces. As a result this typically enables programmers to import JavaScript functionality by just writing a single [Import] attribute on an extern method signature."
This sounds like Volta's Javascript interop is better than GWT, but is it?
Deferred Binding again?
I must sound like a broken record by now, but GWT's Deferred Binding allows a GWT developer to accomplish anything Volta's interop does, and more. It's a general purpose mechanism to achieve compile time metaprogramming, where Javascript Interop is just the tip of the iceberg. If you want Volta-style no-JSNI interop, here's how to get it:
Bob Vawter's No JSNI Interop
While it is not a part of GWT's core, Bob Vawter, a member of the GWT Team, released a package for no-JSNI interop 'convention over configuration' several months ago.
How does it work? Just make an interface where the method names correspond to actual Javascript object methods and extend JSWrapper, for example, with the GMap2 API:
interface GLatLng extends JSWrapper {
/**
* The naming of the method is arbitrary, the only thing that's important is
* the presence of the gwt.constructor annotation.
*
* @gwt.constructor $wnd.GLatLng
*/
public GLatLng construct(double lat, double lng);
public double lat();
public double lng();
}
The constructor annotation tells the generator how to map the construct() call to the Javascript constructor. JSIO supports tons of additional interoperability features that I'm not sure Volta is capable of.
My GWT Exporter library
In addition to being able to import Javascript classes/functions/fields, one also wants to export Java functions to Javascript in a way that makes them callable from non-Java code. Volta can do this for static functions only it appears via the [Export] declaration.
What if you need to export methods, fields, and interfaces, and have polymorphic method dispatch on exported class instances still work? GWT Exporter makes this possible. In fact, one of the coolest features of GWT Exporter that I like is automatic Javascript closure conversion.
Imagine you have the following Java code:
/**
* @gwt.export
*/
public static void foo(FooCallback callback) {
callback.doIt();
}
/**
* @gwt.exportClosure
*/
public interface FooCallback {
void doIt();
}
Then you can invoke the foo() method with an ordinary Javascript closure:
foo(function() { alert("Hello World") })
And that closure will automatically be converted to an instance of the FooCallback interface and mapped to the doIt() function. Neat huh? I'm not saying Volta can't do this, I'm just not sure because the docs don't mention it.
The sky's the limit
If there's a feature missing from Bob's library, or mine, you don't need to fret and think about hacking the compiler, because GWT's Deferred Binding Generator mechanism allows anyone to extend the system to support whatever annotation and code-injection techniques one desires.
Future versions of GWT are likely to obsolete Bob's library and mine, by simply unifying the JS and Java ASTs in a way that a few extra annotations will allow the compiler to 'understand' a Javascript library and automagically map Java's types on top of it. You'll get even more optimal code then, as GWT will even be able to optimize third party JS libraries.
The title of this article is somewhat flamebait, but I wouldn't want people reading the ZDNet interview to come away assuming that GWT can't handle the kind of Javascript interop that Volta can.
-Ray
Posted by Timepedia at 11:15 PM 2 comments
Friday, December 7, 2007
Cloud Computing and GWT
(At the GWT Conference conclusion, someone asked about authoring apps that run on both GWT and Android, I promised I'd detail my findings here...)
One of the purported benefits of Microsoft Volta is "cloud computing" which is the ability to move code execution to different tiers in your application. Now, cloud computing is not really new per se, since mobile code platforms have been able to achieve this for some time with careful architecture, but Microsoft is presenting a vision of doing it painlessly and (relatively) automatically.
I'm of the opinion that it's probably achievable to do it in an automated fashion, but that you are going to hurt user experience. That's because the raw execution performance and I/O bandwidth of the client and server differ dramatically, and simply moving some business logic and data handling code from server to client without thinking about it is going to lead to plenty of pathologically bad cases. It's a case of trying too hard to plug holes in a leaky abstraction.
Choose an abstraction that leaks least
That said, if you are careful, you can do Cloud Computing with GWT and achieve code reuse without suffering too many performance gotchas or pathologically bad cases, but the design work has to be done up front, as it becomes alot harder later to unshackle dependencies and abstraction leaks from you code.
I don't profess to be an expert on this, but here is my experience from porting Chronoscope to several "clouds" (Mobile, JS, Flash, Java2D).
Know your clouds
First, do some up front legwork before you start coding. Hand code a prototype for each cloud to get an intuitive sense of each platform's performance and API support. Then choose a reasonable lowest-common-denominator in terms of API support and performance.
In the case of Chronoscope, I had written JS, Flash, and Java2d prototypes before I wrote the first line of GWT code.
Scaling APIs
Now that you know what your target minimum requirements are, you have two tasks. First, pick an abstraction that represents what's achievable on each of the platforms you're targeting to make sure enough API support is there to either support a feature directly, or emulate it with acceptable performance.
You may have to do several iterations of this to get it right. For example, with Chronoscope, I started out with the Safari/WHATWG Javascript canvas as my abstraction, but I needed text rendering (rotated as well), hit detection, Flash support, and the ability to not have to redraw unchanged portions of the screen.
I started by adding horizontal text rendering by placing DIVs over the Canvas, which of course has several problems (that I addressed later). I then noticed that to make Flash performant I need to ship a whole frame's worth of drawing commands to Flash in a batch. Moreover, Opera's Canvas incrementally updates the display while you're drawing, but they fixed this by adding a non-standard lockCanvasUpdates() function. This led to the addition of OpenGL-style DisplayList capability and a beginFrame()/endFrame() pair of methods.
I initially tried to emulate damage region painting everywhere, but it was too slow. It turns out creating lots of CANVAS elements or MovieClip objects in Flash is not that bad, so I introduced the concept of Layers (ala Photoshop), with a canvas.createLayer() call. The layer system also facilitates adding hit detection in a way which doesn't require alot of scenegraph-style overhead. I'm now confident that this is the right abstraction not only to support slow cloud platforms, but to leverage natively accelerated features of the various platforms.
Decoupling from GWT
Usage of any com.google.gwt.* classes is going to tie your code to running in the GWT cloud. Now, gwt-user is great and we want to use it, but do so in a way that allows it to be swapped out on other platforms.
For Chronoscope, I use the following techniques:
- Stick to JRE Emulation classes as much as possible
- GWT core classes can be reasonably abstracted (Timer, Network requests, etc)
- Abstract away GWT Widgets where possible (MyMenuItem interface vs Menuitem)
- Isolate all JSNI code into a browser specific impl package
- Use a platform specific Factory/Toolkit to create implementation instances of various abstractions (in Chronoscope, this is called the View class)
Package layout
Here's the package layout I use:
- org.timepedia.chronoscope.client - all 'cloud safe' code goes here and in subpackages
- org.timepedia.chronoscope.browser - any class performing any operation that transitively requires gwt-user or JSNI goes here
- org.timepedia.chronoscope.java2d - Java2D cloud specific Canvas implementation
- org.timepedia.chronoscope.client.flash - Flash specific cloud code here
- org.timepedia.chronoscope.android - Android specific View/Canvas stuff here
org.timepedia.chronoscope.server - Servlet Chart Server stuff
Example Abstraction: Timers
Chronoscope does a lot of interpolated animations, and to do so, it needs the use of a timer. Usage of the GWT Timer class would not allow the code to run as an Applet or compile for Android, so instead, this is how timing is done in Chronoscope:
package org.timepedia.chronoscope.client.util;
/**
* Abstraction for running scheduled tasks, independent of JRE environment
*/
public interface PortableTimer {
public void cancelTimer();
public void schedule(int delayMillis);
public void scheduleRepeating(int periodMillis);
double getTime();
}
And of couse, the BrowserView factory class which returns instances that work in GWT:
/**
* Creates a PortableTimer based on GWT's Timer class.
*
* @param run
* @return
*/
public PortableTimer createTimer(final PortableTimerTask run) {
return new BrowserTimer() {
public void run() {
run.run(this);
}
public void cancelTimer() {
cancel();
}
public double getTime() {
return new Date().getTime();
}
};
}
I have created similar abstractions for menus, toolbars, and other things that Timepedia needs, and as I discover new widgets that are needed, I add 'cloud enabled' versions as I go along.
UI Widgets tend to have analogs on every platform that function slightly differently, but it's not hard to seek out minimalist lowest common denominator behavior.
Sometimes this approach fails
So far, I have discussed design techniques to make recompiling for the cloud relatively automatic and painless, but for reasons I cited in the beginning, as well as other fundamental differences, you cannot expect something to be write-once work everywhere. Here's some differences that will bit you:
- JDK1.5 vs JDK1.4 language features (mostly fixed by GWT 1.5)
- JRE Emul collections vs CLDC/J2ME
- Pointing device/events. Not every platform will have gamepad keys, support double-click, single click, drag, etc. The iPhone being the biggest example.
- Network security policies (no access, extremely slow access, same domain access, full access) Sometimes remedied by proxies, but usually a pain in the ass no matter what.
When abstraction fails...
Maintain two branches of your codebase. I do this for J2ME due to CLDC <-> JRE Emul conflicts. I also do it for UI event handling, as each application has to be tailored for the screen format and keyboard/input device of each platform. Years of mobile industry development have taught me the painful fallacy of trying to devise a general purpose app that is not device specific. Want the best user experience? You have to tailor for form-factor and input device.
A preprocessor can sometimes come to the rescue (Foo/*<Bar>*/ -> Foo<Bar> on 1.5+ platforms, Retroweaver can help smooth over 1.5->1.4 collection issues, and there are hacks/tools to inject 1.1 style collection interfaces into J2ME platforms that lack them, such as JDiet)
Future Directions
Both GWT and Android are moving towards compile time declarative UIs, it may be possible in the near future to create a unified declarative UI syntax that allows code generation for both GWT widget layouts and Android.
That's about all I can think of to say on the subject right now. GWT permits Cloud Computing, it was one of the reasons I chose it, and if you're interested in building components that can live within GWT and Android, it is certainly possible.
-Ray
p.s. some might technically quibble and say it's not true cloud computing because all of the tiers I consider are client-side platforms, but Chronoscope does run on the server too, and I can generate old Web 1.0 style image-map interfaces for navigation (JFreeChart can do this as well)
Posted by Timepedia at 3:47 PM 2 comments
Thursday, December 6, 2007
Editorial: Proof of why GWT Deferred Binding rocks
Soap Box On:
So, I admit it. I'm biased. I gave the presentation on Deferred Binding at the GWT Conference. It's hard explaining to people why this is such a powerful and needed feature in GWT. End users especially won't really be able to grok why, but I think Microsoft provides the best evidence in the form of Microsoft Volta, the GWT competitor from Microsoft that was supposed to blow GWT away.
Microsoft's test application certainly blows away my browser: http://labs.live.com/volta/samples/WordWorm.html
It made over 171 HTTP requests to load up all of its generated Javascript, over 2 megabytes of code, it took 20 seconds to startup, ran slow once it did, threw exceptions and sent me into the debugger, and when I looked at the code, I noticed that it had compatibility code for other browsers in my download, code chewing up space and network bandwidth that are useless to my Firefox instance.
I realize that this is a prototype, but come on. Microsoft should not be talking smack about GWT until they've got something to show that doesn't have so many easy to criticize flaws.
-Ray
Posted by Timepedia at 5:02 PM 2 comments
Wednesday, November 28, 2007
Chronoscope in Swing and Servlet environments
After pushing out the open source release of Chronoscope, I finally went back to work on Timelord, which is our all-purpose GWT application for exploring our time series database, data mining, annotating visualizations, and tons more. Hopefully I can get it patched up to show something at the Pearson GWT Conference
Anyway, when data mining, Timelord shows icons to the user of interesting patterns which are server-side generated Chronoscope charts. The intense refactoring for the open source release broke the server-side implementation, so I spent most of yesterday and today, re-merging in and fixing the Java2D Canvas implementation layer.
I just finished getting a prototype up and running, and decided to put up a demo of Chronoscope running as an Applet (it's alittle beefy to download at the moment, I was able to pack200 it down to 120kbytes, but haven't gotten the webserver reconfigured yet to serve pack200 files.) This brings to 4 the number of environments the Chronoscope codebase can exist in: Browser-based Javascript, Swing desktop applications and Applets, Servlet-generators, and the Google Android phone SDK.
After the GWT Conference, I'll push to get a Flash implementation done, in which case, you'll have the choice of generating charts via Canvas, Applet, Flash, or Server. That will leave only J2ME as the final target to crack.
I'll commit the Java2D layer after the conference and things settle down. We've been working on Timepedia slowly for years now, and I'm anxious to start showing some of the real site, so I want to get Chronoscope development stabilized soon. (code-freeze)
-Ray
Posted by Timepedia at 4:04 PM 0 comments
Saturday, November 24, 2007
Chronoscope responsiveness increased
Yesterday, we received reports of slowness in Chronoscope on low end PCs. Admittedly, we didn't do testing on any PCs from yesteryear, but we will do so in the future.
In any case, we made some changes to Chronoscope that should make it feel faster on low end PCs:
- Animation was uninterruptible and fixed at 8 frames. On a slower PC that takes longer than 300ms or so to render these frames, there is a distinct feeling of lag when you press a key. Animations in progress are now interruptible.
- Key frame interpolation was frame count based instead of wall clock based. Interpolation frames are now parameterized based on time, and maximum animation time is capped at 300ms. Faster computers just generate a higher framerate for smoother, less jumpy, rendering.
- Lowered the resolution of the dataset further when animating. Future versions will auto-adapt this to platform speed.
- The final 'full resolution' dataset used to be displayed, no matter what, after an animation sequence finished (between keypresses). Now, it is delayed a short time until the user stops navigating.
The changed version has been deployed to timepedia.org for testing, but not committed to the source code repository yet. Check it out and let us know if you still have performance problems.
The only known issue at the moment is that mouse dragging on some platforms is jerky and laggy. We are looking into fixing this next.
-Timepedia Team
Posted by Timepedia at 12:08 AM 2 comments
Tuesday, November 20, 2007
GWT and Android: A marriage made in heaven?
The Google Web Toolkit has many awesome advantages for developing AJAX applications: Reuse of your favorite toolchain features (IDE, codeassist, debugger, refactor, build, etc), as well as producing much more compact Javascript code than any JS compiler/obfuscator can ever hope for. But one feature that initially caught our eye was the ability to reuse code in multiple environments.
Chronoscope started as two separate projects. The first, a prototype server-side renderer for Timepedia using the very capable JFreeChart. The second, a pure-JS canvas version written to test the ability to do AJAX charts without Flash. The JFreeChart version then became the version which would be used for static chart icons, sparklines, fallback for older browsers, and export to PDF/SVG.
Immediately, we encountered the problem that the two code bases had different features, different rendering style, which required laborious coding to keep them matched as close as possible. Over time, as the feature sets evolved, it became harder and harder to keep the JS client charts and server-side charts in sync.
Then Google released GWT and it immediately offered a solution: Write one chart library, in Java, and deploy to Servlet, Applet, Browser JS, and maybe J2ME and Flash. It was an enormous promise, and we initially adopted GWT for this purpose without realizing the other tremendous benefits that GWT provides for developing large JS codebases.
Early on, we produced prototypes of GWT Chronoscope running in servlet and applet environments, but with the release of the Android SDK, the initial promise has been fulfilled: GWT code running in a mobile environment.
The following is a screencast demo showing Chronoscope, with no changes to the core codebase running in the Android SDK emulator natively (not in the web browser).
It required about 8 hours to get this working. Most of the time was spent finding the Android equivalents of Java2D calls, and writing 6 Java classes (the Chronoscope Canvas abstraction layered over Android Graphics API)
At this point, we've only touched the tip of the iceberg. Future enhancements in GWT may allow this portability to go even further, such as compiling to ActionScript.
If your attending the GWT Conference on December 3-6, feel free to track down Ray Cromwell or Shawn O'Connor for a live demo.
p.s. There are no ticks or axis labels being shown in the sceencast to maximize screen real estate as well as performance. Android SDK text rendering is a drag on performance at the moment.
Posted by Timepedia at 11:41 AM 5 comments
Tuesday, November 13, 2007
Chronoscope Released
Timepedia is happy to announce the first beta release of Chronoscope, our charting and visualization platform for Google Web Toolkit, as open source. Chronoscope provides several features for the AJAX developer:
- Canvas abstraction for drawing vector graphs
- Graph Style Sheets abstraction for configuring the look-and-feel of charts
- Scalable multiresolution rendering supporting up to tens of thousands of points
- Zoom and pan at interactive frame rates, from centuries to milliseconds
- Auto-ranging, auto-layout of chart axes and ticks.
- Auto-legend, and mini-chart Overview
- Add pushpin markers, domain and range markers, and overlays like Google Maps
- Bookmarkable chart state, works with Back button
- JS interopability. GWT API can be used by pure Javascript programmers
- Microformat support. Charts can be configured without programming.
- Server-side Font assistance. Render rotated text.
- Portable, Chronoscope is not tied to GWT, can be used to render from servlets, applets, or other environments.
You can see a demo of the chart in action on our demo site. WARNING: Chronoscope currently supports only Firefox, Safari, and Opera (with a few caveats) for the beta release. Internet Explorer is scheduled to be available soon through a Flash version.
Many of our readers have been eagerly awaiting the release, and we have been burning the midnight oil the last few weeks reorganizing, documenting, and cleaning up the code base for the public.
Chronoscope is free of charge, and is distributed under the LGPL. We hope that many people interested in charting and graphics within GWT will join the Chronoscope project at http://code.google.com/p/gwt-chronoscope and help us build an awesome visualization platform.
Timepedia has been working over the last two years on a search engine which indexes time-related quantitative data, and provides users tools to play with visualizing data, find interesting patterns, and communicating insights. We want to make Chronoscope the best visualization tool, but we need your help to make it better.
There are four ways you can help us:
1) Chronoscope contains a small unobstrustive credit "Powered by Timepedia Chronoscope" which can be switched off. If you use Chronoscope, and want to help us promote Chronoscope, you can choose to leave it switched on.
2) You can become active in contributing code to the project. See the project page.
3) You can choose to donate a small sum of money, whatever you deem appropriate.
4) If you are a commercial company, and interested in official technical support, we sell support licenses, as well as a non-LGPL licensed version. See http://timepedia.org/chronoscope/license if interested.
We thank everyone for their patience over the last month, and hope you think this project is as cool as we do.
For those interested in meeting in person to discuss GWT based visualization and vector graphics, Ray Cromwell will be presenting at the upcoming Voices that Matter: Google Web Toolkit conference. During Breakfast with the Speakers, or after the conference, feel free to hunt him down if you want to chat. For those who wish to register, you can use a code of "GW-CROM" during registration to get $100 off the registration price.
Posted by Timepedia at 2:09 PM 2 comments
Thursday, October 4, 2007
Errata for AccordionPanel
There was an issue with IE7 (and probably IE6) with the Accordion shown in the previous article. The problem is, IE7 doesn't treat an element with overflow: hidden, height: 0px as expected (by making it invisible), instead, it shows it in full.
This is fixed by forcing standards mode with the following declaration in your HTML:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
Posted by Timepedia at 3:22 PM 0 comments
Wednesday, October 3, 2007
Designing a really simple GWT Accordion control
Yesterday, we started implementing some neat context-sensitive information features to Timepedia's "Timelord" GWT application, which integrates Chronoscope, Everett, and several other backend services, and I found myself in great need of an Accordion-like control.
Now, there are a great many accordion controls in third party JS libraries, many of them very slick and full featured, but I found that I didn't need so many features, and I'm trying to limit the number of pure-JS library wrappers I use in GWT because you lose alot of the benefits of the GWT Compiler when you wrap external JS libraries.
How hard could it be to cook up one in GWT? It turns out, not very hard it all.
What I want is a VerticalPanel or HorizontalPanel with the ability to open/close the nested widgets, preferably in a cool animated style. That means, we'll create either an Vertical or Horizontal Panel, store it in a Composite, and use even-numbered widget positions to store a Label, which when clicked, will expand odd-numbered widget positions from 0 to their desired width or height. We'll also simultaneously close any currently expanded widgets, back down to zero width/height. The only trick is to figure out what expanded width/height of a widget is.
First, let's create a Composite to house a Vertical or Horizontal Panel.
public class AccordionPanel extends Composite {
private Panel aPanel;
private String animField;
private String animBounds;
final private static int NUM_FRAMES = 8;
private Widget currentlyExpanded = null;
private Label currentlyExpandedLabel = null;
public AccordionPanel(boolean horizontal) {
if (horizontal) {
aPanel = new HorizontalPanel();
animField = "width";
animBounds = "scrollWidth";
} else {
aPanel = new VerticalPanel();
animField = "height";
animBounds = "scrollHeight";
}
initWidget(aPanel);
setStylePrimaryName("accordion");
}
public AccordionPanel() {
this(false);
}
I don't really need a horizontal accordion, but I threw in support for it (untested) just in case. Here, if we are using a Vertical accordion, we are animating the height field, and the desired expansion size is in the "scrollHeight" field.
Next, we need a method for adding new widgets to an accordion.
public void add(String label, final Widget content) {
final Label l = new Label(label);
l.setStylePrimaryName(getStylePrimaryName()+"-title");
final SimplePanel sp=new SimplePanel();
sp.setWidget(content);
l.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
expand(l, sp);
}
});
aPanel.add(l);
sp.setStylePrimaryName(getStylePrimaryName()+"-content");
DOM.setStyleAttribute(sp.getElement(), animField, "0px");
DOM.setStyleAttribute(sp.getElement(), "overflow", "hidden");
aPanel.add(sp);
}
Given a string label, and a widget, we do two things. First, we create a Label widget, and add a ClickListener to it. The ClickListener simple calls the expand() function with the label, and the content. Secondly, we wrap the content being added in a SimplePanel, to ensure we get a DIV around it which can be animated.
Finally, we add both the Label and the content wrapper to the underlying Vertical or Horizontal panel, set the primary style name of the label/content to be that of the AccordionPanel + "-title"/"-content", and then collapse the newly added component (height/width: 0, overflow: hidden)
Now all we have to do is implement the expand() function. We essentially want to run an animation loop, linearly interpolating between 0 and the max dimensions (for expansion) or the opposite for collapse.
Let's look at the implementation:
private void expand(final Label label, final Widget c) {
if(currentlyExpanded != null)
DOM.setStyleAttribute(currentlyExpanded.getElement(),
"overflow", "hidden");
Here we make sure that any previous expanded section is changed from overflow: auto back to overflow: hidden. Continuing further...
final Timer t = new Timer() {
int frame = 0;
public void run() {
if (currentlyExpanded != null) {
Widget w = currentlyExpanded;
Element elem = w.getElement();
int oSh = DOM.getIntAttribute(elem, animBounds);
DOM.setStyleAttribute(elem, animField, ""+(( NUM_FRAMES -
frame ) * oSh / NUM_FRAMES)+"px");
}
We create a Timer, whose run method will animate the collapse/expansion. We simultaneously collapse any currently expanded Widget and expand the target widget. All we have to do, is fetch the max dimensions from scrollHeight/scrollWidth, and then interpolate them down to zero for collapse.
if (currentlyExpanded != c) {
Widget w = c;
Element elem = w.getElement();
int oSh = DOM.getIntAttribute(elem, animBounds);
DOM.setStyleAttribute(elem, animField, ""+
(frame * oSh / NUM_FRAMES)+"px");
}
frame++;
Likewise for expansion, we simply interpolate from 0 to the maximum dimensions. Finally, we increment the frame, and we take care not to try and collapse and expand the same widget at the same time.
if (frame <= NUM_FRAMES) {
schedule(10);
} else {
if(currentlyExpanded != null) {
currentlyExpanded.removeStyleDependentName("selected");
currentlyExpandedLabel.removeStyleDependentName("selected");
}
c.addStyleDependentName("selected");
if(currentlyExpanded != c) {
currentlyExpanded = c;
currentlyExpandedLabel = label;
currentlyExpandedLabel.addStyleDependentName("selected");
Element elem = c.getElement();
DOM.setStyleAttribute(elem, "overflow", "auto");
DOM.setStyleAttribute(elem, animField, "auto");
} else {
currentlyExpanded = null;
}
}
Finally, we keep rescheduling our timer as long as we haven't reached our maximum number of frames. To finish, we add a final bit of polish, by adding a "selected" CSS class to the Label which is currently expanded. We also change the width/height and overflow fields back to auto.
}
};
t.schedule(10);
The last lines of the expand() function simply kick off our timer.
Here is the simple GWT Module to test
public class AccordionDemo implements EntryPoint {
/**
* This is the entry point method.
*/
public void onModuleLoad() {
AccordionPanel ap=new AccordionPanel();
AccordionPanel ap2=new AccordionPanel();
ap.add("Label 1", new HTML("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nam luctus urna vitae urna. Sed nisl. Praesent nisi nulla, malesuada quis, bibendum quis, egestas nec, pede. Donec ipsum. Duis nulla nisi, tristique eget, fermentum et, gravida non, lorem. Praesent mollis, arcu sed suscipit venenatis, velit erat sollicitudin quam, eget vestibulum odio enim nec libero. Praesent tellus. Vestibulum non justo. Aliquam semper. Nulla mauris ipsum, semper ut, dapibus quis, ultrices nec, est. Mauris nec nisl ut est posuere dignissim. Sed nec magna non purus eleifend mollis. Pellentesque orci. Integer sapien. Cras aliquam."));
ap.add("Label 2", new HTML("Ut tristique convallis nibh. Vestibulum eget nunc eget tellus varius sollicitudin. Vestibulum vestibulum ligula ac nulla. Nulla risus urna, euismod eget, accumsan vitae, posuere sit amet, eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean rhoncus pellentesque justo. Sed purus diam, bibendum ut, posuere a, feugiat sit amet, justo. Donec urna nulla, blandit in, gravida non, mattis a, turpis. Phasellus feugiat leo et justo. Maecenas quam nisl, consectetuer nec, auctor et, pharetra vel, tortor. Nunc justo nulla, tincidunt a, tempor id, viverra eget, sem. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin sagittis nonummy urna. Aenean quis massa ac massa rutrum ornare."));
ap2.setStylePrimaryName("accordion2");
ap2.add("Nested Label 1", new HTML("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nam luctus urna vitae urna. Sed nisl. Praesent nisi nulla, malesuada quis, bibendum quis, egestas nec, pede. Donec ipsum. Duis nulla nisi, tristique eget, fermentum et, gravida non, lorem. Praesent mollis, arcu sed suscipit venenatis, velit erat sollicitudin quam, eget vestibulum odio enim nec libero. Praesent tellus. Vestibulum non justo. Aliquam semper. Nulla mauris ipsum, semper ut, dapibus quis, ultrices nec, est. Mauris nec nisl ut est posuere dignissim. Sed nec magna non purus eleifend mollis. Pellentesque orci. Integer sapien. Cras aliquam."));
ap2.add("Nested Label 2", new HTML("Ut tristique convallis nibh. Vestibulum eget nunc eget tellus varius sollicitudin. Vestibulum vestibulum ligula ac nulla. Nulla risus urna, euismod eget, accumsan vitae, posuere sit amet, eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean rhoncus pellentesque justo. Sed purus diam, bibendum ut, posuere a, feugiat sit amet, justo. Donec urna nulla, blandit in, gravida non, mattis a, turpis. Phasellus feugiat leo et justo. Maecenas quam nisl, consectetuer nec, auctor et, pharetra vel, tortor. Nunc justo nulla, tincidunt a, tempor id, viverra eget, sem. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin sagittis nonummy urna. Aenean quis massa ac massa rutrum ornare."));
ap.add("Nested", ap2);
ap.add("Label 3", new HTML("Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."));
RootPanel.get("slot1").add(ap);
}
}
and the CSS rules used.
.accordion-title {
background: #EFEFE3 url(gray-bg.gif) repeat-x scroll 0%;
border: 0pt none;
padding: 3px;
color: white;
font-weight: bold;
}
.accordion-title-selected {
background: coral none;
}
.accordion {
width: 300px;
}
.accordion2-title {
background: #EFEFE3 url(gray-bg.gif) repeat-x scroll 0%;
border: 0pt none;
padding: 3px;
padding-left: 10px;
color: white;
font-weight: bold;
}
.accordion2-title-selected {
background: blueviolet none;
}
.accordion2 {
width: 300px;
}
-Ray
p.s. caveats. First, scrollHeight/scrollWidth don't always work. See quirksmode. Secondly, there is a chance you could click another label while an expansion is in progress, so you'll need to add an "isAnimating" flag so that expand() will do nothing if another expansion is in progress. Third, you may want ease-in/ease-out in the animation, so instead of a linear interpolation, drop in your favorite easein/out function (sine, 2x^3-3x^2, etc)
Posted by Timepedia at 2:04 PM 4 comments
Saturday, September 15, 2007
Trying to resist Scala...must...not...succumb...
I've frequently been engaged in discussions regarding future Java language extensions (closures, etc) on forums like Javalobby and blogs like Neal Gafter's, and my co-workers aren't as big fans of Java as I am (being more inclined to Python and C), but moving away from Java is hard because of the incredible amount of useful libraries available, mature VM, and our favorite IDE - IntelliJ IDEA.
I would like to have a language, right now, that included a better static type system, first class functions, type inferencing, etc, but also allowed me to use all of my existing Java code, and run on top of a mature VM. Having Java-like performance would be a big plus.
And Scala seems to be it. I had looked at Scala a long time ago, but I hadn't realized how much progress they had made. Recently, I took another look at it, and boy oh boy, is it tempting. There seems to be almost no downside to writing future code in Scala, as it will run in the same VM as my other code, and seamlessly interoperate with Java classes, yet offers similar performance. And don't even get me started on Scala's DSL creation capability, which exceeds even Ruby (though still not Scheme)
However, two missing wishlist features gave me the excuse to leave it alone for now. For one, I told my coworkers that I am not switching to any new language which does not have a REPL. I had assumed that Scala was compiler-only. Secondly, no IntelliJ support is a big turnoff.
To my surprise, Scala *does have* a REPL interpreter. Whoops.
Only one excuse left. Please JetBrains, do not add first-class Scala support to IDEA. Do not tempt me to switch languages, I'm begging you! :)
-Ray
Posted by Timepedia at 1:08 AM 4 comments
Thursday, September 13, 2007
Knowledge extraction with 2 dimensional regular expressions?
Some of my best and worst ideas come when I'm sitting on the toilet. I pretty much have an informal bookshelf next to it, which I use to refresh and reinforce my memories of galois theory, probability, or in this case, automata from time to time. I'm not getting any younger (almost 36) -- it's been ages since college -- so whatever time I can use to reinforce those neurons and keep those skills from slipping away is golden.
So, there I was, automata book in hand, thinking about how I could simplify and modularize, Tardis, Timepedia's web crawler, automated knowledge discovery extractor, and temporal query engine. One part of what Tardis does is look for time related information in documents, and extract timeseries and timeline information.
How it does this exactly, I won't say at this time, but it uses a large battery of heuristics and other classical algorithms. The problem is, this logic is cemented inside of Java code, a sort of implicit 'expert system' with a huge number of cases, and complex interactions. Extending this to support newly discovered rules is hard enough with the source code in hand, but impossible for the end user.
Wouldn't a DSL be better? But if so, which one? An expert system like CLIPS/JESS? Those risk alot of complexity as well as scalability issues. If this was simple text extraction, I'd give the users the ability to author custom regexp match/rewrite rules for particular documents, sort of a mini-online-SED. As you can imagine, a lot of time related information is two dimensional in nature, stored in documents in tabular form, so ordinary regexp's don't capture the problem.
Aha! What about two dimensional regular expressions? Do they even exist? By the time I had gotten off the toilet, I had already devised replacements for regular expression concatenation operators in the form of row/column concatenation operators. I knew however that I was not the first to think of this, so a quick Google Scholar search led me to this paper: Two-Dimensional Languages (1997), Dora Giammarresi, Antonio Restivo, Handbook of Formal Languages
At first glance, this looks like a fertile field, previously applied for recognizing graphical features in pictures. Could I extend it to my needs? More on that later. First, let's look at an example.
As you may recall, a classical regular expression consists of operations like concatenation, union, and Kleene star. Concatenation is usually represented by placing two symbols next to one another, such as "ab" which means "a + b" where + is the concatenation operation. Union is typically represented via the pipe character "|", so "a|b" means "a union b". Kleene star of course is the union of all possible concatenations, e.g. "(a|b)*" = e, a, b, a+b, a+a, b+b, aa+b, ab+a, etc.
The two dimensional analogues of these would row concatenation, column concatenation, and row/column Kleene star operators. Let us denote column concatenation as "+", and row concatenation as "/", so "a + b" recognizes "ab" as long as they are in the same row, and "a/b" recognizes "ab" as long as they are in the same column. Column-wise Kleene star as "+*" and row wise Kleene star as "/*".
Now let's try a problem, recognize the set of all "chessboards", encoded with the symbols 'b' for black, and 'w' for white squares. Here's an example chessboard:
b w b w b w b w
w b w b w b w b
b w b w b w b w
w b w b w b w b
...
First, we match the first row with "(b + w)+*". Next, we match the second row with "(w + b)+*", and then we concatenate these two expressions row wise, to form
"(b + w)+* / (w + b)+*"
This says match "bwbwbw..." followed on the next row by "wbwbwb...". Now all we've got to do is repeat this pattern row rise.
"( (b + w)+* / (w + b)+) )/*"
This will match the above chessboard pattern. I realize it may be confusing to represent column wise concatenation via '+' instead of the empty string as is traditional, but I wanted to make the separate row and column operators explicit and equal in importance.
Now, I was delighted that something like this could be done, and furthermore that theoretical results for NFAs, DFAs, and closure properties existed for such languages, but I still faced the problem that knowledge extraction seems to require processing values semantically, not just lexically, so I tabled it as just a nifty and cool area of research, but with no application to what we were doing.
That was, until I showed it to Mat, who came up with an additional step that would enabled 2D regexps such as those described above to be used by Tardis for time related extraction. And that story, I will leave for a future article. :) The lesson I learned however, is not to give up on an idea that may seem impractical until you've got input from other people.
In the mean time, check out the paper I linked.
-Ray
Posted by Timepedia at 12:07 PM 2 comments
Wednesday, September 12, 2007
When algorithms work better than you dream...
So, Timepedia is building a time machine, right? It sounds pretentious, but for us, it's really a geeky moniker of love for our project, after all, is Google's "search engine" really an "engine"? How much horsepower does it have? :)
One part of Timepedia, readers of this blog are already familar with: Chronoscope. With Chronoscope, we are attempting to build an open platform of visualization tools for time oriented data, in much the same way that Google Maps and Google Earth deal with spatial data.
However, what good is a time machine, if you don't know where to go, or don't understand what you're looking at? Timepedia has another platform, aimed at data mining time related information, called Everett (owned and implemented by another Timepedia founder, Mat). Everett is a collection of many algorithms for both data mining, and forecasting, some of them bleeding edge academic research. When we started, we weren't sure which of them would work, or how well they would work, we only knew that they had promising features, so Everett was less of a end user product, and more of a research platform.
One of the tools of Everett is an algorithm that lets us find hidden recurring patterns in data, even in the presence of noise, or scaling. Last week, we tested the algorithm on real life data for the first time, and had one of those "holy cow!" moments, which don't occur too often for me personally, where your own code surprises you.
To give you an example, I fed Everett an 18,000 data point series of federal funds rates over the last few decades, and it identified a pattern that occured 3 times in history. Visualizing this in another tool we call Timelord (A Chronoscope married to Everett and other server-side services), I was puzzled as to the significance of these three sequences. My co-founder Shawn spent about 1 hour Googling, until he found the correlation: These sequences corresponded to international financial/currency crises (such as the Mexican currency crises), in which the Fed was forced to take action. The leadup to the crises appeared identical each time. A fluke? It sure the hell was very interesting.
I was worried it was a fluke, so I tried something more mundane. A time series of unemployment benefit expenditures in Indiana, and once again, Everett identified a series of puzzling repetitive sequences. What were they? The dates looked very familar, 1980-81, 1990-91, 2000-1...were they recessions? To check, I used Timelord to overlay a National Bureau of Economic Research official measure of economic expansions and contractions, and sure enough, these patterns intersected with NBER recessions. One other interesting property stood out, the patterns returned prefixed the recessions, that is, Everett was showing us a pattern that leads to a recession.
How cool is that? Ambition got the best of me, I went for broke: I tried a historical time series of average hurricane strength (saffir-simpson scale), as well as a yearly count. There appears to be good evidence that a 40-60 cyclical hurricane season exists, and I was hoping that Everett could find these patterns, but alas, it did not.
Still, the initial results are promising, and we hope that Everett will give average users an ability to query time in ways that have not been previously available.
So, if you're wondering why I haven't released Chronoscope yet, it's because I've been working on integrating Timelord with Everett. :)
-Ray
p.s. Timelord is another GWT application, making it our 4th major GWT application. Everett is C++ coupled via JNI a Java/GWT RPC interface, since performance is absolutely critical in Everett.
Posted by Timepedia at 4:11 PM 5 comments
Labels: data mining, everett, google webtoolkit, gwt, timepedia
Converting to Guice, easier than I ever imagined.
I've been working the past month on revising the data layer of Timepedia, which has, shall we say, somewhat interesting storage requirements. However, the RDBMS related portions had become an enormous eyesore, with tons of handcoded hibernate DAO methods. I have been admiring Guice from afar for awhile, but delayed the pain of refactoring the RDBMS code until I had no choice.
Surprisingly, there was no pain at all!
I took a chance, and used Wideplay's warp-persist framework for Guice, deleted all of my DAO implementation classes in favor of Dynamic Finders, and wrote one Guice module (5 lines of Hibernate config code mostly), compiled, deployed, and prayed. Amazingly, it worked the first time. I was done, total conversion time: 30 minutes.
Now, it helps that I wasn't using Spring nor J2EE (I dislike bulky over designed frameworks with hideous XML configurations), but I think this is a good result for Guice. It reduced the number of lines of code (especially the Dynamic Finders) dramatically as well as the amount of configuration.
-Ray
Posted by Timepedia at 3:12 PM 2 comments
Wednesday, August 1, 2007
Timepedia, Chronoscope Quick Status Update
As I mentioned in my last entry, I haven't been able to work for the last few weeks due to a family emergency, but work has now been ramping back up, and we are shooting for a preview of the full site in 2-3 weeks time.
Chronoscope in the meantime is undergoing a heavy refactoring, both to use the GWT Exporter everywhere (instead of hand-rolled bridge functions), as well as performance fixes for the renderer (should be a lot smoother on release), and inclusion of the FontBook Renderer for rotated text. The initial release still won't have a Flash canvas renderer due to the need to work on the Timepedia site.
-Ray
Posted by Timepedia at 5:31 PM 0 comments
Saturday, July 7, 2007
Sorry for the lack of updates...
I (Ray) was supposed to have started a series on modifying the GWT compiler, but I had to fly out on an emergency recently because my mother has fallen gravely ill and is intensive care and I spend most of my time at the hospital now. Everything has been delayed, the blog series, the next version of Chronoscope, the launch of Timepedia, by weeks. Sorry for the delay.
-Ray
Posted by Timepedia at 6:24 AM 0 comments
Sunday, July 1, 2007
Top 16 missing features from iPhone
Don't get me wrong, I love my iPhone, but the following annoyances, bugs, and missing features really bother me. And for a $600 device, I expect Apple to address this in future firmwares.
1. No MMS. Inexcusable really for a modern phone. MMS isn't just about trading photos, because...
2. No ability to exchange vCard or iCal either over MMS, Email, or Bluetooth, because...
3. The iPhone doesn't appear to support vCard or iCalendar. Really, try inviting yourself to a meeting with Google Calendar and the ICS invites will show up as unusable attachments. Not the end of the world, except...
4. iPhone Mail won't properly show the HTML Invite of a Google Calendar invite! You get the subject line, and unusable attachments, and that's it. Embedding a multipart/alternative inside a multipart/mixed seems to be too much for it. So for now, you're stuck with first, checking email with Mail.app, accepting an ICS invite and getting it into your phone via iSync. Hope no one invites you to important meetings on the road.
5. Again, Bluetooth profiles supported seem pretty paultry. No dialup network tethering, no print service, no OBEX exchange, and so on.
6. VPN L2TP only supports shared secret identification and doesn't allow Machine Certificate.
7. Can't use iPhone to haul files around. You know, being able to use it as a USB drive and sync some files or use Bluetooth File Exchange would be nice, which just about every other phone supports.
8. No gestures for text selection. So, you can position the cursor, but if you need to delete a sentence or word, you have to just press the delete button a few dozen times. You can't cut/copy/paste text either. Double-tap word selection and triple-tap sentence would be nice, and would not interfere with the zooming in any application except Safari text fields.
9. no iChat, no jabber, no AIM, no bonjour, well, nothing but email and SMS. And since neither Flash nor J2ME is available, pretty much no hope of third party development of these -- except for web-based hacks which are likely to chew up battery life with HTTP traffic vs idling a TCP socket.
10. Mail application does not support full range of configuration that Mail.app does (e.g. choosing between IMAPS and STARTTLS, picking SMTP/IMAP ports, etc)
11. Landscape mode available in far too few apps. I like the landscape-mode keyboard, but it is not available in Mail, or SMS, or practically anywhere else except Safari.
12. No speed dial? Wouldn't it be nice if double-taping the home button from sleep wake would take you right into the Phone app so you could hit a speed dial number, or better yet, allow double-tap-home to be configured to go to the favorite app screen of your choice (e.g. recents?) Apple talks about how great it is that the buttons are so configurable being virtual instead of plastic, but why not let the enduser have more control over screen UI layout and macros?
13. No Flash...I know, it's coming most likely. Still, very irritating.
14. No Java (J2ME). The iPhone eats up 500M of RAM for the OS, and he says Java is a hog? With J2ME today, on many phones, you can send/receive SMS from Java code, you can open sockets, you can open bluetooth connections, you can access the file system, you can control the camera, record sound, play video, access PIM databases, make accelerated 3D rendering calls, access accelerated 2D sprite/tile logic, the list goes on. I am onboard with the AJAX programming model, great, but AJAX alone can't cover all application classes, unless they massively embellish the Safari JavaScript API to attain feature parity with the various J2ME JSRs.
15. If no Java, then come up with an Objective-C secure sandboxed alternative that allows more access to native iPhone widgets. AJAX developers can't achieve a seemless native-look-and-feel by using HTML rendering, because the iPhone native widgets are relying on Core Animation as well as access to native data. A Java or Obj-C layer could. A Javascript layer cool, if, and I mean, *IF* Apple put serious work into extending the Dashcode API to encompass a hell of alot more functionality than they do today. Without Flash and without Java, even something as simply as writing a game with sound effects becomes hard. I guess the only alternative is to load up a bunch of quicktime objects into the webpage. Sheesh.
16. Last, but not least, Mobile Safari and AJAX. Yeah, it's the 'full web', right? Not exactly.
a) Safari chews up mousedown, mousemove, mouseup, and other DOM events before the browser gets them to implement scrolling and zooming. So, for example, maps.google.com scrolling doesn't work on iPhone. Great, they have a native app that deals with scrolling and zooming already on the iPhone, but what about other web pages that need these events?
b) Any element or frame with overflow: scroll is treated as overflow: clip. That is, with the exception of top level window, you cannot have scrollable regions anywhere else within a page. Safari has no concept of 'focused' scrolling. You'd expect that if you tap an embedded FRAME/IFRAME/DIV and it had scrollbars, that you could scroll them by dragging your finger. Ooopps! They obviously need a mechanism to support scrolling when there are multiple scrollable regions. And the response cannot be 'well, only browse pages designed for iPhone in the first place!' because that's the WAP "mobile Web" argument, which Steve Jobs himself has ridiculed. I want the FULL web experience on my iPhone.
c) Safari should let the browser DOM event handlers get fit dibs at the event. If they call event.preventDefault(), it should CANCEL the default finger-drag-scrolls-whole-window. Would this allow some poorly designed web pages to disable zoom and scroll? Yep.
The iPhone is a magnificent phone. But I feel it is much like the PlayStation 3 - about the same price, underdeveloped firmware, and lacking third party support. A year later, the PS3 firmware has added DLNA, background downloads, enhanced RGB, BluRay, new AVC codecs, Folding@Home, and lots of other features.
Let's hope that the iPhone's lack of features is not a conscious decision to eschew those features, but merely the result of rushed deadlines to get a stable device out before they could finish every feature in their roadmap.
-Ray
Posted by Timepedia at 3:02 AM 13 comments
Saturday, June 30, 2007
Chronoscope on iPhone: It works! Sorta...
Rendering works fine, but navigation doesn't. The iPhone doesn't seem to give mouse move DOM events to elements, focused or not, so no matter how you drag in the browser window, you can't capture the mouse drag/move event before the iPhone Safari handles it. Bummer. I tried Chronoscope, Dojo Toolkit, GWT demos, Google Maps (maps.google.com), all have the same issues with mouse dragging.
Update: Here's code go enable mousemove events on iPhone
function init() {
var drag = document.getElementById("drag");
drag.addEventListener("mousemove", moveme, true);
drag.addEventListener("click", function(evt)
{ evt.preventDefault(); }, false);
}
<span id="drag" style="-khtml-user-drag:element;">Drag Me</span>
I don't know if the style attribute is needed, this is just what I was able to get working. The mousemove event appears, but only as a result of a click, not a drag.
-Ray
Posted by Timepedia at 12:47 AM 2 comments
Saturday, June 9, 2007
GWT Demystified: Generators Part 3, Meet the Oracle
In the Matrix movies, the Oracle told Neo exactly what he needed to hear. In this GWT Demystified, we'll explore TypeOracle(s) and what they need to tell Generators.
Recall from Part 2 of this series, that we created an interface called Exportable, and told GWT that whenever GWT.create(Exportable) is called, to execute our generator on the class.
Let's look at the guts of the generator code:
package org.timepedia.exporter.rebind;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
public class ExporterGenerator extends Generator {
public String generate(TreeLogger logger, GeneratorContext ctx,
String requestedClass)
throws UnableToCompleteException {
ClassExporter classExporter = new ClassExporter(logger, ctx);
String generatedClass = classExporter.exportClass(requestedClass);
return generatedClass;
}
}
Generators by convention are not placed in the .client package, nor .public or .server, but in a package called .rebind. They are compile time Java code, not client side Javascript, nor run-time servlet code. Generators must extend Generator and override the generate(logger, context, requestedClass) method.
TreeLogger is just a logging interface for compiler diagnostics, GeneratorContext allows access to compile time state, and the requested class is the fully qualified classname of the class that was passed to GWT.create().
In my implementation, I store this call state on another class for convenience, let's take a look at my ClassExporter.
package org.timepedia.exporter.rebind;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.user.rebind.SourceWriter;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.io.PrintWriter;
/**
* This class performs the generation of export methods for a single class
*
* @author Ray Cromwell <ray@timepedia.org>
*/
public class ClassExporter {
private TreeLogger logger;
private GeneratorContext ctx;
private ExportableTypeOracle xTypeOracle;
private SourceWriter sw;
private ArrayList<JExportableClassType> exported;
private HashSet<String> visited;
private static final String ARG_PREFIX = "arg";
public ClassExporter(TreeLogger logger, GeneratorContext ctx) {
this(logger, ctx, new HashSet<String>());
}
public ClassExporter(TreeLogger logger, GeneratorContext ctx,
HashSet<String> visited) {
this.logger = logger;
this.ctx = ctx;
// a type oracle that can answer questions about whether types are
// exportable
xTypeOracle = new ExportableTypeOracle(ctx.getTypeOracle(), logger);
// globally, classes that have already been exported by the current generator
this.visited = visited;
// classes exported by *this* ClassExporter instance
exported = new ArrayList<JExportableClassType>();
}
Now, this looks more meaty. Before I can start to explain this code, I need to
explain the GeneratorContext.
In order to get anything done with generators, you need to access some of the compiler's internal state, and you need to be able to generate source code. The GeneratorContext entry provides the ability to get Oracles, as well as create source code files on disk.
What's an Oracle? Think of it as the compile time analog of Java Reflection or Sun's Mirror API. In particular, the TypeOracle returned by GeneratorContext.getTypeOracle() is a pretty close analog to java.lang.Reflect introspection. You can query for classes, their fields, methods, return values, parameters, superclasses, subclasses, etc.
The GeneratorContext also exposes a tryCreate() method that allows one to create Java source files on disk, giving a PrintWriter in return. It returns null if the code has already been generated (generators may be invoked more than once), which is your signal to exit early. In practice, you usually don't use tryCreate() directly, but use helper classes which I'll explain shortly.
We create a class called ExportableTypeOracle whose job it is to answer questions about Exportable types, such as which methods are exportable, which parameters are exportable, what's the JavaScript name of a particular method going to be, etc.
Let's look at the main function, paired down a little bit:
/**
* This method generates an implementation class that implements Exporter
* and returns the fully qualified name of the class.
*
* @param requestedClass
* @return qualified name of generated Exporter class
* @throws UnableToCompleteException
*/
public String exportClass(String requestedClass)
throws UnableToCompleteException {
// JExportableClassType is a wrapper around JClassType
// which provides only the information and logic neccessary for
// the generator
JExportableClassType requestedType =
xTypeOracle.findExportableClassType(requestedClass);
// add this so we don't try to recursively reexport ourselves later
exported.add(requestedType);
visited.add(requestedType.getQualifiedSourceName());
if (requestedType == null) {
logger.log(
TreeLogger.ERROR, "Type '"
+ requestedClass + "' does not implement Exportable", null
);
throw new UnableToCompleteException();
}
// get the name of the Java class implementing Exporter
String genName = requestedType.getExporterImplementationName();
// get the package name of the Exporter implementation
String packageName = requestedType.getPackageName();
// get a fully qualified reference to the Exporter implementation
String qualName =
requestedType.getQualifiedExporterImplementationName();
sw = getSourceWriter(
logger, ctx, packageName,
genName, "Exporter");
if (sw == null) {
return qualName; // null, already generated
}
So we look up our class in the ExportableTypeOracle. If you get null, it means the class isn't Exportable. We then ask for an implementation class name ("Foo" yields class "FooExporterImpl implements Exporter") We then ask getSourceWriter to
create such a class for us on disk and return a Writer. Let's look at that method:
/**
* Get SourceWriter for following class and preamble
* package packageName;
* import com.google.gwt.core.client.GWT;
* import org.timepedia.exporter.client.Exporter;
* public class className implements interfaceName (usually Exporter) {
*
* }
*
* @param logger
* @param context
* @param packageName
* @param className
* @param interfaceNames vararg list of interfaces
* @return
*/
protected SourceWriter getSourceWriter(
TreeLogger logger, GeneratorContext context,
String packageName, String className, String... interfaceNames) {
PrintWriter printWriter = context.tryCreate(
logger, packageName, className
);
if (printWriter == null) {
return null;
}
ClassSourceFileComposerFactory composerFactory =
new ClassSourceFileComposerFactory(packageName, className);
composerFactory.addImport("com.google.gwt.core.client.GWT");
for (String interfaceName : interfaceNames) {
composerFactory.addImplementedInterface(interfaceName);
}
composerFactory.addImport("org.timepedia.exporter.client.Exporter");
return composerFactory.createSourceWriter(context, printWriter);
}
Here we use helper classes provided by GWT to create a class with a given packageName and className. We add imports, add interfaces, and then create a SourceWriter to write to the PrintWriter. SourceWriter differs primarily from PrintWriter in that it can keep track of indent level, and indent generated code.
The rest of the exportClass method looks like this:
sw.indent();
// here we define a JSNI Javascript method called export0()
sw.println("public native void export0() /*-{");
sw.indent();
// if not defined, we create a Javascript package hierarchy
// foo.bar.baz to hold the Javascript bridge
declarePackages(requestedType);
// export Javascript constructors
exportConstructor(requestedType);
// export all static fields
exportFields(requestedType);
// export all exportable methods
exportMethods(requestedType);
// add map from TypeName to JS constructor in ExporterBase
registerTypeMap(requestedType);
sw.outdent();
sw.println("}-*/;");
sw.println();
// the Javascript constructors refer to static factory methods
// on the Exporter implementation, referenced via JSNI
// We generate them here
exportStaticFactoryConstructors(requestedType);
// finally, generate the Exporter.export() method
// which invokes recursively, via GWT.create(),
// every other Exportable type we encountered in the exported ArrayList
// ending with a call to export0()
genExportMethod(requestedType, exported);
sw.outdent();
sw.commit(logger);
// return the name of the generated Exporter implementation class
return qualName;
}
This article is too big to delve into every one of this methods, so I will explain just one: exportFields, but first, you will finally have to meet the TypeOracle.
I created a helper class called ExportableTypeOracle that answers only the questions relevent for my generator:
package org.timepedia.exporter.rebind;
import com.google.gwt.core.ext.typeinfo.*;
import com.google.gwt.core.ext.TreeLogger;
public class ExportableTypeOracle {
private TypeOracle typeOracle;
private TreeLogger log;
static final String EXPORTER_CLASS =
"org.timepedia.exporter.client.Exporter";
static final String EXPORTABLE_CLASS =
"org.timepedia.exporter.client.Exportable";
private JClassType exportableType = null;
private JClassType jsoType = null;
private JClassType stringType = null;
public static final String GWT_EXPORT_MD = "gwt.export";
private static final String GWT_NOEXPORT_MD = "gwt.noexport";
public static final String JSO_CLASS =
"com.google.gwt.core.client.JavaScriptObject";
private static final String STRING_CLASS = "java.lang.String";
private static final String GWT_EXPORTCLOSURE = "gwt.exportClosure";
public ExportableTypeOracle(TypeOracle typeOracle, TreeLogger log) {
this.typeOracle = typeOracle;
this.log = log;
exportableType = typeOracle.findType(EXPORTABLE_CLASS);
jsoType = typeOracle.findType(JSO_CLASS);
stringType = typeOracle.findType(STRING_CLASS);
assert exportableType != null;
}
public JExportableClassType findExportableClassType(String requestedClass) {
JClassType requestedType = typeOracle.findType(requestedClass);
if (requestedType != null) {
if (requestedType.isAssignableTo(exportableType)) {
return new JExportableClassType(this, requestedType);
}
}
return null;
}
The first method needed is findExportableClassType. Given a request like "org.foo.bar.MyClass", it looks this type up in the underlying TypeOracle via findType(), and if found, it checks to see if the JClassType that is returned can be assigned to org.timepedia.exporter.client.Exportable. If so, this class is deemed Exportable, and it returns a wrapper around JClassType that can answer more questions about fields, methods, etc.
package org.timepedia.exporter.rebind;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JConstructor;
import java.util.ArrayList;
public class JExportableClassType implements JExportable, JExportableType {
private ExportableTypeOracle exportableTypeOracle;
private JClassType type;
private static final String IMPL_EXTENSION = "ExporterImpl";
public JExportableClassType(ExportableTypeOracle exportableTypeOracle,
JClassType type) {
this.exportableTypeOracle = exportableTypeOracle;
this.type = type;
}
public JExportableField[] getExportableFields() {
ArrayList<JExportableField> exportableFields =
new ArrayList<JExportableField>();
for (JField field : type.getFields()) {
if (ExportableTypeOracle.isExportable(field)) {
exportableFields.add(new JExportableField(this, field));
}
}
return exportableFields.toArray(new JExportableField[0]);
}
For brevity, I only show the getExportableFields() method. This method enumerates over the real ClassType.getFields() method, and for each JField entry, asks the ExportableTypeOracle if it is exportable or not. If so, it creates another wrapper around JField called JExportableField and adds it to the returned results.
But what makes a field exportable?
public static boolean isExportable(JField field) {
return field.isStatic() &&
field.isPublic() &&
field.isFinal() &&
( isExportable(field.getMetaData(GWT_EXPORT_MD)) ||
( isExportable(field.getEnclosingType()) &&
!isNotExportable(
field.getMetaData(GWT_NOEXPORT_MD))));
}
A field is exportable if and only if it is: 1) "public static final", 2) it has a @gwt.export annotation OR its class has a @gwt.export annotation and the field does not have a @gwt.noexport annotation.
Finally, let's look at the exportFields() method.
/**
* For each exportable field Foo, we generate the following Javascript:
* $wnd.package.className.Foo = JSNI Reference to Foo
*
* @param requestedType
* @throws UnableToCompleteException
*/
private void exportFields(JExportableClassType requestedType)
throws UnableToCompleteException {
for (JExportableField field : requestedType.getExportableFields()) {
sw.print("$wnd." + field.getJSQualifiedExportName() + " = ");
sw.println("@" + field.getJSNIReference() + ";");
}
}
Self-explanatory except for the getJSQualifiedExportName() and getJSNIRefernece(). The former method will return the JavaScript package + JavaScript constructor + exported name of the field. So for a field "FOO" on class "org.foo.Bar", this string might be "org.foo.Bar.FOO" if none of the exported names were overriden with an annotation.
The getJSNIReference() function will return a string like "org.foo.Bar::FOO" to reference to the public static final field of that class.
Finally, when done generating, you call SourceWriter.commit(logger), and return the name of the class you generated.
That about wraps up this edition of GWT Demystified. The GWT Exporter source and module is now available on http://code.google.com/p/gwt-exporter
-Ray
Posted by Timepedia at 2:05 AM 6 comments
Thursday, June 7, 2007
GWT Demystified: Generators Part Deux
In the last part, I described what generators are and what they can be used for. In this post, I'll be explaining how I used them to build the GWT Exporter.
GWT Exporter
The first step is to decide what your generator is going to do. In my case, I want the generator to introspect one of my GWT classes, and generate an exported JS API with bridge methods.
What's a bridge method?
Imagine you have the following GWT Class:
package org.foo.bar;
public class Util {
public static String doSomethingUseful(int x) {
return "Hello "+x;
}
}
And you want to allow JS users to call the doSomethingUseful() function?
Today, you would use JSNI to export 'bridge methods' like so:
package org.foo.bar;
public class Util {
public static String doSomethingUseful(int x) {
return "Hello "+x;
}
public native void export() /*-{
$wnd.doSomethingUseful = function(x) {
@org.foo.bar.Util::doSomethingUseful(I)(x);
}-*/;
}
}
$wnd stands for the top-level window object, and by assigning to its doSomethingUseful property, you ensure GWT won't obfuscate it. The JSNI call to Util.doSomethingUseful will be obfuscated however, thus the bridge method is neccessary to export the obfuscated symbol.
It gets more complicated if you want to bridge a non-static function, but here's an example:
public class Employee {
private String firstName, lastName;
public Employee(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return getLastName;
}
public static Employee createEmployee(String firstName,
String lastName) {
return new Employee(firstName, lastName);
}
public native void export() /*-{
$wnd.Employee = function(firstName, lastName) {
// call factory method and store GWT reference
this.instance =
@org.foo.bar.Employee::createEmployee(Ljava/lang/String;
Ljava/lang/String)(firstName, lastName);
}
var _=$wnd.Employee.prototype;
_.getFirstName = function() {
this.instance.@org.foo.bar.Employee::getFirstName()();
}
_.getLastName = function() {
this.instance.@org.foo.bar.Employee::getLastName()();
}
}-*/;
}
Which you may use as
x = new Employee('Ray', 'Cromwell');
alert(x.getFirstName());
As you can see, manual bridging gets tedious!
Generators to the rescue!
The first step in implementing a generator is to decide on a type that will be used to trigger the generator. So let's introduce a new marker interface called 'Exportable'. We also want the generated class to implement the Exporter interface, primarily, the export() method.
Here they are:
package org.timepedia.exporter.client.Exportable;
public interface Exportable {
}
public interface Exporter {
public void export();
}
Simple eh? Next we'll add the following line to our GwtExporter.gwt.xml module file:
<generate-with
class="org.timepedia.exporter.rebind.ExporterGenerator">
<when-type-assignable
class="org.timepedia.exporter.client.Exportable"/>
</generate-with>
What does this mean?
It means that when GWT.create() is invoked with a type that can be assigned to an Exportable, invoke the generator. That is, we want to write
public class Employee implements Exportable { ... }
Exporter x=(Exporter)GWT.create(Employee.class);
x.export();
and have it invoke the generator.
Specifying what gets exported
Next we have to decide on the rules for exporting. Which methods of an Exportable get exported? How do we control the generated JS namespace? etc. For now, let's settle on the following logic -- a method is exportable IF and ONLY IF:
- The class enclosing the method implements Exportable
- Metadata has determined it's ok to export (more on this later)
- a primitive type (int, float, etc)
- another Exportable
- an immutable JRE type (String, Integer, Double, etc)
- JavaScriptObject
GWT has its own form of annotations similar to JavaDoc/XDocLet tags. We will use this to control export policy. We will support two forms of export policy:
- White List
- By default, nothing exported.
- Each method to be exported must have a "@gwt.export" metadata annotation
- Black List
- Place "@gwt.export" on class itself (in JavaDoc for class)
- By default, all public methods exported
- Each method to be removed from export consideration tagged with "@gwt.noexport"
As an example:
package org.foo.bar;
/**
* @gwt.export
* @gwt.exportPackage examples
*/
public class Employee implements Exportable {
private String firstName, lastName;
public Employee(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
/**
* @gwt.noexport
*/
public String getLastName() {
return getLastName;
}
and uses black-list policy to export getFirstName() but supress the export of getLastName(); Using white-list policy, you would write:
package org.foo.bar;
/**
* @gwt.exportPackage examples
*/
public class Employee implements Exportable {
private String firstName, lastName;
/**
* @gwt.export
*/
public Employee(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
/**
* @gwt.export
*/
public String getFirstName() {
return firstName;
}
public String getLastName() {
return getLastName;
}
to achieve the same effect.
At this time, we are not considering function overloading.
This concludes the specification process, and part 2. Coming up: Part 3, the guts of the generator implementation.
-Ray
Posted by Timepedia at 11:57 PM 4 comments
Labels: bridge methods, generators, gwt, gwt demystified, gwt exporter
GWT Canvas: Rendering rotated text
Chronoscope operates on a Canvas abstraction in GWT. Think of it as a mirror of the Safari Javascript Canvas API, with extensions for text rendering, reading back coordinate transforms, layers, fast-clear, and frame/display list capture.
A typical bit of Chronoscope Canvas code will look like:
Canvas canvas = getCanvas();
canvas.beginFrame();
Layer layer = canvas.getLayer("foo");
layer.setStrokeColor(Color.black);
layer.moveTo(sx,sy);
layer.lineTo(ex, ey);
layer.stroke();
Layer textLayer = canvas.getLayer("bar");
textLayer.setStrokeColor(Color.red);
textLayer.drawText("Hello World", x, y,
gssProperties);
canvas.endFrame();
The begin/end frame abstraction permits capture of display list for faster rendering (Opera lockCanvas), as well as shipping a scenegraph to Flash, Silverlight, or SVG. Layers permit compositing and fast-clear.
Rendering Text
Users of Javascript Canvas dream of support for text rendering. For some unknown reason, Apple (who has awesome text rendering capability in the Quartz canvas) left it out. This has forced many developers to roll their own text rendering, until the day when everyone supports the WHATWG Canvas.
Some of the typical hacks used to get text rendering include:
- Placing DIV tags over the Canvas with text
- Using pre-rendered or on-the-fly server-rendered images
- Extracting glyph vectors from true type fonts, sending them encoded to the browser, and rendering them with Canvas moveto/lineto calls.
The latter two options are the only ones capable of dealing with rotated text.
Chronoscope uses the first solution, even for rotated text, however the individual letters do not get rotated, leading to ugliness. The existing Chronoscope demo shows this on the vertical axis chart labels. Fast-clear comes into play when I want to redraw. I can blow away dozens of hundreds of DIV tags with something like "layerElem.innerHTML=''".
GWT1.4 ClippedImage and FontBook Rendering
I recently began experimenting with the idea of rendering an entire font at the desired 2D transformation and font-properties, and then using GWT1.4 ClippedImage to render letters.
A few hours later, I have an implementation that exceeded my wildest dreams in quality, here's how I did it:
- Create a GWT RPC Service called getFontMetrics(transform, fontProperties, color)
- Servlet calculates FontMetrics (ascent, descent, leading, advance, etc) for first 256 characters of the Font (most European languages)
- Returned metrics includes a URL to a generated font book
- FontRenderer servlet renders 16x16 array of characters with specified transform, color, font, etc
- new drawTransformedText() method of Canvas first renders according to the old "ugly" method mentioned above and kicks off RPC and Image load
- On load completed, "ugly" transformed text replaced with text rendered with clipped characters from the font book image
Devil's in the drawTransformedText() details
So how does this method work? The server returns a 256-element array of advance values (essentially character width), along with maxAscent, maxDescent, and leading obtained from Java2D FontMetrics. The returned RenderedFontMetrics RPC object has a method called 'getBounds(char c, Bounds b)' that for any character, returns a clipping bounding box into the second argument (avoiding excessive object creation in the main loop), which is the position of the rendered character in the font book image.
The main loop essentially obtains the bounding box for each character to be drawn, creates a ClippedImage that will display it, and positions the IMG tag along the font-baseline according to the current transform (coordinate system). It uses the 'advance' values for each character to know how far along the baseline to place each successive clipped image.
Screen Shot
Performance
Font Books can be cached, and rendering a font book on the server takes about 10 milliseconds. The resulting image is about 20kb. After that, text rendering performance is pretty much the same as the old "ugly" DIV method.
Caveats
BIDI not supported. Non-ISO-8859 character sets a pain. The issue is, I can't very well send down font-book with 65k characters. However, statistics are on our side. For example, in a language like Chinese, you'll find that most people only use a few thousand characters in day to day use.
We can compute a histogram on a corpus of Chinese text, and render a fontbook containing the most used characters in descending order. We can render several such tiles, in a monotonically descending fashion, on-demand, and cache them. In most applications, you'll probably infrequently request the second-tier characters, and very very infrequently request characters 3 standard deviations away.
Once Chronoscope is out in the wild, I hope some non-ISO-8859-1 native speakers will help contribute improved fontbook rendering for CJK characters and other languages.
p.s. I'll post a demo up on timepedia.org later, after I can verify all of the code is working properly, since I also moved the code base from 1.3 to 1.4.
-Ray
Posted by Timepedia at 5:49 PM 1 comments
Labels: clippedimage, fontbook, google webtoolkit, gwt, renderer, rotated text