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)))))
(mouse-observer g) to get a map of mouse events from the grid
g (a JPanel). Again, this is a map of
for each mouse event associated with the grid. We easily grab the
event corresponding to mouse dragging by getting the
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
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 –
how the mapping looks very similar to the data-flow style of
transforms we commonly apply to sequences, via
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
With wiring out of the way, we call supplementary functions from the
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.