Skip to content
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

Merged
merged 4 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@

- <https://github.com/georust/gdal/pull/355>

- Exposed various functions on `Geometry`: `make_valid`, `geometry_name`, and `point_count`.

- <https://github.com/georust/gdal/pull/356>

## 0.14

- Added new content to `README.md` and the root docs.
Expand Down
56 changes: 54 additions & 2 deletions src/cpl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@ use std::ffi::CString;
use std::fmt::{Debug, Formatter};
use std::ptr;

use gdal_sys::{CSLCount, CSLDestroy, CSLFetchNameValue, CSLSetNameValue};
use gdal_sys::{CSLCount, CSLDestroy, CSLDuplicate, CSLFetchNameValue, CSLSetNameValue};
use libc::c_char;

use crate::errors::{GdalError, Result};
use crate::utils::{_string, _string_tuple};

/// Wraps a [`gdal_sys::CSLConstList`] (a.k.a. `char **papszStrList`)
/// Wraps a [`gdal_sys::CSLConstList`] (a.k.a. `char **papszStrList`). This data structure
/// (a null-terminated array of null-terminated strings) is used throughout GDAL to pass
/// `KEY=VALUE`-formatted options to various functions.
///
/// See the [`CSL*` GDAL functions](https://gdal.org/api/cpl.html#cpl-string-h) for more details.
pub struct CslStringList {
list_ptr: *mut *mut c_char,
}

impl CslStringList {
/// Creates an empty GDAL string list.
pub fn new() -> Self {
Self {
list_ptr: ptr::null_mut(),
Expand Down Expand Up @@ -104,6 +107,14 @@ impl Default for CslStringList {
}
}

impl Clone for CslStringList {
fn clone(&self) -> Self {
let list_ptr = unsafe { CSLDuplicate(self.list_ptr) };
Self { list_ptr }
}
}

/// State for iterator over [`CslStringList`] entries.
pub struct CslStringListIterator<'a> {
list: &'a CslStringList,
idx: usize,
Expand Down Expand Up @@ -155,6 +166,47 @@ impl Debug for CslStringList {
}
}

/// Convenience shorthand for specifying an empty `CslStringList` to functions accepting
/// `Into<CslStringList>`.
///
/// # Example
///
/// ```rust, no_run
/// use gdal::cpl::CslStringList;
/// fn count_opts<O: Into<CslStringList>>(opts: O) -> usize {
/// opts.into().len()
/// }
///
/// assert_eq!(count_opts(()), 0);
/// ```
impl From<()> for CslStringList {
fn from(_: ()) -> Self {
CslStringList::default()
}
}

/// Convenience for creating a [`CslStringList`] from a slice of _key_/_value_ tuples.
///
/// # Example
///
/// ```rust, no_run
/// use gdal::cpl::CslStringList;
/// fn count_opts<O: Into<CslStringList>>(opts: O) -> usize {
/// opts.into().len()
/// }
///
/// assert_eq!(count_opts(&[("One", "1"), ("Two", "2"), ("Three", "3")]), 3);
/// ```
impl<const N: usize> From<&[(&str, &str); N]> for CslStringList {
fn from(pairs: &[(&str, &str); N]) -> Self {
let mut result = Self::default();
for (k, v) in pairs {
result.set_name_value(k, v).expect("valid key/value pair");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
result.set_name_value(k, v).expect("valid key/value pair");
result.set_name_value(k, v).expect("invalid key/value pair");

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, so try_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)])?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that why you've implemented it for fixed-size arrays

No. More of a result of me playing type golf; Without the const parameter I get this error. Probably me being dumb.

image

The fact that set_name_value is fallible is really annoying, but understandable. Going with try_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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Are you 100% sure? (I'm not) 😄

Per the docs

We recommend that expect messages are used to describe the reason you expect the Result should be Ok.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, this produces:

thread 'cpl::tests::from_impl' panicked at 'valid key/value pair: BadArgument("Invalid characters in name: 'TWO#'")', src/cpl.rs:204:41

compared to a random example from libcore:

self.checked_add(rhs).expect("overflow when adding durations")

}
result
}
}

#[cfg(test)]
mod tests {
use crate::cpl::CslStringList;
Expand Down
2 changes: 1 addition & 1 deletion src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ pub trait Metadata: MajorObject {
}
}

/// Standalone metadata entry, as returned by iterator from [`Metadata::metadata_iter`].
/// Standalone metadata entry, as returned by iterator from [`Metadata::metadata`].
metasim marked this conversation as resolved.
Show resolved Hide resolved
///
/// Defined by it's parent `domain`, and `key`/`value` pair.
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
Expand Down
26 changes: 26 additions & 0 deletions src/test_utils.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::ffi::c_void;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};

/// A struct that contains a temporary directory and a path to a file in that directory.
Expand Down Expand Up @@ -49,3 +51,27 @@ pub(crate) fn fixture(filename: &str) -> PathBuf {
.join("fixtures")
.join(filename)
}

/// Scoped value for temporarily suppressing thread-local GDAL log messages.
///
/// Useful for tests that expect GDAL errors and want to keep the output log clean
/// of distracting yet expected error messages.
pub(crate) struct SuppressGDALErrorLog {
// Make !Sync and !Send, and force use of `new`.
_private: PhantomData<*mut c_void>,
}

impl SuppressGDALErrorLog {
pub(crate) fn new() -> Self {
unsafe { gdal_sys::CPLPushErrorHandler(Some(gdal_sys::CPLQuietErrorHandler)) };
SuppressGDALErrorLog {
_private: PhantomData,
}
}
}

impl Drop for SuppressGDALErrorLog {
fn drop(&mut self) {
unsafe { gdal_sys::CPLPopErrorHandler() };
}
}
153 changes: 141 additions & 12 deletions src/vector/geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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();
Copy link
Member

@lnicola lnicola Jan 10, 2023

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

@metasim metasim Jan 10, 2023

Choose a reason for hiding this comment

The 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.

layer.set_attribute_filter("foo = bar").unwrap_err(),

According to the API docs the approach I used should be thread safe.

This pushes a new error handler on the thread-local error handler stack. This handler will be used until removed with CPLPopErrorHandler().

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I missed it being thread-local. Should be fine, then 👍.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this specific suppression, the log message is

Warning 1: OGR_G_Area() called against non-surface geometry type.

let geom = Geometry::empty(::gdal_sys::OGRwkbGeometryType::wkbMultiPolygon).unwrap();
assert_eq!(geom.area(), 0.0);

Expand Down Expand Up @@ -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:?}");
}
}
7 changes: 4 additions & 3 deletions src/vector/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistency nit: below it says Refer: , maybe See would sound better.

Copy link
Contributor Author

@metasim metasim Jan 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like Refer either... but was trying to be consistent (but forgot the colon here). I'll update.

fn set_feature(&self, feature: Feature) -> Result<()> {
unsafe { gdal_sys::OGR_L_SetFeature(self.c_layer(), feature.c_feature()) };
Ok(())
Expand Down Expand Up @@ -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() {
Expand Down
Loading