Play!ing (2.0) with Twitter Bootstrap, WebSockets, Akka and OpenLayers


The original post can be found on the ekito website.

For one of our client, we need to show a map with vehicles position updated in real-time.
So I began to make a prototype using Play! framework, with its latest released version 2.0, using the Java API. I started from the websocket-chat of the Play! 2.0 samples.

The purpose of the prototype is to show a moving vehicle on a map. The location of the vehicle is sent to the server through a REST call (at the end, it will be sent by an Android app), and the connected users can see the vehicle moving in real-time on their map.

First, let’s look at a small demo !

So, at first, to make the things a bit pretty, I decided to integrate Twitter Bootstrap (v2.0.1) using LessCss. For that, I used the tips from the following article (nothing difficult here).

Then, I integrated OpenLayers, a Javascript framework used for map visualization. I used the Google Maps integration example, and add some KML layers. This is done in the map.scala.html and maptracker.js files, nothing fancy here (it is pure Javascript, and I’m not an expert…).

The interesting part is the one using the WebSockets. On the client side, it is quite standard :

var WS = window['MozWebSocket'] ? MozWebSocket : WebSocket
var mapSocket = new WS("@routes.Application.mapsocket().webSocketURL(request)");

mapSocket.onmessage = function(event) {
    var data = JSON.parse(event.data);
        
    marker = moveMaker(map, marker, data.longitude, data.latitude);
        
}

// if errors on websocket
var onalert = function(event) {
    $(".alert").removeClass("hide");
} 

mapSocket.onerror = onalert;
mapSocket.onclose = onalert;

When the client receive a JSON data from the websocket, it moves the marker on the map. And if an error occurs on the websocket (the server is stopped for instance), a pretty error is displayed thanks to Twitter Bootstrap :

On the server part, the websocket is created by the Application controller, and is handled by the MapAnime.java Akka actor; it accesses Akka native libraries to deal with the events from the controller.

public class MapAnime extends UntypedActor {

	static ActorRef actor = Akka.system().actorOf(new Props(MapAnime.class));

	Map<String, WebSocket.Out<JsonNode>> registrered = new HashMap<String, WebSocket.Out<JsonNode>>();

	/**
	 * 
	 * @param id
	 * @param in
	 * @param out
	 * @throws Exception
	 */
	public static void register(final String id,
			final WebSocket.In<JsonNode> in, final WebSocket.Out<JsonNode> out)
			throws Exception {

		actor.tell(new RegistrationMessage(id, out));

		// For each event received on the socket,
		in.onMessage(new Callback<JsonNode>() {
			@Override
			public void invoke(JsonNode event) {
				// nothing to do
			}
		});

		// When the socket is closed.
		in.onClose(new Callback0() {
			@Override
			public void invoke() {
				actor.tell(new UnregistrationMessage(id));
			}
		});
	}

	public static void moveTo(float longitude, float latitude) {

		actor.tell(new MoveMessage(longitude, latitude));

	}

	@Override
	public void onReceive(Object message) throws Exception {

		if (message instanceof RegistrationMessage) {

			// Received a Join message
			RegistrationMessage registration = (RegistrationMessage) message;

			Logger.info("Registering " + registration.id + "...");
			registrered.put(registration.id, registration.channel);

		} else if (message instanceof MoveMessage) {

			// Received a Move message
			MoveMessage move = (MoveMessage) message;

			for (WebSocket.Out<JsonNode> channel : registrered.values()) {

				ObjectNode event = Json.newObject();
				event.put("longitude", move.longitude);
				event.put("latitude", move.latitude);

				channel.write(event);
			}

		} else if (message instanceof UnregistrationMessage) {

			// Received a Unregistration message
			UnregistrationMessage quit = (UnregistrationMessage) message;

			Logger.info("Unregistering " + quit.id + "...");
			registrered.remove(quit.id);

		} else {
			unhandled(message);
		}

	}

	public static class RegistrationMessage {
		public String id;
		public WebSocket.Out<JsonNode> channel;

		public RegistrationMessage(String id, WebSocket.Out<JsonNode> channel) {
			super();
			this.id = id;
			this.channel = channel;
		}
	}

	public static class UnregistrationMessage {
		public String id;

		public UnregistrationMessage(String id) {
			super();
			this.id = id;
		}
	}

