Skip to content

Commit f5a4199

Browse files
author
Xin Huang
committed
✨ feat(api): Add WKB geometry bounds
- Accumulate X, Y, Z, and M bounds across ISO WKB geometry values - Validate structure, collection child types, and trailing data Generated-by: Codex
1 parent e0afb38 commit f5a4199

2 files changed

Lines changed: 414 additions & 0 deletions

File tree

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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 java.util.Arrays;
24+
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
25+
26+
/**
27+
* Accumulates geometry bounds from values encoded as Well-Known Binary (WKB).
28+
*
29+
* <p>Coordinates are tracked independently for the X, Y, Z, and M dimensions. Null or {@code NaN}
30+
* values do not contribute to a dimension. Z and M are omitted from the resulting bounds when no
31+
* value contributes to that dimension, and no bounds are produced unless both X and Y are present.
32+
*/
33+
public final class WKBGeometryBounds {
34+
35+
private static final int TYPE_POINT = 1;
36+
private static final int TYPE_LINE_STRING = 2;
37+
private static final int TYPE_POLYGON = 3;
38+
private static final int TYPE_MULTI_POINT = 4;
39+
private static final int TYPE_MULTI_LINE_STRING = 5;
40+
private static final int TYPE_MULTI_POLYGON = 6;
41+
private static final int TYPE_GEOMETRY_COLLECTION = 7;
42+
private static final int ANY_GEOMETRY = 0;
43+
44+
private static final int X = 0;
45+
private static final int Y = 1;
46+
private static final int Z = 2;
47+
private static final int M = 3;
48+
private static final int NUM_DIMENSIONS = 4;
49+
50+
private static final int MAX_DEPTH = 100;
51+
52+
private final double[] lower = new double[NUM_DIMENSIONS];
53+
private final double[] upper = new double[NUM_DIMENSIONS];
54+
private final boolean[] hasValue = new boolean[NUM_DIMENSIONS];
55+
56+
public WKBGeometryBounds() {
57+
Arrays.fill(lower, Double.POSITIVE_INFINITY);
58+
Arrays.fill(upper, Double.NEGATIVE_INFINITY);
59+
}
60+
61+
/**
62+
* Adds the coordinates from one WKB geometry to these bounds.
63+
*
64+
* <p>The input is read through a duplicate, so its position and limit are left unchanged.
65+
*
66+
* @param wkb a buffer containing exactly one WKB geometry
67+
* @throws IllegalArgumentException if the WKB is invalid or unsupported
68+
*/
69+
public void add(ByteBuffer wkb) {
70+
Preconditions.checkArgument(wkb != null, "Invalid WKB buffer: null");
71+
ByteBuffer buffer = wkb.duplicate();
72+
parseGeometry(buffer, 0, ANY_GEOMETRY);
73+
Preconditions.checkArgument(!buffer.hasRemaining(), "Invalid WKB: trailing data");
74+
}
75+
76+
/** Returns whether both X and Y have accumulated a non-NaN value. */
77+
public boolean hasBounds() {
78+
return hasValue[X] && hasValue[Y];
79+
}
80+
81+
/** Returns the lower bound, or {@code null} if either X or Y has no value. */
82+
public GeospatialBound lowerBound() {
83+
return hasBounds() ? bound(lower) : null;
84+
}
85+
86+
/** Returns the upper bound, or {@code null} if either X or Y has no value. */
87+
public GeospatialBound upperBound() {
88+
return hasBounds() ? bound(upper) : null;
89+
}
90+
91+
private void parseGeometry(ByteBuffer buffer, int depth, int expectedType) {
92+
Preconditions.checkArgument(depth <= MAX_DEPTH, "Invalid WKB: nesting too deep");
93+
checkRemaining(buffer, 5);
94+
95+
byte order = buffer.get();
96+
if (order == 0) {
97+
buffer.order(ByteOrder.BIG_ENDIAN);
98+
} else if (order == 1) {
99+
buffer.order(ByteOrder.LITTLE_ENDIAN);
100+
} else {
101+
throw new IllegalArgumentException("Invalid WKB byte order: " + order);
102+
}
103+
104+
long typeCode = buffer.getInt() & 0xFFFFFFFFL;
105+
int geometryType = (int) (typeCode % 1000);
106+
CoordinateLayout layout = coordinateLayout((int) (typeCode / 1000), typeCode);
107+
Preconditions.checkArgument(
108+
expectedType == ANY_GEOMETRY || geometryType == expectedType,
109+
"Invalid WKB: expected geometry type %s but found %s",
110+
expectedType,
111+
geometryType);
112+
113+
switch (geometryType) {
114+
case TYPE_POINT:
115+
readCoordinate(buffer, layout);
116+
break;
117+
case TYPE_LINE_STRING:
118+
readCoordinateSequence(buffer, layout);
119+
break;
120+
case TYPE_POLYGON:
121+
int numRings = readCount(buffer);
122+
for (int i = 0; i < numRings; i += 1) {
123+
readCoordinateSequence(buffer, layout);
124+
}
125+
break;
126+
case TYPE_MULTI_POINT:
127+
readCollection(buffer, depth, TYPE_POINT);
128+
break;
129+
case TYPE_MULTI_LINE_STRING:
130+
readCollection(buffer, depth, TYPE_LINE_STRING);
131+
break;
132+
case TYPE_MULTI_POLYGON:
133+
readCollection(buffer, depth, TYPE_POLYGON);
134+
break;
135+
case TYPE_GEOMETRY_COLLECTION:
136+
readCollection(buffer, depth, ANY_GEOMETRY);
137+
break;
138+
default:
139+
throw new IllegalArgumentException("Invalid or unsupported WKB geometry type: " + typeCode);
140+
}
141+
}
142+
143+
private void readCollection(ByteBuffer buffer, int depth, int expectedChildType) {
144+
int numElements = readCount(buffer);
145+
for (int i = 0; i < numElements; i += 1) {
146+
parseGeometry(buffer, depth + 1, expectedChildType);
147+
}
148+
}
149+
150+
private void readCoordinateSequence(ByteBuffer buffer, CoordinateLayout layout) {
151+
int numPoints = readCount(buffer);
152+
checkRemaining(buffer, (long) numPoints * layout.size() * Double.BYTES);
153+
for (int i = 0; i < numPoints; i += 1) {
154+
readCoordinate(buffer, layout);
155+
}
156+
}
157+
158+
private void readCoordinate(ByteBuffer buffer, CoordinateLayout layout) {
159+
checkRemaining(buffer, (long) layout.size() * Double.BYTES);
160+
update(X, buffer.getDouble());
161+
update(Y, buffer.getDouble());
162+
if (layout.hasZ()) {
163+
update(Z, buffer.getDouble());
164+
}
165+
166+
if (layout.hasM()) {
167+
update(M, buffer.getDouble());
168+
}
169+
}
170+
171+
private void update(int dimension, double value) {
172+
if (!Double.isNaN(value)) {
173+
lower[dimension] = Math.min(lower[dimension], value);
174+
upper[dimension] = Math.max(upper[dimension], value);
175+
hasValue[dimension] = true;
176+
}
177+
}
178+
179+
private GeospatialBound bound(double[] values) {
180+
if (hasValue[Z] && hasValue[M]) {
181+
return GeospatialBound.createXYZM(values[X], values[Y], values[Z], values[M]);
182+
} else if (hasValue[Z]) {
183+
return GeospatialBound.createXYZ(values[X], values[Y], values[Z]);
184+
} else if (hasValue[M]) {
185+
return GeospatialBound.createXYM(values[X], values[Y], values[M]);
186+
}
187+
188+
return GeospatialBound.createXY(values[X], values[Y]);
189+
}
190+
191+
private static CoordinateLayout coordinateLayout(int dimensionGroup, long typeCode) {
192+
switch (dimensionGroup) {
193+
case 0:
194+
return CoordinateLayout.XY;
195+
case 1:
196+
return CoordinateLayout.XYZ;
197+
case 2:
198+
return CoordinateLayout.XYM;
199+
case 3:
200+
return CoordinateLayout.XYZM;
201+
default:
202+
throw new IllegalArgumentException("Invalid or unsupported WKB geometry type: " + typeCode);
203+
}
204+
}
205+
206+
private static int readCount(ByteBuffer buffer) {
207+
checkRemaining(buffer, Integer.BYTES);
208+
long count = buffer.getInt() & 0xFFFFFFFFL;
209+
Preconditions.checkArgument(count <= Integer.MAX_VALUE, "Invalid WKB element count: %s", count);
210+
return (int) count;
211+
}
212+
213+
private static void checkRemaining(ByteBuffer buffer, long bytes) {
214+
Preconditions.checkArgument(
215+
buffer.remaining() >= bytes, "Invalid WKB: unexpected end of buffer");
216+
}
217+
218+
private enum CoordinateLayout {
219+
XY(false, false),
220+
XYZ(true, false),
221+
XYM(false, true),
222+
XYZM(true, true);
223+
224+
private final boolean hasZ;
225+
private final boolean hasM;
226+
227+
CoordinateLayout(boolean hasZ, boolean hasM) {
228+
this.hasZ = hasZ;
229+
this.hasM = hasM;
230+
}
231+
232+
private boolean hasZ() {
233+
return hasZ;
234+
}
235+
236+
private boolean hasM() {
237+
return hasM;
238+
}
239+
240+
private int size() {
241+
return 2 + (hasZ ? 1 : 0) + (hasM ? 1 : 0);
242+
}
243+
}
244+
}

0 commit comments

Comments
 (0)