Skip to content

Commit 12bc49c

Browse files
author
Xin Huang
committed
API: Add geometry bounds collector
- Accumulate two-dimensional bounds across the seven OGC WKB geometry types - Reject unsupported dimensions and malformed structures Generated-by: Codex
1 parent e0afb38 commit 12bc49c

2 files changed

Lines changed: 426 additions & 0 deletions

File tree

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.iceberg.geospatial;
20+
21+
import java.nio.ByteBuffer;
22+
import java.nio.ByteOrder;
23+
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
24+
25+
/**
26+
* Accumulates geometry bounds from values encoded as Well-Known Binary (WKB).
27+
*
28+
* <p>The seven OGC geometry types are supported: point, line string, polygon, multi point, multi
29+
* line string, multi polygon, and geometry collection. WKB values carrying Z or M dimensions are
30+
* rejected.
31+
*
32+
* <p>Coordinates are tracked independently for the X and Y dimensions. {@code NaN} values do not
33+
* contribute to a dimension, and no bounds are produced unless both dimensions are present.
34+
*/
35+
public final class GeometryBoundsCollector {
36+
37+
private static final int TYPE_POINT = 1;
38+
private static final int TYPE_LINE_STRING = 2;
39+
private static final int TYPE_POLYGON = 3;
40+
private static final int TYPE_MULTI_POINT = 4;
41+
private static final int TYPE_MULTI_LINE_STRING = 5;
42+
private static final int TYPE_MULTI_POLYGON = 6;
43+
private static final int TYPE_GEOMETRY_COLLECTION = 7;
44+
private static final int ANY_GEOMETRY = 0;
45+
46+
private static final int MAX_DEPTH = 100;
47+
48+
private final DimensionBounds xBounds = new DimensionBounds();
49+
private final DimensionBounds yBounds = new DimensionBounds();
50+
51+
// reusable copies of the accumulated bounds, used to undo a partially parsed value
52+
private final DimensionBounds xSaved = new DimensionBounds();
53+
private final DimensionBounds ySaved = new DimensionBounds();
54+
55+
/**
56+
* Adds the coordinates from one WKB geometry to these bounds.
57+
*
58+
* <p>The input is read through a duplicate, so its position and limit are left unchanged.
59+
*
60+
* @param wkb a buffer containing exactly one WKB geometry
61+
* @throws IllegalArgumentException if the WKB is invalid or unsupported
62+
*/
63+
public void add(ByteBuffer wkb) {
64+
Preconditions.checkArgument(wkb != null, "Invalid WKB buffer: null");
65+
xSaved.copyFrom(xBounds);
66+
ySaved.copyFrom(yBounds);
67+
ByteBuffer buffer = wkb.duplicate();
68+
try {
69+
parseGeometry(buffer, 0, ANY_GEOMETRY);
70+
Preconditions.checkArgument(!buffer.hasRemaining(), "Invalid WKB: trailing data");
71+
} catch (RuntimeException e) {
72+
xBounds.copyFrom(xSaved);
73+
yBounds.copyFrom(ySaved);
74+
throw e;
75+
}
76+
}
77+
78+
/** Returns the accumulated bounding box, or {@code null} if either X or Y has no value. */
79+
public BoundingBox boundingBox() {
80+
if (!xBounds.hasValue() || !yBounds.hasValue()) {
81+
return null;
82+
}
83+
84+
GeospatialBound min = GeospatialBound.createXY(xBounds.lower(), yBounds.lower());
85+
GeospatialBound max = GeospatialBound.createXY(xBounds.upper(), yBounds.upper());
86+
return new BoundingBox(min, max);
87+
}
88+
89+
private void parseGeometry(ByteBuffer buffer, int depth, int expectedType) {
90+
Preconditions.checkArgument(depth <= MAX_DEPTH, "Invalid WKB: nesting too deep");
91+
checkRemaining(buffer, 5);
92+
93+
byte order = buffer.get();
94+
if (order == 0) {
95+
buffer.order(ByteOrder.BIG_ENDIAN);
96+
} else if (order == 1) {
97+
buffer.order(ByteOrder.LITTLE_ENDIAN);
98+
} else {
99+
throw new IllegalArgumentException("Invalid WKB byte order: " + order);
100+
}
101+
102+
long typeCode = buffer.getInt() & 0xFFFFFFFFL;
103+
int geometryType = (int) (typeCode % 1000);
104+
Preconditions.checkArgument(
105+
typeCode / 1000 == 0, "Unsupported WKB: only 2D geometries are supported");
106+
Preconditions.checkArgument(
107+
expectedType == ANY_GEOMETRY || geometryType == expectedType,
108+
"Invalid WKB: expected geometry type %s but found %s",
109+
expectedType,
110+
geometryType);
111+
112+
switch (geometryType) {
113+
case TYPE_POINT:
114+
readCoordinate(buffer);
115+
break;
116+
case TYPE_LINE_STRING:
117+
readCoordinateSequence(buffer, true);
118+
break;
119+
case TYPE_POLYGON:
120+
readPolygon(buffer);
121+
break;
122+
case TYPE_MULTI_POINT:
123+
readCollection(buffer, depth, TYPE_POINT);
124+
break;
125+
case TYPE_MULTI_LINE_STRING:
126+
readCollection(buffer, depth, TYPE_LINE_STRING);
127+
break;
128+
case TYPE_MULTI_POLYGON:
129+
readCollection(buffer, depth, TYPE_POLYGON);
130+
break;
131+
case TYPE_GEOMETRY_COLLECTION:
132+
readCollection(buffer, depth, ANY_GEOMETRY);
133+
break;
134+
default:
135+
throw new IllegalArgumentException("Invalid or unsupported WKB geometry type: " + typeCode);
136+
}
137+
}
138+
139+
private void readPolygon(ByteBuffer buffer) {
140+
int numRings = readCount(buffer);
141+
if (numRings > 0) {
142+
readCoordinateSequence(buffer, true);
143+
}
144+
145+
// interior rings are contained by the exterior ring and cannot widen the bounds
146+
for (int i = 1; i < numRings; i += 1) {
147+
readCoordinateSequence(buffer, false);
148+
}
149+
}
150+
151+
private void readCollection(ByteBuffer buffer, int depth, int expectedChildType) {
152+
int numElements = readCount(buffer);
153+
for (int i = 0; i < numElements; i += 1) {
154+
parseGeometry(buffer, depth + 1, expectedChildType);
155+
}
156+
}
157+
158+
private void readCoordinateSequence(ByteBuffer buffer, boolean updateBounds) {
159+
int numPoints = readCount(buffer);
160+
long numBytes = (long) numPoints * 2 * Double.BYTES;
161+
checkRemaining(buffer, numBytes);
162+
if (!updateBounds) {
163+
buffer.position(buffer.position() + (int) numBytes);
164+
return;
165+
}
166+
167+
for (int i = 0; i < numPoints; i += 1) {
168+
readCoordinate(buffer);
169+
}
170+
}
171+
172+
private void readCoordinate(ByteBuffer buffer) {
173+
checkRemaining(buffer, 2 * Double.BYTES);
174+
double xCoord = buffer.getDouble();
175+
double yCoord = buffer.getDouble();
176+
xBounds.add(xCoord);
177+
yBounds.add(yCoord);
178+
}
179+
180+
private static int readCount(ByteBuffer buffer) {
181+
checkRemaining(buffer, Integer.BYTES);
182+
long count = buffer.getInt() & 0xFFFFFFFFL;
183+
Preconditions.checkArgument(count <= Integer.MAX_VALUE, "Invalid WKB element count: %s", count);
184+
return (int) count;
185+
}
186+
187+
private static void checkRemaining(ByteBuffer buffer, long bytes) {
188+
Preconditions.checkArgument(
189+
buffer.remaining() >= bytes, "Invalid WKB: unexpected end of buffer");
190+
}
191+
192+
private static class DimensionBounds {
193+
private double lower;
194+
private double upper;
195+
private boolean hasValue = false;
196+
197+
private void add(double value) {
198+
if (Double.isNaN(value)) {
199+
return;
200+
}
201+
202+
if (hasValue) {
203+
lower = Math.min(lower, value);
204+
upper = Math.max(upper, value);
205+
} else {
206+
lower = value;
207+
upper = value;
208+
hasValue = true;
209+
}
210+
}
211+
212+
private boolean hasValue() {
213+
return hasValue;
214+
}
215+
216+
private double lower() {
217+
return lower;
218+
}
219+
220+
private double upper() {
221+
return upper;
222+
}
223+
224+
private void copyFrom(DimensionBounds other) {
225+
this.lower = other.lower;
226+
this.upper = other.upper;
227+
this.hasValue = other.hasValue;
228+
}
229+
}
230+
}

0 commit comments

Comments
 (0)