Line Logic

It does, and there are no boundaries :stuck_out_tongue:

I think I can think up some special case solution for negative values (when I come back to this, Iā€™m working on other things now)

If you made each region a rectangle and you could see if the line intersects with the rectangles with
Rectangle r = new Rectangle(x,y,width,height);
ArrayList p = new ArrayList();

then make new instances of the point class and add them to the array and have an enhanced for loop go through the points and see if any Rectangle.containsĀ§

problems with this are you need an arraylist for each line and it probably is very slow compared to something but this is super fast to write.

thatā€™s very slow

For anybody using this code as a reference, I just wanted to share with you that the algorithm in the method ā€˜lineā€™ doesnā€™t work at all, sadly. It skips so many pixels that itā€™s not even remotely close to drawing a line. The other algorithm found in this thread, aptly named ā€˜line2ā€™, however, did the job quite nicely, except that it skips the very first pixel, for which Iā€™ll generously provide a patch :wink:

	public static interface Plotter{
		public void plot(int x, int y);
	}

	public static final void plot(int x1, int y1, int x2, int y2, int resX, int resY, Plotter plotter) {
@@		if ((x1 | y1 | x2 | y2) < 0) {
@@			throw new IllegalArgumentException();
@@		}

		int dist = Math.max(Math.abs(x2 - x1), Math.abs(y2 - y1));
@@		if (dist == 0) {
@@			/* we must not divide by zero (see below)
@@				or we always plot at ((int)NaN,(int)NaN) -> (0,0) */
@@			plotter.plot(x1, y1);
@@			return;
@@		}


		float dx = (float) (x2 - x1) / dist;
		float dy = (float) (y2 - y1) / dist;
		int lastRegX = (int) (x1 + dx + 0.5f) / resX;
		int lastRegY = (int) (y1 + dy + 0.5f) / resY;

@@		/* plot the first coordinate */
@@		plotter.plot(lastRegX, lastRegY);

		for (int d = 2; d <= dist; d++) {
			int x = (int) (x1 + 0.5f + dx * d);
			int y = (int) (y1 + 0.5f + dy * d);
			int regX = x / resX;
			if (x < 0) {
				regX--;
			}
			int regY = y / resY;
			if (y < 0) {
				regY--;
			}
			if (regX != lastRegX || regY != lastRegY) {
				plotter.plot(regX, regY);
			}
			lastRegX = regX;
			lastRegY = regY;
		}
	}

Thanks for doing the grunt work!

Since I tested the approach quite intensively, I am a bit surprised that you find it ā€œnot even remotely close to drawing a lineā€ā€¦

Are you sure you tested the one from this post:

I tested this one: (with resX=32, resY=32)

private static final void line(int x0, int y0, int x1, int y1, int cellWidth, int cellHeight) {
      int col0 = x0 / cellWidth;
      int row0 = y0 / cellHeight;

      int col1 = x1 / cellWidth;
      int row1 = y1 / cellHeight;

      float m = x0==x1? Integer.MAX_VALUE:(float) (y0 - y1) / (x0 - x1);
      float b = y0 - m * x0;

      if (Math.abs(m) > 1) {
         int lastCol = col0;
         int dir = (y0 < y1) ? 1 : -1;
         for (int row = row0; (dir==1? row <= row1:row >= row1); row += dir) {
            float y = row * cellHeight;
            float x = (y - b) / m;
            int col = (int) (x + 0.5f * dir) / cellWidth;
            if (col != lastCol) {
               y = row * cellHeight - dir;
               x = (y - b) / m;
               int missedCol = (int) (x + 0.5f * dir) / cellWidth;
               if (lastCol != missedCol) {
@@                  plotter.plot(...);
               }
               lastCol = col;
            }
         }
      } else {
         int lastRow = row0;
         int dir = (x0 < x1) ? 1 : -1;
         for (int col = col0; (dir==1? col <= col1:col >= col1); col += dir) {
            float x = col * cellWidth;
            float y = m * x + b;
            int row = (int) (y + 0.5f * dir) / cellHeight;
            if (row != lastRow) {
               x = col * cellWidth - dir;
               y = m * x + b;
               int missedRow = (int) (y + 0.5f * dir) / cellHeight;
               if (lastRow != missedRow) {
@@                  plotter.plot(...);
               }
               lastRow = row;
            }
         }
      }
   }

I now see that there is a g.fillRect I missed from the ā€˜original codeā€™ ā€“ why anybody would want to benchmark it without that, is beyond me. Anyway, my fault!



	public static interface Plotter {
		public void plot(int x, int y);
	}

	public static final void plot(int x1, int y1, int x2, int y2, int resX, int resY, Plotter plotter) {
		if ((x1 | y1 | x2 | y2) < 0) {
			throw new IllegalArgumentException();
		}

		int dist = Math.max(Math.abs(x2 - x1), Math.abs(y2 - y1));
		if (dist == 0) {
@@			plotter.plot(x1, y1);
			return;
		}

		float dx = (float) (x2 - x1) / dist;
		float dy = (float) (y2 - y1) / dist;
		int lastRegX = (int) (x1 + dx + 0.5f) / resX;
		int lastRegY = (int) (y1 + dy + 0.5f) / resY;
@@		plotter.plot(lastRegX, lastRegY);

		for (int d = 2; d <= dist; d++) {
			int x = (int) (x1 + 0.5f + dx * d);
			int y = (int) (y1 + 0.5f + dy * d);
			int regX = x / resX;
			if (x < 0) {
				regX--;
			}
			int regY = y / resY;
			if (y < 0) {
				regY--;
			}
			if (regX != lastRegX || regY != lastRegY) {
@@				plotter.plot(regX, regY);
			}
			lastRegX = regX;
			lastRegY = regY;
		}
	}

	public static final void plot2(int x0, int y0, int x1, int y1, int cellWidth, int cellHeight, Plotter plotter) {
		boolean skipMissedCells = false;

		int col0 = x0 / cellWidth;
		int row0 = y0 / cellHeight;

		int col1 = x1 / cellWidth;
		int row1 = y1 / cellHeight;

		// Shitty workaround to prevent division by 0 on x0==x1...
		float m = x0 == x1 ? Integer.MAX_VALUE : (float) (y0 - y1) / (x0 - x1);
		float b = y0 - m * x0;

		// a "steep" line
		if (Math.abs(m) > 1) {
			int lastCol = col0;
			// does it go up or down?
			int dir = (y0 < y1) ? 1 : -1;
			for (int row = row0; (dir == 1 ? row <= row1 : row >= row1); row += dir) {
				float y = row * cellHeight;
				float x = (y - b) / m;
				int col = (int) (x + 0.5f * dir) / cellWidth;
@@				plotter.plot(col, row);
				if (!skipMissedCells && col != lastCol) {
					y = row * cellHeight - dir;
					x = (y - b) / m;
					int missedCol = (int) (x + 0.5f * dir) / cellWidth;
					if (lastCol != missedCol) {
@@						plotter.plot(missedCol, (row - dir));
					}
					lastCol = col;
				}
			}
		} // a "flat" line
		else {
			int lastRow = row0;
			// does it go right or left?
			int dir = (x0 < x1) ? 1 : -1;
			for (int col = col0; (dir == 1 ? col <= col1 : col >= col1); col += dir) {
				float x = col * cellWidth;
				float y = m * x + b;
				int row = (int) (y + 0.5f * dir) / cellHeight;
@@				plotter.plot(col, row);
				if (!skipMissedCells && row != lastRow) {
					x = col * cellWidth - dir;
					y = m * x + b;
					int missedRow = (int) (y + 0.5f * dir) / cellHeight;
					if (lastRow != missedRow) {
@@						plotter.plot(col - dir, missedRow);
					}
					lastRow = row;
				}
			}
		}
	}

