From 4f67c867a0de22d9652cfe865b3e8cbea2fb100b Mon Sep 17 00:00:00 2001 From: R Sai Pranav Date: Sat, 22 Aug 2026 22:08:15 +0530 Subject: [PATCH 1/2] v.to.rast: Fix crash when rasterizing points with the dense flag configure_plot() calls setup_plot() in dense mode and G_setup_plot() otherwise, but plot_points() always plotted through 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. plot_points() now branches on dense the way plot_lines() already does, and dense_line.c gains plot_point_dense() for that path. The dot routines bound check the cell before writing, so a point outside the region is dropped rather than clipped by the caller. Fixes #3105 --- vector/v.to.rast/dense_line.c | 9 ++++++ vector/v.to.rast/do_lines.c | 14 +++++--- vector/v.to.rast/local.h | 1 + vector/v.to.rast/tests/conftest.py | 29 +++++++++++++++++ .../v.to.rast/tests/v_to_rast_dense_test.py | 32 +++++++++++++++++++ 5 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 vector/v.to.rast/tests/conftest.py create mode 100644 vector/v.to.rast/tests/v_to_rast_dense_test.py diff --git a/vector/v.to.rast/dense_line.c b/vector/v.to.rast/dense_line.c index 7b7f143b81c..a91e780f152 100644 --- a/vector/v.to.rast/dense_line.c +++ b/vector/v.to.rast/dense_line.c @@ -122,6 +122,15 @@ void plot_line_dense(double east1, double north1, double east2, double north2) /* dense line plotting, alternative to G_bresenham_line() * x1, y1, x2, y2 are col, row numbers */ +/* 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. */ +void plot_point_dense(double east, double north) +{ + st->dot((int)X(G_adjust_easting(east, &st->window)), (int)Y(north)); +} + void dense_line(double x1, double y1, double x2, double y2, int (*point)(int, int)) { 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..89d85a251c9 --- /dev/null +++ b/vector/v.to.rast/tests/conftest.py @@ -0,0 +1,29 @@ +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" + points.write_text("10|10|1\n20|20|2\n30|15|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..978c46f7419 --- /dev/null +++ b/vector/v.to.rast/tests/v_to_rast_dense_test.py @@ -0,0 +1,32 @@ +"""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 the same way with and without the dense flag + + The flag densifies lines, so for points there is nothing to densify and both + runs have to produce the same cells. + """ + 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_univar(map="plain", format="json") + dense = tools.r_univar(map="dense", format="json") + + assert dense["n"] == 3 + assert dense["min"] == 1 + assert dense["max"] == 3 + assert dense["sum"] == 6 + assert dense["n"] == plain["n"] + assert dense["sum"] == plain["sum"] From 5c578a8470e61b3d46deff47bc8560322bf924d9 Mon Sep 17 00:00:00 2001 From: sai pranav Date: Tue, 25 Aug 2026 23:14:36 +0530 Subject: [PATCH 2/2] v.to.rast: Test the dense point placement off the cell boundaries The coordinates the test used were whole numbers, which land on cell edges where rounding to the nearest cell and truncating to the containing cell agree, so the test could not tell the two apart. Move the points inside cells and compare the cells themselves rather than only the count and sum. Also move plot_point_dense() above dense_line()'s comment block, which it was splitting, and say in its own comment why it truncates. --- vector/v.to.rast/dense_line.c | 9 +++++--- vector/v.to.rast/tests/conftest.py | 6 +++++- .../v.to.rast/tests/v_to_rast_dense_test.py | 21 +++++++++---------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/vector/v.to.rast/dense_line.c b/vector/v.to.rast/dense_line.c index a91e780f152..74453d5838d 100644 --- a/vector/v.to.rast/dense_line.c +++ b/vector/v.to.rast/dense_line.c @@ -120,17 +120,20 @@ void plot_line_dense(double east1, double north1, double east2, double north2) } } -/* dense line plotting, alternative to G_bresenham_line() - * x1, y1, x2, y2 are col, row numbers */ /* 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. */ + * 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, int (*point)(int, int)) { diff --git a/vector/v.to.rast/tests/conftest.py b/vector/v.to.rast/tests/conftest.py index 89d85a251c9..e34ceeafa97 100644 --- a/vector/v.to.rast/tests/conftest.py +++ b/vector/v.to.rast/tests/conftest.py @@ -16,7 +16,11 @@ def xy_points_session(tmp_path): Tools(session=session) as tools, ): points = tmp_path / "points.txt" - points.write_text("10|10|1\n20|20|2\n30|15|3\n") + # 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), 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 index 978c46f7419..6ac9c5ec4c2 100644 --- a/vector/v.to.rast/tests/v_to_rast_dense_test.py +++ b/vector/v.to.rast/tests/v_to_rast_dense_test.py @@ -11,22 +11,21 @@ def test_dense_points_match_plain_points(xy_points_session): - """Points rasterize the same way with and without the dense flag + """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 produce the same cells. + 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_univar(map="plain", format="json") - dense = tools.r_univar(map="dense", format="json") + plain = tools.r_stats(input="plain", flags="gn").text.splitlines() + dense = tools.r_stats(input="dense", flags="gn").text.splitlines() - assert dense["n"] == 3 - assert dense["min"] == 1 - assert dense["max"] == 3 - assert dense["sum"] == 6 - assert dense["n"] == plain["n"] - assert dense["sum"] == plain["sum"] + # 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)