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 N\^2 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.