	public static class MoveMessage {

		public float longitude;

		public float latitude;

		public MoveMessage(float longitude, float latitude) {
			this.longitude = longitude;
			this.latitude = latitude;
		}

	}

}

The “register” and “moveTo” methods are called by the controller, they send messages to the Akka system. These messages are processed by the “onReceive” method. For instance, when it receives a MoveMessage, it creates a JSON object with the longitude and the latitude and it is sent to the clients through the websockets.

I also quickly wrote a test class which parse a text file, and send REST requests with a new location to the server every 100ms.

The project is hosted on Github. It works with Google Chrome v17 and Firefox v11.

To test it,

The problem I need to solve now is that the application is not Stateless, because in the Actor, I store a Map of connected clients. Maybe I’ll need to look at Redis or something, any help would be greatly appreciated.

So in conclusion, I was able to quickly develop a working prototype, and I think I’ll try to use Play! 2.0 in several projects ;-)

What’s good:

  • Highly productive
  • Typesafe view templates based on Scala
  • LessCss integration
  • Akka integration
  • Compiled javascript with Google Closure Compiler
  • No need to learn Scala for the moment, hooray ! ;-)

To be improved:

  • The Scala compile times should be increased, because on my PC, it takes up to 4s to compile a view, and it breaks my flow (I use the “~run” command to gain 1s when switching from my IDE to my web browser)
  • The Scala compiler errors are cryptic
  • I cannot deploy the demo on Heroku because it does not support (yet ?) websockets

Update : A bit later, I discovered an article from @steve_objectify using similar technologies: http://www.objectify.be/wordpress/?p=341

About these ads

