geo_polygonize_core/
arrow_api.rs1use crate::error::PolygonizeError;
2use crate::types::{Coord3D, Line3D};
3use crate::Polygonizer;
4use arrow::array::{Array, AsArray, GenericListArray};
5use arrow::datatypes::{DataType, Field, Float64Type};
6use geo_traits::to_geo::ToGeoLineString;
7use geoarrow::array::{GeoArrowArray, GeoArrowArrayAccessor, LineStringArray, PolygonBuilder};
8use geoarrow::datatypes::{Dimension, GeoArrowType, Metadata, PolygonType};
9use std::convert::TryFrom;
10use std::sync::Arc;
11
12pub use crate::options::PolygonizerOptions;
13
14pub fn polygonize_arrow(
15 array: &dyn Array,
16 field: &Field,
17 options: PolygonizerOptions,
18) -> Result<geoarrow::array::PolygonArray, PolygonizeError> {
19 let mut polygonizer = Polygonizer::with_options(options);
20 let metadata = match GeoArrowType::from_extension_field(field)
21 .map_err(|e| PolygonizeError::ArrowError(e.to_string()))?
22 {
23 Some(GeoArrowType::LineString(typ)) => {
24 if typ.dimension() != Dimension::XY {
25 return Err(PolygonizeError::UnsupportedOptionCombination {
26 reason: format!(
27 "GeoArrow polygonization currently supports XY coordinates only, got {:?}",
28 typ.dimension()
29 ),
30 });
31 }
32 typ.metadata().clone()
33 }
34 Some(other) => {
35 return Err(PolygonizeError::InvalidArgumentType {
36 field: field.name().to_string(),
37 expected: "geoarrow.linestring".to_string(),
38 actual: format!("{other:?}"),
39 });
40 }
41 None => Arc::new(
42 Metadata::try_from(field).map_err(|e| PolygonizeError::ArrowError(e.to_string()))?,
43 ),
44 };
45
46 if let Some(edges) = metadata.edges() {
47 return Err(PolygonizeError::UnsupportedOptionCombination {
48 reason: format!("GeoArrow polygonization supports planar edges only, got {edges:?}"),
49 });
50 }
51
52 let mut lines = Vec::new();
53
54 match LineStringArray::try_from((array, field)) {
55 Ok(arr) => process_linestring_array(&arr, &mut lines)?,
56 Err(error)
57 if field
58 .extension_type_name()
59 .is_some_and(|name| name != "ogc.geoarrow.linestring") =>
60 {
61 return Err(PolygonizeError::ArrowError(error.to_string()));
62 }
63 Err(_) => match array.data_type() {
64 DataType::List(_) => process_list_array(array.as_list::<i32>(), &mut lines)?,
65 DataType::LargeList(_) => process_list_array(array.as_list::<i64>(), &mut lines)?,
66 _ => {
67 return Err(PolygonizeError::ArrowError(format!(
68 "Failed to convert input array to LineStringArray and fallback failed. DataType: {:?}, Field: {:?}.",
69 array.data_type(),
70 field
71 )));
72 }
73 },
74 }
75
76 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
77 polygonizer.add_lines(lines);
78 polygonizer.polygonize()
79 }))
80 .unwrap_or_else(|_| {
81 Err(PolygonizeError::Panic(
82 "Panic occurred in Rust core".to_string(),
83 ))
84 })
85 .map_err(|e| PolygonizeError::TopologyFailure {
86 reason: format!("Polygonization error: {:?}", e),
87 })?;
88
89 let geo_polygons: Vec<geo::Polygon> = result
90 .polygons
91 .into_iter()
92 .map(|p| {
93 let exterior = geo::LineString::from(
94 p.exterior
95 .into_iter()
96 .map(|c| (c.x, c.y))
97 .collect::<Vec<_>>(),
98 );
99 let interiors = p
100 .interiors
101 .into_iter()
102 .map(|ring: Vec<Coord3D>| {
103 geo::LineString::from(ring.into_iter().map(|c| (c.x, c.y)).collect::<Vec<_>>())
104 })
105 .collect();
106 geo::Polygon::new(exterior, interiors)
107 })
108 .collect();
109
110 let mut builder = PolygonBuilder::new(PolygonType::new(Dimension::XY, metadata));
111 for poly in geo_polygons {
112 builder
113 .push_polygon(Some(&poly))
114 .map_err(|e| PolygonizeError::ArrowError(format!("Failed to push polygon: {}", e)))?;
115 }
116 Ok(builder.finish())
117}
118
119fn process_linestring_array(
120 arr: &LineStringArray,
121 lines: &mut Vec<Line3D>,
122) -> Result<(), PolygonizeError> {
123 for i in 0..arr.len() {
124 if let Ok(Some(geom)) = arr.get(i) {
125 let ls = geom.to_line_string();
126 for line in ls.lines() {
127 if !line.start.x.is_finite()
128 || !line.start.y.is_finite()
129 || !line.end.x.is_finite()
130 || !line.end.y.is_finite()
131 {
132 return Err(PolygonizeError::InvalidGeometry {
133 reason: "NaN or Inf coordinates detected in LineStringArray".to_string(),
134 });
135 }
136 let p1 = Coord3D::new(line.start.x, line.start.y, 0.0);
137 let p2 = Coord3D::new(line.end.x, line.end.y, 0.0);
138 lines.push(Line3D::new(p1, p2, 0));
139 }
140 }
141 }
142 Ok(())
143}
144
145fn process_list_array<O: arrow::array::OffsetSizeTrait>(
147 list_arr: &GenericListArray<O>,
148 lines: &mut Vec<Line3D>,
149) -> Result<(), PolygonizeError> {
150 let values = list_arr.values();
152 let struct_arr = values
153 .as_struct_opt()
154 .ok_or_else(|| PolygonizeError::InvalidBufferShape {
155 reason: "List values must be Struct".to_string(),
156 })?;
157
158 if struct_arr.num_columns() != 2 {
159 return Err(PolygonizeError::UnsupportedOptionCombination {
160 reason: format!(
161 "GeoArrow polygonization currently supports XY coordinates only, got {} coordinate columns",
162 struct_arr.num_columns()
163 ),
164 });
165 }
166
167 let x_arr = struct_arr
171 .column_by_name("x")
172 .or_else(|| {
173 if struct_arr.num_columns() > 0 {
174 Some(struct_arr.column(0))
175 } else {
176 None
177 }
178 })
179 .ok_or_else(|| PolygonizeError::InvalidBufferShape {
180 reason: "Struct missing 'x' column".to_string(),
181 })?;
182
183 let y_arr = struct_arr
184 .column_by_name("y")
185 .or_else(|| {
186 if struct_arr.num_columns() > 1 {
187 Some(struct_arr.column(1))
188 } else {
189 None
190 }
191 })
192 .ok_or_else(|| PolygonizeError::InvalidBufferShape {
193 reason: "Struct missing 'y' column".to_string(),
194 })?;
195
196 let x_vals = x_arr.as_primitive_opt::<Float64Type>().ok_or_else(|| {
197 PolygonizeError::InvalidArgumentType {
198 field: "x".to_string(),
199 expected: "Float64".to_string(),
200 actual: format!("{:?}", x_arr.data_type()),
201 }
202 })?;
203 let y_vals = y_arr.as_primitive_opt::<Float64Type>().ok_or_else(|| {
204 PolygonizeError::InvalidArgumentType {
205 field: "y".to_string(),
206 expected: "Float64".to_string(),
207 actual: format!("{:?}", y_arr.data_type()),
208 }
209 })?;
210
211 for i in 0..list_arr.len() {
212 if list_arr.is_null(i) {
213 continue;
214 }
215 let start = list_arr.value_offsets()[i].to_usize().ok_or_else(|| {
216 PolygonizeError::InvalidBufferShape {
217 reason: "Invalid start offset: cannot convert to usize".to_string(),
218 }
219 })?;
220 let end = list_arr.value_offsets()[i + 1].to_usize().ok_or_else(|| {
221 PolygonizeError::InvalidBufferShape {
222 reason: "Invalid end offset: cannot convert to usize".to_string(),
223 }
224 })?;
225
226 if end <= start {
227 continue;
228 }
229
230 if end > x_vals.len() || end > y_vals.len() {
231 return Err(PolygonizeError::InvalidBufferShape {
232 reason: "Offset out of bounds for x/y coordinate arrays".to_string(),
233 });
234 }
235
236 for j in start..end - 1 {
238 let x1 = x_vals.value(j);
239 let y1 = y_vals.value(j);
240 let x2 = x_vals.value(j + 1);
241 let y2 = y_vals.value(j + 1);
242
243 if !x1.is_finite() || !y1.is_finite() || !x2.is_finite() || !y2.is_finite() {
244 return Err(PolygonizeError::InvalidGeometry {
245 reason: "NaN or Inf coordinates detected in list array".to_string(),
246 });
247 }
248
249 let p1 = Coord3D::new(x1, y1, 0.0);
250 let p2 = Coord3D::new(x2, y2, 0.0);
251 lines.push(Line3D::new(p1, p2, 0));
252 }
253 }
254
255 Ok(())
256}