This is the output from the original test program with my algo at the left using x/yres 32 and a line from 50,50 to 400,300

Can you post your test parameters to verify?

Here is the code (I did not update the line2 though):


import java.awt.Canvas;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;

import javax.swing.JFrame;
import javax.swing.WindowConstants;

public class LineTest
{

    private static final int REGION_X = 32;
    private static final int REGION_Y = 32;

    private static int alternate = 0;

    public static void main(String[] args)
    {
        JFrame frame = new JFrame("Line Test");
        frame.setVisible(true);
        final int w = 1024;
        final int h = 512;
        frame.setSize(w, h);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        Canvas canvas = new Canvas()
        {


            @Override
            public void paint(Graphics g)
            {
                g.setFont(new Font("Times New Roman", Font.PLAIN, 9));
                int fH = g.getFontMetrics().getHeight();
                for (int x = 0; x < w / REGION_X / 2; x++)
                {
                    for (int y = 0; y < h / REGION_Y; y++)
                    {
                        if (x % 2 == 0)
                            if (y % 2 == 1)
                                g.setColor(Color.red);
                            else
                                g.setColor(Color.black);
                        else if (y % 2 == 0)
                            g.setColor(Color.red);
                        else
                            g.setColor(Color.black);
                        g.fillRect(x * REGION_X, y * REGION_Y, REGION_X, REGION_Y);
                        g.setColor(g.getColor() == Color.black ? Color.red : Color.black);
                        g.drawString(x + ", " + y, x * REGION_X + 2, y * REGION_Y + fH);
                    }
                }
                line(50, 50, 400, 300, REGION_X, REGION_Y, g);

                g.setColor(Color.white);
                g.fillRect(w / 2 - 1, 0, 2, h);

                for (int x = w / REGION_X / 2; x < w / REGION_X; x++)
                {
                    for (int y = 0; y < h / REGION_Y; y++)
                    {
                        if (x % 2 == 0)
                            if (y % 2 == 1)
                                g.setColor(Color.red);
                            else
                                g.setColor(Color.black);
                        else if (y % 2 == 0)
                            g.setColor(Color.red);
                        else
                            g.setColor(Color.black);
                        g.fillRect(x * REGION_X, y * REGION_Y, REGION_X, REGION_Y);
                        g.setColor(g.getColor() == Color.black ? Color.red : Color.black);
                        g.drawString(x + ", " + y, x * REGION_X + 2, y * REGION_Y + fH);
                    }
                }
                line2(50 + w / 2, 50, 400 + w / 2, 300, REGION_X, REGION_Y, g);
            }
        };
        canvas.setSize(frame.getSize());
        frame.add(canvas);
    }