13 responses to this post.

  1. Posted by Otong on 11 April, 2012 at 8:40

    Hi, I’ve clone from git and then run by “play run”.
    But exception occurred

    [error] application -

    ! Internal server error, for request [GET /map] ->

    sbt.PlayExceptions$AssetCompilationException: Compilation error [Internal Closur
    e Compiler error (see logs)]
    at play.core.jscompile.JavascriptCompiler$.compile(JavascriptCompiler.sc
    ala:53) ~[na:na]

    whats happen anyway ?

    Reply

  2. Posted by Otong on 11 April, 2012 at 10:33

    java.lang.RuntimeException: java.lang.RuntimeException: com.google.javascript.js
    comp.deps.SortedDependencies$MissingProvideException: module$maptracker
    at com.google.javascript.jscomp.Compiler.runCallable(Compiler.java:629)
    at com.google.javascript.jscomp.Compiler.runInCompilerThread(Compiler.ja
    va:574)
    at com.google.javascript.jscomp.Compiler.compile(Compiler.java:556)
    at com.google.javascript.jscomp.Compiler.compile(Compiler.java:515)
    at com.google.javascript.jscomp.Compiler.compile(Compiler.java:497)
    at com.google.javascript.jscomp.Compiler.compile(Compiler.java:483)
    at play.core.jscompile.JavascriptCompiler$$anonfun$compile$1.apply$mcZ$s
    p(JavascriptCompiler.scala:44)
    at play.core.jscompile.JavascriptCompiler$$anonfun$compile$1.apply(Javas
    criptCompiler.scala:44)
    at play.core.jscompile.JavascriptCompiler$$anonfun$compile$1.apply(Javas
    criptCompiler.scala:44)
    at scala.util.control.Exception$Catch$$anonfun$either$1.apply(Exception.
    scala:110)
    at scala.util.control.Exception$Catch$$anonfun$either$1.apply(Exception.
    scala:110)
    at scala.util.control.Exception$Catch.apply(Exception.scala:88)
    at scala.util.control.Exception$Catch.either(Exception.scala:110)
    at play.core.jscompile.JavascriptCompiler$.compile(JavascriptCompiler.sc
    ala:44)
    at sbt.PlayCommands$$anonfun$36.apply(PlayCommands.scala:417)
    at sbt.PlayCommands$$anonfun$36.apply(PlayCommands.scala:417)
    at sbt.PlayCommands$$anonfun$AssetsCompiler$1$$anonfun$30.apply(PlayComm
    ands.scala:378)
    at sbt.PlayCommands$$anonfun$AssetsCompiler$1$$anonfun$30.apply(PlayComm
    ands.scala:376)
    at scala.collection.TraversableLike$$anonfun$flatMap$1.apply(Traversable
    Like.scala:200)
    at scala.collection.TraversableLike$$anonfun$flatMap$1.apply(Traversable
    Like.scala:200)
    at scala.collection.mutable.ResizableArray$class.foreach(ResizableArray.
    scala:60)
    at scala.collection.mutable.ArrayBuffer.foreach(ArrayBuffer.scala:44)
    at scala.collection.TraversableLike$class.flatMap(TraversableLike.scala:
    200)
    at scala.collection.mutable.ArrayBuffer.flatMap(ArrayBuffer.scala:44)
    at sbt.PlayCommands$$anonfun$AssetsCompiler$1.apply(PlayCommands.scala:3
    76)
    at sbt.PlayCommands$$anonfun$AssetsCompiler$1.apply(PlayCommands.scala:3
    63)
    at sbt.Scoped$$anonfun$hf5$1.apply(Structure.scala:476)
    at sbt.Scoped$$anonfun$hf5$1.apply(Structure.scala:476)
    at scala.Function1$$anonfun$compose$1.apply(Function1.scala:41)
    at sbt.Scoped$Reduced$$anonfun$combine$1$$anonfun$apply$11.apply(Structu
    re.scala:295)
    at sbt.Scoped$Reduced$$anonfun$combine$1$$anonfun$apply$11.apply(Structu
    re.scala:295)
    at sbt.$tilde$greater$$anonfun$$u2219$1.apply(TypeFunctions.scala:40)
    at sbt.std.Transform$$anon$5.work(System.scala:67)
    at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:22
    1)
    at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:22
    1)
    at sbt.ErrorHandling$.wideConvert(ErrorHandling.scala:18)
    at sbt.Execute.work(Execute.scala:227)
    at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:221)
    at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:221)
    at sbt.CompletionService$$anon$1$$anon$2.call(CompletionService.scala:26
    )
    at java.util.concurrent.FutureTask$Sync.innerRun(Unknown Source)
    at java.util.concurrent.FutureTask.run(Unknown Source)
    at java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
    at java.util.concurrent.FutureTask$Sync.innerRun(Unknown Source)
    at java.util.concurrent.FutureTask.run(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(Unknown Source
    )
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.lang.Thread.run(Unknown Source)
    Caused by: java.lang.RuntimeException: com.google.javascript.jscomp.deps.SortedD
    ependencies$MissingProvideException: module$maptracker
    at com.google.common.base.Throwables.propagate(Throwables.java:156)
    at com.google.javascript.jscomp.Compiler.processAMDAndCommonJSModules(Co
    mpiler.java:1396)
    at com.google.javascript.jscomp.Compiler.parseInputs(Compiler.java:1232)

    at com.google.javascript.jscomp.Compiler.parse(Compiler.java:678)
    at com.google.javascript.jscomp.Compiler.compileInternal(Compiler.java:6
    36)
    at com.google.javascript.jscomp.Compiler.access$000(Compiler.java:70)
    at com.google.javascript.jscomp.Compiler$1.call(Compiler.java:559)
    at com.google.javascript.jscomp.Compiler$1.call(Compiler.java:556)
    at com.google.javascript.jscomp.Compiler$2.run(Compiler.java:601)
    … 1 more
    Caused by: com.google.javascript.jscomp.deps.SortedDependencies$MissingProvideEx
    ception: module$maptracker
    at com.google.javascript.jscomp.deps.SortedDependencies.getInputProvidin
    g(SortedDependencies.java:120)
    at com.google.javascript.jscomp.JSModuleGraph.manageDependencies(JSModul
    eGraph.java:325)
    at com.google.javascript.jscomp.Compiler.processAMDAndCommonJSModules(Co
    mpiler.java:1389)
    … 8 more

    Reply

  3. Can I ask you where did you get the route for test simulation from? Is it hardcoded file or did you use some kind of API which returns geopoints when given start and stop locations? TIA

    Reply

  4. Posted by MR on 4 January, 2014 at 14:52

    Excellent work, however when i plug UK lon and lats into the test file the test doesn’t work….it doesn’t output any of the string messages…Id love to get the icon moving across a uk location….thanks

    Reply

Leave a Reply

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

WordPress.com Logo

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

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: