diff --git a/vector/v.to.rast/dense_line.c b/vector/v.to.rast/dense_line.c index 7b7f143b81c..74453d5838d 100644 --- a/vector/v.to.rast/dense_line.c +++ b/vector/v.to.rast/dense_line.c @@ -120,6 +120,18 @@ void plot_line_dense(double east1, double north1, double east2, double north2) } } +/* dense point plotting, alternative to G_plot_point() + * east, north are map coordinates. + * Needed because dense mode never calls G_setup_plot(), so the move and cont + * routines G_plot_point() uses are not set. The coordinate is truncated to the + * cell that contains it, matching what dense_line() does with its endpoints: + * setup_plot() frames the region by cell edges, unlike G_setup_plot(), which + * frames it by cell centres and therefore rounds. */ +void plot_point_dense(double east, double north) +{ + st->dot((int)X(G_adjust_easting(east, &st->window)), (int)Y(north)); +} + /* dense line plotting, alternative to G_bresenham_line() * x1, y1, x2, y2 are col, row numbers */ void dense_line(double x1, double y1, double x2, double y2, diff --git a/vector/v.to.rast/do_lines.c b/vector/v.to.rast/do_lines.c index 0a1d402ca2f..0dd42465a01 100644 --- a/vector/v.to.rast/do_lines.c +++ b/vector/v.to.rast/do_lines.c @@ -7,7 +7,7 @@ /* function prototypes */ static int plot_line(double *, double *, int, int, int); -static int plot_points(double *, double *, int); +static int plot_points(double *, double *, int, int); static double v2angle(double[2], double[2], double, double); static double deg_angle(double, double, double, double); @@ -131,7 +131,7 @@ int do_lines(struct Map_info *Map, struct line_pnts *Points, count++; } else if (type & GV_POINTS) { - plot_points(Points->x, Points->y, Points->n_points); + plot_points(Points->x, Points->y, Points->n_points, dense); count++; } } @@ -199,11 +199,15 @@ static double deg_angle(double x0, double y0, double x1, double y1) return (v_ang * 360.0 / M_2PI); } -static int plot_points(double *x, double *y, int n) +static int plot_points(double *x, double *y, int n, int dense) { /* only plot the first point */ - if (n > 0) - G_plot_point(*x, *y); + if (n > 0) { + if (dense) + plot_point_dense(*x, *y); + else + G_plot_point(*x, *y); + } return 0; } diff --git a/vector/v.to.rast/local.h b/vector/v.to.rast/local.h index 18996b97e2f..90886c8cb99 100644 --- a/vector/v.to.rast/local.h +++ b/vector/v.to.rast/local.h @@ -30,6 +30,7 @@ int do_lines(struct Map_info *, struct line_pnts *, dbCatValArray *, int, int, struct cat_list *, int, double, int, int, int *, int); void plot_line_dense(double, double, double, double); +void plot_point_dense(double, double); void setup_plot(double, double, double, double, int (*dot)(int, int)); /* raster.c */ diff --git a/vector/v.to.rast/tests/conftest.py b/vector/v.to.rast/tests/conftest.py new file mode 100644 index 00000000000..e34ceeafa97 --- /dev/null +++ b/vector/v.to.rast/tests/conftest.py @@ -0,0 +1,33 @@ +import os + +import pytest + +import grass.script as gs +from grass.tools import Tools + + +@pytest.fixture +def xy_points_session(tmp_path): + """Active session in an XY project holding a small point map""" + project = tmp_path / "xy_test" + gs.create_project(project) + with ( + gs.setup.init(project, env=os.environ.copy()) as session, + Tools(session=session) as tools, + ): + points = tmp_path / "points.txt" + # Deliberately off the cell boundaries: with a 1 unit resolution + # starting at 0, a whole-numbered coordinate falls on a cell edge, + # where rounding to the nearest cell and truncating to the containing + # cell agree. These do not, so they detect a half cell shift. + points.write_text("10.6|10.7|1\n20.2|20.3|2\n30.9|15.1|3\n") + tools.g_region(s=0, n=50, w=0, e=50, res=1) + tools.v_in_ascii( + input=str(points), + output="points", + separator="pipe", + x=1, + y=2, + cat=3, + ) + yield session diff --git a/vector/v.to.rast/tests/v_to_rast_dense_test.py b/vector/v.to.rast/tests/v_to_rast_dense_test.py new file mode 100644 index 00000000000..6ac9c5ec4c2 --- /dev/null +++ b/vector/v.to.rast/tests/v_to_rast_dense_test.py @@ -0,0 +1,31 @@ +"""Tests for rasterizing points with the dense flag + +Dense mode configures its own plotting routines and never calls G_setup_plot(), +but points were always plotted with G_plot_point(), which uses the move and cont +routines that only G_setup_plot() sets. Rasterizing a point map with -d +therefore called a null function pointer and the module crashed +(https://github.com/OSGeo/grass/issues/3105). +""" + +from grass.tools import Tools + + +def test_dense_points_match_plain_points(xy_points_session): + """Points rasterize into the same cells with and without the dense flag + + The flag densifies lines, so for points there is nothing to densify and + both runs have to place every point in the same cell. The coordinates in + the fixture sit inside cells rather than on their edges, so a half cell + shift in either direction changes the row or the column and fails here. + """ + tools = Tools(session=xy_points_session) + + tools.v_to_rast(input="points", output="plain", use="cat", type="point") + tools.v_to_rast(input="points", output="dense", use="cat", type="point", flags="d") + + plain = tools.r_stats(input="plain", flags="gn").text.splitlines() + dense = tools.r_stats(input="dense", flags="gn").text.splitlines() + + # east, north and category of the centre of the cell each point landed in + assert sorted(plain) == ["10.5 10.5 1", "20.5 20.5 2", "30.5 15.5 3"] + assert sorted(dense) == sorted(plain)