So I've got JPanel inside JScrollPane. Is there any possible way to display only visible area on JPanel? Right now my piece of code looks like that:
public void paintComponent(Graphics g)
{
super.paintComponent(g);
for(int i = 0; i < m_xTiles; i++)
{
for(int j = 0; j < m_yTiles; j++)
{
m_mapTiles.get(i).get(j).DrawImage(g);
}
}
}
It's not something that I want because it's display everything and when I want to make a very big map the movment are very slow. I want to get visible rectangle on screan - positions of four pixels and then I will calculate to my own x and y :)
Normally the graphic system cares about clipping operations outside the visible bounds so these operation become no-ops and are not expensive. So in most cases you don’t need to deal with these information as the graphics operations are the expensive part when painting.
However, sometimes you may encounter the situation that the operations you perform before the calls on the Graphics object become expensive, i.e. if you have a really large but only partially visible component (as you described). In this situation it might be useful to access the clipping information to perform manually skipping, especially if you are tiling your area, thus calculating which items to process is rather easy:
public void paintComponent(Graphics g) {
super.paintComponent(g);
// I assert equal tiling of the actual size here, otherwise you may define
// the tile sizes as constants and calculate preferredSize as tileWidth*m_xTiles
// and tileHeight*m_yTiles, respectively
final int tileWidth=getWidth()/m_xTiles;
final int tileHeight=getHeight()/m_yTiles;
Rectangle clip = g.getClipBounds();
int firstX, lastX, firstY, lastY;
if(clip == null) {
firstX=0; lastX=m_xTiles-1;
firstY=0; lastY=m_yTiles-1;
}
else {
firstX=clip.x/tileWidth; lastX=(clip.x+clip.width)/tileWidth;
firstY=clip.y/tileHeight; lastY=(clip.y+clip.height)/tileHeight;
}
// note that the loop condition is <= now to handle partially visible tiles
for(int i = firstX; i <= lastX; i++)
{
for(int j = firstY; j <= lastY; j++)
{
m_mapTiles.get(i).get(j).DrawImage(g);
}
}
}
Related
I have a large set of data I wish to plot on a graph that can range from 10k points to about 20 Million Points. At 10k points the plot happens at an ok speed(within a second), but at 20 Million points the plot seems to take minutes.
I'm using Java for the program and rewriting the code in another language just to improve the graphical plotting speed for one single plot that only occurs at at maximum data set is not in the cards.
Is this speed something I have to live with because a 20 Million point plot inherently will take this long due to the data size or am I missing out on some optimisation flag/method/etc?
My Data is in a 2d Array of 13,000 by 4096 called Data.
This is populated from outside the Plot Function in Main.java
//In Plot.java
public class PlotG extends JPanel
{
double xscale = 0.0;
double yscale = 0.0;
protected void paintComponent(Graphics g)
{
super.paintCompnent(g);
Graphics2D g2 = (Graphics2D)g;
g2.setRenderingHint(RenderingHint.Key_ANTIALIASING, RenderingHint.VALUE_ANTIALIAS_ON);
//Scaling
int sizew = Data.size();
int sizeh = Data.get(0).size();
xscale = (getWidth()*1.0)/(sizew *1.0);
yscale = (getHeight()*1.0)/(sizeh *1.0);
//Set Colour
g2.setPaint(Color.GREEN);
//Plot
for(int j=0; j<sizew; j++)
{
for(int k=0;k<sizeh; k++)
{
if(Data.get(j).get(k) > MinimumValueToPlot) //I only plot points above the constant value MinimumValueToPlot
{
int x = xscale*j;
int y = yscale*k;
g2.fillOval(x,y,1,1);
}
}
}
return;
}
}
private Plot dataPlot = new Plot()
public PlotStuff(ArrayList<ArrayList<Double>> In)
{
Data = In;
InitPLot(getContentPane());
}
private void InitPlot(Container contentPane)
{
getContentPane().setBackground(Color.GRAY);
getContentPane().setLayout(new FlowLayout(FlowLayout.LEADING));
setMinimumSize(new Dimension(1650, 830));
pack();
GraphPanel = new JPanel();
GraphPanel.setBounds(6,11,1470,750);
GraphPanel.setBorder( BorderFactory.createTitleBorder( BorderFactory.createLineBorder(Color.GREEN, 2),
"Title",
TitledBorder.DEFAULT_JUSTIFICATION,
TitledBorder.DEFAULT_POSITION,
new Font("Tahoma", Font.BOLD, 18),
Color.WHITE));
getContentPanel().add(GraphPanel);
GraphPanel.setLayout(new BorderLayout());
dataPlot.setBackGround(Color.BLUE);
dataPlot.setForeGround(Color.WHITE);
dataPlot.setPreferredSize(new Dimension(1470, 750));
GraphPanel.add(dataPlot);
return;
}
//in Main.java
.
.
PlotStuff p = new PlotStuff(Data);
p.redrawGraph();
p.setVisible();
.
.
Is there anyway to improve the speed if the number of points above my constant MinimumValueToPlot reaches 20 Million and above? Given my maximum possible data set is 13k x 4096 = 53,248,000, it is possible, but the highest experienced number of points above MinimumValueToPlot is so far only 20 Million.
Am I doing something wrong in the JPanel declarations? I have seen some discussions say that setPreferredSize shouldn't be used? Is this the case?
Thanks.
You claim Data is a 2d array, but it is not. It is an ArrayList of ArrayLists. Work with an actual array instead.
Instead of ArrayList<ArrayList<Double>> you'd do double[][].
In your loop you are calling fillOval to set the color of a single pixel. That causes huge unneccessary overhead. You are basically telling your application to calculate the pixels for an oval shape of size 1x1 20 million times!
I suggest you create a BufferedImage, get its pixel array, and set the pixel values directly. Then, when done, draw that image to your Graphics2d object:
BufferedImage img = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_3BYTE_BGR);
Graphics2D imgGraphics2D = img.createGraphics();
imgGraphics2D.setColor(Color.white);
imgGraphics2D.fillRect(0, 0, getWidth(), getHeight());
imgGraphics2D.dispose();
byte[] pixels = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
for(int j=0; j < sizew; j++)
{
for(int k=0; k < sizeh; k++)
{
if(data[j][k] > minimumValueToPlot)
{
int x = (int)(xscale * j);
int y = (int)(yscale * k);
int pixelindex = (y * getWidth() + x) * 3;
pixels[pixelindex + 1] = 255; // set the green byte
}
}
}
g2.setComposite(AlphaComposite.Src);
g2.drawImage(img, 0, 0, null);
Please take this with a grain of salt, as I wrote this from memory.
(Also I took the liberty of renaming Data to data and MinimumValueToPlot to minimumValueToPlot. Per convention variables in Java start with a lower case letter to distinguish them from classes.)
Plotting points like that in your paint components means that you paint that many points every time the component is repainted This means it is an expensive operation to resize the window, or click a button because it has to repaint every point.
In this version, I've made a method to redo the graphics, and and the paintComponent just repaints the backing buffer. Note, that painting to the backing buffer will be about as fast as you can accomplish paint, and it ignores the display properties of swing. That way you can use a profiler and see how fast you can make the painting routine go. For example using some of the suggestions of Max above.
public class PlotG extends JPanel
{
BufferedImage buffer;
double xscale = 0.0;
double yscale = 0.0;
public PlotG(){
buffer = new BufferedImage(1470, 750, BufferedImage.TYPE_INT_ARGB);
}
protected void paintComponent(Graphics g)
{
super.paintCompnent(g);
g.drawImage(buffer, 0, 0, this);
}
void updateBuffer(){
Graphics2D g2 = (Graphics2D)backing.getGraphics();
g2.setRenderingHint(RenderingHint.Key_ANTIALIASING, RenderingHint.VALUE_ANTIALIAS_ON);
//Scaling
int sizew = Data.size();
int sizeh = Data.get(0).size();
xscale = (getWidth()*1.0)/(sizew *1.0);
yscale = (getHeight()*1.0)/(sizeh *1.0);
//Set Colour
g2.setPaint(Color.GREEN);
//Plot
for(int j=0; j<sizew; j++)
{
for(int k=0;k<sizeh; k++)
{
if(Data.get(j).get(k) > MinimumValueToPlot) //I only plot points above the constant value MinimumValueToPlot
{
int x = xscale*j;
int y = yscale*k;
g2.fillOval(x,y,1,1);
}
}
}
repaint();
}
public Dimension getPreferredSize(){
return new Dimension(buffer.getWidth(this), buffer.getHeight(this));
}
}
The problem with setPreferredSize, is not a performance issue, it can conflict with swings swings layout managers. When you have a component, such as your custom graphing panel, you do want to have it determine the size. I've added it to the PlotG class, so now it will try to layout according to the backing buffer.
The you have to update the buffer, I suggest doing it on the main thread after you've set your panel to visible. That way it won't block the edt.
I'm trying to make a grid of squares that change their fill (from black to white and vice-versa) when clicked. I'm able to turn the entire grid on or off currently, but I'm unable to figure out how to specify which particular square should be toggled when the mouse clicks within its borders. I've created buttons using mouseX and mouseY coordinates before, but they were for specific objects that I could adjust manually. I can't figure out how to do this using for loops and arrays.
I've been told to create a boolean array and pass the value of that array to the grid array, but again, I don't know how to specify which part of the array it needs to go to. For example, how do I change the fill value of square [6][3] upon mousePressed?
Here is my code so far:
int size = 100;
int cols = 8;
int rows = 5;
boolean light = false;
int a;
int b;
void setup() {
size (800, 600);
background (0);
}
void draw() {
}
void mousePressed() {
light = !light;
int[][] box = new int[cols][rows];
for (int i = 0; i < cols; i++) {
for (int j = 0; j < rows; j++) {
box[i][j] = i;
int a = i*100;
int b = j*100;
if (light == true) {
fill(255);
} else {
fill(0);
}
rect(a, b, 100, 100);
println(i, j);
}
}
}
First of all, you are currently recreating the entire board whenever the mouse is pressed. You must retain that info between mouse clicks, so make box a global array up there with the others. Further, it's sufficient to make it a boolean array if all you care about is the on/off state of each square:
boolean[][] isSquareLight = new boolean[cols][rows];
Instead of
if (light == true) {
you should then just check
if (isSquareLight[i][j] == true) {
(note that the == true is redundant).
Now, you've already written code that finds the coordinates for each box: You're passing it to rect!
rect(a, b, 100, 100);
All that is left to do is check whether the mouse is inside this rect, i.e. whether mouseX is between a and a+100 (and similar for mouseY) - if that's the case, then the user clicked in the box given by the current (i, j), so you can just negate isSquareLight[i][j] (before checking it like above) and it will work.
There are ways to calculate this without looping through the entire grid every time, but maybe the above helps you find the path yourself instead of just getting the code made for you.
PS: The int a; int b; at the top does nothing and can be removed. You are using the local variables a and b in your function, which is correct.
kinda new to swing and making guis in general, I have been using the drawLine method to create a "floors" and it has been working for the variables of the coordinates are local the the paintComponent class, however I want to change the coordinates using an actionListerner / Timer, so I need the variables to be accessible to the whole class.
This is probably a real simple fix (??) and I'll look like a fool for asking it here, but I can't work it out.
Here is the code.
class Elevator extends JPanel implements ActionListener {
private int numberOfLines = 0;
private int j = 60;
int yco = j - 125;
final private int HEIGHT = 50;
final private int WIDTH = 80;
final private boolean UP = true;
final private int TOP = 60;
final private int BOTTOM = j - 125;
private int mi = 5;
private Timer timer = new Timer(100, this);
public Elevator(int x) {
this.numberOfLines = x;
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
for (int i = 0; i < numberOfLines; i++) {
g.drawLine(0, j, getWidth(), j);
j += 75;
}
g.drawRect(getWidth() / 2, yco, WIDTH, HEIGHT);
}
#Override
public void actionPerformed(ActionEvent e) {
}
}
Any help is appreciated thanks in advance.
The reason your lines are not showing now that your variables (specifically j in your example code) are class variables, is that they are "remembering" state between calls to paintComponent(). Specifically, when j was local, it always got set back to its initial value (presumably j=60). Now however, it gets incremented in your for loop in the line
j += 75;
and never reset to a lower value. This means the second or third time that paintComponent() gets called, the value of j is too large, and your lines are being drawn outside the visible area (off screen). A Java Swing component can easily have its paintComponent() method called two, three, or more times before the component is even rendered on the screen, which is why your lines are no longer getting drawn (well, technically they are getting drawn, just not anywhere you can see them).
To fix this, you can add to your paintComponent() method the single line
j = 60;
just before the for loop. But at this point, you might as well just keep j local (unless you need to read its value but not change it with the timer).
Alternatively, if j needs to change over time, just make sure it gets set inside actionPerformed() by the timer, thenwork with a copy of the value inside of paintComponent() instead of directly with the value of j. As an example:
public void paintComponent(Graphics g) {
...
int jCopy = j;
for (int i = 0; i < numberOfLines; i++) {
g.drawLine(0, jCopy, getWidth(), jCopy);
jCopy += 75;
}
...
}
public void actionPerformed(ActionEvent e) {
...
j += 5;
//If you don't cap it at some max,
//it will go off the bottom of the screen again
if (j > 300) {
j = 60;
}
...
}
This will help stop consistency issues that could be caused by modifying j in both paintComponent() and actionPerformed().
What I'm trying to accomplish is adding 1 to the all the numbers in shipX array list, question is, how? I want to do this when the method move() is called, but how would I make this happen, as I'm new with arrays
class Ship
{
public void paint(Graphics g)
{
int shipX[] = {500,485,500,515,500};
int shipY[] = {500,485,455,485,500};
g.setColor(Color.cyan);
g.fillPolygon(shipX, shipY, 5);
}
public void move()
{
}
}
To start, you will have to move your arrays for points outside the local scope of paint() and into the class so that move() has access to the current values. You would increment in the move() method and call whatever routine you use to redraw your component.
class Ship
{
//make your polygon points members of the class
//so that you can have state that changes
//instead of declaring them in the paint method
int shipX[] = {500,485,500,515,500};
int shipY[] = {500,485,455,485,500};
//set these to the amount you want per update. They can even be negative
int velocityX = 1;
int velocityY = 1;
public void paint(Graphics g)
{
g.setColor(Color.cyan);
g.fillPolygon(shipX, shipY, 5);
}
public void move()
{
//add 1 to each value in shipX
for (int i=0; i<shipX.length; i++)
{
shipX[i] += velocityX;
}
//add 1 to each value in shipY
for (int i=0; i<shipY.length;i++)
{
shipY[i] += velocityY;
}
//call whatever you use to force a repaint
//normally I would assume your class extended
//javax.swing.JComponent, but you don't show it in your code
//if so, just uncomment:
//this.repaint();
}
}
Although I should point out that the repaint() method on JComponent does need to be called from the correct Swing thread, as pointed out in this answer.
If you are also trying to animate the movement, you can check out the Java Tutorial on Swing timers to call your move() method on a schedule. You could also use an ActionListener on a button to either control the Timer or on a button to move the object manually once per click.
All you have to do is iterate through the array and modify the value of each index:
for (int i = 0; i < shipX.length; i++)
{
shipX[i]++;
}
Increase the numbers one by one...
for (i=0; i<shipX.length; i++)
{
shipX[i]++; // same as shipX[i] = shipX[i] +1
}
for (i=0; i<shipY.length;i++)
{
shipY[i]++;
}
The problem I'm having is with my render loop. My application is a series of 'Tile' objects each with an x and y coordinate and image. When the program starts it creates a 10x10 grid of these tiles on screen. However, not all the squares can be seen at the same time, so you can use the arrow keys to pan around them. When the key is pressed it uses a for loop to cycle through all the currently rendered tile (stored in an ArrayList) and shifts them all 16 in the appropriate direction. The problem is some of the tiles flicker. I can see when scrolling that one half of the screen doesn't move in time to be rendered in the right spot, making a black gap between that and the other half of the tiles. how do I ensure that all tiles are moved before rendering?
render function from my Core class
public static void render()
{
while(true)
{
Graphics g = buffer.getDrawGraphics();
try
{
g.setColor(Color.black);
g.fillRect(0, 0, 1280, 720);
if(renderQueue != null)
{
for(int i = 0; i<renderQueue.size(); i++)
{
Tile t = renderQueue.get(i);
g.drawImage(t.getImage(), t.getX(), t.getY(), null);
}
}
if(!buffer.contentsLost())
{
buffer.show();
}
}
finally
{
if(g != null)
{
g.dispose();
}
}
}
}
And here's the movement update function from the Input class
public void keyPressed(KeyEvent ke)
{
int e = ke.getKeyCode();
switch(e)
{
case 38://up
if(scrollY > 0)
{
scrollY -= 16;
for(int i = 0; i<Core.renderQueue.size(); i++)
{
Core.renderQueue.get(i).incrementY(16);
}
}
break;
case 40://down
if(scrollY < 560)
{
scrollY += 16;
for(int i = 0; i<Core.renderQueue.size(); i++)
{
Core.renderQueue.get(i).incrementY(-16);
}
}
break;
case 37://right
if(scrollX < 0)
{
scrollX += 16;
for(int i = 0; i<Core.renderQueue.size(); i++)
{
Core.renderQueue.get(i).incrementX(16);
}
}
break;
case 39://left
if(scrollX > 0)
{
scrollX -= 16;
for(int i = 0; i<Core.renderQueue.size(); i++)
{
Core.renderQueue.get(i).incrementX(-16);
}
}
break;
}
Thanks in advance!
It sounds like the tiles are being rendered while the coordinates for some of the tiles still have to be changed by Input.keyPressed. You could fix that by directly using scrollX and scrollY to draw the tile images in Core.render, instead of changing the coordinates for each of the tiles. If you copy the scroll values to two local variables at the begin of the while loop in render, the same values will be used for each tile.
Another option is to create a new list with tiles that have the modified coordinates (you could use the images from the current list). When the new list is complete, you could set a flag like newRenderQueue which will be picked up in render. When a new iteration of the while loop in render starts, you can replace the render queue with the new list and reset the flag.
P.S. Welcome to Stack Overflow! As Andrew Thompson already mentioned, it's very helpful to provide a complete example of your problem. This way people can quickly investigate the issue and provide (hopefully useful) advice... ;-)