The city in my game is randomly generated but is a graph of roads and intersections that can only form rectangles:
As can be seen, my terrain is pretty empty. What I want to do is find each empty rectangle and store in in a list of rectangles, forming Lots.
As you can see in this illustration, I filled in 3 'lots' and in 1 I showed the 3 rectangles it is made of.
My data structures are:
package com.jkgames.gta;
import android.graphics.Bitmap;
import android.graphics.RectF;
public class Intersection extends Entity
{
Road topRoad;
Road leftRoad;
Road bottomRoad;
Road rightRoad;
Bitmap image;
public Bitmap getImage()
{
return image;
}
public void setImage(Bitmap image)
{
this.image = image;
}
public Intersection(RectF rect, Bitmap image)
{
setRect(rect);
setImage(image);
}
public Road getTopRoad()
{
return topRoad;
}
public void setTopRoad(Road topRoad)
{
this.topRoad = topRoad;
}
public Road getLeftRoad()
{
return leftRoad;
}
public void setLeftRoad(Road leftRoad)
{
this.leftRoad = leftRoad;
}
public Road getBottomRoad()
{
return bottomRoad;
}
public void setBottomRoad(Road bottomRoad)
{
this.bottomRoad = bottomRoad;
}
public Road getRightRoad()
{
return rightRoad;
}
public void setRightRoad(Road rightRoad)
{
this.rightRoad = rightRoad;
}
#Override
public void draw(GraphicsContext c)
{
c.drawRotatedScaledBitmap(image, getCenterX(), getCenterY(),
getWidth(), getHeight(), getAngle());
}
}
public class Road extends Entity
{
private Bitmap image = null;
private Intersection startIntersection;
private Intersection endIntersection;
private boolean topBottom;
public Road(RectF rect, Intersection start, Intersection end,
Bitmap image, boolean topBottom)
{
setRect(rect);
setStartIntersection(start);
setEndIntersection(end);
setImage(image);
setTopBottom(topBottom);
}
#Override
public void draw(GraphicsContext c)
{
//Rect clipRect = c.getCanvas().getClipBounds();
//c.getCanvas().clipRect(getRect());
float sizeW;
float sizeH;
if(isTopBottom())
{
sizeW = getWidth();
sizeH = (sizeW / image.getWidth()) * image.getHeight();
}
else
{
sizeW = getHeight();
sizeH = (sizeW / image.getWidth()) * image.getHeight();
}
int numTiles = isTopBottom() ? (int)Math.ceil(getHeight() / sizeH) :
(int)Math.ceil(getWidth() / sizeW);
for(int i = 0; i < numTiles; ++i)
{
if(isTopBottom())
{
c.drawRotatedScaledBitmap(
image,
getRect().left + (sizeW / 2.0f),
(getRect().top + (sizeH / 2.0f)) + (sizeH * i),
sizeW, sizeH, 0.0f);
}
else
{
c.drawRotatedScaledBitmap(
image,
getRect().left + (sizeH / 2.0f) + (sizeH * i),
getRect().top + (sizeH / 2.0f),
sizeW, sizeH, (float)Math.PI / 2.0f);
}
}
// c.getCanvas().clipRect(clipRect);
}
public Bitmap getImage()
{
return image;
}
public void setImage(Bitmap image)
{
this.image = image;
}
public Intersection getStartIntersection()
{
return startIntersection;
}
public void setStartIntersection(Intersection startIntersection)
{
this.startIntersection = startIntersection;
}
public Intersection getEndIntersection()
{
return endIntersection;
}
public void setEndIntersection(Intersection endIntersection)
{
this.endIntersection = endIntersection;
}
public boolean isTopBottom()
{
return topBottom;
}
public void setTopBottom(boolean topBottom)
{
this.topBottom = topBottom;
}
}
The city is a list of roads and intersections.
Is there some sort of algorithm that could generate these lots and their rectangles?
Thanks
The easiest method that comes to my mind is to use a flood-fill algorithm to build up your list of regions. So basically
foreach square:
if the square isn't part of a region:
create a new empty region list
add the square to it
recursivly add all neighboring squares to the region
The end result will be that you'll have a list of regions which you can then do whatever you want with (look and see if any of the contained squares have buildings on, color in for the user, etc..).
Note: to determine whether a square is part of a region or not, I'd add a marked flag or something to the square data structure, that way when you start, you go through and clear all those flags, then as you add a square to a region you set that flag, and when you want to check to see if a square is in a region, all you need to do is check to see if that flag is set or not. That way you end up with a linear time algorithm to construct your list of regions.
As Markus pointed out in the comments here, this "flag" could be in fact a pointer/reference to a Lot object that holds the list of your squares, which would probably be convenient to have handy anyways.
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++) {
if (x > 0 && squares[y][x].isConnectedTo(squares[y][x-1]) {
// Connected from the left
squares[y][x-1].getLot().addSquare(squares[y][x]);
if (y > 0 && squares[y][x].isConnectedTo(squares[y-1][x]) {
// Connected from both the left and above
squares[y-1][x].getLot().mergeWith(squares[y][x].getLot());
}
}
else if (y > 0 && squares[y][x].isConnectedTo(squares[y-1][x]) {
// Connected from above
squares[y-1][x].getLot().addSquare(squares[y][x]);
}
else {
// Not connected from either
createNewLot().addSquare(squares[y][x]);
}
}
Lot.addSquare(…) adds a square to the lot, and calls setLot(…) on the square.
Lot.mergeWith(…) merges two lots, and reassigns the squares assigned to them, if they are not the same lot.
Square.isConnectedTo(…) checks if they are neighbors, and that there is no road in between.
You could optimize this by using the Disjoint-set data structure
Related
Well, lets say I have the Shape of an O and I want to render it.
Now my current rendering code is this:
public void fill(Shape shape, float xOffset, float yOffset) {
AffineTransform transform = new AffineTransform();
transform.translate(xOffset, yOffset);
PathIterator pi = shape.getPathIterator(transform, 1);
int winding = pi.getWindingRule();
ShapeRenderer r = getInstance();
float[] coords = new float[6];
boolean drawing = false;
while (!pi.isDone()) {
int segment = pi.currentSegment(coords);
switch (segment) {
case PathIterator.SEG_CLOSE:
if (drawing) {
r.endPoly();
}
break;
case PathIterator.SEG_LINETO:
r.lineTo(coords);
break;
case PathIterator.SEG_MOVETO:
if (drawing) {
r.endPoly();
}
drawing = true;
r.beginPoly(winding);
r.moveTo(coords);
break;
default:
throw new IllegalArgumentException("Unexpected value: " + segment);
}
pi.next();
}
}
ShapeRenderer:
private final Deque<Queue<Float>> vertices = new ConcurrentLinkedDeque<>();
#Override
public void beginPoly(int windingRule) {
}
#Override
public void endPoly() {
Queue<Float> q;
GL11.glBegin(GL11.GL_LINE_LOOP);
while ((q = vertices.poll()) != null) {
Float x, y;
while ((x = q.poll()) != null && (y = q.poll()) != null) {
GL11.glVertex2f(x, y);
}
}
GL11.glEnd();
}
#Override
public void moveTo(float[] vertex) {
Queue<Float> q = new ConcurrentLinkedQueue<>();
vertices.offer(q);
lineTo(vertex);
}
#Override
public void lineTo(float[] vertex) {
Queue<Float> q = vertices.peekLast();
q.offer(vertex[0]);
q.offer(vertex[1]);
}
So lets say I want to fill that Shape, just like java awt does, successfully... How would I do that?
(Already tried using GL_POLYGON, but it just fills the entire O and I have a filled circle, not an O. Also tried using parts of jogamp glu but it just rendered nothing, no clue why)
Already tried using GL_POLYGON, but it just fills the entire O and I have a filled circle, "
Yes of course. GL_POLYGON is for Convex shapes and fills the entire area enclosed by the polygon.
You have 2 options:
Use GL_POLYGON twice to draw a white shape and then a red shape inside the white shape.
Use GL_TRIANGLE_STRIP and form a shape that just draws the outline. The primitive can be formed by alternately specifying a point on the outer outline and on the inner outline.
I need to remove the recursion from the drawSquare method. There is a lot more I have to do after removing the recursion from that method however the rest I can figure out on my own. I just really need a working solution that does the exact same thing without recursion and I will figure out the rest.
Here is how I made the Square class:
import java.awt.Color;
public class Square {
final int BLACK = Color.BLACK.getRGB();
final int WHITE = Color.WHITE.getRGB();
protected int center_x;
protected int center_y;
protected int side;
protected int color;
protected Square parentSquare;
public Square(){
this.center_x = 0;
this.center_y = 0;
this.side = 0;
this.color = WHITE;
this.parentSquare = null;
}
public Square(int center_x,int center_y,int side,int color){
this.center_x = center_x;
this.center_y = center_y;
this.side = side;
this.color = color;
this.parentSquare = null;
}
public Square(int center_x,int center_y,int side,int color,Square parentSquare){
this.center_x = center_x;
this.center_y = center_y;
this.side = side;
this.color = color;
this.parentSquare = parentSquare;
}
public void setX(int center_x){
this.center_x = center_x;
}
public int getX(){
return center_x;
}
public void setY(int center_y){
this.center_x = center_y;
}
public int getY(){
return center_y;
}
public void setSide(int side){
this.side = side;
}
public int getSide(){
return side;
}
public void setColor(int color){
this.color = color;
}
public int getColor(){
return color;
}
public void setParent(Square parentSquare){
this.parentSquare = parentSquare;
}
public Square getParent(){
return parentSquare;
}
}
This is the original Tsquare.java that produces a fractal of squares branching from each squares 4 corners until the side = 0: (full TSquare.java class modified to use Square objects)
import java.awt.image.*;
import java.awt.Color;
import java.io.*;
import javax.imageio.*;
import java.util.*;
public class TSquare {
static final int SIDE = 1000; // image is SIDE X SIDE
static BufferedImage image = new BufferedImage(SIDE, SIDE, BufferedImage.TYPE_INT_RGB);
static final int WHITE = Color.WHITE.getRGB();
static final int BLACK = Color.BLACK.getRGB();
static Scanner kbd = new Scanner(System.in);
public static void main(String[] args) throws IOException{
String fileOut = "helloSquares.png";
System.out.print("Enter (x,y) coordinates with a space between: ");
int x = kbd.nextInt();
int y = kbd.nextInt();
System.out.println(x+","+y);//TESTLINE TESTLINE TESTLINE TESTLINE
// make image black
for (int i = 0; i < SIDE; i++) {
for (int j = 0; j < SIDE; j++) {
image.setRGB(i, j, BLACK);
}
}
Square square = new Square(SIDE/2,SIDE/2,SIDE/2,WHITE);
drawSquare(square);
// save image
File outputfile = new File(fileOut);
ImageIO.write(image, "jpg", outputfile);
}
private static void drawSquare(Square square){ // center of square is x,y length of side is s
if (square.side <= 0){ // base case
return;
}else{
// determine corners
int left = square.center_x - (square.side/2);
int top = square.center_y - (square.side/2);
int right = square.center_x + (square.side/2);
int bottom = square.center_y + (square.side/2);
int newColor =square.color-100000;
Square newSquareA = new Square(left,top,square.side/2,newColor);
Square newSquareB = new Square(left,bottom,square.side/2,newColor);
Square newSquareC = new Square(right,top,square.side/2,newColor);
Square newSquareD = new Square(right,bottom,square.side/2,newColor);
for (int i = left; i < right; i++){
for (int j = top; j < bottom; j++){
image.setRGB(i, j, square.color);
}
}
// recursively paint squares at the corners
drawSquare(newSquareA);
drawSquare(newSquareB);
drawSquare(newSquareC);
drawSquare(newSquareD);
}
}
}
I'm looking to reproduce the exact actions of this code just minus the recursion and everything I try doesn't seem to work. I cant even get a single white square to display on top of the original black canvas.
If we want readability without compromising speed I suggest first making some additions to Square:
public int half() {
return side/2;
}
public int left() {
return center_x - half();
}
public int top() {
return center_y - half();
}
public int right() {
return center_x + half();
}
public int bottom() {
return center_y + half();
}
public void draw(BufferedImage image) {
int left = left();
int top = top();
int right = right();
int bottom = bottom();
for (int i = left; i < right; i++){
for (int j = top; j < bottom; j++){
image.setRGB(i, j, color);
}
}
} //End Square
Also moving I/O out to enable unit testing.
package com.stackoverflow.candied_orange;
import java.awt.image.*;
import java.awt.Color;
import java.io.*;
import javax.imageio.*;
import java.util.*;
public class FractalSquareIterative {
public static void main(String[] args) throws IOException{
final int SIDE = 1000; // image is SIDE X SIDE
BufferedImage image = new BufferedImage(SIDE,SIDE,BufferedImage.TYPE_INT_RGB);
drawImage(SIDE, image);
saveImage(image);
}
//Removed IO to enable unit testing
protected static void drawImage(final int SIDE, BufferedImage image) {
final int BLACK = Color.BLACK.getRGB();
final int WHITE = Color.WHITE.getRGB();
final int HALF = SIDE / 2;
//Draw background on whole image
new Square(HALF, HALF, SIDE, BLACK).draw(image);
//Draw foreground starting with centered half sized square
Square square = new Square(HALF, HALF, HALF, WHITE);
drawFractal(square, image);
}
Now that Square is dealing with all the square things the fractal code is a little easier on the eyes.
private static void drawFractal(Square square, BufferedImage image){
Queue<Square> squares = new LinkedList<>();
squares.add(square);
while (squares.size() > 0) {
//Consume
square = squares.remove();
//Produce
int half = square.half();
if (half > 2) {
int left = square.left();
int top = square.top();
int right = square.right();
int bottom = square.bottom();
int newColor = square.color - 100000;
squares.add(new Square(left, top, half, newColor));
squares.add(new Square(left, bottom, half, newColor));
squares.add(new Square(right, top, half, newColor));
squares.add(new Square(right, bottom, half, newColor));
}
square.draw(image);
}
}
protected static void saveImage(BufferedImage image) throws IOException {
String fileOut = "helloSquares.png";
File outputfile = new File(fileOut);
ImageIO.write(image, "jpg", outputfile);
}
} //End FractalSquareIterative
Reliably faster than the recursive version but not significantly so at this size.
If you want a peek at my unit tests you'll find them here.
Here's one implementation using an ArrrayDeque ( https://docs.oracle.com/javase/7/docs/api/java/util/ArrayDeque.html ).
It's worth comparing ArrayDeque against some other Java types :
a) Stack is an interface, and the api page (https://docs.oracle.com/javase/8/docs/api/java/util/Stack.html) says
A more complete and consistent set of LIFO stack operations is provided by the Deque interface and its implementations, which should be used in preference to this class.
b) I originally wrote this using good old familiar ArrayList, with Square square = squares.remove(0); instead of pop. I am quite surprised at how much faster this ArrayDeque implementation appears to be than that ArrayList (not that I have run any formal benchmarks)
private static void drawSquare(Square startSquare){
Deque<Square> squares = new ArrayDeque<Square>(400000);
squares.push(startSquare);
while (!squares.isEmpty()) {
Square square = squares.pop();
System.out.println(square);
// center of square is x,y length of side is s
if (square.side > 0){ // base case
// determine corners
int left = square.center_x - (square.side/2);
int top = square.center_y - (square.side/2);
int right = square.center_x + (square.side/2);
int bottom = square.center_y + (square.side/2);
int newColor =square.color-100000;
addSquare(squares, left,top,square.side/2,newColor);
addSquare(squares, left,bottom,square.side/2,newColor);
addSquare(squares, right,top,square.side/2,newColor);
addSquare(squares, right,bottom,square.side/2,newColor);
}
}
}
private static void addSquare(Deque<Square> squares, int x, int y, int side, int color) {
// STRONGLY recommend having this "if" statement !
// if (side > 0) {
squares.push(new Square(x, y, side, color));
// }
}
As noted in my comment, it is WELL worthwhile to not create squares of size 0 rather than creating them and simply ignoring them when their turn comes around. This would be true for the recursion-based operations as well - but especially so for these non-recursion based ones, since the multitude of squares would be really eating up memory and processing time.
I am having a rectangle which should represent a car. It should rotate when you tuch the display. How to make this with box2d. I already setted all up: Press W to accelerate and touch display for right turn and dont touch it for left turn. Problem is that the car is drifting when you rotate it and not immediately goes only to the direction its facing to. So far this is my code, how to remove this drifting, its like it drives on ice. :/
EDIT: (copied from C++ code: http://www.iforce2d.net/b2dtut/top-down-car I tried to copy everything until the Torque so now It should drive. But when I do accelerate. It does not move. The currentSpeed Variable is 0 and the currentForwardNormal is 0.0 ,0.0. What did I wrong?)
//Main upate
public void update(float delta) {
updateFriction();
}
//Movements and Subclasses of them
public void accelerate() {
//find current speed in forward direction
Vector2 currentForwardNormal = ship.getWorldVector(new Vector2(0,1));
float currentSpeed = getForwardVelocity().dot(currentForwardNormal);
//apply necessary force
float force = 0;
if(maxSpeed > currentSpeed) {
force = maxDriveForce;
} else {
force = -maxDriveForce;
}
System.out.println("Current Speed: "+currentSpeed);
System.out.println("Current Forward Normal: "+currentForwardNormal);
ship.applyForceToCenter(currentForwardNormal.scl(force),true);
}
public void rotate(boolean direction) { //true for Up and false for Down
if(direction) {
//torques
}
}
private void updateFriction() {
Vector2 impulse = getLateralVelocity().scl(-ship.getMass());
ship.applyLinearImpulse(impulse, ship.getWorldCenter(), true);
ship.applyAngularImpulse(0.1f * ship.getInertia() * -ship.getAngularVelocity(),true);
}
private Vector2 getLateralVelocity() {
Vector2 currentRightNormal = ship.getWorldVector(new Vector2(1,0));
return currentRightNormal.scl(currentRightNormal.dot(ship.getLinearVelocity()));
}
private Vector2 getForwardVelocity() {
Vector2 currentRightNormal = ship.getWorldVector(new Vector2(0,1));
return currentRightNormal.scl(currentRightNormal.dot(ship.getLinearVelocity()));
}
Given a simple monochrome bitmap that contains a single, randomly rotated rectangle. How can I find the left/top, left/bottom, right/bottom and right/top corner positions of the rectangle inside the bitmap?
For example, this is how the bitmap could look like, where the X marks the pixels in question:
......... ......... ......... .........
.X11111X. ....X.... ..X11.... ....11X..
.1111111. ...111... ..11111X. X111111..
.1111111. ..X111X.. ..111111. .111111..
.X11111X. ...111... .1111111. .1111111.
......... ....X.... .111111.. ..111111.
......... ......... .X11111.. ..11111X.
......... ......... ....11X.. ..X11....
......... ......... ......... .........
Please excuse the bad ascii art.
For the second example, the corner pixel at the top could either be the rectangles left/top or right/top corner. Either is fine.
What steps are required to determine the corner pixels/positions in the above examples?
The corner pixels are the pixels the furthest apart. Find the top most row and the bottom most row. There will always be a corner pixel in those.
The corner pixel can only be the first or last pixel in this the topmost row row (or both if there's just the one).
So compare the distances between the first pixel in the topmost row and the last pixel in the bottom most row. And last pixel in topmost with the first in bottom most. The corners there are the the ones that are the furthest apart.
Since they are all the same distance in the Y you need the pixels with the greatest difference with regard to their x location. The corners are the pixels for which abs(x0-x1) is the greatest, where x0 is in the topmost row and x1 is in the bottom most.
Repeat this for the rightmost and leftmost rows.
If the topmost corner is on the left then the leftmost corner is on the bottom, the bottom most corner is on the right and the rightmost corner is on the top. Once you have the top, bottom, left, and right rows there's really just the two possibilities that can be solved in an if statement. But, due to the edge condition of having one pixel on the topmost row and two on the rightmost row, you're better off just running the algorithm again with transposed x and ys to solve for the other two corners rather than trying to spare yourself an if statement.
Not every monochrome bitmap is going to give you an answer. A complete algorithm needs an output that says "Unique corners not present". The following figures give an example of the problem:
......... ......... ..........
...XX.... ....X.... ....XX....
..X11X... ...111... ...1111...
..X11X... ..X111X.. ..X1111X..
...XX.... ...111... ..X1111X..
......... ....X.... ...X11X...
......... ......... ....XX....
......... ......... ..........
The degeneracy illustrated happens when the slopes of the rectangle are +1 and -1 and the position of the center is half-integral . It can also occur with other combinations of slopes and positions. The general answer will need to contain pixel-pairs as the best approximation of a vertex.
Start with the bounding box of the rectangle.
For each corner, move it clockwise until there is a black square.
public class Test {
String[][] squares = {
{
".........",
".X11111X.",
".1111111.",
".1111111.",
".X11111X.",
".........",
".........",
".........",
".........",},
{
".........",
"....X....",
"...111...",
"..X111X..",
"...111...",
"....X....",
".........",
".........",
".........",},
{
".........",
"..X11....",
"..11111X.",
"..111111.",
".1111111.",
".111111..",
".X11111..",
"....11X..",
".........",},
{
".........",
"....11X..",
"X111111..",
".111111..",
".1111111.",
"..111111.",
"..11111X.",
"..X11....",
".........",}};
private static final int WHITE = 0;
private static final int BLACK = 1;
class Point {
private final int x;
private final int y;
public Point(Point p) {
this.x = p.x;
this.y = p.y;
}
public Point(int x, int y) {
this.x = x;
this.y = y;
}
#Override
public String toString() {
return "{" + x + "," + y + '}';
}
// What colour is there?
public int colour(int[][] bmp) {
// Make everything off-bmp black.
if (x < 0 || y < 0 || y >= bmp.length || x >= bmp[y].length) {
return BLACK;
}
return bmp[y][x];
}
private Point step(Point d) {
return new Point(x + d.x, y + d.y);
}
}
class Rectangle {
private final Point[] corners = new Point[4];
public Rectangle(Point[] corners) {
// Points are immutable but corners are not.
System.arraycopy(corners, 0, this.corners, 0, corners.length);
}
public Rectangle(Rectangle r) {
this(r.corners());
}
public Rectangle(Point a, Point b, Point c, Point d) {
corners[0] = a;
corners[1] = b;
corners[2] = c;
corners[3] = d;
}
private Rectangle(Point tl, Point br) {
this(tl, new Point(br.x, tl.y), br, new Point(tl.x, br.y));
}
public Point[] corners() {
return Arrays.copyOf(corners, corners.length);
}
#Override
public String toString() {
return Arrays.toString(corners);
}
}
private Rectangle getBoundingBox(int[][] bmp) {
int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, maxX = 0, maxY = 0;
for (int r = 0; r < bmp.length; r++) {
for (int c = 0; c < bmp[r].length; c++) {
if (bmp[r][c] != WHITE) {
if (minX > c) {
minX = c;
}
if (minY > r) {
minY = r;
}
if (maxX < c) {
maxX = c;
}
if (maxY < r) {
maxY = r;
}
}
}
}
return new Rectangle(new Point(minX, minY), new Point(maxX, maxY));
}
Point[] clockwise = new Point[]{
new Point(1, 0),
new Point(0, 1),
new Point(-1, 0),
new Point(0, -1)};
private void test(int[][] bmp) {
// Find the bounding box.
Rectangle bBox = getBoundingBox(bmp);
System.out.println("bbox = " + bBox);
Point[] corners = bBox.corners();
// Move each corner clockwise until it is black.
for (int p = 0; p < corners.length; p++) {
while (corners[p].colour(bmp) == WHITE) {
corners[p] = corners[p].step(clockwise[p]);
}
}
System.out.println("rect = " + new Rectangle(corners));
}
private void test(String[] square) {
// Build the int[][].
// . -> White
// X/1 -> Black
int[][] bmp = new int[square.length][];
for (int r = 0; r < square.length; r++) {
bmp[r] = new int[square[r].length()];
for (int c = 0; c < bmp[r].length; c++) {
switch (square[r].charAt(c)) {
case '.':
bmp[r][c] = WHITE;
break;
case 'X':
case '1':
bmp[r][c] = BLACK;
break;
}
}
}
test(bmp);
}
public void test() {
for (String[] square : squares) {
test(square);
}
}
public static void main(String args[]) {
try {
new Test().test();
} catch (Throwable t) {
t.printStackTrace(System.err);
}
}
}
prints
bbox = [{1,1}, {7,1}, {7,4}, {1,4}]
rect = [{1,1}, {7,1}, {7,4}, {1,4}]
bbox = [{2,1}, {6,1}, {6,5}, {2,5}]
rect = [{4,1}, {6,3}, {4,5}, {2,3}]
bbox = [{1,1}, {7,1}, {7,7}, {1,7}]
rect = [{2,1}, {7,2}, {6,7}, {1,6}]
bbox = [{0,1}, {7,1}, {7,7}, {0,7}]
rect = [{4,1}, {7,4}, {4,7}, {0,2}]
Could be improved by looking for a run of black and choosing the middle of the run.
Scan the image from the top, row by row until you find a black run.
Repeat four ways, from the bottom up, left, right, giving you eight corner candidates.
Take the run endpoints the farthest apart in the top and bottom rows. This tells you which endpoints to take vertically.
I have the following code which does (the first part of) what I want drawing a chessboard with some pieces on it.
Image pieceImage = getImage(currentPiece);
int pieceHeight = pieceImage.getHeight(null);
double scale = (double)side/(double)pieceHeight;
AffineTransform transform = new AffineTransform();
transform.setToTranslation(xPos, yPos);
transform.scale(scale, scale);
realGraphics.drawImage(pieceImage, transform, this);
that is, it gets a chess piece's image and the image's height, it translates the drawing of that image to the square the piece is on and scales the image to the size of the square.
Llet's say I want to rotate the black pieces 180 degrees. Somewhere I expect to have something like:
transform.rotate(Math.toRadians(180) /* ?, ? */);
But I can't figure out what to put in as X and Y. If I put nothing, the image is nicely rotated around the 0,0 point of its chessboard square, putting the piece upside down in the square to the northeast of where it is supposed to be. I've guessed at various other combinations of x,y, with no luck yet.
I am already using translation to put the piece in the right square, the rotation transform wants another x,y around which to rotate things, but I don't know how to tell the transform to rotate the piece around one x,y and write the image to a different x,y. Can someone help me with the rotation parameters, or point me to something that explains how these things work? I've found examples of things that don't explain how they work, and so far I haven't figured out how to alter them to my situation...
Major edit: addition of working code. Sorry, I don't know how to post images, please substitute your own.
When I run the following I get a 2x2 chess board with a rook at the top left and a knight at the bottom right.
If I go into SmallChessboardComponent and take the comment delims off the first rotation transform statement, I get the rook in its original place upside down and the knight does not appear. If I instead take the comment delims off the second transform statement, neither piece appears at all.
I am looking for a way to turn the pieces upside down on the square on which they would appear anyway. I want to draw each piece onto the board; I don't want code that flips the board.
main program:
package main;
import java.awt.BorderLayout;
import javax.swing.JFrame;
import directredraw.SmallChessboardComponent;
public class SmallChessboardMain
{
private static void dbg (String message) { System.out.println(message); }
public static void main(String[] args)
{
//Create the top-level container and add contents to it.
final JFrame frame = new JFrame("Small Chessboard");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// create the chessboard itself and set it in the component
SmallChessboard chessboard = new SmallChessboard();
// create the GUI component that will contain the chessboard
SmallChessboardComponent chessboardComponent = new SmallChessboardComponent();
chessboardComponent.setBoard (chessboard);
frame.getContentPane().add(chessboardComponent, BorderLayout.CENTER);
// pack and display all this
frame.pack();
frame.setVisible(true);
}
}
chessboard class:
package main;
public class SmallChessboard
{
Piece [][] squares = new Piece[2][2];
public SmallChessboard()
{
squares[0][0] = new Piece(Piece.WHITECOLOR, Piece.ROOK);
squares[1][1] = new Piece(Piece.WHITECOLOR, Piece.KNIGHT);
}
/**
* get the piece at the given rank and file; null if
* no piece exists there.
*/
public Piece getPiece(int rank, int file)
{
if (0 > rank || rank > 2 || 0 > file || file > 2) { return null; }
else { return squares[rank][file]; }
}
}
chessboard component class:
package directredraw;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import javax.swing.JPanel;
import main.Piece;
import main.PieceImages;
import main.SmallChessboard;
public class SmallChessboardComponent extends JPanel
{
private static final long serialVersionUID = 1L;
Color whiteSquareColor = Color.yellow;
Color blackSquareColor = Color.blue;
private static void dbg (String msg) { System.out.println(msg); }
private SmallChessboard chessboard = null;
// currently playing with rotating images; this affine transform
// should help
AffineTransform rotationTransform = null;
private final int DEFAULT_PREFERRED_SIDE = 400;
int wholeSide = DEFAULT_PREFERRED_SIDE;
int side = DEFAULT_PREFERRED_SIDE / 8;
public void setBoard (SmallChessboard givenBoard)
{ chessboard = givenBoard;
}
/**
* set either or both colors for this chessboard; if either of
* the arguments are null, they do not change the existing color
* setting.
*/
public void setColors (Color darkSquare, Color lightSquare)
{
if (darkSquare != null) { blackSquareColor = darkSquare; }
if (lightSquare != null) { whiteSquareColor = lightSquare; }
}
/**
* return the preferred size for this component.s
*/
public Dimension getPreferredSize()
{ return new Dimension(wholeSide, wholeSide);
}
/*
* return the image object for the given piece
*/
private Image getImage(Piece piece)
{ return PieceImages.getPieceImage(this, piece);
}
public void paintComponent (Graphics graphics)
{
Graphics2D realGraphics = (Graphics2D) graphics;
// the image container might have been stretched.
// calculate the largest square held by the current container,
// and then 1/2 of that size for an individual square.
int wholeWidth = this.getWidth();
int wholeHeight = this.getHeight();
wholeSide = (wholeWidth / 2) * 2;
if (wholeHeight < wholeWidth) { wholeSide = (wholeHeight / 2) * 2; }
side = wholeSide / 2;
Rectangle clip = realGraphics.getClipBounds();
boolean firstColumnWhite = false;
// for each file on the board:
// set whether top square is white
// set background color according to white/black square
//
for (int fileIndex=0; fileIndex<8; fileIndex++)
{ boolean currentColorWhite = firstColumnWhite;
firstColumnWhite = !firstColumnWhite;
// draw the board and all the pieces
int rankIndex = 2;
for (rankIndex=2; rankIndex>=0; rankIndex--)
{
currentColorWhite = !currentColorWhite;
// x and y position of the top left corner of the square we're drawing,
// and rect becomes the dimensions and position of the square itself.
int xPos = fileIndex * side;
int yPos = rankIndex * side;
Rectangle rect = new Rectangle(xPos, yPos, side, side);
// if this square intersects the clipping rectangle we're drawing,
// then we'll draw the square and the piece on the square.
if (rect.intersects(clip))
{
// this puts down the correct color of square
if (currentColorWhite) { realGraphics.setColor(whiteSquareColor); }
else { realGraphics.setColor(blackSquareColor); }
realGraphics.fillRect(xPos, yPos, side, side);
// if there is a piece on this square and it isn't selected at the
// moment, then draw it.
Piece currentPiece = chessboard.getPiece(rankIndex, fileIndex);
if (currentPiece != null)
{
Image pieceImage = getImage(currentPiece);
int pieceHeight = pieceImage.getHeight(null);
double scalePiece = (double)side/(double)pieceHeight;
AffineTransform transform = new AffineTransform();
// transform.setToRotation(Math.toRadians(180));
transform.setToRotation(Math.toRadians(180), side/2, side/2);
transform.scale(scalePiece, scalePiece);
transform.translate(xPos/scalePiece, yPos/scalePiece);
// if (currentPiece.isBlack())
// {
// transform.translate(xPos + (side+2), yPos + (side+2));
// transform.rotate(Math.toRadians(180) /*, ,*/ );
// }
// else
// {
// transform.translate(xPos, yPos);
// }
realGraphics.drawImage(pieceImage, transform, this);
}
}
}
}
}
}
Piece.java
package main;
public class Piece
{
// piece types; the sum of the piece type and the
// color gives a number unique to both type and color,
// which is used for things like image indices.
public static final int PAWN = 0;
public static final int KNIGHT = 1;
public static final int BISHOP = 2;
public static final int ROOK = 3;
public static final int QUEEN = 4;
public static final int KING = 5;
// one of these is the color of the current piece
public static final int NOCOLOR = -1;
// the sum of the piece type and the
// color gives a number unique to both type and color,
// which is used for things like image indices.
public static final int BLACKCOLOR = 0;
public static final int WHITECOLOR = 6;
int color = NOCOLOR;
int imageIndex;
public Piece(int color, int pieceType)
{
// dbg -- all pieces are white rooks for now...
this.color = color;
imageIndex = color + pieceType;
}
/**
* return the integer associated with this piece's color;
*/
int getPieceColor()
{ return color;
}
/**
* return true if the piece is black
*/
public boolean isBlack()
{
return (color == BLACKCOLOR);
}
/**
* set the color associated with this piece; constants
* found in this class.
*/
public void setPieceColor(int givenColor)
{ color = givenColor;
}
/**
* return the integer designated for the image used for this piece.
*/
int getImageIndex()
{ return imageIndex;
}
}
and PieceImages.java
package main;
import java.awt.Component;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.net.URL;
public class PieceImages
{ static Image images[] = null;
private static void dbg (String msg) { System.out.println(msg); }
public static Image getPieceImage (Component target, Piece piece)
{
if (images == null)
try
{
MediaTracker tracker = new MediaTracker(target);
images = new Image[12];
images[Piece.BLACKCOLOR + Piece.PAWN] = getImage(tracker, "bPawn.gif");
images[Piece.BLACKCOLOR + Piece.KNIGHT] = getImage(tracker, "bKnight.gif");
images[Piece.BLACKCOLOR + Piece.BISHOP] = getImage(tracker, "bBishop.gif");
images[Piece.BLACKCOLOR + Piece.ROOK] = getImage(tracker, "bRook.gif");
images[Piece.BLACKCOLOR + Piece.QUEEN] = getImage(tracker, "bQueen.gif");
images[Piece.BLACKCOLOR + Piece.KING] = getImage(tracker, "bKing.gif");
images[Piece.WHITECOLOR + Piece.PAWN] = getImage(tracker, "wPawn.gif");
images[Piece.WHITECOLOR + Piece.KNIGHT] = getImage(tracker, "wKnight.gif");
images[Piece.WHITECOLOR + Piece.BISHOP] = getImage(tracker, "wBishop.gif");
images[Piece.WHITECOLOR + Piece.ROOK] = getImage(tracker, "wRook.gif");
images[Piece.WHITECOLOR + Piece.QUEEN] = getImage(tracker, "wQueen.gif");
images[Piece.WHITECOLOR + Piece.KING] = getImage(tracker, "wKing.gif");
if (!tracker.waitForAll(10000))
{ System.out.println("ERROR: not all piece main.images loaded");
}
dbg("piece images loaded");
}
catch (Exception xcp)
{ System.out.println("Error loading images");
xcp.printStackTrace();
}
return images[piece.getImageIndex()];
}
private static Image getImage(MediaTracker tracker, String file)
{
URL url = PieceImages.class.getResource("images/" + file);
Image image = Toolkit.getDefaultToolkit().getImage(url);
tracker.addImage(image, 1);
return image;
}
}
Okay, this is a little slight of hand. The example code will only work for 90 degree increments (it was only designed this way), to do smaller increments you to use some trig to calculate the image width and height (there's a answer somewhere for that to ;))
public class ImagePane extends JPanel {
private BufferedImage masterImage;
private BufferedImage renderedImage;
public ImagePane(BufferedImage image) {
masterImage = image;
applyRotation(0);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(renderedImage.getWidth(), renderedImage.getHeight());
}
#Override
public Dimension getMinimumSize() {
return getPreferredSize();
}
protected int getVirtualAngle(int angle) {
float fRotations = (float) angle / 360f;
int rotations = (int) (fRotations - (fRotations / 1000));
int virtual = angle - (rotations * 360);
if (virtual < 0) {
virtual = 360 + virtual;
}
return virtual;
}
public void applyRotation(int angle) {
// This will only work for angles of 90 degrees...
// Normalize the angle to make sure it's only between 0-360 degrees
int virtualAngle = getVirtualAngle(angle);
Dimension size = new Dimension(masterImage.getWidth(), masterImage.getHeight());
int masterWidth = masterImage.getWidth();
int masterHeight = masterImage.getHeight();
double x = 0; //masterWidth / 2.0;
double y = 0; //masterHeight / 2.0;
switch (virtualAngle) {
case 0:
break;
case 180:
break;
case 90:
case 270:
size = new Dimension(masterImage.getHeight(), masterImage.getWidth());
x = (masterHeight - masterWidth) / 2.0;
y = (masterWidth - masterHeight) / 2.0;
break;
}
renderedImage = new BufferedImage(size.width, size.height, masterImage.getTransparency());
Graphics2D g2d = renderedImage.createGraphics();
AffineTransform at = AffineTransform.getTranslateInstance(x, y);
at.rotate(Math.toRadians(virtualAngle), masterWidth / 2.0, masterHeight / 2.0);
g2d.drawImage(masterImage, at, null);
g2d.dispose();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
int width = getWidth() - 1;
int height = getHeight() - 1;
int x = (width - renderedImage.getWidth()) / 2;
int y = (height - renderedImage.getHeight()) / 2;
g2d.drawImage(renderedImage, x, y, this);
}
}
Now, you could simply "flip" the image vertically, if that works better for you
public class FlipPane extends JPanel {
private BufferedImage masterImage;
private BufferedImage renderedImage;
public FlipPane(BufferedImage image) {
masterImage = image;
flipMaster();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(renderedImage.getWidth(), renderedImage.getHeight());
}
#Override
public Dimension getMinimumSize() {
return getPreferredSize();
}
protected void flipMaster() {
renderedImage = new BufferedImage(masterImage.getWidth(), masterImage.getHeight(), masterImage.getTransparency());
Graphics2D g2d = renderedImage.createGraphics();
g2d.setTransform(AffineTransform.getScaleInstance(1, -1));
g2d.drawImage(masterImage, 0, -masterImage.getHeight(), this);
g2d.dispose();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
int width = getWidth() - 1;
int height = getHeight() - 1;
int x = (width - renderedImage.getWidth()) / 2;
int y = (height - renderedImage.getHeight()) / 2;
g2d.drawImage(renderedImage, x, y, this);
}
}
This basically results in:
Original | 180 degree rotation | Vertical inversion...
Now, if you change the flipMaster method to read:
g2d.setTransform(AffineTransform.getScaleInstance(-1, -1));
g2d.drawImage(masterImage, -masterImage.getWidth(), -masterImage.getHeight(), this);
You'll get the same effect as the 180 rotation ;)
Try performing the rotation before translating it into the correct position. Simply reorder the transformations so that first you scale, then you rotate (around the center point of the image), and then you translate:
transform.scale(scale, scale);
transform.rotate(Math.PI, pieceWidth / 2, pieceHeight /2);
transform.translation(xPos, yPos);
By the way, the black pieces on a chess board usually aren't rotated. :)
Update
In what way does it not work? The solution I provided also also differs from your code in that scaling is performed before translating. You can try the rotating, translating, and then scaling.
I strongly suggest that you modify your code so that you can perform the translation last. If you do this, everything will become a lot less complicated. Once you have done so, you only have to scale once to automatically take care of the rotation.
transform.scale(scale, scale); // or transform.scale(scale, -scale); to rotate
transform.translate(xPos, yPos);