    private static final void line(int x0, int y0, int x1, int y1, int cellWidth, int cellHeight, Graphics g) {
        // flag to get back the old slightly faster but inaccurate version
        boolean skipMissedCells = false;
        int alternate = 0;

        int col0 = x0 / cellWidth;
        int row0 = y0 / cellHeight;

        int col1 = x1 / cellWidth;
        int row1 = y1 / cellHeight;

        // Shitty workaround to prevent division by 0 on x0==x1...
        float m = x0==x1? Integer.MAX_VALUE:(float) (y0 - y1) / (x0 - x1);
        float b = y0 - m * x0;

        // a "steep" line
        if (Math.abs(m) > 1) {
            int lastCol = col0;
            // does it go up or down?
            int dir = (y0 < y1) ? 1 : -1;
            for (int row = row0; (dir==1? row <= row1:row >= row1); row += dir) {
                float y = row * cellHeight;
                float x = (y - b) / m;
                int col = (int) (x + 0.5f * dir) / cellWidth;
                g.setColor(alternate++ % 2 == 0 ? Color.green : new Color(0, 127, 0));
                g.fillRect(col * cellWidth, row * cellHeight, cellWidth, cellHeight);
                // when the sampled column changes, there might be another missed cell
                if (!skipMissedCells && col != lastCol) {
                    // sample again - slightly before the row boundary
                    y = row * cellHeight - dir;
                    x = (y - b) / m;
                    int missedCol = (int) (x + 0.5f * dir) / cellWidth;
                    // really missed that one?
                    if (lastCol != missedCol) {
                        g.setColor(new Color(127, 127, 0));
                        // must be (row-dir), since the missed cell belongs to the last sampled row
                        g.fillRect(missedCol * cellWidth, (row - dir) * cellHeight, cellWidth, cellHeight);
                    }
                    lastCol = col;
                }
            }
        } // a "flat" line
        else {
            int lastRow = row0;
            // does it go right or left?
            int dir = (x0 < x1) ? 1 : -1;
            for (int col = col0; (dir==1? col <= col1:col >= col1); col += dir) {
                float x = col * cellWidth;
                float y = m * x + b;
                int row = (int) (y + 0.5f * dir) / cellHeight;
                g.setColor(alternate++ % 2 == 0 ? Color.green : new Color(0, 127, 0));
                g.fillRect(col * cellWidth, row * cellHeight, cellWidth, cellHeight);
                // when the sampled column changes, there might be another missed cell
                if (!skipMissedCells && row != lastRow) {
                    // sample again - slightly before the column boundary
                    x = col * cellWidth - dir;
                    y = m * x + b;
                    int missedRow = (int) (y + 0.5f * dir) / cellHeight;
                    // really missed that one?
                    if (lastRow != missedRow) {
                        g.setColor(new Color(127, 127, 0));
                        // must be (col-dir), since the missed cell belongs to the last sampled column
                        g.fillRect((col - dir) * cellWidth, missedRow * cellHeight, cellWidth, cellHeight);
                    }
                    lastRow = row;
                }
            }
        }
        g.setColor(Color.pink);
        g.drawLine(x0, y0, x1, y1);
    }

    private static final void line2(int x0, int y0, int x1, int y1, int regionXSize, int regionYSize, Graphics g)
    {
        boolean steep = Math.abs(y1 - y0) > Math.abs(x1 - x0);
        if (steep)
        {
            int tmp = x0;
            x0 = y0;
            y0 = tmp;
            tmp = x1;
            x1 = y1;
            y1 = tmp;
        }
        if (x0 > x1)
        {
            int tmp = x0;
            x0 = x1;
            x1 = tmp;
            tmp = y0;
            y0 = y1;
            y1 = tmp;
        }
        int deltax = x1 - x0;
        int deltay = Math.abs(y1 - y0);
        int error = deltax / 2;
        int ystep = -1;
        int y = y0;
        if (y0 < y1)
            ystep = 1;
        for (int x = x0; x <= x1; x++)
        {
            int absX;
            int absY;
            if (steep)
            {
                absX = y;
                absY = x;
            }
            else
            {
                absX = x;
                absY = y;
            }
            int regX = absX / regionXSize;
            int regY = absY / regionYSize;
            if (absX < 0)
                regX--;
            if (absY < 0)
                regY--;
            g.setColor(alternate++ % 2 == 0 ? Color.green : new Color(0, 127, 0));
            g.fillRect(regX * REGION_X, regY * REGION_Y, REGION_X, REGION_Y);
            int dX;
            int dY;
            if (steep)
            {
                dX = regionYSize - (absY - (regionYSize * regY));
                dY = absX - (regionXSize * regX);
            }
            else
            {
                dX = regionXSize - (absX - (regionXSize * regX));
                dY = absY - (regionYSize * regY);
            }
            int ceil = x + dX;
            for (; x < ceil; x++)
            {
                error -= deltay;
                if (error < 0)
                {
                    y += ystep;
                    error += deltax;
                    dY += ystep;
                    if (dY < 0 || dY > (steep ? regionXSize : regionYSize))
                        break;
                }
            }
        }
        g.setColor(Color.pink);
        g.drawLine(x0, y0, x1, y1);
    }


}


