diff --git a/a5_rust/Cargo.lock b/a5_rust/Cargo.lock index 307ce73..f2edbb6 100644 --- a/a5_rust/Cargo.lock +++ b/a5_rust/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "a5" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810fd8faa0a83f71c3b7f5ecbb9918f98e71afc127736c06245bc5f2686f1df2" +checksum = "2ed6d76405b82e5dbbceed9305e0e3d8e1c63a2aeb236fe3ebd0f49c6f76d55e" dependencies = [ "lazy_static", ] diff --git a/a5_rust/Cargo.toml b/a5_rust/Cargo.toml index 5037b78..d088906 100644 --- a/a5_rust/Cargo.toml +++ b/a5_rust/Cargo.toml @@ -9,4 +9,4 @@ name = "a5_rust" crate-type = ["staticlib"] [dependencies] -a5 = "0.8.0" +a5 = "0.9.0" diff --git a/a5_rust/src/lib.rs b/a5_rust/src/lib.rs index 70b7dcf..6e5223b 100644 --- a/a5_rust/src/lib.rs +++ b/a5_rust/src/lib.rs @@ -14,13 +14,6 @@ pub struct ResultLonLat { pub error: *mut std::os::raw::c_char, // null if no error } -#[repr(C)] -pub struct ResultSpherical { - pub theta: f64, - pub phi: f64, - pub error: *mut std::os::raw::c_char, -} - #[repr(C)] pub struct CellBoundaryOptions { pub closed_ring: bool, @@ -251,17 +244,6 @@ pub extern "C" fn a5_get_num_children(parent_res: i32, child_res: i32) -> usize a5::get_num_children(parent_res, child_res) } -#[no_mangle] -pub extern "C" fn a5_cell_to_spherical(cell: u64) -> ResultSpherical { - match a5::cell_to_spherical(cell) { - Ok(sph) => ResultSpherical { theta: sph.theta.get(), phi: sph.phi.get(), error: std::ptr::null_mut() }, - Err(e) => { - let err_msg = CString::new(e.to_string()).unwrap(); - ResultSpherical { theta: 0.0, phi: 0.0, error: err_msg.into_raw() } - } - } -} - #[no_mangle] pub extern "C" fn a5_spherical_cap(cell_id: u64, radius: f64) -> CellArray { cell_vec_result_to_c(a5::spherical_cap(cell_id, radius)) @@ -294,18 +276,6 @@ pub extern "C" fn a5_is_valid_cell(index: u64) -> bool { } } -#[no_mangle] -pub extern "C" fn a5_spherical_to_cell(theta: f64, phi: f64, resolution: i32) -> ResultU64 { - let spherical = a5::coordinate_systems::Spherical::new(a5::Radians::new(theta), a5::Radians::new(phi)); - match a5::spherical_to_cell(spherical, resolution) { - Ok(cell) => ResultU64 { value: cell, error: std::ptr::null_mut() }, - Err(e) => { - let err_msg = CString::new(e.to_string()).unwrap(); - ResultU64 { value: 0, error: err_msg.into_raw() } - } - } -} - // Build a Vec from a C array of LonLatDegrees (lon, lat) input points. fn lonlat_slice_to_vec(points: *const LonLatDegrees, len: usize) -> Vec { let slice = unsafe { std::slice::from_raw_parts(points, len) }; @@ -321,12 +291,35 @@ pub extern "C" fn a5_line_string_to_cells(points: *const LonLatDegrees, len: usi cell_vec_result_to_c(a5::line_string_to_cells(&lonlats, resolution)) } +// GeoJSON-style polygon: ring 0 is the outer ring, rings 1.. are holes. The +// rings are passed flattened into a single `points` buffer, with `ring_lengths` +// giving the vertex count of each ring (so the offsets can be reconstructed). +// Holes are excluded by the a5 crate itself - the caller does no hole handling. #[no_mangle] -pub extern "C" fn a5_polygon_to_cells(points: *const LonLatDegrees, len: usize, resolution: i32) -> CellArray { - if points.is_null() || len == 0 { +pub extern "C" fn a5_polygon_to_cells( + points: *const LonLatDegrees, + ring_lengths: *const usize, + ring_count: usize, + resolution: i32, +) -> CellArray { + if points.is_null() || ring_lengths.is_null() || ring_count == 0 { return CellArray { data: std::ptr::null_mut(), len: 0, error: std::ptr::null_mut() }; } - let lonlats = lonlat_slice_to_vec(points, len); - cell_vec_result_to_c(a5::polygon_to_cells(&lonlats, resolution)) + let lengths = unsafe { std::slice::from_raw_parts(ring_lengths, ring_count) }; + let total: usize = lengths.iter().sum(); + let flat = unsafe { std::slice::from_raw_parts(points, total) }; + + let mut rings: Vec> = Vec::with_capacity(ring_count); + let mut offset = 0; + for &len in lengths { + rings.push( + flat[offset..offset + len] + .iter() + .map(|p| a5::LonLat::new(p.lon, p.lat)) + .collect(), + ); + offset += len; + } + cell_vec_result_to_c(a5::polygon_to_cells(&rings, resolution)) } diff --git a/docs/README.md b/docs/README.md index d40cd29..bb0c1f8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -155,8 +155,6 @@ Visualizing that A5 cell shows: |----------|---------|-------------| | `a5_lonlat_to_cell(lon, lat, res)` | `UBIGINT` | Cell containing a coordinate | | `a5_cell_to_lonlat(cell)` | `DOUBLE[2]` | Cell center `[lon, lat]` | -| `a5_cell_to_spherical(cell)` | `DOUBLE[2]` | Cell center `[theta, phi]` (radians) | -| `a5_spherical_to_cell(theta, phi, res)` | `UBIGINT` | Cell from spherical coords (inverse of above) | | `a5_cell_to_boundary(cell [, closed, segments])` | `DOUBLE[2][]` | Boundary vertices | | `a5_cell_area(res)` | `DOUBLE` | Cell area (m²) at a resolution | | `a5_get_resolution(cell)` | `INTEGER` | Resolution of a cell | @@ -347,38 +345,6 @@ SELECT unnest(a5_cell_to_boundary(207618739568, false, 5)) as boundary_points; └───────────────────────────────────────────┘ ``` - - -#### `a5_cell_to_spherical(cell_id) -> DOUBLE[2]` - -Returns the spherical coordinates [theta, phi] in radians of an A5 cell center, where theta is the azimuthal angle and phi is the polar angle. - -**Example:** -```sql -SELECT a5_cell_to_spherical(a5_lonlat_to_cell(-74.0060, 40.7128, 15)) as spherical_coords; -``` - -#### `a5_spherical_to_cell(theta, phi, resolution) -> UBIGINT` - -Returns the A5 cell at the given resolution containing the spherical coordinates [theta, phi] (in radians). This is the inverse of `a5_cell_to_spherical`. - -**Parameters:** - -- `theta` (DOUBLE): Azimuthal angle in radians -- `phi` (DOUBLE): Polar angle in radians -- `resolution` (INTEGER): Resolution level (0-30) - -**Example:** -```sql -SELECT a5_spherical_to_cell(-0.512679, 0.913528, 10) as cell; -┌─────────────────────┐ -│ cell │ -│ uint64 │ -├─────────────────────┤ -│ 1937278465245970432 │ -└─────────────────────┘ -``` - ### Region Functions #### `a5_geometry_to_cells(geom, resolution) -> UBIGINT[]` diff --git a/src/a5_extension.cpp b/src/a5_extension.cpp index 68a22cd..f1badb3 100644 --- a/src/a5_extension.cpp +++ b/src/a5_extension.cpp @@ -12,7 +12,7 @@ namespace duckdb { #define MAX_RESOLUTION 30 -#define A5_EXTENSION_VERSION "2026060904" +#define A5_EXTENSION_VERSION "2026061701" // Helper function to validate resolution and throw with a clear error message inline void ValidateResolution(int32_t resolution, const char *function_name) { @@ -369,35 +369,6 @@ inline void A5GetNumChildrenFun(DataChunk &args, ExpressionState &state, Vector }); } -inline void A5CellToSphericalFun(DataChunk &args, ExpressionState &state, Vector &result) { - auto &cell_vector = args.data[0]; - - auto &result_data_children = ArrayVector::GetEntry(result); - double *data_ptr = FlatVector::GetData(result_data_children); - - UnifiedVectorFormat cell_id_format; - cell_vector.ToUnifiedFormat(args.size(), cell_id_format); - uint64_t *input_data_ptr = FlatVector::GetData(cell_vector); - - for (idx_t i = 0; i < args.size(); i++) { - auto cell_idx = cell_id_format.sel->get_index(i); - if (!cell_id_format.validity.RowIsValid(cell_idx)) { - FlatVector::SetNull(result, i, true); - continue; - } - - struct ResultSpherical res = a5_cell_to_spherical(input_data_ptr[cell_idx]); - ThrowRustError(res.error, "a5_cell_to_spherical"); - - data_ptr[i * 2] = res.theta; - data_ptr[i * 2 + 1] = res.phi; - } - - if (args.size() == 1) { - result.SetVectorType(VectorType::CONSTANT_VECTOR); - } -} - inline void A5SphericalCapFun(DataChunk &args, ExpressionState &state, Vector &result) { ListVector::Reserve(result, args.size() * 4); uint64_t offset = 0; @@ -492,21 +463,6 @@ inline void A5IsValidCellFun(DataChunk &args, ExpressionState &state, Vector &re [&](uint64_t cell) { return a5_is_valid_cell(cell); }); } -inline void A5SphericalToCellFun(DataChunk &args, ExpressionState &state, Vector &result) { - auto &theta_vector = args.data[0]; - auto &phi_vector = args.data[1]; - auto &resolution_vector = args.data[2]; - - TernaryExecutor::Execute( - theta_vector, phi_vector, resolution_vector, result, args.size(), - [&](double theta, double phi, int32_t resolution) { - ValidateResolution(resolution, "a5_spherical_to_cell"); - struct ResultU64 res = a5_spherical_to_cell(theta, phi, resolution); - ThrowRustError(res.error, "a5_spherical_to_cell"); - return res.value; - }); -} - // --------------------------------------------------------------------------- // GEOMETRY (WKB) writers // @@ -641,74 +597,34 @@ static vector WkbReadRing(WkbCursor &cur, idx_t dims) { // Fill a polygon (outer ring minus any holes) into the accumulator. // -// A5's polygon_to_cells returns a *compacted* (mixed-resolution) covering, so the outer -// covering and a hole's covering generally share no cell IDs and cannot be differenced -// directly. To subtract holes we uncompact both to the target resolution, take the set -// difference at that uniform resolution, then re-compact for output. The (common) no-hole -// case skips all of this and passes the crate's compacted covering through unchanged. +// Holes are excluded by the a5 crate itself: we flatten all rings (outer first, then +// holes) into a single point buffer plus a per-ring length array and hand them to +// a5_polygon_to_cells, which returns the compacted covering of the outer ring with the +// holes already removed. Empty rings are dropped so a degenerate ring never shifts the +// outer-ring-is-first convention. static void PolygonRingsToCells(const vector> &rings, int32_t resolution, CellAccumulator &acc, const char *function_name) { if (rings.empty() || rings[0].empty()) { return; } - auto outer = a5_polygon_to_cells(rings[0].data(), rings[0].size(), resolution); - ThrowCellArrayError(outer, function_name); - - bool has_holes = false; - for (size_t r = 1; r < rings.size(); r++) { - if (!rings[r].empty()) { - has_holes = true; - break; - } - } - if (!has_holes) { - for (size_t i = 0; i < outer.len; i++) { - acc.Add(outer.data[i]); - } - a5_free_cell_array(outer); - return; - } - // Expand the outer covering to a uniform resolution. - auto outer_uniform = a5_uncompact(outer.data, outer.len, resolution); - ThrowCellArrayError(outer_uniform, function_name); - a5_free_cell_array(outer); - - // Collect the uniform-resolution cells of every hole. - std::unordered_set holes; - for (size_t r = 1; r < rings.size(); r++) { - if (rings[r].empty()) { + vector points; + vector ring_lengths; + ring_lengths.reserve(rings.size()); + for (const auto &ring : rings) { + if (ring.empty()) { continue; } - auto hole = a5_polygon_to_cells(rings[r].data(), rings[r].size(), resolution); - ThrowCellArrayError(hole, function_name); - auto hole_uniform = a5_uncompact(hole.data, hole.len, resolution); - ThrowCellArrayError(hole_uniform, function_name); - a5_free_cell_array(hole); - for (size_t i = 0; i < hole_uniform.len; i++) { - holes.insert(hole_uniform.data[i]); - } - a5_free_cell_array(hole_uniform); + ring_lengths.push_back(ring.size()); + points.insert(points.end(), ring.begin(), ring.end()); } - // Difference, then re-compact so the output matches the no-hole convention. - vector kept; - kept.reserve(outer_uniform.len); - for (size_t i = 0; i < outer_uniform.len; i++) { - if (holes.find(outer_uniform.data[i]) == holes.end()) { - kept.push_back(outer_uniform.data[i]); - } - } - a5_free_cell_array(outer_uniform); - if (kept.empty()) { - return; - } - auto compacted = a5_compact(kept.data(), kept.size()); - ThrowCellArrayError(compacted, function_name); - for (size_t i = 0; i < compacted.len; i++) { - acc.Add(compacted.data[i]); + auto cells = a5_polygon_to_cells(points.data(), ring_lengths.data(), ring_lengths.size(), resolution); + ThrowCellArrayError(cells, function_name); + for (size_t i = 0; i < cells.len; i++) { + acc.Add(cells.data[i]); } - a5_free_cell_array(compacted); + a5_free_cell_array(cells); } // Recursively read a (possibly multi-part) geometry and accumulate its A5 cells. @@ -1063,21 +979,6 @@ static void LoadInternal(ExtensionLoader &loader) { loader.RegisterFunction(std::move(info)); } - // a5_cell_to_spherical: Returns the spherical coordinates of a cell center - { - auto func = ScalarFunction("a5_cell_to_spherical", {LogicalType::UBIGINT}, - LogicalType::ARRAY(LogicalType::DOUBLE, 2), A5CellToSphericalFun); - CreateScalarFunctionInfo info(func); - FunctionDescription desc; - desc.description = "Returns the spherical coordinates [theta, phi] in radians of an A5 cell center"; - desc.parameter_names = {"cell"}; - desc.parameter_types = {LogicalType::UBIGINT}; - desc.examples = {"a5_cell_to_spherical(a5_lonlat_to_cell(-122.4, 37.8, 10))"}; - desc.categories = {"a5", "geospatial"}; - info.descriptions.push_back(std::move(desc)); - loader.RegisterFunction(std::move(info)); - } - // a5_spherical_cap: Returns cells within a spherical cap radius { auto func = ScalarFunction("a5_spherical_cap", {LogicalType::UBIGINT, LogicalType::DOUBLE}, @@ -1153,23 +1054,6 @@ static void LoadInternal(ExtensionLoader &loader) { loader.RegisterFunction(std::move(info)); } - // a5_spherical_to_cell: Returns the cell containing the given spherical coordinates - { - auto func = - ScalarFunction("a5_spherical_to_cell", {LogicalType::DOUBLE, LogicalType::DOUBLE, LogicalType::INTEGER}, - LogicalType::UBIGINT, A5SphericalToCellFun); - CreateScalarFunctionInfo info(func); - FunctionDescription desc; - desc.description = "Returns the A5 cell at the given resolution containing the spherical coordinates " - "[theta, phi] (in radians); the inverse of a5_cell_to_spherical"; - desc.parameter_names = {"theta", "phi", "resolution"}; - desc.parameter_types = {LogicalType::DOUBLE, LogicalType::DOUBLE, LogicalType::INTEGER}; - desc.examples = {"a5_spherical_to_cell(2.14, 0.92, 10)"}; - desc.categories = {"a5", "geospatial"}; - info.descriptions.push_back(std::move(desc)); - loader.RegisterFunction(std::move(info)); - } - // a5_geometry_to_cells: Returns the cells covering any geometry { auto func = ScalarFunction("a5_geometry_to_cells", {LogicalType::GEOMETRY(), LogicalType::INTEGER}, diff --git a/src/include/rust.h b/src/include/rust.h index 0682a6f..ea17a1c 100644 --- a/src/include/rust.h +++ b/src/include/rust.h @@ -1,4 +1,4 @@ -/* Generated with cbindgen:0.29.0 */ +/* Generated with cbindgen:0.29.4 */ /* This file is automatically generated by cbindgen. */ @@ -42,12 +42,6 @@ struct CellBoundaryOptions { int32_t segments; }; -struct ResultSpherical { - double theta; - double phi; - char *error; -}; - extern "C" { ResultU64 a5_lon_lat_to_cell(double longitude, double latitude, int32_t resolution); @@ -84,8 +78,6 @@ char *a5_u64_to_hex(uint64_t value); uintptr_t a5_get_num_children(int32_t parent_res, int32_t child_res); -ResultSpherical a5_cell_to_spherical(uint64_t cell); - CellArray a5_spherical_cap(uint64_t cell_id, double radius); CellArray a5_grid_disk(uint64_t cell_id, uintptr_t k); @@ -96,10 +88,11 @@ uint64_t a5_world_cell(); bool a5_is_valid_cell(uint64_t index); -ResultU64 a5_spherical_to_cell(double theta, double phi, int32_t resolution); - CellArray a5_line_string_to_cells(const LonLatDegrees *points, uintptr_t len, int32_t resolution); -CellArray a5_polygon_to_cells(const LonLatDegrees *points, uintptr_t len, int32_t resolution); +CellArray a5_polygon_to_cells(const LonLatDegrees *points, + const uintptr_t *ring_lengths, + uintptr_t ring_count, + int32_t resolution); } // extern "C" diff --git a/test/sql/a5.test b/test/sql/a5.test index 030b372..b1b9719 100644 --- a/test/sql/a5.test +++ b/test/sql/a5.test @@ -411,12 +411,6 @@ select a5_get_num_children(0, 1), a5_get_num_children(0, 2), a5_get_num_children ---- 5 20 1 -# a5_cell_to_spherical: Get spherical coordinates of cell center -query I -select list_transform(a5_cell_to_spherical(a5_lonlat_to_cell(-122.4, 37.8, 10)), x -> round(x, 6)) ----- -[-0.512679, 0.913528] - # a5_spherical_cap: Get cells within radius query II select length(a5_spherical_cap(a5_lonlat_to_cell(-122.4, 37.8, 10), 1000.0)), length(a5_spherical_cap(a5_lonlat_to_cell(-122.4, 37.8, 10), 50000.0)) @@ -479,19 +473,6 @@ select a5_is_valid_cell(NULL) ---- NULL -# a5_spherical_to_cell: inverse of a5_cell_to_spherical (round-trip) -query I -select a5_spherical_to_cell(s[1], s[2], 10) = c -from (select a5_lonlat_to_cell(-122.4, 37.8, 10) as c, a5_cell_to_spherical(a5_lonlat_to_cell(-122.4, 37.8, 10)) as s) ----- -true - -# a5_spherical_to_cell: resolution out of range error -statement error -select a5_spherical_to_cell(0.5, 0.9, 31) ----- -a5_spherical_to_cell: Resolution must be between 0 and 30 - # a5_geometry_to_cells: LINESTRING is traced query I select length(a5_geometry_to_cells('LINESTRING(0 0, 10 10, 20 5)'::GEOMETRY, 5))