11.2. Making a clickable ZoomRaster

There is no doubt that one of the most impressive aspects of a Swarm presentation is the visually intriguing movement of agents on a landscape. The ability to stop a simulation from the control panel and then click on objects, open up displays that reveal their internal variables and allow them to be changed, is one of the main strengths of the Swarm library.

In order to introduce the way in which these ZoomRaster displays are created, we have to introduce a number of inter-linked Swarm toolkit items. Before we are done, we have to talk about objects created using the space library, as well as objects from the gui library. In the Arborgames simulation, there is a set of standard examples that quite nicely illustrate the vital steps. For emphasis, we now consider these elements individually.

  1. Create a ColorMap. Swarm was first developed for the Unix operating system. Programmers who have worked in X will probably already know that the X server offers a long list of possible colors. If we want to access those colors, we can build a ColorMap object in Swarm. After it is defined, and commands are executed which draw on the screen, then the ColorMap object will control which colors are displayed.

    Here is an excerpt from the file ForestObserverSwarm.m in which an object, called colormap, is created and then told to remember that the numbers 25, 26,and 27 refer to the colors "white","LightCyan1" and "PaleTurquoise" which is defined deep in the bowels of the video display.

    colormap = [Colormap create: [self getZone]];
    
    [colormap setColor: 25 ToName: "white"];
    [colormap setColor: 26 ToName: "LightCyan1"];
    [colormap setColor: 27 ToName: "PaleTurquoise"];

    ColorMaps can have fancier items. For example, colors need not be referred to by simple names. Rather, each color can be referred to in a numerical format. All colors can be referred to by the intensity of their red, blue, and green components, for example. If one needs to assign the many available shades of red to the numbers between 100 and 150, it can easily be done with commands that use the RGB format. There is an example of such a use of the ColorMap protocol in the heatbugs source code.

  2. Create a ZoomRaster. A ZoomRaster is a visual placeholder, a rectangular entity of a certain size. After trimming out some of the detail, the steps that create the ZoomRaster called forestRaster in Arborgames look like this:

    forestRaster = [ZoomRaster createBegin: [self getZone]];
    SET_WINDOW_GEOMETRY_RECORD_NAME (forestRaster);
    forestRaster = [forestRaster createEnd];
    
    [forestRaster setColormap: colormap];
    [forestRaster setZoomFactor: 4];
    [forestRaster setWidth: [forestModelSwarm getWorldSize] 
       Height: [forestModelSwarm getWorldSize]];
    [forestRaster setWindowTitle: "The Forest"];
    [forestRaster pack];

    This code should be viewed as foundation-building. The ZoomRaster object is created, and the macro SET_WINDOW_GEOMETRY_RECORD_NAME is executed. This means that, when the user clicks save on the control panel, the window position of the forestRaster object will be saved in a file in the user's account.

    To briefly summarize the effect of the other commands, we note the following. The fourth line tells the new raster object to use the colormap we just created. The fifth line controls the magnification of the display, which in this case is 4. The sixth line asks the forestModelSwarm object to report back the horizontal and vertical dimensions of the grid on which trees exist and then uses those to set the width and height of the ZoomRaster object's display. The eighth line gives the display window a name and the last line, which tells the forestRaster to pack itself, causes the display to be initialized according to the settings we just provided.

  3. Map a Swarm Space object onto the ZoomRaster. By itself, a ZoomRaster is just a nice looking set of edges around a blank background. In order to display things inside that window, we need to create a connection between the agents who live in the model swarm (and lower level swarms) and then display them in the observer swarm. This is done most commonly by telling each agent that it lives in a Swarm object known as a Grid2d. As the agent goes through its lifetime, one of its activities is to put itself at a position in the grid and then (possibly) erase itself from the old spot and put itself in the new spot.

    The Swarm protocol Object2dDisplay can handle the work of drawing the positions of agents in a Grid2d object on a ZoomRaster. In the Arborgames example, the forestRaster is used by an object called treeDisplay which connects the agents in the Grid2d to the graphical display.

    treeDisplay = [Object2dDisplay createBegin: [self getZone]];
    [treeDisplay setDisplayWidget: forestRaster];
    [treeDisplay setDiscrete2dToDisplay: 
    [[forestModelSwarm getTheForest] getTreeGrid]]; 
    [treeDisplay setObjectCollection:
    [[forestModelSwarm getTheForest] getTreeList]]; 
    [treeDisplay setDisplayMessage: M(drawSelfOn:)];
    treeDisplay = [treeDisplay createEnd];

    This example is slightly more complicated than most, because the Grid2d object is not retrieved directly from forestModelSwarm , but rather from another object that is defined in the forestModelSwarm . Except for that wrinkle, this is a standard example. The Object2dDisplay protocol is told to use the forestRaster as its "display widget." It is necessary to tell the treeDisplay which Grid2d to use, and this chore is accomplished by the setDiscrete2dDisplay command.

    Why the setObjectCollection message and the setDisplayMessage are used is interesting and important. The Object2dDisplay protocol has a method called display, which can be put in a schedule by the user. When the display method is executed, the treeDisplay (since it follows the Object2dDisplay protocol) will send each of the agent-objects in the Grid2d a message telling it to draw itself in the forestRaster. How does it tell the object to draw itself? We tell it how by passing it the selector for the agent-object's drawSelfOn: method. Each agent must be able to respond to a message of this sort:

    [anAgent drawSelfOn: aRaster];

    The program will crash if each agent that is positioned in the Grid2d is not able to respond to drawSelfOn:.

    The message setObjectCollection: [[forestModelSwarm getTheForest] getTreeList]] is not strictly necessary and the program will run without it. It may not run so quickly, however. Without this command, the treeDisplay will respond to the display message by searching in each possible position of the Grid2d and sending each object it finds the drawSelfOn: message. If the grid is large relative to the number of agents, then this might be a very slow process. The setObjectCollection method eliminates the need for treeDisplay to search through the whole grid. When the object collection is set, then the treeDisplay will simply go through the list of objects and tell each one to display itself.

  4. Tell the ZoomRaster Where to Send Mouse Clicks. A ZoomRaster object is highly self-aware. If you stop a simulation and right-click on an object, you may see a probe display pop up. That does not happen by magic, of course. It is necessary to tell the raster that, when there is a certain kind of click, that it is supposed to pass that click to some other object that knows what to do with it. That's why there is a command like this in the buildObjects phase:

    [forestRaster setButton: ButtonRight 
                     Client: treeDisplay 
                    Message: M(makeProbeAtX:Y:)];

    The treeDisplay is told to make a probe for the object that exists at a particular point in the grid.

  5. Schedule the Display. This is one of the aspects of Swarm that could use some standardization. In the schedule, one generally includes steps that erase the raster, then the Object2dDisplay is told to update itself by the display command, and then that display is drawn to the screen by telling the ZoomRaster to drawSelf.

    In a simple model, one in which we only have one ZoomRaster to update, then the schedule could be as simple. In the buildActions part of the code, one could create an ActionGroup like this:

    displayActions = [ActionGroup create: self];
    [displayActions createActionTo: forestRaster message: M(erase)];
    [displayActions createActionTo: treeDisplay message:M(display)];
    [displayActions createActionTo: forestRaster message: M(drawSelf)];

    (Of course, that action group has to be put into a schedule, which will probably execute it at each time point.) The buildActions method in arborgames is a bit more complicated than that since a large number of displays are managed.

  6. Make sure the Agents Put Themselves in the Grid! Inside the code that creates the individual agents who are to be drawn on the grid, one must be careful to accomplish two things. First, the drawSelfOn: method must be created. Second, if one wants to have a clickable ZoomRaster that allows agents to be probed, it is also vital to have the agents report their positions.

    It is fairly standard in Swarm models to manage this by creating a Grid2d object in the model swarm level and then, when an agent is created, use a setWorld method to notify the agent where it lives. In heatbugs, for example, each heatbug has a step method that controls how it searches for heat and moves to find a better spot. When it has decided where to go, the heatbug puts "nil" at it spot in the grid where it used to be and then it puts itself at the new coordinates. Here is the relevant code from Heatbug.m:

     [world putObject: nil atX: x Y: y];
    [world putObject: self atX: newX Y: newY];

    In the Arborgames example, the trees don't consciously move themselves. Rather, they are created and destroyed according to a set of rules that put them in a spot for a while. When a tree is created, it is added to the grid with this command that is in the Forest.m file:

    - addTree: aTree atX: (int) xVal Y: (int) yVal
    {
      [treeList addFirst: aTree];
      [treeGrid putObject: aTree atX: xVal Y: yVal];
      return self; 
    }

    Trees don't move (so far as we know), so we only need to update this tree's position arises when the tree dies. The Forest.m file creates a class of methods common to the different kinds of forests, and then the subclasses like MatureForest are created to provide additional detail. There are methods that remove a tree from the simulation and take it off the grid. The trees that are supposed to die are added to a Swarm list called the exitQ. For each timestep, the forest tells each kind of tree to do its step method, which adds trees to the exitQ list, and then the forest's step method removes those trees from the grid. In the MatureForest.m file,

    - step 
    {
      id aTree, index;
    
      [treeList forEach: M(step)];
    
      index = [exitQ begin: [self getZone]];
      while( (aTree = [index next]) )
       {
        [treeList remove: aTree];
        [treeGrid putObject: nil atX: [aTree getX] Y: [aTree getY]];
        [index remove];
        [aTree drop];
      }
      [index drop];
      return self;
    }