Skip to content

Rasterization throws ArrayIndexOutOfBoundsException for points on raster edges and ClassCastException for polygons that only graze the raster #3414

Description

@jiayuasu

Expected behavior

Rasterizing any geometry that intersects the raster succeeds. This path is shared by RS_AsRaster, RS_Clip, RS_SetValues with a geometry, and RS_ZonalStats / RS_ZonalStatsAll. A point on the raster edge or on a grid line burns the pixel(s) it touches. A polygon that touches the raster only at an edge or a corner burns the touched pixel with allTouched = true and nothing with allTouched = false.

Actual behavior

On master (53e4e7b) these cases throw:

Case Result
Point on the bottom-right corner of a 256x256 tile with 1/3600 degree pixels ArrayIndexOutOfBoundsException: Index 65536 out of bounds for length 65536 (full extent); Index 1 out of bounds for length 1 (cropped to the geometry)
Point on the row 0 / row 1 grid line ArrayIndexOutOfBoundsException: Index -246 out of bounds for length 65536 (full extent); Index -1 out of bounds for length 1 (cropped)
Polygon that touches the tile only at its bottom-right corner ClassCastException: class org.locationtech.jts.geom.Point cannot be cast to class org.locationtech.jts.geom.Polygon with both allTouched values, and from RS_ZonalStatsAll
Polygon whose clip against the raster is a GeometryCollection (polygons plus lines) ClassCastException: ... GeometryCollection cannot be cast to ... Polygon
POLYGON ((1 1, 2 1, 2 0.5, 1 1)) on a 2x2 unit raster, allTouched=false ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4

Causes, all in Rasterization.java:

  1. rasterizePoint walks the extent by repeatedly adding scaleX / scaleY to a world coordinate and counts the pixel index separately. 1/3600 has no exact double representation, so the running coordinate can fall a fraction of an ULP short of the extent's far edge and take one more step. JAI's banded raster setSample does no bounds check, so the write lands outside the data buffer. The same drift moves the tested cell edges a few ULPs off the grid lines.
  2. rasterizePolygon casts the clipped geometry to Polygon. A polygon that only grazes the raster clips to a point, a line or a GeometryCollection.
  3. fillPolygon writes an unpaired scanline intercept from a vertical edge on the extent maximum one column past the grid.

This came up with RS_ZonalStatsAll(rast, zone, 1, true) over Copernicus GLO-30 DEM tiles (256x256, 1/3600 degree). A zone that only touches a tile at its edge crashes the query. That happened for 750 of about 243k zones, so small samples do not reproduce it.

A randomized probe (seed 444) placed 400 points exactly on a horizontal grid line, 400 on a vertical one and 400 on grid corners of 1/3600 degree 64x64 tiles, every fifth tile bottom-up, with allTouched=true. On master: 2 crashes and 29 wrong-row burns with full-extent output, and 65 crashes with output cropped to the geometry.

Steps to reproduce

double p = 1.0 / 3600.0;
GeometryFactory gf = new GeometryFactory();

// 1. point on the tile's bottom-right corner
GridCoverage2D tile =
    RasterConstructors.makeEmptyRaster(1, "D", 256, 256, -180.0, 60.0, p, -p, 0, 0, 4326);
Geometry corner = gf.createPoint(new Coordinate(-180.0 + 255 * p + p, 60.0 - 255 * p - p));
RasterConstructors.asRasterWithRasterExtent(corner, tile, "D", true, 150, null);
// ArrayIndexOutOfBoundsException: Index 65536 out of bounds for length 65536

// 2. point on the row 0 / row 1 grid line
double uly = 60.0 - 2 * p;
GridCoverage2D tile2 =
    RasterConstructors.makeEmptyRaster(1, "D", 256, 256, -180.0, uly, p, -p, 0, 0, 4326);
Geometry onRow = gf.createPoint(new Coordinate(-180.0 + 10.5 * p, uly - p));
RasterConstructors.asRasterWithRasterExtent(onRow, tile2, "D", true, 150, null);
// ArrayIndexOutOfBoundsException: Index -246 out of bounds for length 65536

// 3. zone touching the tile only at its bottom-right corner
double ulx = -180.0 + 822 * p, uly3 = 60.0 - 546 * p;
GridCoverage2D tile3 =
    RasterConstructors.makeEmptyRaster(1, "D", 256, 256, ulx, uly3, p, -p, 0, 0, 4326);
double right = ulx + 256 * p, bottom = uly3 - 256 * p;
Geometry zone = gf.createPolygon(new Coordinate[] {
    new Coordinate(right, bottom), new Coordinate(right + 1, bottom),
    new Coordinate(right + 1, bottom - 1), new Coordinate(right, bottom - 1),
    new Coordinate(right, bottom)});
RasterBandAccessors.getZonalStatsAll(tile3, zone, 1, true);
// ClassCastException: Point cannot be cast to Polygon

// 4. unpaired scanline intercept past the grid
GridCoverage2D small = RasterConstructors.makeEmptyRaster(1, "D", 2, 2, 0, 2, 1, -1, 0, 0, 0);
Geometry tri = new WKTReader().read("POLYGON ((1 1, 2 1, 2 0.5, 1 1))");
RasterConstructors.asRasterWithRasterExtent(tri, small, "D", false, 150, null);
// ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4

Expected: 1 burns (255,255), 2 burns (10,0) and (10,1), 3 returns count 1 with allTouched=true and 0 with allTouched=false, and 4 burns nothing.

Proposed fix: walk rasterizePoint by integer column and row, clamped to the grid, and build each cell envelope from its index on the reference raster's grid. Delegate non-polygonal clips to rasterizeGeometry, keeping only their polygonal parts when allTouched is false. In fillPolygon, clamp fill ranges to the grid and discard out-of-grid unpaired intercepts. With that change case 2 no longer crashes but burns only (10,0). The missing (10,1) is a separate allTouched extent bug, filed separately.

Sedona version

master (53e4e7b, 2.0.0-SNAPSHOT)

API type

Java (reached from SQL through RS_AsRaster, RS_Clip, RS_SetValues and RS_ZonalStats / RS_ZonalStatsAll)

JRE version

11

Environment

Local unit tests (macOS)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions