1
\$\begingroup\$

I have this generic "matrix" holding elements of arbitrary type. It supports any horizontal or vertical rotations (of any step size) in \$\mathcal{O}(1)\$. See what I have:

package net.coderodde.util;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;

/**
 * This class implements a generic, fixed-sized matrix of generic elements. It 
 * supports any rotations in constant time.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Apr 5, 2016)
 */
public final class RotableMatrix<E> {

    private final Object[][] matrix;
    private final int width;
    private final int height;

    private int cursorX;
    private int cursorY;

    public RotableMatrix(int width, int height) {
        checkWidth(width);
        checkHeight(height);

        this.width  = width;
        this.height = height;
        this.matrix = new Object[height][width];
    }

    public void rotateUp() {
        rotateUp(1);
    }

    public void rotateRight() {
        rotateRight(1);
    }

    public void rotateDown() {
        rotateDown(1);
    }

    public void rotateLeft() {
        rotateLeft(1);
    }

    public void rotateUp(int steps) {
        if (steps < 0) {
            rotateDown(-steps);
            return;
        }

        cursorY = (cursorY + steps) % height;
    }

    public void rotateRight(int steps) {
        if (steps < 0) {
            rotateLeft(-steps);
            return;
        }

        cursorX = (cursorX - steps) % width;

        if (cursorX < 0) {
            cursorX += width;
        }
    }

    public void rotateDown(int steps) {
        if (steps < 0) {
            rotateUp(-steps);
            return;
        }

        cursorY = (cursorY - steps) % height;

        if (cursorY < 0) {
            cursorY += height;
        }
    }

    public void rotateLeft(int steps) {
        if (steps < 0) {
            rotateRight(-steps);
            return;
        }

        cursorX = (cursorX + steps) % width;
    }

    public E get(int x, int y) {
        checkXCoordinate(x);
        checkYCoordinate(y);
        return (E) matrix[(y + cursorY) % height][(x + cursorX) % width];
    }

    public void set(int x, int y, E element) {
        checkXCoordinate(x);
        checkYCoordinate(y);
        matrix[(y + cursorY) % height][(x + cursorX) % width] = element;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();

        for (int y = 0; y < height; ++y) {
            for (int x = 0; x < width; ++x) {
                sb.append(get(x, y));

                if (x < width - 1) {
                    sb.append(" ");
                }
            }

            if (y < height - 1) {
                sb.append("\n");
            }
        }

        return sb.toString();
    }

    private void checkWidth(int width) {
        checkIntegerIsNoLessThan(width, 
                                 1, 
                                 "The matrix width is too small (" + width + 
                                 "). Must be at least 1.");
    }

    private void checkHeight(int height) {
        checkIntegerIsNoLessThan(height,
                                 1,
                                 "The matrix height is too small (" + height +
                                 "). Must be at least 1.");
    }

    private void checkIntegerIsNoLessThan(int test,
                                          int lowerBound, 
                                          String errorMessage) {
        if (test < lowerBound) {
            throw new IllegalArgumentException(errorMessage);
        }
    }

    private void checkXCoordinate(int x) {
        if (x < 0) {
            throw new IndexOutOfBoundsException(
                    "The x-coordinate is negative: " + x);
        }

        if (x >= width) {
            throw new IndexOutOfBoundsException(
                    "The x-coordinate is too large: " + x + ". The width of " +
                    "this matrix is " + width);
        }
    }

    private void checkYCoordinate(int y) {
        if (y < 0) {
            throw new IndexOutOfBoundsException(
                    "The y-coordinate is negative: " + y);
        }

        if (y >= height) {
            throw new IndexOutOfBoundsException(
                    "The y-coordinate is too large: " + y + ". The height of " +
                    "this matrix is " + height);
        }
    }

    private static final class MyCanvas extends Canvas {

        private final RotableMatrix<Color> matrix;

        MyCanvas(RotableMatrix<Color> matrix) {
            this.matrix = matrix;
            this.setPreferredSize(new Dimension(matrix.getWidth(), 
                                                matrix.getHeight()));
            this.addKeyListener(new KeyListener() {
                @Override
                public void keyTyped(KeyEvent e) {
                    switch (e.getKeyChar()) {
                        case 'w':
                        case 'W':
                            matrix.rotateUp(10);
                            repaint();
                            break;

                        case 'd':
                        case 'D':
                            matrix.rotateRight(10);
                            repaint();
                            break;

                        case 's':
                        case 'S':
                            matrix.rotateDown(10);
                            repaint();
                            break;

                        case 'a':
                        case 'A':
                            matrix.rotateLeft(10);
                            repaint();
                            break;
                    }
                }

                @Override
                public void keyPressed(KeyEvent e) {}

                @Override
                public void keyReleased(KeyEvent e) {}
            });
        }

