Related
For a project in school we got a simple simulator for a parking lot, and it is our job to add features to this simulator. I'm trying to add a GUI to the simulator and so that you can fill in your values and that the simulation bases its simulation based upon those values. But i first made a simple button to go from 1 frame to the other but when i run the Frame1 file with the following code:
JButton btnSubmit = new JButton("Submit");
btnSubmit.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
frame.dispose();
Simulator mysimulator = new Simulator();
mysimulator.run();
}
it doesn't load, it just gives me a blank window.
the view of the simulator is made in the following code:
//package Parkeersimulator;
import javax.swing.*;
import java.awt.*;
public class SimulatorView extends JFrame {
public JFrame frame;
private CarParkView carParkView;
private int numberOfFloors;
private int numberOfRows;
private int numberOfPlaces;
private int numberOfOpenSpots;
private Car[][][] cars;
public SimulatorView(int numberOfFloors, int numberOfRows, int numberOfPlaces) {
this.numberOfFloors = numberOfFloors;
this.numberOfRows = numberOfRows;
this.numberOfPlaces = numberOfPlaces;
this.numberOfOpenSpots =numberOfFloors*numberOfRows*numberOfPlaces;
cars = new Car[numberOfFloors][numberOfRows][numberOfPlaces];
carParkView = new CarParkView();
Container contentPane = getContentPane();
contentPane.add(carParkView, BorderLayout.CENTER);
pack();
setVisible(true);
updateView();
}
public void updateView() {
carParkView.updateView();
}
public int getNumberOfFloors() {
return numberOfFloors;
}
public int getNumberOfRows() {
return numberOfRows;
}
public int getNumberOfPlaces() {
return numberOfPlaces;
}
public int getNumberOfOpenSpots(){
return numberOfOpenSpots;
}
public Car getCarAt(Location location) {
if (!locationIsValid(location)) {
return null;
}
return cars[location.getFloor()][location.getRow()][location.getPlace()];
}
public boolean setCarAt(Location location, Car car) {
if (!locationIsValid(location)) {
return false;
}
Car oldCar = getCarAt(location);
if (oldCar == null) {
cars[location.getFloor()][location.getRow()][location.getPlace()] = car;
car.setLocation(location);
numberOfOpenSpots--;
return true;
}
return false;
}
public Car removeCarAt(Location location) {
if (!locationIsValid(location)) {
return null;
}
Car car = getCarAt(location);
if (car == null) {
return null;
}
cars[location.getFloor()][location.getRow()][location.getPlace()] = null;
car.setLocation(null);
numberOfOpenSpots++;
return car;
}
public Location getFirstFreeLocation() {
for (int floor = 0; floor < getNumberOfFloors(); floor++) {
for (int row = 0; row < getNumberOfRows(); row++) {
for (int place = 0; place < getNumberOfPlaces(); place++) {
Location location = new Location(floor, row, place);
if (getCarAt(location) == null) {
return location;
}
}
}
}
return null;
}
public Car getFirstLeavingCar() {
for (int floor = 0; floor < getNumberOfFloors(); floor++) {
for (int row = 0; row < getNumberOfRows(); row++) {
for (int place = 0; place < getNumberOfPlaces(); place++) {
Location location = new Location(floor, row, place);
Car car = getCarAt(location);
if (car != null && car.getMinutesLeft() <= 0 && !car.getIsPaying()) {
return car;
}
}
}
}
return null;
}
public void tick() {
for (int floor = 0; floor < getNumberOfFloors(); floor++) {
for (int row = 0; row < getNumberOfRows(); row++) {
for (int place = 0; place < getNumberOfPlaces(); place++) {
Location location = new Location(floor, row, place);
Car car = getCarAt(location);
if (car != null) {
car.tick();
}
}
}
}
}
private boolean locationIsValid(Location location) {
int floor = location.getFloor();
int row = location.getRow();
int place = location.getPlace();
if (floor < 0 || floor >= numberOfFloors || row < 0 || row > numberOfRows || place < 0 || place > numberOfPlaces) {
return false;
}
return true;
}
private class CarParkView extends JPanel {
private Dimension size;
private Image carParkImage;
/**
* Constructor for objects of class CarPark
*/
public CarParkView() {
size = new Dimension(0, 0);
}
/**
* Overridden. Tell the GUI manager how big we would like to be.
*/
public Dimension getPreferredSize() {
return new Dimension(800, 500);
}
/**
* Overriden. The car park view component needs to be redisplayed. Copy the
* internal image to screen.
*/
public void paintComponent(Graphics g) {
if (carParkImage == null) {
return;
}
Dimension currentSize = getSize();
if (size.equals(currentSize)) {
g.drawImage(carParkImage, 0, 0, null);
}
else {
// Rescale the previous image.
g.drawImage(carParkImage, 0, 0, currentSize.width, currentSize.height, null);
}
}
public void updateView() {
// Create a new car park image if the size has changed.
if (!size.equals(getSize())) {
size = getSize();
carParkImage = createImage(size.width, size.height);
}
Graphics graphics = carParkImage.getGraphics();
for(int floor = 0; floor < getNumberOfFloors(); floor++) {
for(int row = 0; row < getNumberOfRows(); row++) {
for(int place = 0; place < getNumberOfPlaces(); place++) {
Location location = new Location(floor, row, place);
Car car = getCarAt(location);
Color color = car == null ? Color.white : car.getColor();
drawPlace(graphics, location, color);
}
}
}
repaint();
}
/**
* Paint a place on this car park view in a given color.
*/
private void drawPlace(Graphics graphics, Location location, Color color) {
graphics.setColor(color);
graphics.fillRect(
location.getFloor() * 260 + (1 + (int)Math.floor(location.getRow() * 0.5)) * 75 + (location.getRow() % 2) * 20,
60 + location.getPlace() * 10,
20 - 1,
10 - 1); // TODO use dynamic size or constants
}
}
}
and the simultion self is made in the following code:
//package Parkeersimulator;
import java.util.Random;
import javax.swing.*;
public class Simulator {
private static final String AD_HOC = "1";
private static final String PASS = "2";
private CarQueue entranceCarQueue;
private CarQueue entrancePassQueue;
private CarQueue paymentCarQueue;
private CarQueue exitCarQueue;
private SimulatorView simulatorView;
private int day = 0;
private int hour = 0;
private int minute = 0;
private int tickPause = 100;
int weekDayArrivals= 100; // average number of arriving cars per hour
int weekendArrivals = 200; // average number of arriving cars per hour
int weekDayPassArrivals= 50; // average number of arriving cars per hour
int weekendPassArrivals = 5; // average number of arriving cars per hour
int enterSpeed = 3; // number of cars that can enter per minute
int paymentSpeed = 7; // number of cars that can pay per minute
int exitSpeed = 5; // number of cars that can leave per minute
public static void main(String[] args){
Simulator mySimulator = new Simulator();
mySimulator.run();
}
public Simulator() {
entranceCarQueue = new CarQueue();
entrancePassQueue = new CarQueue();
paymentCarQueue = new CarQueue();
exitCarQueue = new CarQueue();
simulatorView = new SimulatorView(3, 6, 30);
}
public void run() {
for (int i = 0; i < 10000; i++) {
tick();
}
}
private void tick() {
advanceTime();
handleExit();
updateViews();
// Pause.
try {
Thread.sleep(tickPause);
} catch (InterruptedException e) {
e.printStackTrace();
}
handleEntrance();
}
private void advanceTime(){
// Advance the time by one minute.
minute++;
while (minute > 59) {
minute -= 60;
hour++;
}
while (hour > 23) {
hour -= 24;
day++;
}
while (day > 6) {
day -= 7;
}
}
private void handleEntrance(){
carsArriving();
carsEntering(entrancePassQueue);
carsEntering(entranceCarQueue);
}
private void handleExit(){
carsReadyToLeave();
carsPaying();
carsLeaving();
}
private void updateViews(){
simulatorView.tick();
// Update the car park view.
simulatorView.updateView();
}
private void carsArriving(){
int numberOfCars=getNumberOfCars(weekDayArrivals, weekendArrivals);
addArrivingCars(numberOfCars, AD_HOC);
numberOfCars=getNumberOfCars(weekDayPassArrivals, weekendPassArrivals);
addArrivingCars(numberOfCars, PASS);
}
private void carsEntering(CarQueue queue){
int i=0;
// Remove car from the front of the queue and assign to a parking space.
while (queue.carsInQueue()>0 &&
simulatorView.getNumberOfOpenSpots()>0 &&
i<enterSpeed) {
Car car = queue.removeCar();
Location freeLocation = simulatorView.getFirstFreeLocation();
simulatorView.setCarAt(freeLocation, car);
i++;
}
}
private void carsReadyToLeave(){
// Add leaving cars to the payment queue.
Car car = simulatorView.getFirstLeavingCar();
while (car!=null) {
if (car.getHasToPay()){
car.setIsPaying(true);
paymentCarQueue.addCar(car);
}
else {
carLeavesSpot(car);
}
car = simulatorView.getFirstLeavingCar();
}
}
private void carsPaying(){
// Let cars pay.
int i=0;
while (paymentCarQueue.carsInQueue()>0 && i < paymentSpeed){
Car car = paymentCarQueue.removeCar();
// TODO Handle payment.
carLeavesSpot(car);
i++;
}
}
private void carsLeaving(){
// Let cars leave.
int i=0;
while (exitCarQueue.carsInQueue()>0 && i < exitSpeed){
exitCarQueue.removeCar();
i++;
}
}
private int getNumberOfCars(int weekDay, int weekend){
Random random = new Random();
// Get the average number of cars that arrive per hour.
int averageNumberOfCarsPerHour = day < 5
? weekDay
: weekend;
// Calculate the number of cars that arrive this minute.
double standardDeviation = averageNumberOfCarsPerHour * 0.3;
double numberOfCarsPerHour = averageNumberOfCarsPerHour + random.nextGaussian() * standardDeviation;
return (int)Math.round(numberOfCarsPerHour / 60);
}
private void addArrivingCars(int numberOfCars, String type){
// Add the cars to the back of the queue.
switch(type) {
case AD_HOC:
for (int i = 0; i < numberOfCars; i++) {
entranceCarQueue.addCar(new AdHocCar());
}
break;
case PASS:
for (int i = 0; i < numberOfCars; i++) {
entrancePassQueue.addCar(new ParkingPassCar());
}
break;
}
}
private void carLeavesSpot(Car car){
simulatorView.removeCarAt(car.getLocation());
exitCarQueue.addCar(car);
}
}
please do note that i only just began programming and i have little to no knowledge at all.
You need a add the button to a panel.
JPanel panel = new JPanel();
JButton btnSubmit = new JButton("Submit");
Container content = this.getContentPane(); // Get the content pane
content.add(panel, BorderLayout.CENTER);
content.add(btnSubmit, BorderLayout.SOUTH);
setVisible(true); // Display the window
I am creating a simple 9x9 grid for Minesweeper. One of the primary functions of this game is to have a recursion to check all the sides when the tile clicked has no bombs surrounding it. In the code attached below, I have been able to create a function that checks the upper and left side of the tile. If I add more directions, such as lower and right side, the program will crash and will not properly display the tiles. (Check the method countBorders under the line //MY MAIN PROBLEM)
//displays the main GUI
package Minesweeper4;
public class mainFrame {
public static void main(String[] args) {
new Grid().setVisible(true);
}
}
// the main code
package Minesweeper4;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Random;
import javax.swing.*;
public class Grid extends JFrame implements ActionListener {
private JPanel mainGrid;
private JButton button1, button2;
private JButton[][] buttons = new JButton[9][9];
private String[][] mines = new String[9][9];
private ArrayList<ParentSquare> parentSquare = new ArrayList<ParentSquare>();
Random rand = new Random();
NumberSquare numberSquare = new NumberSquare();
MineSquare mineSquare = new MineSquare();
public void addMines() {
for (int j = 0; j < 9; j++) {
for (int k = 0; k < 9; k++) {
mines[j][k] = ".";
}
}
for (int i = 0; i < 3; i++) {
int temp_x = rand.nextInt(9);
int temp_y = rand.nextInt(9);
mines[temp_x][temp_y] = "x";
}
}
public void showMines() {
for (int x = 0; x < 9; x++) {
for (int y = 0; y < 9; y++) {
String temp = mines[x][y];
if (temp.equals("x")) {
System.out.println("X: " + (x + 1) + " Y: " + (y + 1) + " Value: " + temp);
}
}
}
}
public Grid() {
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(500, 500);
this.setTitle("Minesweeper 1.0");
mainGrid = new JPanel();
mainGrid.setLayout(new GridLayout(9, 9));
this.add(mainGrid);
button1 = new JButton("Boop");
button2 = new JButton("Poop");
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
buttons[i][j] = new JButton("");
buttons[i][j].addActionListener(this);
buttons[i][j].setBackground(Color.GRAY);
}
}
for (int k = 0; k < 9; k++) {
for (int l = 0; l < 9; l++) {
mainGrid.add(buttons[k][l]);
}
}
addMines();
showMines();
}
public void countBorders(int x, int y) {
int UL = 0, UU = 0, UR = 0, LL = 0, RR = 0, DL = 0, DD = 0, DR = 0, SUM = 0;
if (x > 0) {
UU = checkTile(x - 1, y);
}
if (y > 0) {
LL = checkTile(x, y - 1);
}
if (y < 8) {
RR = checkTile(x, y + 1);
}
if (x < 8) {
DD = checkTile(x + 1, y);
}
if ((x > 0) && (y > 0)) {
UL = checkTile(x - 1, y - 1);
}
if ((x > 0) && (y < 8)) {
UR = checkTile(x - 1, y + 1);
}
if ((x < 8) && (y > 0)) {
DL = checkTile(x + 1, y - 1);
}
if ((x < 8) && (y < 8)) {
DR = checkTile(x + 1, y + 1);
}
SUM = UL + UU + UR + LL + RR + DL + DD + DR;
printTile(x, y, SUM);
if (SUM == 0) { //MY MAIN PROBLEM
// if ((x > 0) && (y > 0)) {countBorders(x-1, y-1);} //Upper left
if (x > 0) {
countBorders(x - 1, y);
} //Upper
// if ((x > 0) && (y < 8)) {countBorders(x-1, y+1);} //Upper right
if (y > 0) {
countBorders(x, y - 1);
} //Left
// if (y < 8) {countBorders(x, y+1);} //Right
// if ((x < 8) && (y > 0)) {countBorders(x+1, y-1);} //Down Left
// if (x < 8) {countBorders(x+1, y);} //Down
// if ((x < 8) && (y < 8)) {countBorders(x+1, y+1);} //Down Right
}
}
public void printTile(int x, int y, int SUM) {
String text = Integer.toString(SUM);
buttons[x][y].setText(text);
buttons[x][y].setBackground(Color.CYAN);
}
public int checkTile(int x, int y) {
String c = mines[x][y];
if (c.equals("x")) {
return 1;
} else {
return 0;
}
}
public void click(int x, int y) {
String mine = mines[x][y];
if (mine.equals("x")) {
System.out.println("Bomb!!!");
buttons[x][y].setText("!");
buttons[x][y].setBackground(Color.RED);
} else {
countBorders(x, y);
System.out.println("Safe!!!");
// buttons[x][y].setText("√");
// buttons[x][y].setBackground(Color.WHITE);
}
}
#Override
public void actionPerformed(ActionEvent e) {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (e.getSource() == buttons[i][j]) {
System.out.println("Clicked Tile X: " + (i + 1) + " Y: " + (j + 1));
//buttons[i][j].setText("!");
click(i, j);
}
}
}
}
}
Is there a way on how to fix this recursion problem?
Thank you in advance and I'm really trying to learn Java. Have a nice day!
Your error-causing recursion has no stopping logic that I can find, and what you need to do is to somehow check to make sure that a cell hasn't already been counted or pressed before re-counting it. Otherwise the code risks throwing a stackoverflow error. This will require giving the cells being counted some state that would tell you this information, that would tell you if the cell has already been counted.
For an example of a successful program that does this logic, feel free to look at my Swing GUI example, one I created 5 years ago. In this code, I've got a class, MineCellModel, that provides the logic (not the GUI) for a single mine sweeper cell, and the class contains a boolean field, pressed, that is false until the cell is "pressed", either by the user pressing the equivalent button, or recursively in the model's logic. If the cell is pressed, if the boolean is true, the recursion stops with this cell.
You can find the code here: Minesweeper Action Events. It's an old program, and so I apologize for any concepts or code that may be off.
Running the code results in this:
Here's the code present in a single file:
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.SwingConstants;
import javax.swing.event.SwingPropertyChangeSupport;
#SuppressWarnings("serial")
public class MineSweeper {
private JPanel mainPanel = new JPanel();
private MineCellGrid mineCellGrid;
private JButton resetButton = new JButton("Reset");
public MineSweeper(int rows, int cols, int mineTotal) {
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.PAGE_AXIS));
mineCellGrid = new MineCellGrid(rows, cols, mineTotal);
resetButton.setMnemonic(KeyEvent.VK_R);
resetButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
mineCellGrid.reset();
}
});
mainPanel.add(mineCellGrid);
mainPanel.add(new JSeparator());
mainPanel.add(new JPanel() {
{
add(resetButton);
}
});
}
private JPanel getMainPanel() {
return mainPanel;
}
private static void createAndShowUI() {
JFrame frame = new JFrame("MineSweeper");
// frame.getContentPane().add(new MineSweeper(20, 20,
// 44).getMainPanel());
frame.getContentPane().add(new MineSweeper(12, 12, 13).getMainPanel());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
createAndShowUI();
}
});
}
}
#SuppressWarnings("serial")
class MineCellGrid extends JPanel {
private MineCellGridModel model;
private List<MineCell> mineCells = new ArrayList<MineCell>();
public MineCellGrid(final int maxRows, final int maxCols, int mineNumber) {
model = new MineCellGridModel(maxRows, maxCols, mineNumber);
setLayout(new GridLayout(maxRows, maxCols));
for (int row = 0; row < maxRows; row++) {
for (int col = 0; col < maxCols; col++) {
MineCell mineCell = new MineCell(row, col);
add(mineCell);
mineCells.add(mineCell);
model.add(mineCell.getModel(), row, col);
}
}
reset();
}
public void reset() {
model.reset();
for (MineCell mineCell : mineCells) {
mineCell.reset();
}
}
}
class MineCellGridModel {
private MineCellModel[][] cellModelGrid;
private List<Boolean> mineList = new ArrayList<Boolean>();
private CellModelPropertyChangeListener cellModelPropChangeListener = new CellModelPropertyChangeListener();
private int maxRows;
private int maxCols;
private int mineNumber;
private int buttonsRemaining;
public MineCellGridModel(final int maxRows, final int maxCols, int mineNumber) {
this.maxRows = maxRows;
this.maxCols = maxCols;
this.mineNumber = mineNumber;
for (int i = 0; i < maxRows * maxCols; i++) {
mineList.add((i < mineNumber) ? true : false);
}
cellModelGrid = new MineCellModel[maxRows][maxCols];
buttonsRemaining = (maxRows * maxCols) - mineNumber;
}
public void add(MineCellModel model, int row, int col) {
cellModelGrid[row][col] = model;
model.addPropertyChangeListener(cellModelPropChangeListener);
}
public void reset() {
buttonsRemaining = (maxRows * maxCols) - mineNumber;
// randomize the mine location
Collections.shuffle(mineList);
// reset the model grid and set mines
for (int r = 0; r < cellModelGrid.length; r++) {
for (int c = 0; c < cellModelGrid[r].length; c++) {
cellModelGrid[r][c].reset();
cellModelGrid[r][c].setMined(mineList.get(r * cellModelGrid[r].length + c));
}
}
// advance value property of all neighbors of a mined cell
for (int r = 0; r < cellModelGrid.length; r++) {
for (int c = 0; c < cellModelGrid[r].length; c++) {
if (cellModelGrid[r][c].isMined()) {
int rMin = Math.max(r - 1, 0);
int cMin = Math.max(c - 1, 0);
int rMax = Math.min(r + 1, cellModelGrid.length - 1);
int cMax = Math.min(c + 1, cellModelGrid[r].length - 1);
for (int row2 = rMin; row2 <= rMax; row2++) {
for (int col2 = cMin; col2 <= cMax; col2++) {
cellModelGrid[row2][col2].incrementValue();
}
}
}
}
}
}
private class CellModelPropertyChangeListener implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent evt) {
MineCellModel model = (MineCellModel) evt.getSource();
int row = model.getRow();
int col = model.getCol();
if (evt.getPropertyName().equals(MineCellModel.BUTTON_PRESSED)) {
if (cellModelGrid[row][col].isMineBlown()) {
mineBlown();
} else {
buttonsRemaining--;
if (buttonsRemaining <= 0) {
JOptionPane.showMessageDialog(null, "You've Won!!!", "Congratulations",
JOptionPane.PLAIN_MESSAGE);
}
if (cellModelGrid[row][col].getValue() == 0) {
zeroValuePress(row, col);
}
}
}
}
private void mineBlown() {
for (int r = 0; r < cellModelGrid.length; r++) {
for (int c = 0; c < cellModelGrid[r].length; c++) {
MineCellModel model = cellModelGrid[r][c];
if (model.isMined()) {
model.setMineBlown(true);
}
}
}
}
private void zeroValuePress(int row, int col) {
int rMin = Math.max(row - 1, 0);
int cMin = Math.max(col - 1, 0);
int rMax = Math.min(row + 1, cellModelGrid.length - 1);
int cMax = Math.min(col + 1, cellModelGrid[row].length - 1);
for (int row2 = rMin; row2 <= rMax; row2++) {
for (int col2 = cMin; col2 <= cMax; col2++) {
cellModelGrid[row2][col2].pressedAction();
}
}
}
}
}
#SuppressWarnings("serial")
class MineCell extends JPanel {
private static final String LABEL = "label";
private static final String BUTTON = "button";
private static final int PS_WIDTH = 24;
private static final int PS_HEIGHT = PS_WIDTH;
private static final float LABEL_FONT_SIZE = (float) (24 * PS_WIDTH) / 30f;
private static final float BUTTON_FONT_SIZE = (float) (14 * PS_WIDTH) / 30f;
private JButton button = new JButton();
private JLabel label = new JLabel(" ", SwingConstants.CENTER);
private CardLayout cardLayout = new CardLayout();
private MineCellModel model;
public MineCell(final boolean mined, int row, int col) {
model = new MineCellModel(mined, row, col);
model.addPropertyChangeListener(new MyPCListener());
label.setFont(label.getFont().deriveFont(Font.BOLD, LABEL_FONT_SIZE));
button.setFont(button.getFont().deriveFont(Font.PLAIN, BUTTON_FONT_SIZE));
button.setMargin(new Insets(1, 1, 1, 1));
setLayout(cardLayout);
add(button, BUTTON);
add(label, LABEL);
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
pressedAction();
}
});
button.addMouseListener(new MouseAdapter() {
#Override
public void mousePressed(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON3) {
model.upDateButtonFlag();
}
}
});
}
public MineCell(int row, int col) {
this(false, row, col);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(PS_WIDTH, PS_HEIGHT);
}
public void pressedAction() {
if (model.isFlagged()) {
return;
}
model.pressedAction();
}
public void showCard(String cardConstant) {
cardLayout.show(this, cardConstant);
}
// TODO: have this change the button's icon
public void setFlag(boolean flag) {
if (flag) {
button.setBackground(Color.yellow);
button.setForeground(Color.red);
button.setText("f");
} else {
button.setBackground(null);
button.setForeground(null);
button.setText("");
}
}
private void setMineBlown(boolean mineBlown) {
if (mineBlown) {
label.setBackground(Color.red);
label.setOpaque(true);
showCard(LABEL);
} else {
label.setBackground(null);
}
}
public MineCellModel getModel() {
return model;
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
model.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
model.removePropertyChangeListener(listener);
}
private class MyPCListener implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent evt) {
String propName = evt.getPropertyName();
if (propName.equals(MineCellModel.MINE_BLOWN)) {
setMineBlown(true);
} else if (propName.equals(MineCellModel.FLAG_CHANGE)) {
setFlag(model.isFlagged());
} else if (propName.equals(MineCellModel.BUTTON_PRESSED)) {
if (model.isMineBlown()) {
setMineBlown(true);
} else {
String labelText = (model.getValue() == 0) ? "" : String.valueOf(model
.getValue());
label.setText(labelText);
}
showCard(LABEL);
}
}
}
public void reset() {
setFlag(false);
setMineBlown(false);
showCard(BUTTON);
label.setText("");
}
}
class MineCellModel {
public static final String FLAG_CHANGE = "Flag Change";
public static final String BUTTON_PRESSED = "Button Pressed";
public static final String MINE_BLOWN = "Mine Blown";
private int row;
private int col;
private int value = 0;
private boolean mined = false;;
private boolean flagged = false;
private SwingPropertyChangeSupport pcSupport = new SwingPropertyChangeSupport(this);
private boolean pressed = false;
private boolean mineBlown = false;
public MineCellModel(boolean mined, int row, int col) {
this.mined = mined;
this.row = row;
this.col = col;
}
public void incrementValue() {
int temp = value + 1;
setValue(temp);
}
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setMineBlown(boolean mineBlown) {
this.mineBlown = mineBlown;
PropertyChangeEvent evt = new PropertyChangeEvent(this, MINE_BLOWN, false, true);
pcSupport.firePropertyChange(evt);
}
public boolean isMineBlown() {
return mineBlown;
}
public void setMined(boolean mined) {
this.mined = mined;
}
public void setFlagged(boolean flagged) {
this.flagged = flagged;
}
public int getRow() {
return row;
}
public int getCol() {
return col;
}
public boolean isMined() {
return mined;
}
public boolean isFlagged() {
return flagged;
}
public void pressedAction() {
if (pressed) {
return;
}
pressed = true;
if (mined) {
setMineBlown(true);
}
PropertyChangeEvent evt = new PropertyChangeEvent(this, BUTTON_PRESSED, -1, value);
pcSupport.firePropertyChange(evt);
}
public void upDateButtonFlag() {
boolean oldValue = flagged;
setFlagged(!flagged);
PropertyChangeEvent evt = new PropertyChangeEvent(this, FLAG_CHANGE, oldValue, flagged);
pcSupport.firePropertyChange(evt);
}
public void reset() {
mined = false;
flagged = false;
pressed = false;
mineBlown = false;
value = 0;
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcSupport.removePropertyChangeListener(listener);
}
}
Edit Regarding Recursion
My code uses recursion, but with a level of indirection, since it is based on a Model-View-Controller type of design pattern, and the recursion is within the notification of listeners. Note that each GUI MineCell object holds its own MineCellModel object, the latter holds the MineCell's state. When a GUI JButton held within the MineCell object is pressed, its ActionListener calls the same class's pressed() method:
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
pressedAction();
}
});
This method first checks the corresponding MineCellModel to see if it has been "flagged", if a boolean called flagged is true. If so, this means that the user has right-clicked on the button, and it is not active, and so the method returns. Otherwise the MineCellModel's pressedAction() method is called,
public void pressedAction() {
if (model.isFlagged()) {
return;
}
model.pressedAction();
}
and here is where the recursion starts, and it does so through an Observer Design Pattern:
// within MineCellModel
public void pressedAction() {
if (pressed) {
// if the button's already been pressed -- return, do nothing
return;
}
// otherwise make pressed true
pressed = true;
// if we've hit a mine -- blow it!
if (mined) {
setMineBlown(true);
}
// *** Here's the key *** notify all listeners that this button has been pressed
PropertyChangeEvent evt = new PropertyChangeEvent(this, BUTTON_PRESSED, -1, value);
pcSupport.firePropertyChange(evt);
}
The two lines of code on the bottom notify any listeners to this model that its BUTTON_PRESSED state has been changed, and it sends the MineCellModel's value to all listeners. The value int is key as its the number of neighbors that have mines. So what listens to the MineCellModel? Well, one key object is the MineCellGridModel, the model that represents the state of the entire grid. It has a CellModelPropertyChangeListener class that does the actual listening, and within this class is the following code:
private class CellModelPropertyChangeListener implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent evt) {
// first get the MineCellModel for the cell that triggered this notification
MineCellModel model = (MineCellModel) evt.getSource();
int row = model.getRow();
int col = model.getCol();
// if the event is a button pressed event
if (evt.getPropertyName().equals(MineCellModel.BUTTON_PRESSED)) {
// first check if a mine was hit, and if so, call mineBlown()
if (cellModelGrid[row][col].isMineBlown()) {
mineBlown(); // this method iterates through all cells and blows all mines
} else {
// here we check for a winner
buttonsRemaining--;
if (buttonsRemaining <= 0) {
JOptionPane.showMessageDialog(null, "You've Won!!!", "Congratulations",
JOptionPane.PLAIN_MESSAGE);
}
// here is the key spot -- if cell's value is 0, call the zeroValuePress method
if (cellModelGrid[row][col].getValue() == 0) {
zeroValuePress(row, col);
}
}
}
}
private void mineBlown() {
// ... code to blow all the un-blown mines
}
// this code is called if a button pressed has 0 value -- no mine neighbors
private void zeroValuePress(int row, int col) {
// find the boundaries of the neighbors
int rMin = Math.max(row - 1, 0); // check for the top edge
int cMin = Math.max(col - 1, 0); // check for the left edge
int rMax = Math.min(row + 1, cellModelGrid.length - 1); // check for the bottom edge
int cMax = Math.min(col + 1, cellModelGrid[row].length - 1); // check for right edge
// iterate through the neighbors
for (int row2 = rMin; row2 <= rMax; row2++) {
for (int col2 = cMin; col2 <= cMax; col2++) {
// *** Here's the recursion ***
// call pressedAction on all the neighbors
cellModelGrid[row2][col2].pressedAction();
}
}
}
}
So the key method in the listener above is the zeroValuePress(...) method. It first finds the boundaries of the neighbors around the current mine cell, using Math.min(...) and Math.max(...) to be careful not to go beyond the right, left, or top or bottom boundaries of the grid. It then iterates through the cell neighbors calling pressedAction() on each one of the neighbors MineCellModels held by this grid. As you know from above, the pressedAction() method will check if the cell has already been pressed, and if not, changes its state, which then notifies this same listener, resulting in recursion.
One of the primary functions of this game is to have a recursion to check all the sides when the tile clicked has no bombs surrounding it.
Looks like you are stucked on the part where you need to update the cell with number according to the number of bombs surrounding it.
These are the things for you to take note:
To update the numbers on the cells, there is no need to use recursion. The only part I used recursion is when user clicks on a cell with value == 0(stepped on an empty grid).
Checking all 8 directions can be done easily without writing large number of if-conditions. All you need is a pair of nested for-loop. Just traverse the 3x3 grid like a 2D array (see diagram below for illustration).
In the loop, set conditions to ensure you are within bounds (of the 3x3 matrix) before reading current grid's value (see code below).
To traverse the 3x3 matrix as shown in the diagram, we can use a pair of nested loops:
for(int x=(coordX-1); x<=(coordX+1); x++)
for(int y=(coordY-1); y<=(coordY+1); y++)
if(x!=-1 && y!= -1 && x! = ROWS && y! = COLS && map[x][y] != 'B')
if(map[x][y] == '.')
map[x][y] = '1';
else
map[x][y] += 1;
The if-condition prevents working on array element which is out of bounds.
I'm currently making a space invaders-esque game for my software engineering course. I've already got everything working that satisfies the requirements, so this isn't a 'solve my homework' kind of question. My problem is that the game will lag (at what seems like random times & intervals) to the point where it becomes too frustrating to play. Some things I think might be causing this - though I'm not positive - are as follows:
Problem with timer event every 10 ms (I doubt this because of the very limited resources required for this game).
Problem with collision detection (checking for collision with every visible enemy every 10 ms seems like it would take up a large chunk of resources)
Problem with repainting? This seems unlikely to me however...
#SuppressWarnings("serial")
public class SIpanel extends JPanel {
private SIpanel panel;
private Timer timer;
private int score, invaderPace, pulseRate, mysteryCount, distanceToEdge;
private ArrayList<SIthing> cast;
private ArrayList<SIinvader> invaders, dead;
private ArrayList<SImissile> missileBase, missileInvader;
private SIinvader[] bottomRow;
private SIbase base;
private Dimension panelDimension;
private SImystery mysteryShip;
private boolean gameOver, left, right, mysteryDirection, space, waveDirection;
private boolean runningTimer;
private Music sound;
private void pulse() {
pace();
processInputs();
if (gameOver) gameOver();
repaint();
}
private void pace() {
// IF invaders still live
if (!invaders.isEmpty()) {
invaderPace++;
// Switch back manager
if (distanceToEdge <= 10) {
switchBack();
pulseRate = (pulseRate >= 16) ? (int) (pulseRate*(0.8)) : pulseRate;
waveDirection = !waveDirection;
distanceToEdge = calculateDistanceToEdge();
}
// Move invaders left/right
else if (invaderPace >= pulseRate) {
invaderPace = 0;
distanceToEdge = calculateDistanceToEdge();
moveAI();
invadersFire();
if (!dead.isEmpty()) removeDead();
if (mysteryCount < 1) tryInitMysteryShip();
}
// All invaders are kill, create new wave
} else if (missileBase.isEmpty() && missileInvader.isEmpty() && !cast.contains(mysteryShip)) {
// System.out.println("New Wave!");
newWave();
}
// Every pace
if (!missileBase.isEmpty()) moveMissileBase();
// Every two paces
if (invaderPace % 2 == 0) {
if (!missileInvader.isEmpty()) moveMissileInvader();
if (mysteryCount > 0) moveMysteryShip();
}
}
private void processInputs() {
if (left) move(left);
if (right) move(!right);
if (space) fireMissile(base, true);
}
protected void fireMissile(SIship ship, boolean isBase) {
if(isBase && missileBase.isEmpty()) {
base.playSound();
SImissile m = new SImissile(ship.getX()+(ship.getWidth()/2), ship.getY()-(ship.getHeight()/4));
missileBase.add(m);
cast.add(m);
} else if (!isBase && missileInvader.size()<3) {
base.playSound();
SImissile m = new SImissile(ship.getX()+(ship.getWidth()/2), ship.getY()+(ship.getHeight()/4));
missileInvader.add(m);
cast.add(m);
}
}
private void newWave() {
pulseRate = 50;
int defaultY=60, defaultX=120, defaultWidth=30, defaultHeight=24;
for(int i=0; i<5; i++) {
for(int j=0; j<10; j++) {
if (i<1) invaders.add(new SItop((j*defaultWidth)+defaultX, (i*defaultHeight)+defaultY, defaultWidth, defaultHeight));
else if (i<3) invaders.add(new SImiddle((j*defaultWidth)+defaultX, (i*defaultHeight)+defaultY, defaultWidth, defaultHeight));
else if (i<5) invaders.add(new SIbottom((j*defaultWidth)+defaultX, (i*defaultHeight)+defaultY, defaultWidth, defaultHeight));
}
}
for (SIinvader s: invaders) {
cast.add(s);
}
if (!cast.contains(base)) {
cast.add(base);
}
bottomRow = getBottomRow();
}
private void tryInitMysteryShip() {
Random rand = new Random();
int x=rand.nextInt(1000);
if (x<=3) {
mysteryCount = 1;
if (rand.nextBoolean()) {
mysteryDirection = true;
}
if (mysteryDirection) {
mysteryShip = new SImystery(0, 60, 36, 18);
} else {
mysteryShip = new SImystery(480, 60, 36, 18);
}
cast.add(mysteryShip);
}
}
private void moveMysteryShip() {
int distance = 0;
if (mysteryDirection) {
mysteryShip.moveRight(5);
distance = getWidth() - mysteryShip.getX();
} else {
mysteryShip.moveLeft(5);
distance = 30+mysteryShip.getX()-mysteryShip.getWidth();
}
if (distance <= 5) {
dead.add(mysteryShip);
mysteryShip = null;
mysteryCount = 0;
}
}
private void removeDead() {
#SuppressWarnings("unchecked")
ArrayList<SIinvader> temp = (ArrayList<SIinvader>) dead.clone();
dead.clear();
for (SIinvader s : temp) {
invaders.remove(s);
cast.remove(s);
}
bottomRow = getBottomRow();
}
private void invadersFire() {
int[] p = new int[bottomRow.length];
for (int i=0; i<p.length; i++) {
for (int j=0; j<p.length; j++) {
p[j] = j;
}
Random rand = new Random();
int a=rand.nextInt(101);
if (a>=20) {
int b=rand.nextInt(p.length);
fireMissile(bottomRow[b], false);
}
}
}
private int calculateDistanceToEdge() {
int distance = 0;
SIinvader[] outliers = getOutliers();
if (waveDirection) {
distance = getWidth() - outliers[0].getX()-outliers[0].getWidth();
} else {
distance = outliers[1].getX();
}
return distance;
}
private SIinvader[] getOutliers() {
SIinvader leftMost = invaders.get(0), rightMost = invaders.get(0);
for (SIinvader s : invaders) {
if (s.getX() < leftMost.getX()) {
leftMost = s;
}
if (s.getX() > rightMost.getX()) {
rightMost = s;
}
}
return new SIinvader[] { rightMost, leftMost };
}
private SIinvader[] getBottomRow() {
SIinvader[] x = new SIinvader[(invaders.size()>10)?10:invaders.size()];
for (int i=0; i<x.length; i++) {
x[i] = invaders.get(i);
for (SIinvader s:invaders) {
if (s.getX() == x[i].getX()) {
if (s.getY() > x[i].getY()) {
x[i] = s;
}
}
}
}
return x;
}
private void move(boolean b) {
int defaultX = 5;
if (b) base.moveLeft(defaultX);
else base.moveRight(defaultX);
}
private void moveAI() {
for(SIinvader s : invaders) {
s.changeImage();
int defaultX = 5;
if (waveDirection) s.moveRight(defaultX);
else s.moveLeft(defaultX);
}
}
private void moveMissileBase() {
if (invaders.isEmpty()) return;
int movement = -5, bound = 0;
SImissile missile = missileBase.get(0);
missile.moveDown(movement);
SIinvader lowestInvader = getLowestInvader();
if (missile.getY() < (lowestInvader.getY() + lowestInvader.getHeight())) {
for (SIinvader s:bottomRow) {
if (checkCollision(missile, s)) {
s.setHit();
dead.add(s);
cast.remove(missile);
missileBase.clear();
score += s.value;
return;
}
}
if (mysteryCount > 0) {
if (checkCollision(missile, mysteryShip)) {
mysteryShip.setHit();
dead.add(mysteryShip);
cast.remove(missile);
missileBase.clear();
score += mysteryShip.value;
return;
}
}
if (missile.getY() < bound) {
missileBase.remove(missile);
cast.remove(missile);
}
}
}
private SIinvader getLowestInvader() {
SIinvader lowest = bottomRow[0];
for (SIinvader invader : bottomRow) {
if (invader.getY() > lowest.getY()) {
lowest = invader;
}
}
return lowest;
}
private void moveMissileInvader() {
int movement = 5, bound = (int) panelDimension.getHeight();
for (SImissile missile : missileInvader) {
missile.moveDown(movement);
if(missile.getY() >= base.getY()) {
if (checkCollision(missile, base)) {
base.setHit();
gameOver = true;;
missileInvader.remove(missile);
cast.remove(missile);
return;
} else if (missile.getY() >= bound-25) {
missileInvader.remove(missile);
cast.remove(missile);
return;
}
}
}
}
private boolean checkCollision(SIthing missile, SIthing ship) {
Rectangle2D rect1 = new Rectangle2D.Double(
missile.getX(),
missile.getY(),
missile.getWidth(),
missile.getHeight()
);
Rectangle2D rect2 = new Rectangle2D.Double(
ship.getX(),
ship.getY(),
ship.getWidth(),
ship.getHeight()
);
return rect1.intersects(rect2);
}
private void switchBack() {
int defaultY = 12;
for (SIinvader s : invaders) {
if (s.getY() > getHeight()) {
gameOver = true;
return;
}
s.moveDown(defaultY);
}
}
private void gameOver() {
pause(true);
SI.setGameOverLabelVisibile(true);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setColor(Color.GREEN);
Font font = new Font("Arial", 0, 20);
setFont(font);
String score = "Score: "+this.score;
Rectangle2D rect = font.getStringBounds(score, g2.getFontRenderContext());
int screenWidth = 0;
try { screenWidth = (int) panelDimension.getWidth(); }
catch (NullPointerException e) {}
g2.setColor(Color.GREEN);
g2.drawString(score, (int) (screenWidth - (10 + rect.getWidth())), 20);
for(SIthing a:cast) {
a.paint(g);
}
}
public SIpanel() {
super();
setBackground(Color.BLACK);
cast = new ArrayList<SIthing>();
missileBase = new ArrayList<SImissile>();
score = invaderPace = mysteryCount = pulseRate = 0;
sound = new Music("AmbientMusic.wav");
panel = this;
addKeyListener(new KeyAdapter() {
#Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT : left = true; break;
case KeyEvent.VK_RIGHT : right = true; break;
case KeyEvent.VK_SPACE : space = true; break;
}
}
#Override
public void keyReleased(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT : left = false; break;
case KeyEvent.VK_RIGHT : right = false; break;
case KeyEvent.VK_SPACE : space = false; break;
}
}
});
setFocusable(true);
timer = new Timer(10, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
pulse();
}
});
}
public void reset() {
SI.setGameOverLabelVisibile(false);
score = invaderPace = mysteryCount = 0;
pulseRate = 50;
cast = new ArrayList<SIthing>();
invaders = new ArrayList<SIinvader>();
dead = new ArrayList<SIinvader>();
missileBase = new ArrayList<SImissile>();
missileInvader = new ArrayList<SImissile>();
base = new SIbase(230, 370, 26, 20);
waveDirection = true;
gameOver = false;
sound.stop();
sound.loop();
panelDimension = SI.getFrameDimensions();
bottomRow = getBottomRow();
newWave();
timer.start();
runningTimer=true;
}
public SIpanel getPanel() {
return this.panel;
}
public void pause(boolean paused) {
if (paused) timer.stop();
else timer.start();
}
}
I believe that collision detection may be the reason for lagging and you should simply investigate it by trying to increase and decrease count of enemies or missiles drastically to see if that makes a difference.
Consider garbage collector your enemy. In your checkCollision method you are instantiating two (very simple) objects. It may not seem like a lot, but consider that your might be creating them for each collision check, and that at 60fps it adds up until it may reach critical mass when GC says "stop the world" and you see noticeable lag.
If that is the case, possible solution to that would be to not instantiate any objects in a method called so frequently. You may create Rectangle2D once, and then update its position, instead of creating a new one each time, so you will avoid unnecessary memory allocation.
Within the scope of a paper I am writing at high school I chose to make my own audio-file-to-spectrogram-converter from scratch in order to create landscapes out of these spectrograms.
I already do have my implementation of an FFT and of using that to make a heightmap, a spectrogram. But I often get weird artifacts in the form of vertical stripes when the frequencies get dense, as you can see in the image below.
The example is right at the beginning with a window length of 2048 and on a log^2-scale. The FFT I am using is flawless, I've already compared it to others and they produce the same result.
This is the function which transforms the amplitudes into frequencies and stores them in a 2D-array:
private void transform(int from, int until) {
double val, step;
for(int i=from; i<until; i++) {
for(int j=0; j<n; j++)
chunk[j] = data[0][i*n+j+start];
fft.realForward(chunk);
for(int j=0; j<height; j++) {
val = Math.sqrt(chunk[2*j]*chunk[2*j] + chunk[2*j+1]*chunk[2*j+1]);
map[i][j] = val;
}
}
}
Now my Question: Where do these vertical stripes come from and how do I get rid of them?
I currently don't employ a window function and every calculation is stringed to one another, which means there is no overlapping. It is the simplest way you can think of making a spectrogram. Could it help introducing a window function or doing each calculation independent of whether the frame was already involved in a previous calculation, that is to say overlapping the frame-windows?
Also, what other ways are there to improve on my basic approach in order to get a better result?
This is the whole class. I feed it the data and all the necessary information from an audio file:
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.ImageIO;
import javax.swing.*;
import org.jtransforms.fft.DoubleFFT_1D;
public class Heightmap extends JFrame implements WindowListener{
public static final int LOG_SCALE = 0;
public static final int LOG_SQUARE_SCALE = 1;
public static final int SQUARE_SCALE = 2;
public static final int LINEAR_SCALE = 3;
private BufferedImage heightmap;
private FileDialog chooser;
private JMenuBar menuBar;
private JMenu fileMenu;
private JMenuItem save, close;
private DoubleFFT_1D fft;
private int[][] data;
private double[][] map;
private double[] chunk;
private int width, height, n, start, scale;
private String name;
private boolean inactive;
public Heightmap(int[][] data, int resolution, int start,
int width, int height, int scale, String name) {
this.data = data;
this.n = resolution;
this.start = start;
this.width = width;
this.height = height;
this.scale = scale;
this.name = name;
fft = new DoubleFFT_1D(n);
map = new double[width][height];
heightmap = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
chunk = new double[n];
System.out.println("Starting transformation...");
long time;
time = System.currentTimeMillis();
transform();
time = System.currentTimeMillis() - time;
System.out.println("Time taken for calculation: "+time+" ms");
time = System.currentTimeMillis();
makeHeightmap();
initComponents();
time = System.currentTimeMillis() - time;
System.out.println("Time taken for drawing heightmap: "+time+" ms");
}
private void initComponents() {
this.setSize(width, height);
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
this.setResizable(false);
this.setLocationRelativeTo(null);
this.setTitle(name);
createMenuBar();
chooser = new FileDialog(this, "Choose a directory", FileDialog.SAVE);
chooser.setDirectory("/Users/<user>/Desktop");
this.addMouseListener(new HeightmapMouseListener());
this.addKeyListener(new HeightmapKeyListener());
this.addWindowListener(this);
this.setVisible(true);
}
private void createMenuBar() {
menuBar = new JMenuBar();
fileMenu = new JMenu();
fileMenu.setText("File");
save = new JMenuItem("Save...", KeyEvent.VK_S);
save.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.META_DOWN_MASK));
save.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent arg0) {
chooser.setVisible(true);
String fileName = chooser.getFile();
String dir = chooser.getDirectory();
chooser.setDirectory(dir);
if(fileName != null) {
try {
File outputfile = new File(dir + fileName + ".png");
ImageIO.write(heightmap, "png", outputfile);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Saved "+fileName+".png to "+dir);
}
}
});
close = new JMenuItem("Close", KeyEvent.VK_C);
close.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.META_DOWN_MASK));
close.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
setVisible(false);
dispose();
}
});
fileMenu.add(save);
fileMenu.addSeparator();
fileMenu.add(close);
menuBar.add(fileMenu);
this.setJMenuBar(menuBar);
}
public void paint(Graphics g) {
g.drawImage(heightmap, 0, 0, null);
}
private void transform() {
transform(0, width);
}
private void transform(int from, int until) {
double max = Double.MIN_VALUE;
double min = Double.MAX_VALUE;
double val, step;
for(int i=from; i<until; i++) {
for(int j=0; j<n; j++) {
chunk[j] = data[0][i*n+j+start];
}
fft.realForward(chunk);
for(int j=0; j<height; j++) {
val = Math.sqrt(chunk[2*j]*chunk[2*j] + chunk[2*j+1]*chunk[2*j+1]);
if(val > max)
max = val;
if(val < min)
min = val;
map[i][j] = val;
}
if(min != 0) {
step = max/(max-min);
for(int j=0; j<height; j++)
map[i][j] = (map[i][j]-min)*step;
}
}
}
/*
* Paints heightmap into the BufferedImage
*/
private void makeHeightmap() {
double max = 0;
switch(scale) {
case LOG_SCALE: max = Math.log(findMax(map)+1); break;
case LOG_SQUARE_SCALE: max = Math.pow(Math.log(findMax(map)+1), 2); break;
case SQUARE_SCALE: max = Math.sqrt(findMax(map)); break;
case LINEAR_SCALE: max = findMax(map); break;
default: max = Math.pow(Math.log(findMax(map)+1), 2); break;
}
double stepsize = 255.0/max;
int val, rgb;
for(int x=0; x<width; x++)
for(int y=0; y<height; y++) {
switch(scale) {
case LOG_SCALE: val = (int) (Math.log(map[x][y]+1)*stepsize); break;
case LOG_SQUARE_SCALE: val = (int) (Math.log(map[x][y]+1)*stepsize); val *= val; break;
case SQUARE_SCALE: val = (int) (Math.sqrt(map[x][y])*stepsize); break;
case LINEAR_SCALE: val = (int) (map[x][y]*stepsize); break;
default: val = (int) (Math.log(map[x][y]+1)*stepsize); val *= val; break;
}
rgb = 255<<24 | val<<16 | val<<8 | val;
heightmap.setRGB(x, height-y-1, rgb);
}
}
private double findMax(double[][] data) {
double max = 0;
for(double[] val1: data)
for(double d: val1)
if(d > max)
max = d;
return max;
}
private class HeightmapKeyListener implements KeyListener {
boolean busy = false;
public void keyPressed(KeyEvent e) {
if(e.getKeyCode() == KeyEvent.VK_RIGHT && !busy && start < data[0].length-width*n) {
busy = true;
for(int x=0; x<width-1; x++)
map[x] = map[x+1].clone();
start += n;
transform(width-1, width);
makeHeightmap();
repaint();
busy = false;
}
else if(e.getKeyCode() == KeyEvent.VK_LEFT && !busy && start > 0) {
busy = true;
for(int x=width-1; x>0; x--)
map[x] = map[x-1];
start -= n;
transform(0, 1);
makeHeightmap();
repaint();
busy = false;
}
}
public void keyReleased(KeyEvent e) { }
public void keyTyped(KeyEvent e) { }
}
private class HeightmapMouseListener implements MouseListener {
public void mouseClicked(MouseEvent e) {
if(inactive) {
inactive = false;
return;
}
long time = System.currentTimeMillis();
int posX = e.getX();
int diff = posX - width/2; //difference between old and new center in pixels
int oldStart = start;
start = start + diff*n;
if(start < 0) start = 0;
int maxFrame = data[0].length-width*n;
if(start > maxFrame) start = maxFrame;
if(start == oldStart) return;
System.out.println("Changing center...");
int absDiff = Math.abs(diff);
if(start < oldStart) { //shift the start backward, recalculate the start
for(int x=width-1; x>=absDiff; x--)
map[x] = map[x-absDiff].clone();
transform(0, absDiff);
}
else if(start > oldStart) { //shift the back forward, recalculate the back
for(int x=0; x<width-absDiff; x++)
map[x] = map[x+absDiff].clone();
transform(width-absDiff, width);
}
makeHeightmap();
repaint();
System.out.println("Time taken: "+(System.currentTimeMillis()-time)+" ms");
}
public void mousePressed(MouseEvent e) { }
public void mouseReleased(MouseEvent e) { }
public void mouseEntered(MouseEvent e) { }
public void mouseExited(MouseEvent e) { }
}
public void windowActivated(WindowEvent arg0) { }
public void windowClosed(WindowEvent arg0) { }
public void windowClosing(WindowEvent arg0) { }
public void windowDeactivated(WindowEvent arg0) {
inactive = true;
}
public void windowDeiconified(WindowEvent arg0) { }
public void windowIconified(WindowEvent arg0) { }
public void windowOpened(WindowEvent arg0) { }
}
EDIT:
Implementing a window function improved the result drastically. I really didn't understand what a window function would do and therefore underestimated its effect.
However, after doing so I tried mapping a cosine wave with a frequency of 10kHz which (again) produced some strange artifacts:
What could be the cause of this one? I implemented a overflow protection by clipping everything under 0 to 0 and over 255 to 255 with no change whatsoever.
This type of artifact can be due to overflow or otherwise exceeding parameter bounds before or in your color mapping function, or perhaps with some function (log?) returning NaN values. You might be able to find this by putting in some asserts for out-of-range or illegal values.
This is related to How to get X and Y index of element inside GridLayout? post and its answers.
For whatever reason none of them suggested to extend JButton to include its position in the grid and in associated array of buttons.
I have made the following illustration that simply displays button's coordinates when it's clicked.
Extended JButton:
package buttons_array;
import javax.swing.*;
#SuppressWarnings("serial")
public class ButtonWithCoordinates extends JButton {
int coordX;
int coordY;
public ButtonWithCoordinates(String buttonText, int coordX, int coordY) {
super(buttonText);
this.coordX = coordX;
this.coordY = coordY;
}
/**
* #return the coordX
*/
public int getCoordX() {
return coordX;
}
/**
* #return the coordY
*/
public int getCoordY() {
return coordY;
}
}
sample GUI:
package buttons_array;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ButtonsArray implements ActionListener {
private ButtonWithCoordinates buttons[][];
private int nRows;
private int nCols;
private JFrame frame;
private JPanel panel;
public ButtonsArray(int x, int y) {
if (x > 0 && y > 0) {
nRows = x;
nCols = y;
buttons = new ButtonWithCoordinates[nRows][nCols];
for (int i=0; i < nRows; ++i) {
for (int j=0; j < nCols; ++j) {
buttons[i][j] = new ButtonWithCoordinates(" ", i, j);
buttons[i][j].addActionListener(this);
}
}
} else {
throw new IllegalArgumentException("Illegal array dimensions!!!");
}
}
#Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
ButtonWithCoordinates button = (ButtonWithCoordinates) e.getSource();
button.setText(button.getCoordX() + ", " + button.getCoordY());
}
public void GUI() {
if (buttons == null) { throw new NullPointerException("Array is not initialized!!!"); }
frame = new JFrame();
panel = new JPanel();
frame.setContentPane(panel);
panel.setLayout(new GridLayout(nRows, nCols));
for (int i=0; i < nRows; ++i) {
for (int j=0; j < nCols; ++j) {
panel.add(buttons[i][j]);
}
}
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new ButtonsArray(3, 5).GUI();
}
});
}
}
Now my questions:
Have I been reinventing the wheel here? I mean, is there a more straightforward way to achieve the same?
Is it in any way inferior to searching through the array each time we need to find the coordinates?
The original version of this example used extension:
GridButton extends JButton
The updated version was predicated on the colloquy seen here. While extension may be appropriate in some contexts, a few alternatives are mentioned here; a client property is particularly convenient. Identifying a button from its grid coordinates is also easy:
private static final int N = 5;
List<JButton> list = new ArrayList<>();
…
private JButton getGridButton(int r, int c) {
int index = r * N + c;
return list.get(index);
}