Map rendering using JPanel

Continuing the tiled map case study, we now look at how we might render a tiled map on a JPanel component of the Java swing windows API. We will cover the following topics:



Using inheritance to access the JPanel.

Let us consider creating a JPanelRenderer class to render a map on a JPanel. In the ConsoleRenderer class we know the console output device; it is defined by the System.out stream and is available to all our code. However we cannot know which JPanel to use for rendering our map since this is created by client code when it creates a user interface.

the JPanelRenderer will therefore have to have some mechanism to "know" which JPanel to use for its rendering. Following standard practice for Java swing development, define the JPanelRenderer class as inheriting from the JPanel class as well as implementing the MapRenderer interface.



   public final class JPanelRenderer extends JPanel implements MapRenderer
   {
      @Override
      public final void render (TiledMap aMap)
      {
         ... ... ...
      } // end render method.

   } // end JPanelRenderer class.

... but how do we draw on the panel?
In Java windows programming drawing normally takes place in the paintComponent(...) method that is invoked on a component repaint.

Remember that JPanelRenderer is also a JPanel component and so wil have this paint method that we can override to provide the rendering we require both for invocation via the render method and for window interactions by the user.

The render(TiledMap aMap) method that we expose through the MapRenderer interface stores the supplied TiledMap object and then calls repaint(); this then calls paintComponent(...) automatically.

We therefore override the paintComponent(...) method to perform the actual rendering of the map by drawing on the panel via its Graphics argument.

  1. Create the file 'JPanelRenderer.java' with the code outlined above for the JPanelRenderer class.
  2. Define a private TiledMap field called mRenderer.
  3. Since JPanelRenderer inherits from JPanel it must reflect the behaviour of this component, in this case by defining a constructor that allows client code to define the size of the component.
    Here is the constructor to add to the class; ensure you add the code that is commented for you to complete.
       //--- constructor method.
       //--- width and height parameters are in pixels.
       //--- If supplied width or height arguements <0 then 0 is used for that dimension.
       public JPanelRenderer (int aWidth, int aHeight)
       {
          //TODO validate the arguments are >0 and set to 0 if not.
    
          setPreferredSize (new Dimension (aWidth, aHeight));
    
          mMap = null;
       } // end constructor method.
    

  4. Code the render method as follows:
       @Override
       public final void render (TiledMap aMap)
       {
          mMap = aMap;
          repaint();
       } // end render method.
    

    When called, this method does what we identified above; it remembers the map, calls repaint() which will then call paintComponent(...).
  5. Add the following method to the class:
       @Override
       protected void paintComponent (Graphics g)
       {
          super.paintComponent (g);
    
          // we will add map rendering code here.
       } // end paintComponent method.
    
    

  6. In the paintComponent(...) method, add validation code so that if the mMap field is null a message is displayed on the panel using g.drawString(...) and the paintComponent(...) method returns.

    We will complete the rest of the rendering code in the following sections.


Map and tile representation.

The map must be represented on the panel in some manner and one approach is to consider the panel to be a grid of squares; this grid being the size of the map to be rendered. Each square area can then be filled with a colour representing the terrain tile type of the corresponding position on the map.

  1. Review the java.awt.Color class reference.
    • this is the simplest representation of colours and the one we shall use here.
    • Although limited in its directly exposed colour palette of defined values, the use of brighter() and darker() methods can provide a wider selection of colours. For example, Color.RED.darker().darker() provides us with a Color value distinct from Color.RED.
    • Note that this is the Color class we are looking at here; not to be confused with the java.awt.color namespace (only difference is the capital 'C')!
    • We shall use the British spelling of colour in the documents and code we write but remember that Java uses the American spelling.
  2. Determining the colour to use for a terrain tile could be handled by a switch statement in the rendering engine but, as for the console rendering character and passable indicator, we can associate the colours to use in the Terraintype enumeration rather than having to hard-code it in the render method.

    Amend the Terraintype enumeration to expose the asColour() method that returns the java.awt.Color value to use for a terrain type value.

    this is done in the same way as for the asChar() method introduced for accessing rendering characters for the console.


Drawing the map.

To summarise where we are, we know that

Two problems remain however before we can start implementing the drawing code.

the first problem is that the panel has a different coordinate system than the map. We consider the map to have its origin (0,0) at the bottom left with positive X and Y coordinates going to the right and up respectively.
The panel, however, has its origin at the top left with positive Y coordinates going down.
Therefore a transform will have to be applied to the map coordinates in order to draw the map the correct way up.

the second problem is that the panel size is in pixels and will be set by client code in order to fit in with their desired user interface. This size may not be large enough for the map to be displayed at all (even with 1 pixel per tile); it may require multiple pixels to be drawn for each tile; and/or it may not have the same aspect ratio as the map but we will still wish to retain this aspect ratio and render our tiles as squares of colour, not rectangles.
Therefore, prior to drawing, the panel size will require to be checked and a resolution (pixels per tile side) determined .

