-
Notifications
You must be signed in to change notification settings - Fork 96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implements Geometry::make_valid
.
#356
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ use std::ptr::null_mut; | |
|
||
use libc::{c_char, c_double, c_int, c_void}; | ||
|
||
use crate::cpl::CslStringList; | ||
use gdal_sys::{self, OGRErr, OGRGeometryH, OGRwkbGeometryType}; | ||
|
||
use crate::errors::*; | ||
|
@@ -224,11 +225,16 @@ impl Geometry { | |
unsafe { gdal_sys::OGR_G_AddPoint_2D(self.c_geometry(), x as c_double, y as c_double) }; | ||
} | ||
|
||
pub fn get_point(&self, i: i32) -> (f64, f64, f64) { | ||
/// Get point coordinates from a line string or a point geometry. | ||
/// | ||
/// `index` is the line string vertex index, from 0 to `point_count()-1`, or `0` when a point. | ||
/// | ||
/// Refer: [`OGR_G_GetPoint`](https://gdal.org/api/vector_c_api.html#_CPPv414OGR_G_GetPoint12OGRGeometryHiPdPdPd) | ||
pub fn get_point(&self, index: i32) -> (f64, f64, f64) { | ||
let mut x: c_double = 0.; | ||
let mut y: c_double = 0.; | ||
let mut z: c_double = 0.; | ||
unsafe { gdal_sys::OGR_G_GetPoint(self.c_geometry(), i, &mut x, &mut y, &mut z) }; | ||
unsafe { gdal_sys::OGR_G_GetPoint(self.c_geometry(), index, &mut x, &mut y, &mut z) }; | ||
(x, y, z) | ||
} | ||
|
||
|
@@ -292,15 +298,50 @@ impl Geometry { | |
Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) | ||
} | ||
|
||
/// Get the geometry type ordinal | ||
/// | ||
/// Refer: [OGR_G_GetGeometryType](https://gdal.org/api/vector_c_api.html#_CPPv421OGR_G_GetGeometryType12OGRGeometryH) | ||
pub fn geometry_type(&self) -> OGRwkbGeometryType::Type { | ||
unsafe { gdal_sys::OGR_G_GetGeometryType(self.c_geometry()) } | ||
} | ||
|
||
/// Get the WKT name for the type of this geometry. | ||
/// | ||
/// Refer: [`OGR_G_GetGeometryName`](https://gdal.org/api/vector_c_api.html#_CPPv421OGR_G_GetGeometryName12OGRGeometryH) | ||
pub fn geometry_name(&self) -> String { | ||
// Note: C API makes no statements about this possibly returning null. | ||
// So we don't have to result wrap this, | ||
let c_str = unsafe { gdal_sys::OGR_G_GetGeometryName(self.c_geometry()) }; | ||
if c_str.is_null() { | ||
"".into() | ||
} else { | ||
_string(c_str) | ||
} | ||
} | ||
|
||
/// Get the number of elements in a geometry, or number of geometries in container. | ||
/// | ||
/// Only geometries of type `wkbPolygon`, `wkbMultiPoint`, `wkbMultiLineString`, `wkbMultiPolygon` | ||
/// or `wkbGeometryCollection` may return a non-zero value. Other geometry types will return 0. | ||
/// | ||
/// For a polygon, the returned number is the number of rings (exterior ring + interior rings). | ||
/// | ||
/// Refer: [`OGR_G_GetGeometryCount`](https://gdal.org/api/vector_c_api.html#_CPPv422OGR_G_GetGeometryCount12OGRGeometryH) | ||
pub fn geometry_count(&self) -> usize { | ||
let cnt = unsafe { gdal_sys::OGR_G_GetGeometryCount(self.c_geometry()) }; | ||
cnt as usize | ||
} | ||
|
||
/// Get the number of points from a Point or a LineString/LinearRing geometry. | ||
/// | ||
/// Only `wkbPoint` or `wkbLineString` may return a non-zero value. Other geometry types will return 0. | ||
/// | ||
/// Refer: [`OGR_G_GetPointCount`](https://gdal.org/api/vector_c_api.html#_CPPv419OGR_G_GetPointCount12OGRGeometryH) | ||
pub fn point_count(&self) -> usize { | ||
let cnt = unsafe { gdal_sys::OGR_G_GetPointCount(self.c_geometry()) }; | ||
cnt as usize | ||
} | ||
|
||
/// Returns the n-th sub-geometry as a non-owned Geometry. | ||
/// | ||
/// # Safety | ||
|
@@ -388,21 +429,18 @@ impl Geometry { | |
unsafe { gdal_sys::OGR_G_Area(self.c_geometry()) } | ||
} | ||
|
||
/// May or may not contain a reference to a SpatialRef: if not, it returns | ||
/// an `Ok(None)`; if it does, it tries to build a SpatialRef. If that | ||
/// succeeds, it returns an Ok(Some(SpatialRef)), otherwise, you get the | ||
/// Err. | ||
/// Get the spatial reference system for this geometry. | ||
/// | ||
/// Returns `Some(SpatialRef)`, or `None` if one isn't defined. | ||
/// | ||
/// Refer: [OGR_G_GetSpatialReference](https://gdal.org/doxygen/ogr__api_8h.html#abc393e40282eec3801fb4a4abc9e25bf) | ||
metasim marked this conversation as resolved.
Show resolved
Hide resolved
|
||
pub fn spatial_ref(&self) -> Option<SpatialRef> { | ||
let c_spatial_ref = unsafe { gdal_sys::OGR_G_GetSpatialReference(self.c_geometry()) }; | ||
|
||
if c_spatial_ref.is_null() { | ||
None | ||
} else { | ||
match unsafe { SpatialRef::from_c_obj(c_spatial_ref) } { | ||
Ok(sr) => Some(sr), | ||
Err(_) => None, | ||
} | ||
unsafe { SpatialRef::from_c_obj(c_spatial_ref) }.ok() | ||
metasim marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
|
@@ -416,6 +454,61 @@ impl Geometry { | |
pub fn to_geo(&self) -> Result<geo_types::Geometry<f64>> { | ||
self.try_into() | ||
} | ||
|
||
/// Attempts to make an invalid geometry valid without losing vertices. | ||
/// | ||
/// Already-valid geometries are cloned without further intervention. | ||
/// | ||
/// Extended options are available via [`CslStringList`] if GDAL is built with GEOS >= 3.8. | ||
/// They are defined as follows: | ||
/// | ||
/// * `METHOD=LINEWORK`: Combines all rings into a set of node-ed lines and then extracts | ||
/// valid polygons from that "linework". | ||
/// * `METHOD=STRUCTURE`: First makes all rings valid, then merges shells and subtracts holes | ||
/// from shells to generate valid result. Assumes holes and shells are correctly categorized. | ||
/// * `KEEP_COLLAPSED=YES/NO`. Only for `METHOD=STRUCTURE`. | ||
/// - `NO` (default): Collapses are converted to empty geometries | ||
/// - `YES`: collapses are converted to a valid geometry of lower dimension | ||
/// | ||
/// When GEOS < 3.8, this method will return `Ok(self.clone())` if it is valid, or `Err` if not. | ||
/// | ||
/// Refer: [OGR_G_MakeValidEx](https://gdal.org/api/vector_c_api.html#_CPPv417OGR_G_MakeValidEx12OGRGeometryH12CSLConstList) | ||
/// | ||
/// # Example | ||
/// ```rust, no_run | ||
/// use gdal::vector::Geometry; | ||
/// # fn main() -> gdal::errors::Result<()> { | ||
/// let src = Geometry::from_wkt("POLYGON ((0 0, 10 10, 0 10, 10 0, 0 0))")?; | ||
/// let dst = src.make_valid(())?; | ||
/// assert_eq!("MULTIPOLYGON (((10 0,0 0,5 5,10 0)),((10 10,5 5,0 10,10 10)))", dst.wkt()?); | ||
/// # Ok(()) | ||
/// # } | ||
/// ``` | ||
pub fn make_valid<O: Into<CslStringList>>(&self, opts: O) -> Result<Geometry> { | ||
let opts = opts.into(); | ||
|
||
fn inner(geom: &Geometry, opts: CslStringList) -> Result<Geometry> { | ||
#[cfg(all(major_ge_3, minor_ge_4))] | ||
let c_geom = unsafe { gdal_sys::OGR_G_MakeValidEx(geom.c_geometry(), opts.as_ptr()) }; | ||
|
||
#[cfg(not(all(major_ge_3, minor_ge_4)))] | ||
let c_geom = { | ||
if !opts.is_empty() { | ||
return Err(GdalError::BadArgument( | ||
"Options to make_valid require GDAL >= 3.4".into(), | ||
)); | ||
} | ||
unsafe { gdal_sys::OGR_G_MakeValid(geom.c_geometry()) } | ||
}; | ||
|
||
if c_geom.is_null() { | ||
Err(_last_null_pointer_err("OGR_G_MakeValid")) | ||
} else { | ||
Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) | ||
} | ||
} | ||
inner(self, opts) | ||
} | ||
} | ||
|
||
impl Drop for Geometry { | ||
|
@@ -482,13 +575,14 @@ impl Debug for GeometryRef<'_> { | |
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use crate::spatial_ref::SpatialRef; | ||
|
||
use super::{geometry_type_to_name, Geometry}; | ||
use crate::test_utils::SuppressGDALErrorLog; | ||
|
||
#[test] | ||
#[allow(clippy::float_cmp)] | ||
pub fn test_area() { | ||
let _nolog = SuppressGDALErrorLog::new(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure we should hide these GDAL warnings, especially given that the tests are run on multiple threads. GDAL doesn't synchronize accesses to that list (not that it makes sense to use it from multiple threads), so there's a chance we'll corrupt its handler stack. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I only put these in in tests that are supposed to generate errors. e.g.
According to the API docs the approach I used should be thread safe.
My reasoning behind this utility was to make life easier when going through test logs when there is a failure. We have a bunch of stay messages that look like something went wrong when the error condition was expected. I really don't like logs that have false error messages in them. IMO, in good Unix style, successes should be quiet. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I missed it being thread-local. Should be fine, then 👍. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without this specific suppression, the log message is
|
||
let geom = Geometry::empty(::gdal_sys::OGRwkbGeometryType::wkbMultiPolygon).unwrap(); | ||
assert_eq!(geom.area(), 0.0); | ||
|
||
|
@@ -593,4 +687,39 @@ mod tests { | |
// We don't care what it returns when passed an invalid value, just that it doesn't crash. | ||
geometry_type_to_name(4372521); | ||
} | ||
|
||
#[test] | ||
/// Simple clone case. | ||
pub fn test_make_valid_clone() { | ||
let src = Geometry::from_wkt("POINT (0 0)").unwrap(); | ||
let dst = src.make_valid(()); | ||
assert!(dst.is_ok()); | ||
} | ||
|
||
#[test] | ||
/// Un-repairable geometry case | ||
pub fn test_make_valid_invalid() { | ||
let _nolog = SuppressGDALErrorLog::new(); | ||
let src = Geometry::from_wkt("LINESTRING (0 0)").unwrap(); | ||
let dst = src.make_valid(()); | ||
assert!(dst.is_err()); | ||
} | ||
|
||
#[test] | ||
/// Repairable case (self-intersecting) | ||
pub fn test_make_valid_repairable() { | ||
let src = Geometry::from_wkt("POLYGON ((0 0,10 10,0 10,10 0,0 0))").unwrap(); | ||
let dst = src.make_valid(()); | ||
assert!(dst.is_ok()); | ||
} | ||
|
||
#[cfg(all(major_ge_3, minor_ge_4))] | ||
#[test] | ||
/// Repairable case, but use extended options | ||
pub fn test_make_valid_ex() { | ||
let src = | ||
Geometry::from_wkt("POLYGON ((0 0,0 10,10 10,10 0,0 0),(5 5,15 10,15 0,5 5))").unwrap(); | ||
let dst = src.make_valid(&[("STRUCTURE", "LINEWORK")]); | ||
assert!(dst.is_ok(), "{dst:?}"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -251,7 +251,8 @@ pub trait LayerAccess: Sized { | |
} | ||
|
||
/// Set a feature on this layer layer. | ||
/// Refer[SetFeature](https://gdal.org/doxygen/classOGRLayer.html#a681139bfd585b74d7218e51a32144283) | ||
/// | ||
/// Refer [SetFeature](https://gdal.org/doxygen/classOGRLayer.html#a681139bfd585b74d7218e51a32144283) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consistency nit: below it says There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like |
||
fn set_feature(&self, feature: Feature) -> Result<()> { | ||
unsafe { gdal_sys::OGR_L_SetFeature(self.c_layer(), feature.c_feature()) }; | ||
Ok(()) | ||
|
@@ -416,11 +417,11 @@ pub trait LayerAccess: Sized { | |
} | ||
} | ||
|
||
/// Fetch the spatial reference system for this layer. | ||
/// Get the spatial reference system for this layer. | ||
/// | ||
/// Returns `Some(SpatialRef)`, or `None` if one isn't defined. | ||
/// | ||
/// Refer [OGR_L_GetSpatialRef](https://gdal.org/doxygen/classOGRLayer.html#a75c06b4993f8eb76b569f37365cd19ab) | ||
/// Refer: [OGR_L_GetSpatialRef](https://gdal.org/doxygen/classOGRLayer.html#a75c06b4993f8eb76b569f37365cd19ab) | ||
fn spatial_ref(&self) -> Option<SpatialRef> { | ||
let c_obj = unsafe { gdal_sys::OGR_L_GetSpatialRef(self.c_layer()) }; | ||
if c_obj.is_null() { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the message would be something like
valid key/value pair: "Invalid characters in name: '#'"
, which doesn't make sense.And going back a little, I'm not sure what to say about this impl.
from
should be infallible, sotry_from
would be better, which this clearly isn't. On the other hand, it's probably going to be used with compile-constant keys and values. Is that why you've implemented it for fixed-size arrays (as opposed to&[(&str, &str)]
)?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. More of a result of me playing type golf; Without the
const
parameter I get this error. Probably me being dumb.The fact that
set_name_value
is fallible is really annoying, but understandable. Going withtry_from
pretty much eliminates the benefits of having this. So if you're in doubt about it, I think I should just remove it all together.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oof, yes, I think you need to cast it, like in https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=0c8e96e6b31a1d7ac28a76cce7abbd1b. Anyway, I suppose it's fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you 100% sure? (I'm not) 😄
Per the docs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you test? 😀 I can't right now, sorry.
My reading of the docs is something like
CslStringList::try_from(&["FOO", "BAR"]).expect("obviously correct")
, that is, user code. It's different in library code which can and will actually panic.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I'm following your line of thinking here. Happy to submit a new PR revisiting this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, this produces:
compared to a random example from
libcore
: