| /* | |
| * 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. | |
| */ | |
| #include "iceberg/geospatial.h" | |
| #include <cmath> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <limits> | |
| #include <optional> | |
| #include <span> | |
| #include <utility> | |
| #include <vector> | |
| #include "iceberg/type.h" | |
| #include "iceberg/util/endian.h" | |
| #include "iceberg/util/macros.h" | |
| namespace iceberg { | |
| namespace { | |
| constexpr size_t kDoubleSize = sizeof(double); | |
| std::optional<double> OptionalOrdinate(double value) { | |
| return std::isnan(value) ? std::nullopt : std::make_optional(value); | |
| } | |
| template <EndianConvertible T> | |
| void AppendLittleEndian(std::vector<uint8_t>& bytes, T value) { | |
| const size_t offset = bytes.size(); | |
| bytes.resize(offset + sizeof(value)); | |
| WriteLittleEndian(value, bytes.data() + offset); | |
| } | |
| bool IsValidBoundSize(size_t size) { | |
| return size == 2 * kDoubleSize || size == 3 * kDoubleSize || size == 4 * kDoubleSize; | |
| } | |
| bool RangeIntersects(double min1, double max1, double min2, double max2) { | |
| return min1 <= max2 && max1 >= min2; | |
| } | |
| bool RangeIntersectsWithWrapAround(double min1, double max1, double min2, double max2) { | |
| // A wrapped longitude interval [min, max], where min > max, represents | |
| // [min, 180] plus [-180, max]. | |
| const bool interval1_wraps = min1 > max1; | |
| const bool interval2_wraps = min2 > max2; | |
| if (!interval1_wraps && !interval2_wraps) { | |
| return RangeIntersects(min1, max1, min2, max2); | |
| } | |
| if (interval1_wraps && interval2_wraps) { | |
| // Both intervals contain the antimeridian, so they always intersect. | |
| return true; | |
| } | |
| if (interval1_wraps) { | |
| return min1 <= max2 || max1 >= min2; | |
| } | |
| return min2 <= max1 || max2 >= min1; | |
| } | |
| Status ValidateOptionalRanges(const BoundingBox& bbox) { | |
| if (bbox.lower().z().has_value() && bbox.upper().z().has_value() && | |
| *bbox.lower().z() > *bbox.upper().z()) { | |
| return InvalidArgument("Invalid Z range: zmin cannot be greater than zmax"); | |
| } | |
| if (bbox.lower().m().has_value() && bbox.upper().m().has_value() && | |
| *bbox.lower().m() > *bbox.upper().m()) { | |
| return InvalidArgument("Invalid M range: mmin cannot be greater than mmax"); | |
| } | |
| return {}; | |
| } | |
| Status ValidateGeometry(const BoundingBox& bbox) { | |
| if (!(bbox.lower().x() <= bbox.upper().x())) { | |
| return InvalidArgument("Invalid X range: xmin cannot be greater than xmax"); | |
| } | |
| if (!(bbox.lower().y() <= bbox.upper().y())) { | |
| return InvalidArgument("Invalid Y range: ymin cannot be greater than ymax"); | |
| } | |
| return ValidateOptionalRanges(bbox); | |
| } | |
| Status ValidateGeography(const BoundingBox& bbox) { | |
| if (!(bbox.lower().y() >= -90.0 && bbox.lower().y() <= 90.0 && | |
| bbox.upper().y() >= -90.0 && bbox.upper().y() <= 90.0)) { | |
| return InvalidArgument("Invalid latitude: out of range [-90, 90]"); | |
| } | |
| if (!(bbox.lower().x() >= -180.0 && bbox.lower().x() <= 180.0 && | |
| bbox.upper().x() >= -180.0 && bbox.upper().x() <= 180.0)) { | |
| return InvalidArgument("Invalid longitude: out of range [-180, 180]"); | |
| } | |
| if (bbox.lower().y() > bbox.upper().y()) { | |
| return InvalidArgument("Invalid latitude range: ymin cannot be greater than ymax"); | |
| } | |
| return ValidateOptionalRanges(bbox); | |
| } | |
| bool IntersectsYzm(const BoundingBox& lhs, const BoundingBox& rhs) { | |
| if (lhs.lower().z().has_value() && lhs.upper().z().has_value() && | |
| rhs.lower().z().has_value() && rhs.upper().z().has_value() && | |
| !RangeIntersects(*lhs.lower().z(), *lhs.upper().z(), *rhs.lower().z(), | |
| *rhs.upper().z())) { | |
| return false; | |
| } | |
| if (lhs.lower().m().has_value() && lhs.upper().m().has_value() && | |
| rhs.lower().m().has_value() && rhs.upper().m().has_value() && | |
| !RangeIntersects(*lhs.lower().m(), *lhs.upper().m(), *rhs.lower().m(), | |
| *rhs.upper().m())) { | |
| return false; | |
| } | |
| return RangeIntersects(lhs.lower().y(), lhs.upper().y(), rhs.lower().y(), | |
| rhs.upper().y()); | |
| } | |
| } // namespace | |
| GeospatialBound::GeospatialBound(double x, double y, std::optional<double> z, | |
| std::optional<double> m) | |
| : x_(x), y_(y), z_(std::move(z)), m_(std::move(m)) {} | |
| GeospatialBound GeospatialBound::XY(double x, double y) { | |
| return GeospatialBound{x, y, std::nullopt, std::nullopt}; | |
| } | |
| GeospatialBound GeospatialBound::XYZ(double x, double y, double z) { | |
| return GeospatialBound{x, y, OptionalOrdinate(z), std::nullopt}; | |
| } | |
| GeospatialBound GeospatialBound::XYM(double x, double y, double m) { | |
| return GeospatialBound{x, y, std::nullopt, OptionalOrdinate(m)}; | |
| } | |
| GeospatialBound GeospatialBound::XYZM(double x, double y, double z, double m) { | |
| return GeospatialBound{x, y, OptionalOrdinate(z), OptionalOrdinate(m)}; | |
| } | |
| Result<GeospatialBound> GeospatialBound::Deserialize(std::span<const uint8_t> bytes) { | |
| if (!IsValidBoundSize(bytes.size())) { | |
| return InvalidArgument( | |
| "Invalid geospatial bound size: {}. Valid sizes are 16, 24, or 32 bytes", | |
| bytes.size()); | |
| } | |
| auto x = ReadLittleEndian<double>(bytes.data()); | |
| auto y = ReadLittleEndian<double>(bytes.data() + kDoubleSize); | |
| if (bytes.size() == 2 * kDoubleSize) { | |
| return XY(x, y); | |
| } | |
| auto z = ReadLittleEndian<double>(bytes.data() + 2 * kDoubleSize); | |
| if (bytes.size() == 3 * kDoubleSize) { | |
| return XYZ(x, y, z); | |
| } | |
| auto m = ReadLittleEndian<double>(bytes.data() + 3 * kDoubleSize); | |
| if (std::isnan(z)) { | |
| return XYM(x, y, m); | |
| } | |
| return XYZM(x, y, z, m); | |
| } | |
| std::vector<uint8_t> GeospatialBound::Serialize() const { | |
| size_t size = 2 * kDoubleSize; | |
| if (z_.has_value() && !m_.has_value()) { | |
| size = 3 * kDoubleSize; | |
| } else if (m_.has_value()) { | |
| size = 4 * kDoubleSize; | |
| } | |
| std::vector<uint8_t> bytes; | |
| bytes.reserve(size); | |
| AppendLittleEndian(bytes, x_); | |
| AppendLittleEndian(bytes, y_); | |
| if (z_.has_value() || m_.has_value()) { | |
| AppendLittleEndian(bytes, z_.value_or(std::numeric_limits<double>::quiet_NaN())); | |
| } | |
| if (m_.has_value()) { | |
| AppendLittleEndian(bytes, *m_); | |
| } | |
| return bytes; | |
| } | |
| BoundingBox::BoundingBox(GeospatialBound lower, GeospatialBound upper) | |
| : lower_(std::move(lower)), upper_(std::move(upper)) {} | |
| Result<BoundingBox> BoundingBox::Deserialize(std::span<const uint8_t> lower, | |
| std::span<const uint8_t> upper) { | |
| ICEBERG_ASSIGN_OR_RAISE(auto lower_bound, GeospatialBound::Deserialize(lower)); | |
| ICEBERG_ASSIGN_OR_RAISE(auto upper_bound, GeospatialBound::Deserialize(upper)); | |
| return BoundingBox(std::move(lower_bound), std::move(upper_bound)); | |
| } | |
| Result<BoundingBox> BoundingBox::Deserialize(std::span<const uint8_t> bytes) { | |
| constexpr size_t kLengthSize = sizeof(uint32_t); | |
| if (bytes.size() < kLengthSize) { | |
| return InvalidArgument("Truncated bounding box encoding"); | |
| } | |
| const size_t lower_size = ReadLittleEndian<uint32_t>(bytes.data()); | |
| if (!IsValidBoundSize(lower_size) || | |
| bytes.size() < kLengthSize + lower_size + kLengthSize) { | |
| return InvalidArgument("Invalid or truncated lower geospatial bound"); | |
| } | |
| const size_t upper_length_offset = kLengthSize + lower_size; | |
| const size_t upper_size = | |
| ReadLittleEndian<uint32_t>(bytes.data() + upper_length_offset); | |
| const size_t upper_offset = upper_length_offset + kLengthSize; | |
| if (!IsValidBoundSize(upper_size) || bytes.size() < upper_offset + upper_size) { | |
| return InvalidArgument("Invalid or truncated upper geospatial bound"); | |
| } | |
| return Deserialize(bytes.subspan(kLengthSize, lower_size), | |
| bytes.subspan(upper_offset, upper_size)); | |
| } | |
| std::vector<uint8_t> BoundingBox::Serialize() const { | |
| auto lower_bytes = lower_.Serialize(); | |
| auto upper_bytes = upper_.Serialize(); | |
| std::vector<uint8_t> bytes; | |
| bytes.reserve(sizeof(uint32_t) + lower_bytes.size() + sizeof(uint32_t) + | |
| upper_bytes.size()); | |
| AppendLittleEndian(bytes, static_cast<uint32_t>(lower_bytes.size())); | |
| bytes.insert(bytes.end(), lower_bytes.begin(), lower_bytes.end()); | |
| AppendLittleEndian(bytes, static_cast<uint32_t>(upper_bytes.size())); | |
| bytes.insert(bytes.end(), upper_bytes.begin(), upper_bytes.end()); | |
| return bytes; | |
| } | |
| Result<bool> GeospatialBoundsIntersect(const Type& type, const BoundingBox& lhs, | |
| const BoundingBox& rhs) { | |
| switch (type.type_id()) { | |
| case TypeId::kGeometry: | |
| ICEBERG_RETURN_UNEXPECTED(ValidateGeometry(lhs)); | |
| ICEBERG_RETURN_UNEXPECTED(ValidateGeometry(rhs)); | |
| if (!IntersectsYzm(lhs, rhs)) { | |
| return false; | |
| } | |
| return RangeIntersects(lhs.lower().x(), lhs.upper().x(), rhs.lower().x(), | |
| rhs.upper().x()); | |
| case TypeId::kGeography: | |
| ICEBERG_RETURN_UNEXPECTED(ValidateGeography(lhs)); | |
| ICEBERG_RETURN_UNEXPECTED(ValidateGeography(rhs)); | |
| if (!IntersectsYzm(lhs, rhs)) { | |
| return false; | |
| } | |
| return RangeIntersectsWithWrapAround(lhs.lower().x(), lhs.upper().x(), | |
| rhs.lower().x(), rhs.upper().x()); | |
| default: | |
| return NotSupported("Unsupported type for BoundingBox: {}", type.ToString()); | |
| } | |
| } | |
| } // namespace iceberg |