Actually I hackily switched the test program to your Plotter interface, but still looks good (this time the right green one is mine):

So you might have hit a corner case?

The code:


import javax.swing.*;
import java.awt.*;

public class PlotTest
{
    public static interface Plotter {
        public void plot(int x, int y);
    };

    private static int alternate = 0;

    public static void main(String[] args)
    {
        JFrame frame = new JFrame("Line Test");
        frame.setVisible(true);

        final int xRes = 32;
        final int yRes = 32;

        final int w = xRes*32;
        final int h = yRes*16;

@@        final int x1 = 32;
@@        final int y1 = 64;
@@        final int x2 = 480;
@@        final int y2 = 257;

        frame.setSize(w, h);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        Canvas canvas = new Canvas()
        {
            @Override
            public void paint(Graphics g1)
            {
                final Graphics2D g=(Graphics2D)g1;

                g.setFont(new Font("Times New Roman", Font.PLAIN, 9));
                int fH = g.getFontMetrics().getHeight();
                for (int x = 0; x < w / xRes / 2; x++)
                {
                    for (int y = 0; y < h / yRes; y++)
                    {
                        if (x % 2 == 0)
                            if (y % 2 == 1)
                                g.setColor(Color.red);
                            else
                                g.setColor(Color.black);
                        else if (y % 2 == 0)
                            g.setColor(Color.red);
                        else
                            g.setColor(Color.black);
                        g.fillRect(x * xRes, y * yRes, xRes, yRes);
                        g.setColor(g.getColor() == Color.black ? Color.red : Color.black);
                        g.drawString(x + ", " + y, x * xRes + 2, y * yRes + fH);
                    }
                }

                g.setColor(Color.white);
                g.fillRect(w / 2 - 1, 0, 2, h);

                for (int x = w / xRes / 2; x < w / xRes; x++)
                {
                    for (int y = 0; y < h / yRes; y++)
                    {
                        if (x % 2 == 0)
                            if (y % 2 == 1)
                                g.setColor(Color.red);
                            else
                                g.setColor(Color.black);
                        else if (y % 2 == 0)
                            g.setColor(Color.red);
                        else
                            g.setColor(Color.black);
                        g.fillRect(x * xRes, y * yRes, xRes, yRes);
                        g.setColor(g.getColor() == Color.black ? Color.red : Color.black);
                        g.drawString(x + ", " + y, x * xRes + 2, y * yRes + fH);
                    }
                }
                g.setStroke(new BasicStroke(2.5f));

                Plotter plotter1=new Plotter()
                {
                    @Override
                    public void plot(int x, int y)
                    {
                        g.setColor(Color.CYAN);
                        g.drawRect(x * xRes, y * yRes, xRes, yRes);
                    }
                };

                Plotter plotter2=new Plotter()
                {
                    @Override
                    public void plot(int x, int y)
                    {
                        g.setColor(Color.green);
                        g.drawRect(x * xRes, y * yRes, xRes, yRes);
                    }
                };

@@                PlotTest.plot(x1, y1, x2, y2, xRes, yRes, plotter1);
@@                PlotTest.plot2(x1 + w / 2, y1, x2 + w / 2, y2, xRes, yRes, plotter2);

                g.setColor(Color.pink);
                g.drawLine(x1, y1, x2, y2);
                g.drawLine(x1 + w / 2, y1, x2 + w / 2, y2);
            }
        };
        canvas.setSize(frame.getSize());
        frame.add(canvas);
    }