In the paintComponent(...) method we already have code to validate that a map is available to render; now do the following in this method:

  1. Add code to verify if the map is too large for the panel in either width or height dimension; if either, then use g.drawString(...) to indicate this on the panel and return immediately.
    Use getWidth() and getHeight() to retrieve the JPanel dimensions (since the JPanel is this object).
  2. Compute a resolution for the map when it is displayed on this panel; the resolution is the integer number of pixels per side of one tile (integer since we cannot draw parts of a pixel!).

    We should maintain the square aspect ratio of a tile but the map may not be square, the JPanel may not be square, and the map & JPanel may have different aspect ratios.
    We can determine a resolution that maintains the square aspect ratio of a tile while maximising the map size within the available JPanel size.
    1. Compute the number of pixels per tile in a horizontal direction.
    2. Compute the number of pixels per tile in a vertical direction
    3. the resolution is the smaller of these two values.
  3. We are now in a position to add the actual drawing code.
    1. Use a nested for loop as usual to iterate through the tiles of the map at their (x,y) positions.
    2. for a specific map tile, use g.setColor(...) to specify the colour to use; the actual colour value can be retrieved from the tile at (x,y) on the map through our previously defined asColour() method.
    3. The tile square is drawn using g.fillRect (...); this takes 4 arguments:
      • X-coordinate of top left corner of the rectangle;
      • Y-coordinate of top left corner of the rectangle;
      • number of pixels wide - this will be our resolution value;
      • number of pixels high - this will also be our resolution value since we are drawing a square.

      You will have to do some simple arithmetic to calculate the (X-coordinate, Y-coordinate) of the top left of the square to be drawn from the current (x,y) position of the tile in the map.
      Remember that (0,0) is the bottom left of our map but the top left of our panel; and each tile will be drawn as a square of resolution pixels so affecting the coordinates on the panel of the next tile to be drawn.
      So, assuming a map of size 100x50 and a resolution of 5 pixels per tile side, the tile at position (2,3) in our map will be drawn at position (10, 230) on our panel.
      • X-coordinate on the panel is the map x position * the number of pixels per tile side;
      • Y-coordinate on the panel is the number of pixels for the entire map height minus the map y position(+1) * number of pixels per tile side.

      Some paper and pen will help in working this out!


Client usage.

We will now look at a simple client that displays a map in a window.

  1. Get the application executable and client source code:
    1. Download the '6_map.jar' archive file.
      this is an executable archive file that also contains the source code for the main application only.
    2. Execute the application using the command
       java -jar 6_map.jar 
      and review the result.
    3. Use the command
       jar xf 6_map.jar SimpleMap.java 
      to extract the file 'SimpleMap.java' that is the main application.
  2. Review the source code in the 'SimpleMap.java' file identifying the structure and functional components.
  3. Walkthrough the execution of the main(...) method as the application entry point.
    You will find it useful to print out a copy of the program so that you can annotate it as you walk through the execution path.
    The core execution path is that main(...) will
    • call the createMap() method to create and return a map as described in the comments;
    • add a new thread to the windows event dispatch thread (EDT) using an inner Runnable class that
      • calls the createWindowAndRender() method that
        • creates the window as a JFrame containing a JPanel, this JPanel actually being an instance of jPanelRenderer;
        • sets the JPanelRenderer as the map's rendering engine since JPanelRenderer is also a MapRenderer;
        • calls the render() method of the map object, this
          • calls the render(map) method of the JPanelRenderer object that
            • sets the mMap field of the JPanelRenderer object;
            • calls repaint() that
              • calls the overridden paintComponent(...) method defined in the JPanelRenderer object.
                this method is passed the Graphics context for the JPanel,
                and has access to the map through the mMap field,
                ... and so can finally draw the map !!!

    WOW - appears complex but this is all the different components of our case study working together at execution time.
    At development time we designed and implemented the components separately, only considering the issues relevant to the single component we were developing at that time.
    What we see here is the complexity introduced by combining simpler components together, a common attribute of all software systems.
  4. Verify that the code you have developed over this and previous chapters works by plugging the supplied 'SimpleMap.java' application into your own tiled map model. You should have the following files that you have created yourself:
    • ConsoleRenderer.java
    • ITiledMap.java
    • JPanelRenderer.java
    • MapRenderer.java
    • TiledMap.java
    • Terraintype.java
    You will have created alternative Terraintype definitions, for this exercise use the file that defines the outdoor landscape with grass, trees, water, etc.
    1. If you have adopted the names outlined in previous chapters these classes should be compatible with the code of 'SimpleMap.java'; if not, then amend the client code to be compatible with your classes.
    2. Compile the client 'SimpleMap.java' with your tiled map model files; execute and check for correct operation.
  5. You can use the 'SimpleMap.java' client as a template for your own applications although more complex user interfaces may require a slightly different approach. There are many resources on the internet that provide such templates.

    Create an application that creates a map of all possible combinations of terrain tile types and then renders this on a panel.
    • Note that TerrainType.values() returns a Terraintype[] array of all possible enumeration values; this gives you access to all the values you need.
    • Download the '6_colours.jar' executable archive as an example of such an application (note no source code is archived, only the .class files).
      You can execute this with the command
       java -jar 6_colours.jar 
    • This application can be used as a tool to check that colours assigned to terrain tiles work together when you are designing a new terrain context.