We draw some density pictures, improve some code, and recognize at least some of our flaws.

Visualizing the Pattern

It’s time to bring you up to date on the past couple of days’ work. Not too much code to report for the first day, though perhaps I’ll include what we wrote for your delectation. Our plan was to display a rectangular grid in gray-scale, to get a sense of the graphical reports we would present to our eventual clients.

We discussed two ways to go. The first was just to take one of our two existing programs for drawing picture and hack it into displaying the grid. The other was to refactor those two programs into better shape, and then proceed to add a third. We decided to write the third, because the refactorings we could see were trivial and didn’t lead to any truly better structuring of the program.

Our grid capability seemed to work well under both JUnit and FitNesse tests, and our cunning plan was just to plug it into a copy of our picture-drawing main program. I’ll just display here the resulting program, since nothing really interesting changed from the beginning to the end, although some interesting things did happen.

public class CreateShadedPatternImage {
    final int numberOfSquares = 20;
    final int rectangleWidth = 2048/numberOfSquares;
    final int rectangleHeight = 1536/numberOfSquares;

    final String folder = "Data\\";

    public static void main(String[] args) {
        CreateShadedPatternImage image = new CreateShadedPatternImage();
        image.doit();
    }

    public void doit() {

    RenderedImage rendImage = patternImage();

    try {
        File file = new File(folder+"shadedPatternImage.png");
        ImageIO.write(rendImage, "png", file);

        file = new File(folder+"shadedPatternImage.jpg");
        ImageIO.write(rendImage, "jpg", file);
    } catch (IOException e) {
    }
    }

    public RenderedImage patternImage() {
        int width = 2048;
        int height = 1536;

        BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);    
        Graphics2D g2d = bufferedImage.createGraphics();
        g2d.setColor(Color.white);
        g2d.fillRect(0, 0, width, height);
        // g2d.translate(1024, 768);
        // g2d.scale(1, -1);

        ShotPattern pattern = new ShotPattern(folder+"PB270011.bmp", 2048, 1536);

        grayBlobs(g2d, pattern);

        g2d.setColor(Color.RED);
        g2d.fillOval((int) pattern.centerOfMass().x()-15, (int)pattern.centerOfMass().y()-15, 30, 30);

        g2d.setColor(Color.RED);
        g2d.drawOval(1024-20, 784-20, 40, 40);
        g2d.drawOval(1024-10, 784-10, 20, 20);
        g2d.setStroke(new BasicStroke(3));
        g2d.drawLine(1024-25, 784, 1024+25, 784);
        g2d.drawLine(1024,784-25,1024,784+25);
        g2d.setColor(Color.BLACK);
        for (Hit hit: pattern.hits)
            g2d.fillOval((int)hit.x()-5, (int)hit.y()-5, 10, 10);

        g2d.dispose();    
        return bufferedImage;
    }

    private void grayBlobs(Graphics2D g2d, ShotPattern pattern) {
        RectangularGrid grid = new RectangularGrid(pattern, rectangleWidth, rectangleHeight);
        for ( int row = 0; row < numberOfSquares; row++)
            for (int col = 0; col < numberOfSquares; col++)
                grayBlob(g2d, grid, row, col);

    }

    private void grayBlob(Graphics2D g2d, RectangularGrid grid, int row, int col) {
        int count = grid.hitsIn(col, row);
        int color = color(count);
        g2d.setColor(new Color(color));
        g2d.fillRect(col*rectangleWidth, row*rectangleHeight, rectangleWidth, rectangleHeight);
    }

    private int color(int count) {
//      int gray = (255 - count*4 + count/2) & 0xFF;
        int gray = (255 - count*6) & 0xFF;
        int rgb = (gray << 16) | (gray << 8) | (gray << 0);
        return rgb;
    }

The new bits are highlighted above. We coded that up top down, by intention, and it went well except for some scaling issues and one amusing problem with calculating shades of gray.

The scaling came about from a problem we keep encountering. Some things are calculated in coordinates, x,y, you know the sort of thing I mean. And when drawing the display, we naturally think in rows and columns. It is clear to you, I’m sure, but confuses us that when we increase the horizontal row, that is an increment to y, and when we go across the columns, we must increment x. Somehow to us it seems that it ought to be the other way.

The result of getting that wrong is generally a striped pattern in the picture, as you increment something by 2048 that should have been a 1536, or the like. We haven’t figured out yet what we’re doing wrong, but we keep making that mistake.