    public static final void plot(int x1, int y1, int x2, int y2, int resX, int resY, Plotter plotter) {
        if ((x1 | y1 | x2 | y2) < 0) {
            throw new IllegalArgumentException();
        }

        int dist = Math.max(Math.abs(x2 - x1), Math.abs(y2 - y1));
        if (dist == 0) {
@@            plotter.plot(x1, y1);
            return;
        }

        float dx = (float) (x2 - x1) / dist;
        float dy = (float) (y2 - y1) / dist;
        int lastRegX = (int) (x1 + dx + 0.5f) / resX;
        int lastRegY = (int) (y1 + dy + 0.5f) / resY;
@@        plotter.plot(lastRegX, lastRegY);

        for (int d = 2; d <= dist; d++) {
            int x = (int) (x1 + 0.5f + dx * d);
            int y = (int) (y1 + 0.5f + dy * d);
            int regX = x / resX;
            if (x < 0) {
                regX--;
            }
            int regY = y / resY;
            if (y < 0) {
                regY--;
            }
            if (regX != lastRegX || regY != lastRegY) {
@@                plotter.plot(regX, regY);
            }
            lastRegX = regX;
            lastRegY = regY;
        }
    }

    public static final void plot2(int x0, int y0, int x1, int y1, int cellWidth, int cellHeight, Plotter plotter) {
        boolean skipMissedCells = false;

        int col0 = x0 / cellWidth;
        int row0 = y0 / cellHeight;

        int col1 = x1 / cellWidth;
        int row1 = y1 / cellHeight;

        // Shitty workaround to prevent division by 0 on x0==x1...
        float m = x0 == x1 ? Integer.MAX_VALUE : (float) (y0 - y1) / (x0 - x1);
        float b = y0 - m * x0;

        // a "steep" line
        if (Math.abs(m) > 1) {
            int lastCol = col0;
            // does it go up or down?
            int dir = (y0 < y1) ? 1 : -1;
            for (int row = row0; (dir == 1 ? row <= row1 : row >= row1); row += dir) {
                float y = row * cellHeight;
                float x = (y - b) / m;
                int col = (int) (x + 0.5f * dir) / cellWidth;
@@                plotter.plot(col, row);
                if (!skipMissedCells && col != lastCol) {
                    y = row * cellHeight - dir;
                    x = (y - b) / m;
                    int missedCol = (int) (x + 0.5f * dir) / cellWidth;
                    if (lastCol != missedCol) {
@@                        plotter.plot(missedCol, (row - dir));
                    }
                    lastCol = col;
                }
            }
        } // a "flat" line
        else {
            int lastRow = row0;
            // does it go right or left?
            int dir = (x0 < x1) ? 1 : -1;
            for (int col = col0; (dir == 1 ? col <= col1 : col >= col1); col += dir) {
                float x = col * cellWidth;
                float y = m * x + b;
                int row = (int) (y + 0.5f * dir) / cellHeight;
@@                plotter.plot(col, row);
                if (!skipMissedCells && row != lastRow) {
                    x = col * cellWidth - dir;
                    y = m * x + b;
                    int missedRow = (int) (y + 0.5f * dir) / cellHeight;
                    if (lastRow != missedRow) {
@@                        plotter.plot(col - dir, missedRow);
                    }
                    lastRow = row;
                }
            }
        }
    }
}

Ofcourse I make my tests interactive, so I can test more than a handful of hardcoded examples.

In about 80% of the cases your code provides the correct path.

If youā€™re looking for an antialiased line, thereā€™s always Wuā€™s algorithm, or Hugo Eliasā€™s take on it:

https://bitbucket.org/chuck/jcod/src/c2e8ae6fd81bafc50bce94e2b2c9a0a26f3137d6/src/main/java/net/fishbulb/jcod/util/PlotAlgorithms.java?at=default#cl-67

Here is a plotter that supports negative values and doesnā€™t have faulty edge cases.

Using your last code, these (and many more) result in incorrect plotting:

(55,46) -> (120,140) (195,404) -> (81,498) (218,557) -> (67,427) (218,557) -> (100,423) (60,403) -> (77,355) (299,551) -> (203,487)

Ah, ok - I occasionally ā€œattachā€ the missed grid cells at the wrong side of the found one. I probably wonā€™t put any effort into fixing this, since I doubt it will beat your implementation.