huan233usc commented on code in PR #17509: URL: https://github.com/apache/iceberg/pull/17509#discussion_r3832918695
########## core/src/main/java/org/apache/iceberg/GeometryBoundsBuilder.java: ########## @@ -0,0 +1,407 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.iceberg; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import org.apache.iceberg.geospatial.BoundingBox; +import org.apache.iceberg.geospatial.GeospatialBound; +import org.apache.iceberg.relocated.com.google.common.base.Preconditions; + +/** + * Accumulates geometry bounds from values encoded as Well-Known Binary (WKB). + * + * <p>The seven OGC geometry types are supported: point, line string, polygon, multi point, multi + * line string, multi polygon, and geometry collection. + * + * <p>Coordinates are tracked independently for the X and Y dimensions. A {@code NaN} ordinate marks + * an empty value and does not contribute to its dimension; an infinite ordinate is a real position + * and is kept as a bound, since the spec forbids only NaN as a lower or upper bound. No bounds are + * produced unless both dimensions are present. + * + * <p>These bounds apply to {@code geometry} columns, whose edges are always interpolated linearly, + * so a box that contains every vertex contains the whole geometry. They are not valid for {@code + * geography} columns: geodesic edges can reach beyond their endpoints, longitude is periodic, and a + * geography box may cross the antimeridian. + * + * <p>Only the X and Y dimensions contribute to the box. Z and M ordinates are valid in the ISO WKB + * serializations that Iceberg accepts, so they are read past and ignored rather than rejected. + * + * <p>Every ring of a polygon, including interior rings, contributes to the bounds. In a valid + * polygon the holes lie inside the shell, so this is the shell's own box. + */ +class GeometryBoundsBuilder { + + private static final int TYPE_POINT = 1; + private static final int TYPE_LINE_STRING = 2; + private static final int TYPE_POLYGON = 3; + private static final int TYPE_MULTI_POINT = 4; + private static final int TYPE_MULTI_LINE_STRING = 5; + private static final int TYPE_MULTI_POLYGON = 6; + private static final int TYPE_GEOMETRY_COLLECTION = 7; + private static final int ANY_GEOMETRY = 0; + + // ISO WKB encodes the dimensions of a geometry in the thousands digit of its type code + private static final int DIMENSION_DIVISOR = 1000; + private static final int XY_GROUP = 0; + private static final int XYZ_GROUP = 1; + private static final int XYM_GROUP = 2; + private static final int XYZM_GROUP = 3; + private static final int ANY_DIMENSION = -1; + + private static final int MIN_RING_POINTS = 4; + + private final DimensionBounds xBounds = new DimensionBounds(); + private final DimensionBounds yBounds = new DimensionBounds(); + // set when a value carried bytes beyond its declared geometry, meaning it was not fully parsed + // and an object may be missing from the box; build() then suppresses the bounds + private boolean incomplete = false; + + /** + * Adds one WKB geometry value to these bounds. + * + * <p>The input is read through a duplicate, so its position and limit are left unchanged. + * + * <p>If the value carries bytes beyond the single geometry it declares, it was not fully parsed + * (for example a multi geometry whose declared element count is short), so an object never + * reached the bounds and the box can no longer cover the whole value. {@link #build()} then + * returns no bounds, since suppressing optional metrics is safe where under-covering a file is + * not. + * + * <p>If this throws, the builder's state is undefined: coordinates parsed before the failure may + * already be folded in, so a caller that continues after a rejected value must discard this + * builder. + * + * @param wkb a buffer containing one WKB geometry; trailing bytes suppress the bounds + * @throws IllegalArgumentException if the WKB is malformed + */ + public void addValue(ByteBuffer wkb) { + Preconditions.checkArgument(wkb != null, "Invalid WKB buffer: null"); + ByteBuffer buffer = wkb.duplicate(); + parseGeometry(buffer, ANY_GEOMETRY, ANY_DIMENSION); + if (buffer.hasRemaining()) { Review Comment: Done — `addValue` now catches a private `InvalidWkbException` from the parse paths and sets the suppress flag, so an unclosed ring, a short count, a mismatched child, a bad type code or byte order, and truncation all cost the file its bounds instead of failing the write. That retired the "state undefined after a throw" contract and its test. Thanks for the parquet-java correction — the flag drops the whole file's bounds rather than skip-and-under-cover. ########## core/src/main/java/org/apache/iceberg/GeometryBoundsBuilder.java: ########## @@ -0,0 +1,407 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.iceberg; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import org.apache.iceberg.geospatial.BoundingBox; +import org.apache.iceberg.geospatial.GeospatialBound; +import org.apache.iceberg.relocated.com.google.common.base.Preconditions; + +/** + * Accumulates geometry bounds from values encoded as Well-Known Binary (WKB). + * + * <p>The seven OGC geometry types are supported: point, line string, polygon, multi point, multi + * line string, multi polygon, and geometry collection. + * + * <p>Coordinates are tracked independently for the X and Y dimensions. A {@code NaN} ordinate marks + * an empty value and does not contribute to its dimension; an infinite ordinate is a real position + * and is kept as a bound, since the spec forbids only NaN as a lower or upper bound. No bounds are + * produced unless both dimensions are present. + * + * <p>These bounds apply to {@code geometry} columns, whose edges are always interpolated linearly, + * so a box that contains every vertex contains the whole geometry. They are not valid for {@code + * geography} columns: geodesic edges can reach beyond their endpoints, longitude is periodic, and a + * geography box may cross the antimeridian. + * + * <p>Only the X and Y dimensions contribute to the box. Z and M ordinates are valid in the ISO WKB + * serializations that Iceberg accepts, so they are read past and ignored rather than rejected. + * + * <p>Every ring of a polygon, including interior rings, contributes to the bounds. In a valid + * polygon the holes lie inside the shell, so this is the shell's own box. + */ +class GeometryBoundsBuilder { + + private static final int TYPE_POINT = 1; + private static final int TYPE_LINE_STRING = 2; + private static final int TYPE_POLYGON = 3; + private static final int TYPE_MULTI_POINT = 4; + private static final int TYPE_MULTI_LINE_STRING = 5; + private static final int TYPE_MULTI_POLYGON = 6; + private static final int TYPE_GEOMETRY_COLLECTION = 7; + private static final int ANY_GEOMETRY = 0; + + // ISO WKB encodes the dimensions of a geometry in the thousands digit of its type code + private static final int DIMENSION_DIVISOR = 1000; + private static final int XY_GROUP = 0; + private static final int XYZ_GROUP = 1; + private static final int XYM_GROUP = 2; + private static final int XYZM_GROUP = 3; + private static final int ANY_DIMENSION = -1; + + private static final int MIN_RING_POINTS = 4; + + private final DimensionBounds xBounds = new DimensionBounds(); + private final DimensionBounds yBounds = new DimensionBounds(); + // set when a value carried bytes beyond its declared geometry, meaning it was not fully parsed + // and an object may be missing from the box; build() then suppresses the bounds + private boolean incomplete = false; + + /** + * Adds one WKB geometry value to these bounds. + * + * <p>The input is read through a duplicate, so its position and limit are left unchanged. + * + * <p>If the value carries bytes beyond the single geometry it declares, it was not fully parsed + * (for example a multi geometry whose declared element count is short), so an object never + * reached the bounds and the box can no longer cover the whole value. {@link #build()} then + * returns no bounds, since suppressing optional metrics is safe where under-covering a file is + * not. + * + * <p>If this throws, the builder's state is undefined: coordinates parsed before the failure may + * already be folded in, so a caller that continues after a rejected value must discard this + * builder. + * + * @param wkb a buffer containing one WKB geometry; trailing bytes suppress the bounds + * @throws IllegalArgumentException if the WKB is malformed + */ + public void addValue(ByteBuffer wkb) { + Preconditions.checkArgument(wkb != null, "Invalid WKB buffer: null"); + ByteBuffer buffer = wkb.duplicate(); + parseGeometry(buffer, ANY_GEOMETRY, ANY_DIMENSION); + if (buffer.hasRemaining()) { + incomplete = true; + } + } + + /** + * Builds the bounding box covering every geometry added, or {@code null} if either the X or Y + * dimension has no value, or if any value carried trailing bytes (see {@link #addValue}). + */ + public BoundingBox build() { + if (incomplete || !xBounds.hasValue() || !yBounds.hasValue()) { + return null; + } + + GeospatialBound min = GeospatialBound.createXY(xBounds.lower(), yBounds.lower()); + GeospatialBound max = GeospatialBound.createXY(xBounds.upper(), yBounds.upper()); + return new BoundingBox(min, max); + } + + private void parseGeometry(ByteBuffer buffer, int expectedType, int expectedDimension) { + // a geometry header is a one-byte order flag followed by a four-byte type code: + // +-------+-----------------------+ + // | order | type code | + // | (1 B) | (4 B) | + // +-------+-----------------------+ + checkRemaining(buffer, Byte.BYTES + Integer.BYTES); + + // each geometry sets its own byte order; restore the caller's order before returning so a + // sibling read after a nested geometry is not misread with the wrong endianness + ByteOrder callerOrder = buffer.order(); + byte order = buffer.get(); + if (order == 0) { + buffer.order(ByteOrder.BIG_ENDIAN); + } else if (order == 1) { + buffer.order(ByteOrder.LITTLE_ENDIAN); + } else { + throw new IllegalArgumentException("Invalid WKB byte order: " + order); + } + + parseGeometryBodyAndUpdateBound(buffer, expectedType, expectedDimension); + buffer.order(callerOrder); + } + + private void parseGeometryBodyAndUpdateBound( + ByteBuffer buffer, int expectedType, int expectedDimension) { + long typeCode = Integer.toUnsignedLong(buffer.getInt()); + int dimensionGroup = (int) (typeCode / DIMENSION_DIVISOR); + int geometryType = (int) (typeCode % DIMENSION_DIVISOR); + Preconditions.checkArgument( Review Comment: Good point — those now go through the same catch, so an unsupported-but-valid OGC type (PolyhedralSurface/TIN/Triangle) leaves the file without bounds instead of failing every insert. Added a PolyhedralSurface (15) case to the suppressed-bounds test. ########## core/src/main/java/org/apache/iceberg/GeometryBoundsBuilder.java: ########## @@ -0,0 +1,407 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.iceberg; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import org.apache.iceberg.geospatial.BoundingBox; +import org.apache.iceberg.geospatial.GeospatialBound; +import org.apache.iceberg.relocated.com.google.common.base.Preconditions; + +/** + * Accumulates geometry bounds from values encoded as Well-Known Binary (WKB). + * + * <p>The seven OGC geometry types are supported: point, line string, polygon, multi point, multi + * line string, multi polygon, and geometry collection. + * + * <p>Coordinates are tracked independently for the X and Y dimensions. A {@code NaN} ordinate marks + * an empty value and does not contribute to its dimension; an infinite ordinate is a real position + * and is kept as a bound, since the spec forbids only NaN as a lower or upper bound. No bounds are + * produced unless both dimensions are present. + * + * <p>These bounds apply to {@code geometry} columns, whose edges are always interpolated linearly, + * so a box that contains every vertex contains the whole geometry. They are not valid for {@code + * geography} columns: geodesic edges can reach beyond their endpoints, longitude is periodic, and a + * geography box may cross the antimeridian. + * + * <p>Only the X and Y dimensions contribute to the box. Z and M ordinates are valid in the ISO WKB Review Comment: Reworded — the javadoc now says producing Z and M bounds is a deliberate non-goal here (the ordinates are read past), not a parsing consequence. `createXYZ`/`createXYZM` make adding them straightforward if a caller needs them later. ########## core/src/main/java/org/apache/iceberg/GeometryBoundsBuilder.java: ########## @@ -0,0 +1,407 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.iceberg; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import org.apache.iceberg.geospatial.BoundingBox; +import org.apache.iceberg.geospatial.GeospatialBound; +import org.apache.iceberg.relocated.com.google.common.base.Preconditions; + +/** + * Accumulates geometry bounds from values encoded as Well-Known Binary (WKB). + * + * <p>The seven OGC geometry types are supported: point, line string, polygon, multi point, multi + * line string, multi polygon, and geometry collection. + * + * <p>Coordinates are tracked independently for the X and Y dimensions. A {@code NaN} ordinate marks + * an empty value and does not contribute to its dimension; an infinite ordinate is a real position + * and is kept as a bound, since the spec forbids only NaN as a lower or upper bound. No bounds are + * produced unless both dimensions are present. + * + * <p>These bounds apply to {@code geometry} columns, whose edges are always interpolated linearly, + * so a box that contains every vertex contains the whole geometry. They are not valid for {@code + * geography} columns: geodesic edges can reach beyond their endpoints, longitude is periodic, and a + * geography box may cross the antimeridian. + * + * <p>Only the X and Y dimensions contribute to the box. Z and M ordinates are valid in the ISO WKB + * serializations that Iceberg accepts, so they are read past and ignored rather than rejected. + * + * <p>Every ring of a polygon, including interior rings, contributes to the bounds. In a valid + * polygon the holes lie inside the shell, so this is the shell's own box. + */ +class GeometryBoundsBuilder { + + private static final int TYPE_POINT = 1; + private static final int TYPE_LINE_STRING = 2; + private static final int TYPE_POLYGON = 3; + private static final int TYPE_MULTI_POINT = 4; + private static final int TYPE_MULTI_LINE_STRING = 5; + private static final int TYPE_MULTI_POLYGON = 6; + private static final int TYPE_GEOMETRY_COLLECTION = 7; + private static final int ANY_GEOMETRY = 0; + + // ISO WKB encodes the dimensions of a geometry in the thousands digit of its type code + private static final int DIMENSION_DIVISOR = 1000; + private static final int XY_GROUP = 0; + private static final int XYZ_GROUP = 1; + private static final int XYM_GROUP = 2; + private static final int XYZM_GROUP = 3; + private static final int ANY_DIMENSION = -1; + + private static final int MIN_RING_POINTS = 4; + + private final DimensionBounds xBounds = new DimensionBounds(); + private final DimensionBounds yBounds = new DimensionBounds(); + // set when a value carried bytes beyond its declared geometry, meaning it was not fully parsed + // and an object may be missing from the box; build() then suppresses the bounds + private boolean incomplete = false; + + /** + * Adds one WKB geometry value to these bounds. + * + * <p>The input is read through a duplicate, so its position and limit are left unchanged. + * + * <p>If the value carries bytes beyond the single geometry it declares, it was not fully parsed + * (for example a multi geometry whose declared element count is short), so an object never + * reached the bounds and the box can no longer cover the whole value. {@link #build()} then + * returns no bounds, since suppressing optional metrics is safe where under-covering a file is + * not. + * + * <p>If this throws, the builder's state is undefined: coordinates parsed before the failure may + * already be folded in, so a caller that continues after a rejected value must discard this + * builder. + * + * @param wkb a buffer containing one WKB geometry; trailing bytes suppress the bounds + * @throws IllegalArgumentException if the WKB is malformed + */ + public void addValue(ByteBuffer wkb) { + Preconditions.checkArgument(wkb != null, "Invalid WKB buffer: null"); + ByteBuffer buffer = wkb.duplicate(); + parseGeometry(buffer, ANY_GEOMETRY, ANY_DIMENSION); + if (buffer.hasRemaining()) { + incomplete = true; + } + } + + /** + * Builds the bounding box covering every geometry added, or {@code null} if either the X or Y + * dimension has no value, or if any value carried trailing bytes (see {@link #addValue}). + */ + public BoundingBox build() { + if (incomplete || !xBounds.hasValue() || !yBounds.hasValue()) { + return null; + } + + GeospatialBound min = GeospatialBound.createXY(xBounds.lower(), yBounds.lower()); + GeospatialBound max = GeospatialBound.createXY(xBounds.upper(), yBounds.upper()); + return new BoundingBox(min, max); + } + + private void parseGeometry(ByteBuffer buffer, int expectedType, int expectedDimension) { + // a geometry header is a one-byte order flag followed by a four-byte type code: + // +-------+-----------------------+ + // | order | type code | + // | (1 B) | (4 B) | + // +-------+-----------------------+ + checkRemaining(buffer, Byte.BYTES + Integer.BYTES); + + // each geometry sets its own byte order; restore the caller's order before returning so a + // sibling read after a nested geometry is not misread with the wrong endianness + ByteOrder callerOrder = buffer.order(); + byte order = buffer.get(); + if (order == 0) { + buffer.order(ByteOrder.BIG_ENDIAN); + } else if (order == 1) { + buffer.order(ByteOrder.LITTLE_ENDIAN); + } else { + throw new IllegalArgumentException("Invalid WKB byte order: " + order); + } + + parseGeometryBodyAndUpdateBound(buffer, expectedType, expectedDimension); + buffer.order(callerOrder); + } + + private void parseGeometryBodyAndUpdateBound( + ByteBuffer buffer, int expectedType, int expectedDimension) { + long typeCode = Integer.toUnsignedLong(buffer.getInt()); + int dimensionGroup = (int) (typeCode / DIMENSION_DIVISOR); + int geometryType = (int) (typeCode % DIMENSION_DIVISOR); + Preconditions.checkArgument( + geometryType >= TYPE_POINT + && geometryType <= TYPE_GEOMETRY_COLLECTION + && dimensionGroup <= XYZM_GROUP, + "Invalid or unsupported WKB geometry type: %s", + typeCode); + // an element of a multi geometry or collection must match its parent's member type and + // dimensions; if/throw so the message is built only when a value is actually rejected + if (expectedType != ANY_GEOMETRY && geometryType != expectedType) { + throw new IllegalArgumentException( + "Invalid WKB: expected geometry type " + + typeName(expectedType) + + " but found " + + typeName(geometryType)); + } + if (expectedDimension != ANY_DIMENSION && dimensionGroup != expectedDimension) { + throw new IllegalArgumentException( + "Invalid WKB: expected dimensions " + + dimensionName(expectedDimension) + + " but found " + + dimensionName(dimensionGroup)); + } + + int numDimensions = numDimensions(dimensionGroup); + + switch (geometryType) { + case TYPE_POINT: + readCoordinate(buffer, numDimensions); + break; + case TYPE_LINE_STRING: + readCoordinateSequence(buffer, numDimensions); + break; + case TYPE_POLYGON: + readPolygon(buffer, numDimensions); + break; + case TYPE_MULTI_POINT: + readCollection(buffer, TYPE_POINT, dimensionGroup); + break; + case TYPE_MULTI_LINE_STRING: + readCollection(buffer, TYPE_LINE_STRING, dimensionGroup); + break; + case TYPE_MULTI_POLYGON: + readCollection(buffer, TYPE_POLYGON, dimensionGroup); + break; + case TYPE_GEOMETRY_COLLECTION: + readCollection(buffer, ANY_GEOMETRY, dimensionGroup); + break; + default: + throw new IllegalArgumentException("Invalid or unsupported WKB geometry type: " + typeCode); + } + } + + private static int numDimensions(int dimensionGroup) { + switch (dimensionGroup) { + case XY_GROUP: + return 2; + case XYZ_GROUP: + case XYM_GROUP: + return 3; + default: // XYZM_GROUP, the only remaining group the caller accepts + return 4; + } + } + + private static String typeName(int geometryType) { + switch (geometryType) { + case TYPE_POINT: + return "Point"; + case TYPE_LINE_STRING: + return "LineString"; + case TYPE_POLYGON: + return "Polygon"; + case TYPE_MULTI_POINT: + return "MultiPoint"; + case TYPE_MULTI_LINE_STRING: + return "MultiLineString"; + case TYPE_MULTI_POLYGON: + return "MultiPolygon"; + case TYPE_GEOMETRY_COLLECTION: + return "GeometryCollection"; + default: + return String.valueOf(geometryType); + } + } + + private static String dimensionName(int dimensionGroup) { + switch (dimensionGroup) { + case XY_GROUP: + return "XY"; + case XYZ_GROUP: + return "XYZ"; + case XYM_GROUP: + return "XYM"; + default: + return "XYZM"; + } + } + + // a ring count, then that many rings, each a coordinate sequence: + // +----------+-----------------------+ + // | # rings | ring 0, ring 1, ... | + // | (4 B) | (each a point seq) | + // +----------+-----------------------+ + private void readPolygon(ByteBuffer buffer, int numDimensions) { + // read every ring, interior rings included: they must be consumed to reach the end of the + // polygon anyway, and folding their coordinates in can only widen the box, never under-cover it + int numRings = readCount(buffer); + for (int i = 0; i < numRings; i += 1) { + readRing(buffer, numDimensions); + } + } + + // a point count, then that many coordinates forming a linear ring; a non-empty ring must be + // closed (its first and last vertices share the same X and Y) and hold at least four points: + // +----------+-----------------------+ + // | # points | coord 0 .. coord n-1 | + // | (4 B) | (n coordinates) | + // +----------+-----------------------+ + private void readRing(ByteBuffer buffer, int numDimensions) { + int numPoints = readCount(buffer); + checkRemaining(buffer, (long) numPoints * numDimensions * Double.BYTES); + if (numPoints == 0) { + return; + } + + Preconditions.checkArgument( + numPoints >= MIN_RING_POINTS, + "Invalid WKB: polygon ring has fewer than %s points: %s", + MIN_RING_POINTS, + numPoints); + + // a ring is closed on X and Y only: the closing vertex's Z or M may differ from the first (a + // measure can increase around the ring), and -0.0 must count as 0.0, so compare the two X/Y + // pairs with == rather than the whole coordinate. Every vertex still folds into the bounds. + double firstX = buffer.getDouble(); + double firstY = buffer.getDouble(); + skipExtraOrdinates(buffer, numDimensions); + xBounds.add(firstX); + yBounds.add(firstY); + + for (int i = 1; i < numPoints - 1; i += 1) { + readCoordinate(buffer, numDimensions); + } + + double lastX = buffer.getDouble(); + double lastY = buffer.getDouble(); + skipExtraOrdinates(buffer, numDimensions); + xBounds.add(lastX); + yBounds.add(lastY); + + Preconditions.checkArgument( + firstX == lastX && firstY == lastY, "Invalid WKB: polygon ring is not closed"); + } + + // an element count, then that many complete WKB geometries: + // +----------+-----------------------+ + // | # elems | geometry 0, 1, ... | + // | (4 B) | (each a full WKB) | + // +----------+-----------------------+ + private void readCollection(ByteBuffer buffer, int expectedChildType, int expectedDimension) { + int numElements = readCount(buffer); + for (int i = 0; i < numElements; i += 1) { + // each child carries its own byte order and type code, and must match the parent's member + // type and dimensions + parseGeometry(buffer, expectedChildType, expectedDimension); + } + } + + // a point count, then that many coordinates: + // +----------+-----------------------+ + // | # points | coord 0 .. coord n-1 | + // | (4 B) | (n coordinates) | + // +----------+-----------------------+ + private void readCoordinateSequence(ByteBuffer buffer, int numDimensions) { + int numPoints = readCount(buffer); + long numBytes = (long) numPoints * numDimensions * Double.BYTES; + checkRemaining(buffer, numBytes); + for (int i = 0; i < numPoints; i += 1) { + readCoordinate(buffer, numDimensions); + } + } + + // one coordinate; only X and Y bound the box, any Z and M are read past: + // +-------+-------+-------+-------+ + // | X | Y | [Z] | [M] | + // | (8 B) | (8 B) | (8 B) | (8 B) | + // +-------+-------+-------+-------+ + private void readCoordinate(ByteBuffer buffer, int numDimensions) { + checkRemaining(buffer, (long) numDimensions * Double.BYTES); + double xCoord = buffer.getDouble(); + double yCoord = buffer.getDouble(); + skipExtraOrdinates(buffer, numDimensions); + xBounds.add(xCoord); + yBounds.add(yCoord); + } + + // skip any Z and M ordinates that follow X and Y; only X and Y contribute to the box + private static void skipExtraOrdinates(ByteBuffer buffer, int numDimensions) { + for (int i = 2; i < numDimensions; i += 1) { + buffer.getDouble(); + } + } + + // a 4-byte unsigned count (rings, points, or elements): + // +---------+ + // | count | + // | (4 B) | + // +---------+ + private static int readCount(ByteBuffer buffer) { + checkRemaining(buffer, Integer.BYTES); + long count = Integer.toUnsignedLong(buffer.getInt()); + // every element or point occupies at least one more byte, so a count larger than the bytes left + // cannot be valid; catch it here with a precise message instead of looping until the buffer + // ends + Preconditions.checkArgument( + count <= buffer.remaining(), + "Invalid WKB element count: %s exceeds %s remaining bytes", + count, + buffer.remaining()); + return (int) count; + } + + private static void checkRemaining(ByteBuffer buffer, long bytes) { + Preconditions.checkArgument( + buffer.remaining() >= bytes, "Invalid WKB: unexpected end of buffer"); + } + + private static class DimensionBounds { + private double lower; + private double upper; + private boolean hasValue = false; + + private void add(double value) { + // NaN marks an empty ordinate and is skipped per the spec; an infinite value is a real + // position and is kept, since the spec forbids only NaN as a lower or upper bound + if (Double.isNaN(value)) { + return; + } + + if (hasValue) { + lower = Math.min(lower, value); Review Comment: Added `negativeZeroSortsBeforePositiveZero`: `POINT(0 0)` then `POINT(-0.0 -0.0)`, asserting lower `-0.0` and upper `+0.0`. `GeospatialBound` compares with `Double.compare`, so the assertion fails if `Math.min`/`Math.max` were ever swapped for `<`/`>`. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