More interesting was the color calculation, shown here:

   private int color(int count) {
//      int gray = (255 - count*4 + count/2) & 0xFF;
        int gray = (255 - count*6) & 0xFF;
        int rgb = (gray << 16) | (gray << 8) | (gray << 0);
        return rgb;
    }

When we’re hacking, it’s our practice to comment out lines that are nearly useful, as we try new things. This code above is not properly factored: it has two functions. It computes a desired level of gray, based on the number of hits in a cell. And then it computes an int that can be converted to a color in the drawing code shown before.

The on-line docs for that method said that the RGB values were each 0-255, and were packed into bits 0-24 of an int. Now I was raised in an era where bit zero was the high-order bit of a word or character, so I imagined that the code needed was

int rgb = (gray << 24) | (gray << 16) | (gray << 8);

That gave us this surprising picture:

image

Cheerfully sunny, perhaps, but not what we had in mind. With a bit more tweaking, we had what we found pretty pleasing, this one:

image

We like it. We may have to come up with a more sophisticated way of selecting the gray color, dependent on the actual data. The current one is just scaled to assume that there might be as many as 20 hits in a cell, and that would give a gray of 120/255, that is, about 50 percent. That works fine for now, but we may wish to adjust when we get some real data.

You’ll also notice that we changed the center of hit marker from a green oval to a small red circle, and that the target is now a cool pair of circles with a cross-hair over it. The former we think makes for a more understandable picture. The target we just did because it looked good.

Scaling ...

The next day, we talked about what to do and decided that we would bite the bullet and change the PatterningSheet to scale into the unit square. We’d just make the change, run the tests, and see what broke, and fix it. It has turned out to be surprisingly easy so far. We changed PatterningSheet to scale Hits to the range 0-1 in both dimensions:

   private Hit makeHit(int y, int x) {
        Hit hit = new Hit(x/widthInPixels(), y/heightInPixels());
        consumeAdditionalPointsInThisHit(x,y);
        return hit;
    }

Then we decided to let ShotPattern work in the unit square as well, with this change:

   public ShotPattern(String fileName, double widthInInches, double heightInInches) {
        PatterningSheet sheet = new PatterningSheet(fileName);
        double xPixelsPerInch = sheet.widthInPixels() / widthInInches;
        double yPixelsPerInch = sheet.heightInPixels() / heightInInches;
        this.hits = sheet.getHits();
        // convertHits(xPixelsPerInch, yPixelsPerInch);
    }

Just a few unit tests broke, and they were all easily changed. We took the easy way out. Since the Hits are now all in the 0-1 range, we changed the tests to think in terms of that range as well. It went like this:

   @Test
    public void grid00() {
        pattern = new ShotPattern(folder+"tm1subset.bmp", 4.0, 3.0);
        RectangularGrid grid2x2 = new RectangularGrid(pattern,1/4.0,1/3.0);
        assertEquals(1, grid2x2.hitsIn(0,0));
    }

    @Test
    public void grid32() {
        pattern = new ShotPattern(folder+"tm1subset.bmp", 4.0, 3.0);
        RectangularGrid grid2x2 = new RectangularGrid(pattern,1/4.0,1/3.0);
        assertEquals(7, grid2x2.hitsIn(3,2));
    }

    @Test
    public void grid02() {
        pattern = new ShotPattern(folder+"tm1subset.bmp", 4.0, 3.0);
        RectangularGrid grid2x2 = new RectangularGrid(pattern,1/4.0,1/3.0);
        assertEquals(2, grid2x2.hitsIn(0,2));
    }

    @Test
    public void grid30() {
        pattern = new ShotPattern(folder+"tm1subset.bmp", 4.0, 3.0);
        RectangularGrid grid2x2 = new RectangularGrid(pattern,1/4.0,1/3.0);
        assertEquals(4, grid2x2.hitsIn(3,0));
    }

The thinking is simple, and it is this: the Pattern is now always in the unit square. So if this test wants to think of the pattern as being dimensioned 4 by 3, his grid sizes want to be 1/4 by 1/3. Very simple, and those tests all ran.

