Geospatial Support¶
Hardwood reads the Parquet geospatial metadata layer (GEOMETRY / GEOGRAPHY logical types and per-chunk GeospatialStatistics) and offers a bounding-box filter predicate that pushes spatial selectivity down to the row group level. Hardwood does not decode WKB payloads itself. The snippets below decode WKB with the JTS Topology Suite; any WKB decoder works.
Try it yourself
Want to run it or explore the capabilities yourself? The Geospatial example reads a GEOMETRY column, pushes a bounding-box filter down to the row-group level, and decodes WKB points with JTS.
To use JTS in the examples below, add the jts-core dependency:
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.20.0</version>
</dependency>
Identifying Geospatial Columns¶
GEOMETRY (planar) and GEOGRAPHY (geodesic on an ellipsoid) appear as LogicalType.GeometryType and LogicalType.GeographyType on the column schema. Both carry a CRS (defaulting to OGC:CRS84); GEOGRAPHY also carries an edge-interpolation algorithm, defaulting to SPHERICAL and reported as EdgeInterpolationAlgorithm.UNKNOWN when the file names one this version does not recognize.
import dev.hardwood.metadata.LogicalType;
import dev.hardwood.reader.ParquetFileReader;
import dev.hardwood.schema.ColumnSchema;
import dev.hardwood.schema.FileSchema;
try (ParquetFileReader reader = ParquetFileReader.open(InputFile.of(path))) {
FileSchema schema = reader.getFileSchema();
for (int i = 0; i < schema.getColumnCount(); i++) {
ColumnSchema column = schema.getColumn(i);
if (column.logicalType() instanceof LogicalType.GeometryType geomType) {
System.out.println(column.name() + " is a GEOMETRY column, CRS: " + geomType.crs());
}
else if (column.logicalType() instanceof LogicalType.GeographyType geoType) {
System.out.println(column.name() + " is a GEOGRAPHY column, CRS: " + geoType.crs()
+ ", edge interpolation: " + geoType.edgeInterpolation());
}
}
}
Bounding-Box Statistics¶
Per-chunk geospatial statistics are exposed on ColumnMetaData.geospatialStatistics(). The BoundingBox carries xmin/xmax/ymin/ymax (always present) plus optional zmin/zmax/mmin/mmax. For GEOGRAPHY columns, xmin > xmax is legal and indicates a chunk that wraps the antimeridian.
import dev.hardwood.metadata.BoundingBox;
import dev.hardwood.metadata.ColumnChunk;
import dev.hardwood.metadata.GeospatialStatistics;
import dev.hardwood.metadata.RowGroup;
for (RowGroup rowGroup : reader.getFileMetaData().rowGroups()) {
for (ColumnChunk chunk : rowGroup.columns()) {
GeospatialStatistics geo = chunk.metaData().geospatialStatistics();
if (geo == null) {
continue;
}
BoundingBox bbox = geo.bbox();
if (bbox != null) {
System.out.println(" bbox: x=[" + bbox.xmin() + ", " + bbox.xmax() + "], "
+ "y=[" + bbox.ymin() + ", " + bbox.ymax() + "]");
}
// Geospatial type codes (Point=1, LineString=2, Polygon=3, MultiPoint=4, etc.)
System.out.println(" types: " + geo.geospatialTypes());
}
}
Spatial Filter Pushdown¶
FilterPredicate.intersects(column, xmin, ymin, xmax, ymax) produces a predicate that drops row groups whose stored bounding box does not overlap the query box. The argument order follows the GeoJSON / WKT convention (bottom-left corner, then top-right corner). Antimeridian wrapping is handled both for a stored box and for a query box with xmin > xmax, which covers x >= xmin or x <= xmax on a GEOMETRY column as on a GEOGRAPHY one. A NaN bound throws IllegalArgumentException when the predicate is built.
import dev.hardwood.reader.FilterPredicate;
import dev.hardwood.reader.ParquetFileReader;
import dev.hardwood.reader.RowReader;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.WKBReader;
FilterPredicate filter = FilterPredicate.intersects("location", -25.0, 35.0, 45.0, 72.0);
try (ParquetFileReader fileReader = ParquetFileReader.open(InputFile.of(path));
RowReader rowReader = fileReader.buildRowReader().filter(filter).build()) {
WKBReader wkbReader = new WKBReader();
while (rowReader.hasNext()) {
rowReader.next();
byte[] wkb = rowReader.getBinary("location");
Geometry geom = wkbReader.read(wkb);
// Apply any exact-geometry tests here.
}
}
The predicate composes with the standard and / or combinators, e.g.:
FilterPredicate.and(
FilterPredicate.intersects("location", -5.0, 50.0, 2.0, 60.0),
FilterPredicate.gt("population", 100_000));
Limitations¶
intersects is coarse-grained: Hardwood drops row groups whose bounding box is disjoint from the query box, but every row in a surviving row group is returned. Apply your own per-row check on the WKB payload (e.g. via JTS) when you need exact geometric filtering.
Negation of intersects is not supported: FilterPredicate.not(FilterPredicate.intersects(...)), or not over any predicate holding one, throws IllegalArgumentException at reader creation, naming the column. If you need "geometries outside this box", read without a spatial filter and apply the negation yourself against getBinary("location").
Both limitations are tracked by hardwood#414.