        @Override
        public void update(Graphics g) {
            BufferedImage image = new BufferedImage(matrix.getWidth(),
                                                    matrix.getHeight(),
                                                    BufferedImage.TYPE_INT_RGB);

            for (int y = 0; y < matrix.getHeight(); ++y) {
                for (int x = 0; x < matrix.getWidth(); ++x) {
                    image.setRGB(x, y, matrix.get(x, y).getRGB());
                }
            }

            g.drawImage(image, 0, 0, this);
        }

        @Override
        public void paint(Graphics g) {
            update(g);
        }
    }

    private static final int WIDTH  = 1000;
    private static final int HEIGHT = 1000;

    public static void main(String[] args) {
        Random random = new Random();
        RotableMatrix<Color> matrix = createColorMatrix(WIDTH, HEIGHT, random); 
        JFrame frame = new JFrame();
        JPanel panel = new JPanel();
        MyCanvas canvas = new MyCanvas(matrix);

        canvas.setPreferredSize(new Dimension(1000, 1000));
        panel.add(canvas);
        frame.getContentPane().add(panel);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(false);
        frame.setVisible(true);
    }

    private static RotableMatrix<Color> createColorMatrix(int width, 
                                                          int height,
                                                          Random random) {
        RotableMatrix<Color> matrix = new RotableMatrix<>(width, height);

        matrix.set(0, 0, createRandomColor(random));

        for (int y = 1; y < height; ++y) {
            matrix.set(0, y, createRandomColor(random, matrix.get(0, y - 1)));
        }

        for (int y = 0; y < height; ++y) {
            for (int x = 1; x < width; ++x) {
                Color previousColor = matrix.get(x - 1, y);
                matrix.set(x, y, createRandomColor(random, previousColor));
            }
        }

        return matrix;
    }

    private static Color createRandomColor(Random random) {
        return new Color(random.nextInt(256),
                         random.nextInt(256),
                         random.nextInt(256));
    }

    private static Color createRandomColor(Random random, Color previousColor) {
        int prevRed   = previousColor.getRed();
        int prevGreen = previousColor.getGreen();
        int prevBlue  = previousColor.getBlue();

        int nextRed   = prevRed   + random.nextInt(17) - 8;
        int nextGreen = prevGreen + random.nextInt(17) - 8;
        int nextBlue  = prevBlue  + random.nextInt(17) - 8;

        return new Color(filter(nextRed), filter(nextGreen), filter(nextBlue));
    }

    private static int filter(int a) {
        if (a < 0) {
            return 0;
        }

        if (a > 255) {
            return 255;
        }

        return a;
    }
}

The demonstration uses this matrix for efficient rotations of an image. As always, tell me anything that comes to mind.

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

I took a look at the way you handle negative rotations...

and you seem to just negate the value, and then pass it to its opposite function. Which is a pretty clever idea. Except that there is one case in which it doesn't work.

    System.out.println(Integer.MAX_VALUE);
    System.out.println(-Integer.MAX_VALUE);
    System.out.println(Integer.MIN_VALUE);
    System.out.println(-Integer.MIN_VALUE);

Gives the following output:

2147483647
-2147483647
-2147483648
-2147483648

Yeah, that's right - negating MIN_VALUE will do absolutely nothing!

And as a result, your code dies from java.lang.StackOverflowError.

See this answer for a detailed explanation as to WHY this problem occurs - the simplified explanation is that it overflows because 2147483648 cannot fit in an int and it overflows to the next value which is -2147483648.

\$\endgroup\$
4
  • \$\begingroup\$ As a remedy take steps %= width/height? \$\endgroup\$
    – coderodde
    Commented Apr 5, 2016 at 14:17
  • \$\begingroup\$ That could work. \$\endgroup\$
    – Pimgd
    Commented Apr 5, 2016 at 14:21
  • \$\begingroup\$ Thanks for the link, but I know what happens on the bit level. :^) \$\endgroup\$
    – coderodde
    Commented Apr 5, 2016 at 14:31
  • \$\begingroup\$ It's for the other people who read these questions and answers. \$\endgroup\$
    – Pimgd
    Commented Apr 5, 2016 at 14:36
1
\$\begingroup\$

You do repaint in every case statement, put it out and stay DRY.

public void keyTyped(KeyEvent e) {
    switch (e.getKeyChar()) {
        case 'w':
        case 'W':
            matrix.rotateUp(10);
            break;
        case 'd':
        case 'D':
            matrix.rotateRight(10);
            break;
        case 's':
        case 'S':
            matrix.rotateDown(10);
            break;
        case 'a':
        case 'A':
            matrix.rotateLeft(10);
            break;
        default:
            return;
    }
    repaint();
}
\$\endgroup\$
3
  • \$\begingroup\$ That way I repaint on every key input, and I don't like having computation I can avoid. \$\endgroup\$
    – coderodde
    Commented Apr 12, 2016 at 16:10
  • \$\begingroup\$ @coderodde I added a default statement for something, didn't I? \$\endgroup\$ Commented Apr 12, 2016 at 19:03
  • \$\begingroup\$ Did not notice it. \$\endgroup\$
    – coderodde
    Commented Apr 12, 2016 at 19:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.