We then looked at the other tests that had broken, which were the wedge tests. The resulting test code was this:

   @Test
    public void polarDensity() throws Exception {

        HitCollection hits = new HitCollection();

        hits.addHit(20,5); // Octant 0
        hits.addHit(25,5);

        hits.addHit(10,10); // 1
        hits.addHit(10,20);

        hits.addHit(-5,20); // 2

        hits.addHit(-10,5); // 3
        hits.addHit(-20,10);

        hits.addHit(-20,-5); // 4
        hits.addHit(-15,-10);
        hits.addHit(-20,-10);

        hits.addHit(-5,-20); // 5
        hits.addHit(-5,-25);

        hits.addHit(10,-20); // 6

        hits.addHit(20,-10); // 7

        hits.addHit(50,10); // 8
        hits.addHit(50,20);
        hits.addHit(30,10);

        hits.addHit(30,40); // 9
        hits.addHit(20,40);
        hits.addHit(10,50);

        hits.addHit(-10,50); // 10
        hits.addHit(-10,40);
        hits.addHit(-20,50);
        hits.addHit(-20,40);

        hits.addHit(-40,30); // 11
        hits.addHit(-50,20);
        hits.addHit(-40,10);

        hits.addHit(-50,-10); // 12
        hits.addHit(-40,-20);
        hits.addHit(-40,-30);

        hits.addHit(-30,-40); // 13
        hits.addHit(-20,-40);
        hits.addHit(-20,-50);
        hits.addHit(-10,-50);

        hits.addHit(10,-40); // 14
        hits.addHit(10,-50);
        hits.addHit(20,-40);
        hits.addHit(20,-50);
        hits.addHit(30,-50);

        hits.addHit(50,-10); // 15
        hits.addHit(40,-20);
        hits.addHit(40,-30);

        pattern = new ShotPattern(hits.hits());

        int[] densities = pattern.analyzePolar();
        assertEquals(2, densities[0]);
        assertEquals(2, densities[1]);
        assertEquals(1, densities[2]);
        assertEquals(2, densities[3]);
        assertEquals(3, densities[4]);
        assertEquals(2, densities[5]);
        assertEquals(1, densities[6]);
        assertEquals(1, densities[7]); 

        assertEquals(3, densities[8]);
        assertEquals(3, densities[9]);
        assertEquals(4, densities[10]);
        assertEquals(3, densities[11]);
        assertEquals(3, densities[12]);
        assertEquals(4, densities[13]);
        assertEquals(5, densities[14]);
        assertEquals(3, densities[15]);
    }

I left all that repetitive code there so that you can perhaps understand why we did as we did. We created a new constructor on ShotPattern that accepts the Hit list as an input. Since all the Pattern does with the list is process it as it stands, it was not necessary to scale this list to the unit square. The math, of course, works from -infinity to infinity, or thereabouts, so this test runs as it stands. That saved us from having to manually rescale all those values into the unit square to get the test to run.

However. This does reduce our confidence a bit, since we have to reason to the conclusion that this test is OK. So we’ll probably produce another test in the future, either another unit test, or perhaps a FitNesse acceptance test, since we haven’t done one of those yet.

Time did not permit us to refactor the FitNesse tests, most of which now fail because everything is scaled into the unit square. We’ll work on that next time.

Bottom Line -- Near Term

We rather quickly produced a gray-scale pattern that we think will make a good display to our clients, showing the overall pattern of shot on the target. The code for our displays now consists of three main programs with very similar contents. There’s a need to refactor those. We can do a simple job quite easily, and we want to think just a little bit about whether we should do some clever Model View Presenter or Visitor or other pattern, or whether we should just make the code clean and obvious. We’re leaning toward the latter but want to give cleverness a fair chance.

Converting to the unit square (thanks, Kelly Anderson) turned out to be very simple. The tests that broke took us to just the right places, and our existing structure was well enough factored that we only had to change a very few lines.

This supports our thesis that if we keep the code simple, clear, and with little duplication, future design changes will go in smoothly. Of course there is no proof that tomorrow’s change won’t ruin our lives in some way, but our experience so far leads us to think that we’ll be just fine.

And how could it be any other way? If a program has each object doing essentially one thing, and each concept is represented in just one object, what could happen to make a change difficult or time-consuming? Our answer is: nothing.

Changes are only difficult if the code embodies a concept that is not explicitly expressed, or has a concept that is spread all over the code instead of centralized. Good factoring avoids those characteristics, making the code easy to change when new things come up.1

Bottom Line -- Longer Term

We’re noticing that our code is more loose than we generally like to create and certainly more loose than we would recommend to the likes of you. Part of this is surely due to the fact that we’re working on an application that is more real than our usual toys. Part of it is probably due to the fact that we’re just learning the graphical tools available in Java, so our code is a bit closer to hackery.

We are trying to be mindful of the possibility that we are holding you lot to a higher standard than we should. We don’t think so. We acknowledge freely that we are not living up to our own standards at the moment, and we are bearing down just a bit in order to come back up to a place where we can be fully proud of what we’re doing. As we try to do that, we’ll keep reporting on the reality, not the fantasy, of software development.


  1. Thanks, George!