Reactive Swing and Observables Pt 6
tom, 2012-02-14
Updated: 2013-01-11

This is a simple library for drawing and interacting with grids. It’s meant to serve as the basis for a visualization testbed for various graph searches, but it’s got a bunch of uses beyond that. One of them is to get some practical experience with Swing, and in particular, to test out the Reactive extensions I ported/added via Clojure.

You’ll notice a dependency on cljgui.gui. This is a gui library I have been building around examples in the Joy Of Clojure by Michael Fogus and Stuart Sierra. They define some nice primitive combinators for Swing components, which I use at the end of this post.

(ns cljgui.grid
  (:use [cljgui.gui]
        [cljgui.events base observe native])
  (:import [java.awt Graphics  Graphics2D Polygon Point Rectangle Shape Color]
           [java.awt.geom AffineTransform Point2D Rectangle2D Line2D]
           [java.awt.image BufferedImage ImageObserver]))

As a reminder, the grid component is intended to serve as a testbed for visualizing various graph algorithms, specifically shortest paths, etc.

What I’d like to do is steal some inspiration from Matt Buckland, and use his excellent examples in his Game AI book.

Matt’s world exists largely in a grid. He breaks up a view into nxn tiles. He then defines sets of polygons that fill said world, and act as boundaries for any path-finding algorithm.

Coordinates in the grid-world are actually centered on a tile. This only really matters for drawing purposes.

We define a few supplementary functions (which will probably get moved to/hidden in the cljgui.gui namespace later.

(defn make-iobserver []
  (proxy [ImageObserver] []
     (imageUpdate []
        (proxy-super imageUpdate))))

(def null-observer (make-iobserver))

(defn get-grid [g x y]
  (get g [x y]))

(defn draw-line 
  ([g x1 y1 x2 y2]
    (.. g (drawLine x1 y1 x2 y2)))
  ([g [x1 y1 x2 y2]]
    (draw-line g x1 y1 x2 y2)))

A grid is basically decomposed into….N horizontal lines and N vertical lines offset by Height/N, Width/N. Ideally we force the grid to be a square so tiling is easy. Another way to look at it is to draw N2 rectangles. This might be desirable, because it lets us use compound primitives, but you have to draw more rectangles than lines.

We define some primitive drawing operations and functions to get coordinates that define the lines in our grid:

(defn make-rect [x1 y1 x2 y2] 
  (java.awt.geom.Rectangle2D$Double. x1 y1 x2 y2))

(defn draw-rect [g x1 y1 x2 y2]
  (.. g (draw (make-rect x1 y1 x2 y2))))

(defn get-lines [width height n]
  (let [vs (for [i (range n)] [(* i (int (/ width n))) 0 
                                   (* i (int (/ width n))) height])
        hs (for [i (range n)] [0 (* i (int (/ height n))) width 
                                  (* i (int (/ height n)))])]
    (concat vs hs)))                         

(defn draw-lines [g width height n]
  (let [w (min width height)]
    (.setColor g Color/BLACK)
    (doseq [[x1 y1 x2 y2] (get-lines w w n)]
      (.drawLine g x1 y1 x2 y2))))

Given a desired width and a tile-width, we can create grid components using new-grid. I shamelessly borrowed Lau Jensen’s excellent example for buffered drawing in Swing from Brian’s Functional Brain. It turns out buffered drawing (to an imagebuffer) is very common in Swing. I also implemented a paintpanel function (not displayed) that Proxies a JPanel, and overrides it’s paint method to paint a static picture. In this case, canvas is a paintpanel that’s painting from the imagebuffer. That way, we get a constant nxn grid, even when the frame is resized or moved.

(defn new-grid [width n] 
    (let [buffer  (make-imgbuffer width width)
          bg (.getGraphics buffer)
          paint (fn [g] (clear-background bg Color/WHITE width width)
                        (draw-lines bg width width n)
                        (.drawImage g buffer 0 0 nil))
          canvas (paintpanel width width paint)]
      canvas))

Finally, we have a little swing app the displays the grid with a label that reacts to the current mouse coordinates, whenever a mouse button is pressed.

(defn grid-app []
  (let [g (new-grid 500 50)
        gridevents (mouse-observer g)
        lbl (label "Current Coordinate: 0 0")
        mousedown (->> (-> gridevents :dragged)
                     (map-obs event-data)
                     (map-obs (fn [data] [(:X data) (:Y data)]))
                     (map-obs (fn [[x y]] (format "Current Coordinate: %s %s"
                                                  (str x) (str y)))))
        changelabel (->> mousedown
                      (subscribe (fn [newlabel] (.setText lbl newlabel))))]
    (display (empty-frame)
       (splitter
         (stack g)
         (stack lbl)))))

We use (mouse-observer g) to get a map of mouse events from the grid g (a JPanel). Again, this is a map of {:eventname (observable)} for each mouse event associated with the grid. We easily grab the event corresponding to mouse dragging by getting the :dragged key from gridevents. The dragged event is then threaded through a sequence of combinators from the the observable library.

map-obs takes a function of a single argument and conceptually “maps” it against the observed value of an observable. In this case, we use the convenient event-data function, from the IEvent protocol in cljgui.events.base, in which all of our Java events participate, to get a simple map of all the data associated with the dragged event. In this case, it’s all of the public fields in the java.awt.MouseEvent, rendered into a simple map.

Two of the fields in MouseEvent are the x and y coordinates of the mouse’s position when the event was triggered. We extract them using keywordized versions of their property names – :X and :Y. Notice how the mapping looks very similar to the data-flow style of transforms we commonly apply to sequences, via map, filter, reduce, etc. Finally, we map a function to the intermediate observable containing the extracted mouse coordinates, which takes the mouse coordinates and converts them into an observable string reporting where the mouse is.

At this point, mousedown is bound to this final observable….it is, in essence, a completely new Event that can be subscribed to, or further composed with other events. changelabel is the side-effected result of subscribing a function that updates the label’s text value in response to mousedown events.

With wiring out of the way, we call supplementary functions from the aforementioned cljgui.gui namespace: display, splitter, and stack. display composes the contents of its second argument (which turn into a composite JPanel), and attaches it an empty JFrame for rendering. splitter defines a JPanel that contains two other JPanels, vertically juxtaposed. Each sub-panel is laid out using a vertical stack layout. One contains our grid and the other contains the reactive string label.

If you run the app, and click the mouse over the grid, it’ll update the mouse coordinates in the string label in real-time.