≡ Menu

Smart Grids, part seacht: jQuery and Java2D

In Part 6 of the smart grids series, we saw how to create a web service with RestEasy to provide data to a web-based client (and we also saw how to create some data for that service.)

In this chapter, we’re going to create a web page that uses that data to display our data, which has two parts: the web page itself, and a servlet that creates an image to represent the data point.

Setting the table

Before we start coding our user interface, we should think about what our user interface is actually doing. This is our “requirements” section, and determines what our measure for success is.

We are gathering light levels associated with geolocations, remember. So the “geolocations” part makes me think of a map; our user interface should be map-based.

The light levels, well… that’s a little more interesting. We could just display a number representing the percentage of light sensed, but that’s boring. I’d love to display a light bulb instead, that would glow brighter as the light level went up.

That’s beyond my graphics skills, though, so I’m going to go for a simple yellow dot: the brighter the yellow, the higher the light level. (If anyone wants to help create a better image, please do; this is serviceable but not fantastic, which fits my abilities when it comes to visualization.)

The HTML Page

Our map will be based on Google’s Map API. There are others we could use, and this shouldn’t be considered a blanket endorsement of Google Maps, but it works.

Our HTML page will be pretty simple. We will be able to think of a lot of enhancements as time goes by, and we might put some into place but our first revision will be very simple.

We’re going to put our user interface in “data.html“.

We could set this to be our default page (we could name it “index.html“, for example, or set it to be a default welcome page) but there’re good reasons not to, at first.

One reason: if it’s our welcome-page, we can’t conveniently vet any userdata. What if we want to capture a geolocation for the map? What if we want to restrict user access? In a welcome-page, all this can be done, of course, but it’s not as convenient.

So for now “data.html” wins. So let’s dive in.

The header is preparation for the body text of the HTML. We want to include two scripting libraries; one is Google Maps, of course:

<script type="text/javascript"
    src="http://maps.googleapis.com/maps/api/js?sensor=false">
</script>

We should include a Google API key: we’d do so by appending “&key=YOUR_KEY” to the URL, but I’m not doing it here.

The sensor=false tells Google that we’re not using a GPS sensor on the requesting application. This is a Good Thing, since we don’t have a GPS location with which to work for this test page.

The next thing we’ll include in our UI will be jQuery, a well-known and well-documented JavaScript library that makes the interaction with our services very easily accomplished. (I’m not a JavaScript guru by any measure, so making things easy for me is very important.)

We are going to use a content delivery network (in this case, Google!) to acquire jQuery, so we don’t have to include it as source with our application. The <script> code will look like this:

<script type="text/javascript"
    src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js">
</script>

We’ll also include a simple style to set our map to take up the whole browser window:

#map_canvas {
    height: 100%;
    width: 100%;
    background-color: silver;
}

There are two tags in our HTML body: one is a placeholder, a <div> tag, and the other is another JavaScript body that actually serves as our user interface application.

The <div> is painfully simple:

<div id="map_canvas" style="width:100%; height:100%"></div>

It’s the <script> where things get interesting.

var locations = {};
var map;

First, we have a few variable declarations. The map is going to hold a reference to (surprise!) the Google Map object; locations is used to help us explicitly discard old locations. (As they’re updated, they’re replaced; we’re explicitly setting the reference to null because I don’t trust the JavaScript garbage collector much.)

The next thing in our script is a function, called initialize(). It initializes the map variable.

function initialize() {
    var mapOptions = {
        center:new google.maps.LatLng(35.773371, -78.67743),
        zoom:8,
        mapTypeId:google.maps.MapTypeId.TERRAIN
    };
    map = new google.maps.Map(document.getElementById("map_canvas"),
            mapOptions);
}

We have one other function in this block, called updateMap(). This is the workhorse of our application.

It is designed to be called repeatedly (thus it stores its state in locations).

What it does: it uses jQuery to issue calls to our Provider service via AJAX. It then uses the list of DataPoints the service returns to create a set of Markers on the map, using an image URL for the marker icon.

function updateMap() {
    $.ajax(
        {
            url:'provider/data',
            success:function (data) {
                var tsData = eval(data);
                for (id in tsData) {
                    var myLatLng = new google.maps.LatLng(
                            tsData[id]['latitude'],
                            tsData[id]['longitude']);
                    if (locations[id] != null) {
                        locations[id].setMap(null);
                        locations[id] = null;
                    }
                    var marker = new google.maps.Marker({
                        position:myLatLng,
                        title:id
                    });
                    marker.setMap(map);
                    marker.setIcon("./marker?stroke=" +
                            tsData[id]['level'] +
                            "&maxLevel=" +
                            tsData[id]['maxLevel']);

                    locations[id] = marker;
                }
            }
        }
    );
}

Finally, we include some code to call the initialize() method, and start calling updateMap every second:

initialize();
setInterval(function () {
    updateMap()
}, 1000);

However, without the service that generates the icon (with the marker.setIcon() call), the application’s not very impressive yet: let’s create that service, and then take a look at the output.

The Image Generator

The image generator will be a servlet. (Why? Because it’s technology I’m familiar with, of course; I’m sure JavaScript wizards could do it in HTML5 without a problem.)

The process is actually pretty simple, but the code is … interesting, far more complex than the problem statement might indicate.

The pseudocode is really simple:

  • Parse the request to determine the light level to be rendered.
  • Copy a transparent background to a new image.
  • Draw a circle with the light level on the transparent image’s copy.
  • Render the image to the output stream.

Since we’re working with a servlet, let’s create the class (MarkerServlet) and map it to “/marker“:

package com.redhat.osas.sensor.web;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;

@WebServlet("/marker")
public class MarkerServlet extends HttpServlet {
}

We see from our pseudocode that we need a convenient way to parse request information, and we also need a transparent image to copy around! (We need to copy it around because the process of setting an image to be transparent was computationally heavier than it really could be.)

So let’s create a few static methods in our MarkerServlet to provide these capabilities for us:

private static int parseInteger(HttpServletRequest request,
                                String name,
                                int defaultValue) {
    try {
        return Integer.parseInt(request.getParameter(name));
    } catch (Exception ignored) {
        // falling through will return the default value
    }

    return defaultValue;
}

private static Image buildTransparentImage() {
    BufferedImage image = new BufferedImage(800, 800,
            BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2 = (Graphics2D) image.getGraphics();
    // force the background to be white...
    g2.setBackground(Color.WHITE);
    g2.clearRect(0, 0, 800, 800);

    return makeColorTransparent(image, Color.WHITE);
}


private static Image makeColorTransparent(final BufferedImage im,
                                          final Color color) {
    final ImageFilter filter = new RGBImageFilter() {
        // the color we are looking for (white)...
        // Alpha bits are set to opaque
        public final int markerRGB = 0xFFFFFFFF;

        public final int filterRGB(final int x,
                                   final int y,
                                   final int rgb) {
            if ((rgb | 0xFF000000) == markerRGB) {
                // Mark the alpha bits as zero - transparent
                return 0x00FFFFFF & rgb;
            } else {
                // nothing to do
                return rgb;
            }
        }
    };

    final ImageProducer ip = new FilteredImageSource(im.getSource(),
            filter);
    return Toolkit.getDefaultToolkit().createImage(ip);
}

We could put these in a utility class, but I’m not doing a whole lot of servlet code in this application, so they’re probably okay in MarkerServlet; if I do more images, I will break them out into a utility class.

Now we can write the rest of the servlet code:

Image transparentImage;

public MarkerServlet() {
    transparentImage = buildTransparentImage();
}

protected void doGet(HttpServletRequest request, 
                     HttpServletResponse response) 
    throws ServletException, IOException {
    int stroke = parseInteger(request, "stroke", 0);
    int maxLevel = parseInteger(request, "maxLevel", 255);
    int size = parseInteger(request, "size", 20);

    if (size > 799) {
        System.err.println("Size of image has been truncated to 800x800");
        size = 799;
    }

    float shade = stroke / (maxLevel * 1.0f);

    BufferedImage image = drawCircle(size, shade);

    response.setContentType("image/png");
    OutputStream out = response.getOutputStream();
    ImageIO.write(image, "png", out);
    out.close();
}

private BufferedImage drawCircle(int size, float shade) {
    BufferedImage image = new BufferedImage(size, size, 
                                            BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2 = (Graphics2D) image.getGraphics();
    g2.drawImage(transparentImage, 0, 0, null);

    // we're going for yellows here
    g2.setColor(new Color(shade, shade, 0));
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
                        RenderingHints.VALUE_ANTIALIAS_ON);
    g2.fillOval(0, 0, size, size);
    return image;
}

Mostly grunt code here: we parse the request parameters and set up the shade of the image; drawCircle() does the creation of the new image, copies the transparent background to it, and creates a (nontransparent, yellowish) circle on the image. ImageIO is used to write the response as an image.

PNG is used instead of JPEG mostly because OpenJDK supports PNG whereas it does not support JPEG due to licensing issues. That’ll surely be solved sometime in the future, and if you’re using the Oracle JDK instead of OpenJDK you can use JPEG if you’d like, but there’s really no need.

Tying it together

So we have the parts: we have the service that produces information, we have an HTML page that consumes that information, and we have an image generator that presents the data such that it’s all pretty.

Let’s tie them together. Our web application can be built very easily, from the top-level Maven project:

mvn package

(Note that you’ll want to have the Android toolkit installed to build the project as it stands in the source tree; we haven’t gotten to Android in the series yet – that’s coming in the next section – but we still want to see the pretty pictures. If you need to, just comment out the “<module>android-sensor</module>” line in the pom.xml.)

Deployment is simple as well:

cp sensor-web/target/sensor-web.war $JBOSS_HOME/standalone/deployments

After we’ve deployed successfully, we can open up http://localhost:8080/sensor-web/data.html, and we should see something that looks like this:

Now, it may not look very exciting yet, but we’ve really done a heck of a lot.

Once we create something that sends live data in, we’ll actually have an end-to-end solution, and we can start to go about making it highly available.