Core: Add spherical geography bounds builder - #17788
Conversation
Generated-by: Codex
Generated-by: Codex
- Cover reference edge bounds in both endpoint orders across antimeridian, pole, and latitude-extremum cases. - Cover multi-edge and point-set aggregation over circular longitude intervals. Generated-by: Codex
Replace raw vector arrays with a typed private helper and use independent Haversine calculations in randomized tests. Generated-by: Codex
Document the numerical thresholds and conservative latitude expansion with the associated vector tests and an ASCII arc diagram. Generated-by: Codex
Replace independent coordinate, validity, and full-world flags with one explicit builder state machine. Generated-by: Codex
Replace repeated latitude, longitude, and full-span literals with descriptive constants. Generated-by: Codex
Generated-by: Codex
addPoint kept a coordinate on the antimeridian as given, but minimumLongitudeInterval folded an edge with both endpoints there onto -180, so the same physical point produced different bounds as a point versus a degenerate edge. Drop the sign-folding so +180 and -180 are preserved on both paths. Also document that a pole vertex contributes latitude only, since all meridians meet there.
szehon-ho
left a comment
There was a problem hiding this comment.
The spherical kernel looks sound — I couldn't find a case where the latitude extrema or the longitude union under-cover an arc, and I traced the sweep by hand through the antimeridian, pole-only, and wrapping-interval cases. Two things seem worth resolving before merge: the pole convention, which can produce an X bound that excludes an X value present in the file, and the per-vertex interval accumulation, which makes writer memory scale with a file's vertex count.
Two questions: will the follow-up integration compute the query-side geography bounding box with this same builder? That decides whether the pole convention is an internal consistency choice or a cross-implementation hazard. And with no production caller yet, the pole and NaN conventions can't be validated end-to-end, which is exactly where a mismatch would surface.
| // All meridians meet at a pole, so a vertex there has no single longitude that constrains | ||
| // the box; it contributes latitude only. A geography consisting solely of pole vertices | ||
| // leaves longitude unconstrained, which build() reports as the full range. | ||
| if (!isPole(latitude)) { | ||
| longitudeIntervals.add(new LongitudeInterval(longitude, longitude)); | ||
| } |
There was a problem hiding this comment.
Add the pole vertex's stored longitude to longitudeIntervals rather than dropping it. The spec's geography X rule is numeric — an object matches if x >= xmin OR x <= xmax — and GeographyEvaluator.intersects compares the raw x values with no notion that the pole lies on every meridian. So addPoint(-120, 90) followed by addPoint(40, 10) gives x=[40, 40], a bound that excludes an X value present in the file, and a predicate whose literal bound keeps the pole's longitude (POINT(-120 90) → x=[-120, -120]) won't intersect it, so the file is pruned even though it holds a matching row. Including the longitude only widens the box, so it stays valid under the spherical reading too. addEdgeWithPole drops the pole endpoint's longitude the same way.
| // under-covering bound; the result is then clamped to [-90, 90]. | ||
| private static final double LATITUDE_SCALING_FACTOR = 1.0000001; | ||
|
|
||
| private final List<LongitudeInterval> longitudeIntervals = Lists.newArrayList(); |
There was a problem hiding this comment.
Merge or compact these intervals as they accumulate instead of appending one per vertex. Nothing is ever removed, so a builder accumulating a whole data file holds one LongitudeInterval per vertex, and longitudeBounds() then allocates up to four LongitudeEvents per interval and sorts them — a file with 1M polygons at 100 vertices each is 100M edges. GeometryBoundsBuilder keeps O(1) state for the same job.
| private static boolean coordinatesAreValid(double longitude, double latitude) { | ||
| return Double.isFinite(longitude) | ||
| && Double.isFinite(latitude) | ||
| && longitude >= MIN_LONGITUDE | ||
| && longitude <= MAX_LONGITUDE | ||
| && latitude >= MIN_LATITUDE | ||
| && latitude <= MAX_LATITUDE; | ||
| } |
There was a problem hiding this comment.
Skip a NaN coordinate instead of latching INVALID for the whole file. POINT EMPTY is encoded as POINT(NaN NaN), so a single empty geography costs every other value in the file its bounds. The spec says NaN ordinates are skipped per dimension, and GeometryBoundsBuilder.DimensionBounds.add already does that. Suppressing on out-of-range coordinates still makes sense, since the sphere math has no meaning there.
| import org.apache.iceberg.geospatial.GeospatialBound; | ||
| import org.apache.iceberg.relocated.com.google.common.collect.Lists; | ||
|
|
||
| /** Builds an XY bounding box from geography points and minor great-circle edges on a sphere. */ |
There was a problem hiding this comment.
Document the contract here: build() returns null for empty input, one invalid coordinate turns bounds off permanently, an ambiguous antipodal edge yields world bounds, the box may wrap with west > east, and latitude extrema are deliberately widened so bounds are not tight. These are the details the follow-up integration has to reason about, and GeometryBoundsBuilder spells out the equivalent.
| double longitude1 = random.nextDouble() * 360.0 - 180.0; | ||
| double latitude1 = random.nextDouble() * 140.0 - 70.0; | ||
| double longitude2 = normalizeLongitude(longitude1 + random.nextDouble() * 240.0 - 120.0); | ||
| double latitude2 = random.nextDouble() * 140.0 - 70.0; |
There was a problem hiding this comment.
Consider widening these ranges to reach the poles and near-180 longitude separations. Latitudes are drawn from ±70 and the separation from ±120, so no sampled arc passes near or over a pole — which is where "the minor arc spans the shorter endpoint longitude difference" is hardest to verify by inspection, since longitude sweeps almost 180° over a short arc there. Only (5, 10)-(175, 10) and (5, 10)-(-175.1, 10) cover that today.
| edgeCase(10.0, 0.0, 120.0, 0.0, 10.0, 0.0, 120.0, 0.0), | ||
| edgeCase(10.0, 0.0, 120.0, 1.0, 10.0, 0.0, 120.0, 1.06416356550489), | ||
| edgeCase(10.0, 10.0, 20.0, 20.0, 10.0, 10.0, 20.0, 20.0), | ||
| edgeCase(10.0, 60.0, 70.0, 70.0, 10.0, 60.0, 70.0, 70.20558550568438), |
There was a problem hiding this comment.
Assert that the bound is at least the true extremum rather than pinning the widened value. 70.20558550568438 is the exact vertex latitude times LATITUDE_SCALING_FACTOR, and the 1e-9 tolerance is far tighter than that 7e-6 offset, so tuning the margin means rewriting about ten expectations.
Summary
Geography bounds metrics need an XY box that covers geodesic edges on a sphere, where an edge can bulge in latitude beyond its endpoints and where longitude is periodic. This adds a package-private
SphericalGeographyBoundsBuilderincorethat accumulates the bounds of geography points and minor great-circle edges throughaddPoint/addEdge/build, modeling each geography as a sphere.west > east) when that is the tighter box+180and-180are preserved rather than folded onto one sign and the same coordinate yields the same interval whether it arrives as a point or a degenerate edgeThe builder exposes only the spherical
addPoint/addEdge/buildkernel. Pole handling and the spherical interior-latitude extrema are kept separate internally so a follow-up can compose this kernel behind geography WKB traversal and algorithm dispatch, without introducing a single-implementation abstraction here.Scope: geography, spherical only
This is the spherical algorithm kernel for a follow-up geography metrics integration. It deliberately does not add a generic
GeographyBoundsBuilder, WKB traversal, Parquet writer wiring, polygon pole-containment detection, or spheroidal interpolation strategies such as Karney. Those belong in the integration layer, once another interpolation strategy exists, so this PR does not introduce a single-implementation abstraction prematurely.Test Plan
NaN, and infinite coordinates suppress the boxVerification Commands
AI Disclosure