From 0ab98e0941cbff2c4ce495a709e1bf1c73f62f66 Mon Sep 17 00:00:00 2001 From: "Simeon H.K. Fitch" Date: Sat, 11 Feb 2023 13:21:02 -0500 Subject: [PATCH] Refactoring to improve maintainability of `vector` module. Changes include: * Grouping methods of similar purpose into separate files * Putting associated tests in the same file as the implementation * Adding or expanding documentation on existing methods * Normalizing documentation patterns and format. --- src/dataset.rs | 34 +- src/metadata.rs | 3 +- src/raster/mdarray.rs | 11 +- src/raster/rasterband.rs | 6 +- src/spatial_ref/srs.rs | 14 +- src/vector/geometry.rs | 372 +------- src/vector/layer.rs | 761 +++++++++++++++ src/vector/mod.rs | 13 +- src/vector/ops/conversions/formats.rs | 121 +++ .../{ => ops/conversions}/gdal_to_geo.rs | 0 .../{ => ops/conversions}/geo_to_gdal.rs | 0 src/vector/ops/conversions/mod.rs | 187 ++++ src/vector/ops/geometry/mod.rs | 1 - src/vector/ops/mod.rs | 6 +- src/vector/ops/predicates.rs | 4 +- .../ops/{geometry/intersection.rs => set.rs} | 29 +- src/vector/ops/transformations.rs | 313 +++++++ src/vector/sql.rs | 139 +++ src/vector/vector_tests/convert_geo.rs | 165 ---- src/vector/vector_tests/mod.rs | 867 ------------------ src/vector/vector_tests/sql.rs | 135 --- 21 files changed, 1615 insertions(+), 1566 deletions(-) create mode 100644 src/vector/ops/conversions/formats.rs rename src/vector/{ => ops/conversions}/gdal_to_geo.rs (100%) rename src/vector/{ => ops/conversions}/geo_to_gdal.rs (100%) create mode 100644 src/vector/ops/conversions/mod.rs delete mode 100644 src/vector/ops/geometry/mod.rs rename src/vector/ops/{geometry/intersection.rs => set.rs} (79%) create mode 100644 src/vector/ops/transformations.rs delete mode 100644 src/vector/vector_tests/convert_geo.rs delete mode 100644 src/vector/vector_tests/mod.rs delete mode 100644 src/vector/vector_tests/sql.rs diff --git a/src/dataset.rs b/src/dataset.rs index 2b96fa57c..ea6f478cb 100644 --- a/src/dataset.rs +++ b/src/dataset.rs @@ -702,19 +702,19 @@ impl Dataset { Ok(self.child_layer(c_layer)) } - /// Affine transformation called geotransformation. + /// Set the [`Dataset`]'s affine transformation; also called a _geo-transformation_. /// /// This is like a linear transformation preserves points, straight lines and planes. /// Also, sets of parallel lines remain parallel after an affine transformation. - /// # Arguments - /// * transformation - coeficients of transformations /// - /// x-coordinate of the top-left corner pixel (x-offset) - /// width of a pixel (x-resolution) - /// row rotation (typically zero) - /// y-coordinate of the top-left corner pixel - /// column rotation (typically zero) - /// height of a pixel (y-resolution, typically negative) + /// # Arguments + /// * `transformation` - coefficients of the transformation, which are: + /// - x-coordinate of the top-left corner pixel (x-offset) + /// - width of a pixel (x-resolution) + /// - row rotation (typically zero) + /// - y-coordinate of the top-left corner pixel + /// - column rotation (typically zero) + /// - height of a pixel (y-resolution, typically negative) pub fn set_geo_transform(&mut self, transformation: &GeoTransform) -> Result<()> { assert_eq!(transformation.len(), 6); let rv = unsafe { @@ -726,14 +726,15 @@ impl Dataset { Ok(()) } - /// Get affine transformation coefficients. + /// Get the coefficients of the [`Dataset`]'s affine transformation. /// - /// x-coordinate of the top-left corner pixel (x-offset) - /// width of a pixel (x-resolution) - /// row rotation (typically zero) - /// y-coordinate of the top-left corner pixel - /// column rotation (typically zero) - /// height of a pixel (y-resolution, typically negative) + /// # Returns + /// - x-coordinate of the top-left corner pixel (x-offset) + /// - width of a pixel (x-resolution) + /// - row rotation (typically zero) + /// - y-coordinate of the top-left corner pixel + /// - column rotation (typically zero) + /// - height of a pixel (y-resolution, typically negative) pub fn geo_transform(&self) -> Result { let mut transformation = GeoTransform::default(); let rv = @@ -833,7 +834,6 @@ impl Dataset { /// `None`, which is distinct from an empty [`sql::ResultSet`]. /// /// # Arguments - /// /// * `query`: The SQL query /// * `spatial_filter`: Limit results of the query to features that intersect the given /// [`Geometry`] diff --git a/src/metadata.rs b/src/metadata.rs index 92d9eb60e..96a15aed9 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -100,8 +100,7 @@ pub trait Metadata: MajorObject { /// Entries in the returned `Vec` are formatted as "Name=value" pairs /// /// # Arguments - /// - /// * domain – the domain of interest. Use `""` for the default domain. + /// * `domain` – the domain of interest. Use `""` for the default domain. /// /// # Example /// diff --git a/src/raster/mdarray.rs b/src/raster/mdarray.rs index 3e7184b96..18d074857 100644 --- a/src/raster/mdarray.rs +++ b/src/raster/mdarray.rs @@ -233,11 +233,12 @@ impl<'a> MDArray<'a> { /// Read a 'Array2' from this band. T implements 'GdalType'. /// /// # Arguments - /// * window - the window position from top left - /// * window_size - the window size (GDAL will interpolate data if window_size != array_size) - /// * array_size - the desired size of the 'Array' - /// * e_resample_alg - the resample algorithm used for the interpolation - /// # Docs + /// * `window` - the window position from top left + /// * `window_size` - the window size (GDAL will interpolate data if window_size != array_size) + /// * `array_size` - the desired size of the 'Array' + /// * `e_resample_alg` - the resample algorithm used for the interpolation + /// + /// # Notes /// The Matrix shape is (rows, cols) and raster shape is (cols in x-axis, rows in y-axis). pub fn read_as_array( &self, diff --git a/src/raster/rasterband.rs b/src/raster/rasterband.rs index 5cab93255..3690e4aa4 100644 --- a/src/raster/rasterband.rs +++ b/src/raster/rasterband.rs @@ -225,7 +225,6 @@ impl<'a> RasterBand<'a> { /// Read data from this band into a slice, where `T` implements [`GdalType`] /// /// # Arguments - /// /// * `window` - the window position from top left /// * `window_size` - the window size (GDAL will interpolate data if window_size != buffer_size) /// * `size` - the desired size to read @@ -296,7 +295,6 @@ impl<'a> RasterBand<'a> { /// Read a [`Buffer`] from this band, where `T` implements [`GdalType`]. /// /// # Arguments - /// /// * `window` - the window position from top left /// * `window_size` - the window size (GDAL will interpolate data if `window_size` != `buffer_size`) /// * `buffer_size` - the desired size of the 'Buffer' @@ -376,7 +374,6 @@ impl<'a> RasterBand<'a> { /// Read a [`Array2`] from this band, where `T` implements [`GdalType`]. /// /// # Arguments - /// /// * `window` - the window position from top left /// * `window_size` - the window size (GDAL will interpolate data if window_size != array_size) /// * `array_size` - the desired size of the 'Array' @@ -413,7 +410,7 @@ impl<'a> RasterBand<'a> { /// # Arguments /// * `block_index` - the block index /// - /// # Note + /// # Notes /// The Matrix shape is (rows, cols) and raster shape is (cols in x-axis, rows in y-axis). pub fn read_block(&self, block_index: (usize, usize)) -> Result> { let size = self.block_size(); @@ -443,7 +440,6 @@ impl<'a> RasterBand<'a> { /// Write a [`Buffer`] into a [`Dataset`]. /// /// # Arguments - /// /// * `window` - the window position from top left /// * `window_size` - the window size (GDAL will interpolate data if window_size != Buffer.size) /// * `buffer` - the data to write into the window diff --git a/src/spatial_ref/srs.rs b/src/spatial_ref/srs.rs index 725d02c61..10e65df21 100644 --- a/src/spatial_ref/srs.rs +++ b/src/spatial_ref/srs.rs @@ -37,14 +37,14 @@ impl CoordTransform { /// transformations. /// /// # Arguments - /// * bounds - array of [axis0_min, axis1_min, axis0_max, axis1_min], + /// * `bounds` - array of [axis0_min, axis1_min, axis0_max, axis1_min], /// interpreted in the axis order of the source SpatialRef, /// typically [xmin, ymin, xmax, ymax] - /// * densify_pts - number of points per edge (recommended: 21) + /// * `densify_pts` - number of points per edge (recommended: 21) /// /// # Returns - /// Some([f64; 4]) with bounds in axis order of target SpatialRef - /// None if there is an error. + /// `Ok([f64; 4])` with bounds in axis order of target SpatialRef + /// `Err` if there is an error. #[cfg(all(major_ge_3, minor_ge_4))] pub fn transform_bounds(&self, bounds: &[f64; 4], densify_pts: i32) -> Result<[f64; 4]> { let mut out_xmin: f64 = 0.; @@ -88,9 +88,9 @@ impl CoordTransform { /// Transform coordinates in place. /// /// # Arguments - /// * x - slice of x coordinates - /// * y - slice of y coordinates (must match x in length) - /// * z - slice of z coordinates, or an empty slice to ignore + /// * `x` - slice of x coordinates + /// * `y` - slice of y coordinates (must match x in length) + /// * `z` - slice of z coordinates, or an empty slice to ignore pub fn transform_coords(&self, x: &mut [f64], y: &mut [f64], z: &mut [f64]) -> Result<()> { let nb_coords = x.len(); assert_eq!( diff --git a/src/vector/geometry.rs b/src/vector/geometry.rs index 9f95ff101..26fbb34bc 100644 --- a/src/vector/geometry.rs +++ b/src/vector/geometry.rs @@ -1,18 +1,15 @@ use std::cell::RefCell; -use std::ffi::CString; use std::fmt::{self, Debug, Formatter}; use std::marker::PhantomData; use std::mem::MaybeUninit; use std::ops::Deref; -use std::ptr::null_mut; -use libc::{c_char, c_double, c_int, c_void}; +use libc::{c_double, c_int}; -use crate::cpl::CslStringList; use gdal_sys::{self, OGRErr, OGRGeometryH, OGRwkbGeometryType}; use crate::errors::*; -use crate::spatial_ref::{CoordTransform, SpatialRef}; +use crate::spatial_ref::SpatialRef; use crate::utils::{_last_null_pointer_err, _string}; use crate::vector::{Envelope, Envelope3D}; @@ -78,46 +75,6 @@ impl Geometry { unsafe { gdal_sys::OGR_G_IsEmpty(self.c_geometry()) == 1 } } - /// Create a geometry by parsing a - /// [WKT](https://en.wikipedia.org/wiki/Well-known_text) string. - pub fn from_wkt(wkt: &str) -> Result { - let c_wkt = CString::new(wkt)?; - // OGR_G_CreateFromWkt does not write to the pointed-to memory, but this is not reflected - // in its signature (`char**` instead of `char const**`), so we need a scary looking cast. - let mut c_wkt_ptr = c_wkt.as_ptr() as *mut c_char; - let mut c_geom = null_mut(); - let rv = unsafe { gdal_sys::OGR_G_CreateFromWkt(&mut c_wkt_ptr, null_mut(), &mut c_geom) }; - if rv != OGRErr::OGRERR_NONE { - return Err(GdalError::OgrError { - err: rv, - method_name: "OGR_G_CreateFromWkt", - }); - } - Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) - } - - /// Creates a geometry by parsing a slice of bytes in - /// [WKB](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Well-known_binary) - /// (Well-Known Binary) format. - pub fn from_wkb(wkb: &[u8]) -> Result { - let mut c_geom = null_mut(); - let rv = unsafe { - gdal_sys::OGR_G_CreateFromWkb( - wkb.as_ptr() as *const std::ffi::c_void, - null_mut(), - &mut c_geom, - wkb.len() as i32, - ) - }; - if rv != gdal_sys::OGRErr::OGRERR_NONE { - return Err(GdalError::OgrError { - err: rv, - method_name: "OGR_G_CreateFromWkb", - }); - } - Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) - } - /// Create a rectangular geometry from West, South, East and North values. pub fn bbox(w: f64, s: f64, e: f64, n: f64) -> Result { Geometry::from_wkt(&format!( @@ -125,52 +82,6 @@ impl Geometry { )) } - /// Serialize the geometry as JSON. - pub fn json(&self) -> Result { - let c_json = unsafe { gdal_sys::OGR_G_ExportToJson(self.c_geometry()) }; - if c_json.is_null() { - return Err(_last_null_pointer_err("OGR_G_ExportToJson")); - }; - let rv = _string(c_json); - unsafe { gdal_sys::VSIFree(c_json as *mut c_void) }; - Ok(rv) - } - - /// Serialize the geometry as WKT. - pub fn wkt(&self) -> Result { - let mut c_wkt = null_mut(); - let rv = unsafe { gdal_sys::OGR_G_ExportToWkt(self.c_geometry(), &mut c_wkt) }; - if rv != OGRErr::OGRERR_NONE { - return Err(GdalError::OgrError { - err: rv, - method_name: "OGR_G_ExportToWkt", - }); - } - let wkt = _string(c_wkt); - unsafe { gdal_sys::OGRFree(c_wkt as *mut c_void) }; - Ok(wkt) - } - - /// Serializes the geometry to - /// [WKB](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Well-known_binary) - /// (Well-Known Binary) format. - pub fn wkb(&self) -> Result> { - let wkb_size = unsafe { gdal_sys::OGR_G_WkbSize(self.c_geometry()) as usize }; - // We default to little-endian for now. A WKB string explicitly indicates the byte - // order, so this is not a problem for interoperability. - let byte_order = gdal_sys::OGRwkbByteOrder::wkbNDR; - let mut wkb = vec![0; wkb_size]; - let rv = - unsafe { gdal_sys::OGR_G_ExportToWkb(self.c_geometry(), byte_order, wkb.as_mut_ptr()) }; - if rv != gdal_sys::OGRErr::OGRERR_NONE { - return Err(GdalError::OgrError { - err: rv, - method_name: "OGR_G_ExportToWkb", - }); - } - Ok(wkb) - } - /// Returns a C pointer to the wrapped Geometry /// /// # Safety @@ -244,61 +155,6 @@ impl Geometry { (0..length).map(|i| self.get_point(i)).collect() } - /// Compute the convex hull of this geometry. - pub fn convex_hull(&self) -> Result { - let c_geom = unsafe { gdal_sys::OGR_G_ConvexHull(self.c_geometry()) }; - if c_geom.is_null() { - return Err(_last_null_pointer_err("OGR_G_ConvexHull")); - }; - Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) - } - - #[cfg(any(all(major_is_2, minor_ge_1), major_ge_3))] - pub fn delaunay_triangulation(&self, tolerance: Option) -> Result { - let c_geom = unsafe { - gdal_sys::OGR_G_DelaunayTriangulation(self.c_geometry(), tolerance.unwrap_or(0.0), 0) - }; - if c_geom.is_null() { - return Err(_last_null_pointer_err("OGR_G_DelaunayTriangulation")); - }; - - Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) - } - - pub fn simplify(&self, tolerance: f64) -> Result { - let c_geom = unsafe { gdal_sys::OGR_G_Simplify(self.c_geometry(), tolerance) }; - if c_geom.is_null() { - return Err(_last_null_pointer_err("OGR_G_Simplify")); - }; - - Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) - } - - /// Compute buffer of geometry - /// - /// The `distance` parameter the buffer distance to be applied. Should be expressed into - /// the same unit as the coordinates of the geometry. `n_quad_segs` specifies the number - /// of segments used to approximate a 90 degree (quadrant) of curvature. - pub fn buffer(&self, distance: f64, n_quad_segs: u32) -> Result { - let c_geom = - unsafe { gdal_sys::OGR_G_Buffer(self.c_geometry(), distance, n_quad_segs as i32) }; - if c_geom.is_null() { - return Err(_last_null_pointer_err("OGR_G_Buffer")); - }; - - Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) - } - - pub fn simplify_preserve_topology(&self, tolerance: f64) -> Result { - let c_geom = - unsafe { gdal_sys::OGR_G_SimplifyPreserveTopology(self.c_geometry(), tolerance) }; - if c_geom.is_null() { - return Err(_last_null_pointer_err("OGR_G_SimplifyPreserveTopology")); - }; - - Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) - } - /// Get the geometry type ordinal /// /// See: [OGR_G_GetGeometryType](https://gdal.org/api/vector_c_api.html#_CPPv421OGR_G_GetGeometryType12OGRGeometryH) @@ -377,55 +233,6 @@ impl Geometry { Ok(()) } - /// Transform the geometry inplace (when we own the Geometry) - pub fn transform_inplace(&mut self, htransform: &CoordTransform) -> Result<()> { - let rv = unsafe { gdal_sys::OGR_G_Transform(self.c_geometry(), htransform.to_c_hct()) }; - if rv != OGRErr::OGRERR_NONE { - return Err(GdalError::OgrError { - err: rv, - method_name: "OGR_G_Transform", - }); - } - Ok(()) - } - - /// Return a new transformed geometry (when the Geometry is owned by a Feature) - pub fn transform(&self, htransform: &CoordTransform) -> Result { - let new_c_geom = unsafe { gdal_sys::OGR_G_Clone(self.c_geometry()) }; - let rv = unsafe { gdal_sys::OGR_G_Transform(new_c_geom, htransform.to_c_hct()) }; - if rv != OGRErr::OGRERR_NONE { - return Err(GdalError::OgrError { - err: rv, - method_name: "OGR_G_Transform", - }); - } - Ok(unsafe { Geometry::with_c_geometry(new_c_geom, true) }) - } - - pub fn transform_to_inplace(&mut self, spatial_ref: &SpatialRef) -> Result<()> { - let rv = unsafe { gdal_sys::OGR_G_TransformTo(self.c_geometry(), spatial_ref.to_c_hsrs()) }; - if rv != OGRErr::OGRERR_NONE { - return Err(GdalError::OgrError { - err: rv, - method_name: "OGR_G_TransformTo", - }); - } - Ok(()) - } - - /// Transforms a geometry's coordinates into another SpatialRef - pub fn transform_to(&self, spatial_ref: &SpatialRef) -> Result { - let new_c_geom = unsafe { gdal_sys::OGR_G_Clone(self.c_geometry()) }; - let rv = unsafe { gdal_sys::OGR_G_TransformTo(new_c_geom, spatial_ref.to_c_hsrs()) }; - if rv != OGRErr::OGRERR_NONE { - return Err(GdalError::OgrError { - err: rv, - method_name: "OGR_G_TransformTo", - }); - } - Ok(unsafe { Geometry::with_c_geometry(new_c_geom, true) }) - } - /// Compute geometry area in square units of the spatial reference system in use. /// /// Supported for `LinearRing`, `Polygon` or `MultiPolygon`. @@ -479,68 +286,14 @@ impl Geometry { }; } - /// Create a copy of self as a `geo-types` geometry. - pub fn to_geo(&self) -> Result> { - 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. - /// - /// See: [OGR_G_MakeValidEx](https://gdal.org/api/vector_c_api.html#_CPPv417OGR_G_MakeValidEx12OGRGeometryH12CSLConstList) - /// - /// # Example - /// ```rust, no_run - /// use gdal::cpl::CslStringList; - /// 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(&CslStringList::new())?; - /// 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(&self, opts: &CslStringList) -> Result { - #[cfg(all(major_ge_3, minor_ge_4))] - let c_geom = unsafe { gdal_sys::OGR_G_MakeValidEx(self.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(self.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) }) - } - } - /// Test if the geometry is valid. /// + /// # Notes /// This function requires the GEOS library. /// If OGR is built without the GEOS library, this function will always return `false`. /// Check with [`VersionInfo::has_geos`][has_geos]. /// + /// See: [`Self::make_valid`] /// See: [`OGR_G_IsValid`](https://gdal.org/api/vector_c_api.html#_CPPv413OGR_G_IsValid12OGRGeometryH) /// /// [has_geos]: crate::version::VersionInfo::has_geos @@ -615,9 +368,15 @@ impl Debug for GeometryRef<'_> { #[cfg(test)] mod tests { use super::*; - use crate::assert_almost_eq; use crate::spatial_ref::SpatialRef; use crate::test_utils::SuppressGDALErrorLog; + use gdal_sys::OGRwkbGeometryType::{wkbLineString, wkbLinearRing, wkbPolygon}; + + #[test] + fn test_create_bbox() { + let bbox = Geometry::bbox(-27., 33., 52., 85.).unwrap(); + assert_eq!(bbox.json().unwrap(), "{ \"type\": \"Polygon\", \"coordinates\": [ [ [ -27.0, 85.0 ], [ 52.0, 85.0 ], [ 52.0, 33.0 ], [ -27.0, 33.0 ], [ -27.0, 85.0 ] ] ] }"); + } #[test] #[allow(clippy::float_cmp)] @@ -699,23 +458,40 @@ mod tests { } #[test] - pub fn test_wkb() { - let wkt = "POLYGON ((45.0 45.0, 45.0 50.0, 50.0 50.0, 50.0 45.0, 45.0 45.0))"; - let orig_geom = Geometry::from_wkt(wkt).unwrap(); - let wkb = orig_geom.wkb().unwrap(); - let new_geom = Geometry::from_wkb(&wkb).unwrap(); - assert_eq!(new_geom, orig_geom); + fn test_ring_points() { + let mut ring = Geometry::empty(wkbLinearRing).unwrap(); + ring.add_point_2d((1179091.1646903288, 712782.8838459781)); + ring.add_point_2d((1161053.0218226474, 667456.2684348812)); + ring.add_point_2d((1214704.933941905, 641092.8288590391)); + ring.add_point_2d((1228580.428455506, 682719.3123998424)); + ring.add_point_2d((1218405.0658121984, 721108.1805541387)); + ring.add_point_2d((1179091.1646903288, 712782.8838459781)); + assert!(!ring.is_empty()); + assert_eq!(ring.get_point_vec().len(), 6); + let mut poly = Geometry::empty(wkbPolygon).unwrap(); + poly.add_geometry(ring.to_owned()).unwrap(); + // Points are in ring, not containing geometry. + // NB: In Python SWIG bindings, `GetPoints` is fallible. + assert!(poly.get_point_vec().is_empty()); + assert_eq!(poly.geometry_count(), 1); + let ring_out = poly.get_geometry(0); + // NB: `wkb()` shows it to be a `LINEARRING`, but returned type is LineString + assert_eq!(ring_out.geometry_type(), wkbLineString); + assert!(!&ring_out.is_empty()); + assert_eq!(ring.get_point_vec(), ring_out.get_point_vec()); } #[test] - pub fn test_buffer() { - let geom = Geometry::from_wkt("POINT(0 0)").unwrap(); - let buffered = geom.buffer(10.0, 2).unwrap(); - assert_eq!( - buffered.geometry_type(), - ::gdal_sys::OGRwkbGeometryType::wkbPolygon - ); - assert!(buffered.area() > 10.0); + fn test_get_inner_points() { + let geom = Geometry::bbox(0., 0., 1., 1.).unwrap(); + assert!(!geom.is_empty()); + assert_eq!(geom.geometry_count(), 1); + assert!(geom.area() > 0.); + assert_eq!(geom.geometry_type(), OGRwkbGeometryType::wkbPolygon); + assert!(geom.json().unwrap().contains("Polygon")); + let inner = geom.get_geometry(0); + let points = inner.get_point_vec(); + assert!(!points.is_empty()); } #[test] @@ -727,68 +503,4 @@ 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(&CslStringList::new()); - assert!(dst.is_ok()); - assert!(dst.unwrap().is_valid()); - } - - #[test] - /// Un-repairable geometry case - pub fn test_make_valid_invalid() { - let _nolog = SuppressGDALErrorLog::new(); - let src = Geometry::from_wkt("LINESTRING (0 0)").unwrap(); - assert!(!src.is_valid()); - let dst = src.make_valid(&CslStringList::new()); - 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(); - assert!(!src.is_valid()); - let dst = src.make_valid(&CslStringList::new()); - assert!(dst.is_ok()); - assert!(dst.unwrap().is_valid()); - } - - #[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 opts = CslStringList::try_from(&[("STRUCTURE", "LINEWORK")]).unwrap(); - let dst = src.make_valid(&opts); - assert!(dst.is_ok(), "{dst:?}"); - assert!(dst.unwrap().is_valid()); - } - - #[test] - fn test_envelope() { - let geom = Geometry::from_wkt("MULTIPOINT((1.0 2.0), (2.0 4.0))").unwrap(); - let envelope = geom.envelope(); - assert_almost_eq(envelope.MinX, 1.0); - assert_almost_eq(envelope.MaxX, 2.0); - assert_almost_eq(envelope.MinY, 2.0); - assert_almost_eq(envelope.MaxY, 4.0); - } - - #[test] - fn test_envelope3d() { - let geom = Geometry::from_wkt("MULTIPOINT((1.0 2.0 3.0), (2.0 4.0 5.0))").unwrap(); - let envelope = geom.envelope_3d(); - assert_almost_eq(envelope.MinX, 1.0); - assert_almost_eq(envelope.MaxX, 2.0); - assert_almost_eq(envelope.MinY, 2.0); - assert_almost_eq(envelope.MaxY, 4.0); - assert_almost_eq(envelope.MinZ, 3.0); - assert_almost_eq(envelope.MaxZ, 5.0); - } } diff --git a/src/vector/layer.rs b/src/vector/layer.rs index bdcbe24fc..4b1e26ff3 100644 --- a/src/vector/layer.rs +++ b/src/vector/layer.rs @@ -640,3 +640,764 @@ impl FieldDefn { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::{LayerCaps::*, *}; + use crate::test_utils::{fixture, SuppressGDALErrorLog, TempFixture}; + use crate::{assert_almost_eq, Dataset, DatasetOptions, DriverManager, GdalOpenFlags}; + use gdal_sys::OGRwkbGeometryType; + + fn ds_with_layer(ds_name: &str, layer_name: &str, f: F) + where + F: Fn(Layer), + { + let ds = Dataset::open(fixture(ds_name)).unwrap(); + let layer = ds.layer_by_name(layer_name).unwrap(); + f(layer); + } + + fn with_layer(name: &str, f: F) + where + F: Fn(Layer), + { + let ds = Dataset::open(fixture(name)).unwrap(); + let layer = ds.layer(0).unwrap(); + f(layer); + } + + fn with_owned_layer(name: &str, f: F) + where + F: Fn(OwnedLayer), + { + let ds = Dataset::open(fixture(name)).unwrap(); + let layer = ds.into_layer(0).unwrap(); + f(layer); + } + + fn with_features(name: &str, f: F) + where + F: Fn(FeatureIterator), + { + with_layer(name, |mut layer| f(layer.features())); + } + + fn with_feature(name: &str, fid: u64, f: F) + where + F: Fn(Feature), + { + with_layer(name, |layer| f(layer.feature(fid).unwrap())); + } + + #[test] + fn test_layer_count() { + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + assert_eq!(ds.layer_count(), 1); + } + + #[test] + fn test_many_layer_count() { + let ds = Dataset::open(fixture("three_layer_ds.s3db")).unwrap(); + assert_eq!(ds.layer_count(), 3); + } + + #[test] + fn test_layer_get_extent() { + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + let layer = ds.layer(0).unwrap(); + let extent = layer.get_extent().unwrap(); + assert_almost_eq(extent.MinX, 26.100768); + assert_almost_eq(extent.MaxX, 26.103515); + assert_almost_eq(extent.MinY, 44.429858); + assert_almost_eq(extent.MaxY, 44.431818); + } + + #[test] + fn test_layer_try_get_extent() { + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + let layer = ds.layer(0).unwrap(); + assert!(layer.try_get_extent().unwrap().is_none()); + } + + #[test] + fn test_layer_spatial_ref() { + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + let layer = ds.layer(0).unwrap(); + let srs = layer.spatial_ref().unwrap(); + assert_eq!(srs.auth_code().unwrap(), 4326); + } + + #[test] + fn test_layer_capabilities() { + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + let layer = ds.layer(0).unwrap(); + + assert!(!layer.has_capability(OLCFastSpatialFilter)); + assert!(layer.has_capability(OLCFastFeatureCount)); + assert!(!layer.has_capability(OLCFastGetExtent)); + assert!(layer.has_capability(OLCRandomRead)); + assert!(layer.has_capability(OLCStringsAsUTF8)); + } + + #[test] + fn test_feature_count() { + with_layer("roads.geojson", |layer| { + assert_eq!(layer.feature_count(), 21); + }); + } + + #[test] + fn test_many_feature_count() { + ds_with_layer("three_layer_ds.s3db", "layer_0", |layer| { + assert_eq!(layer.feature_count(), 3); + }); + } + + #[test] + fn test_try_feature_count() { + with_layer("roads.geojson", |layer| { + assert_eq!(layer.try_feature_count(), Some(21)); + }); + } + + #[test] + fn test_feature() { + with_layer("roads.geojson", |layer| { + assert!(layer.feature(236194095).is_some()); + assert!(layer.feature(23489660).is_some()); + assert!(layer.feature(0).is_none()); + assert!(layer.feature(404).is_none()); + }); + } + + #[test] + fn test_iterate_features() { + with_features("roads.geojson", |features| { + assert_eq!(features.size_hint(), (21, Some(21))); + assert_eq!(features.count(), 21); + }); + } + + #[test] + fn test_iterate_layers() { + let ds = Dataset::open(fixture("three_layer_ds.s3db")).unwrap(); + let layers = ds.layers(); + assert_eq!(layers.size_hint(), (3, Some(3))); + assert_eq!(layers.count(), 3); + } + + #[test] + fn test_owned_layers() { + let ds = Dataset::open(fixture("three_layer_ds.s3db")).unwrap(); + + assert_eq!(ds.layer_count(), 3); + + let mut layer = ds.into_layer(0).unwrap(); + + { + let feature = layer.features().next().unwrap(); + assert_eq!(feature.field("id").unwrap(), None); + } + + // convert back to dataset + + let ds = layer.into_dataset(); + assert_eq!(ds.layer_count(), 3); + } + + #[test] + fn test_iterate_owned_features() { + with_owned_layer("roads.geojson", |layer| { + let mut features = layer.owned_features(); + + assert_eq!(features.as_mut().size_hint(), (21, Some(21))); + assert_eq!(features.count(), 21); + + // get back layer + + let layer = features.into_layer(); + assert_eq!(layer.name(), "roads"); + }); + } + + #[test] + fn test_fid() { + with_feature("roads.geojson", 236194095, |feature| { + assert_eq!(feature.fid(), Some(236194095)); + }); + } + + #[test] + fn test_string_field() { + with_feature("roads.geojson", 236194095, |feature| { + assert_eq!( + feature.field("highway").unwrap().unwrap().into_string(), + Some("footway".to_string()) + ); + }); + with_features("roads.geojson", |features| { + assert_eq!( + features + .filter(|field| { + let highway = field.field("highway").unwrap().unwrap().into_string(); + highway == Some("residential".to_string()) + }) + .count(), + 2 + ); + }); + } + + #[test] + fn test_null_field() { + with_features("null_feature_fields.geojson", |mut features| { + let feature = features.next().unwrap(); + assert_eq!( + feature.field("some_int").unwrap(), + Some(FieldValue::IntegerValue(0)) + ); + assert_eq!(feature.field("some_string").unwrap(), None); + }); + } + + #[test] + fn test_string_list_field() { + with_features("soundg.json", |mut features| { + let feature = features.next().unwrap(); + assert_eq!( + feature.field("a_string_list").unwrap().unwrap(), + FieldValue::StringListValue(vec![ + String::from("a"), + String::from("list"), + String::from("of"), + String::from("strings") + ]) + ); + }); + } + + #[test] + fn test_set_string_list_field() { + with_features("soundg.json", |mut features| { + let feature = features.next().unwrap(); + let value = FieldValue::StringListValue(vec![ + String::from("the"), + String::from("new"), + String::from("strings"), + ]); + feature.set_field("a_string_list", &value).unwrap(); + assert_eq!(feature.field("a_string_list").unwrap().unwrap(), value); + }); + } + + #[test] + #[allow(clippy::float_cmp)] + fn test_get_field_as_x_by_name() { + with_features("roads.geojson", |mut features| { + let feature = features.next().unwrap(); + + assert_eq!( + feature.field_as_string_by_name("highway").unwrap(), + Some("footway".to_owned()) + ); + + assert_eq!( + feature.field_as_string_by_name("sort_key").unwrap(), + Some("-9".to_owned()) + ); + assert_eq!( + feature.field_as_integer_by_name("sort_key").unwrap(), + Some(-9) + ); + assert_eq!( + feature.field_as_integer64_by_name("sort_key").unwrap(), + Some(-9) + ); + assert_eq!( + feature.field_as_double_by_name("sort_key").unwrap(), + Some(-9.) + ); + + // test failed conversions + assert_eq!( + feature.field_as_integer_by_name("highway").unwrap(), + Some(0) + ); + assert_eq!( + feature.field_as_integer64_by_name("highway").unwrap(), + Some(0) + ); + assert_eq!( + feature.field_as_double_by_name("highway").unwrap(), + Some(0.) + ); + + // test nulls + assert_eq!(feature.field_as_string_by_name("railway").unwrap(), None); + assert_eq!(feature.field_as_integer_by_name("railway").unwrap(), None); + assert_eq!(feature.field_as_integer64_by_name("railway").unwrap(), None); + assert_eq!(feature.field_as_double_by_name("railway").unwrap(), None); + + assert!(matches!( + feature.field_as_string_by_name("not_a_field").unwrap_err(), + GdalError::InvalidFieldName { + field_name, + method_name: "OGR_F_GetFieldIndex", + } + if field_name == "not_a_field" + )); + }); + } + + #[test] + #[allow(clippy::float_cmp)] + fn test_get_field_as_x() { + with_features("roads.geojson", |mut features| { + let feature = features.next().unwrap(); + + let highway_field = 6; + let railway_field = 5; + let sort_key_field = 1; + + assert_eq!( + feature.field_as_string(highway_field).unwrap(), + Some("footway".to_owned()) + ); + + assert_eq!( + feature.field_as_string(sort_key_field).unwrap(), + Some("-9".to_owned()) + ); + assert_eq!(feature.field_as_integer(sort_key_field).unwrap(), Some(-9)); + assert_eq!( + feature.field_as_integer64(sort_key_field).unwrap(), + Some(-9) + ); + assert_eq!(feature.field_as_double(sort_key_field).unwrap(), Some(-9.)); + + // test failed conversions + assert_eq!(feature.field_as_integer(highway_field).unwrap(), Some(0)); + assert_eq!(feature.field_as_integer64(highway_field).unwrap(), Some(0)); + assert_eq!(feature.field_as_double(highway_field).unwrap(), Some(0.)); + + // test nulls + assert_eq!(feature.field_as_string(railway_field).unwrap(), None); + assert_eq!(feature.field_as_integer(railway_field).unwrap(), None); + assert_eq!(feature.field_as_integer64(railway_field).unwrap(), None); + assert_eq!(feature.field_as_double(railway_field).unwrap(), None); + + // test error + assert!(matches!( + feature.field_as_string(23).unwrap_err(), + GdalError::InvalidFieldIndex { + index: 23, + method_name: "field_as_string", + } + )); + }); + } + + #[test] + fn test_get_field_as_datetime() { + use chrono::{FixedOffset, TimeZone}; + + let hour_secs = 3600; + + with_features("points_with_datetime.json", |mut features| { + let feature = features.next().unwrap(); + + let dt = FixedOffset::east_opt(-5 * hour_secs) + .unwrap() + .with_ymd_and_hms(2011, 7, 14, 19, 43, 37) + .unwrap(); + + let d = FixedOffset::east_opt(0) + .unwrap() + .with_ymd_and_hms(2018, 1, 4, 0, 0, 0) + .unwrap(); + + assert_eq!(feature.field_as_datetime_by_name("dt").unwrap(), Some(dt)); + + assert_eq!(feature.field_as_datetime(0).unwrap(), Some(dt)); + + assert_eq!(feature.field_as_datetime_by_name("d").unwrap(), Some(d)); + + assert_eq!(feature.field_as_datetime(1).unwrap(), Some(d)); + }); + + with_features("roads.geojson", |mut features| { + let feature = features.next().unwrap(); + + let railway_field = 5; + + // test null + assert_eq!(feature.field_as_datetime_by_name("railway").unwrap(), None); + assert_eq!(feature.field_as_datetime(railway_field).unwrap(), None); + + // test error + assert!(matches!( + feature + .field_as_datetime_by_name("not_a_field") + .unwrap_err(), + GdalError::InvalidFieldName { + field_name, + method_name: "OGR_F_GetFieldIndex", + } if field_name == "not_a_field" + )); + assert!(matches!( + feature.field_as_datetime(23).unwrap_err(), + GdalError::InvalidFieldIndex { + index: 23, + method_name: "field_as_datetime", + } + )); + }); + } + + #[test] + fn test_field_in_layer() { + ds_with_layer("three_layer_ds.s3db", "layer_0", |mut layer| { + let feature = layer.features().next().unwrap(); + assert_eq!(feature.field("id").unwrap(), None); + }); + } + + #[test] + fn test_int_list_field() { + with_features("soundg.json", |mut features| { + let feature = features.next().unwrap(); + assert_eq!( + feature.field("an_int_list").unwrap().unwrap(), + FieldValue::IntegerListValue(vec![1, 2]) + ); + }); + } + + #[test] + fn test_set_int_list_field() { + with_features("soundg.json", |mut features| { + let feature = features.next().unwrap(); + let value = FieldValue::IntegerListValue(vec![3, 4, 5]); + feature.set_field("an_int_list", &value).unwrap(); + assert_eq!(feature.field("an_int_list").unwrap().unwrap(), value); + }); + } + + #[test] + fn test_real_list_field() { + with_features("soundg.json", |mut features| { + let feature = features.next().unwrap(); + assert_eq!( + feature.field("a_real_list").unwrap().unwrap(), + FieldValue::RealListValue(vec![0.1, 0.2]) + ); + }); + } + + #[test] + fn test_set_real_list_field() { + with_features("soundg.json", |mut features| { + let feature = features.next().unwrap(); + let value = FieldValue::RealListValue(vec![2.5, 3.0, 4.75]); + feature.set_field("a_real_list", &value).unwrap(); + assert_eq!(feature.field("a_real_list").unwrap().unwrap(), value); + }); + } + + #[test] + fn test_long_list_field() { + with_features("soundg.json", |mut features| { + let feature = features.next().unwrap(); + assert_eq!( + feature.field("a_long_list").unwrap().unwrap(), + FieldValue::Integer64ListValue(vec![5000000000, 6000000000]) + ); + }); + } + + #[test] + fn test_set_long_list_field() { + with_features("soundg.json", |mut features| { + let feature = features.next().unwrap(); + let value = FieldValue::Integer64ListValue(vec![7000000000, 8000000000]); + feature.set_field("a_long_list", &value).unwrap(); + assert_eq!(feature.field("a_long_list").unwrap().unwrap(), value); + }); + } + + #[test] + fn test_float_field() { + with_feature("roads.geojson", 236194095, |feature| { + assert_almost_eq( + feature + .field("sort_key") + .unwrap() + .unwrap() + .into_real() + .unwrap(), + -9.0, + ); + }); + } + + #[test] + fn test_missing_field() { + with_feature("roads.geojson", 236194095, |feature| { + assert!(feature.field("no such field").is_err()); + }); + } + + #[test] + fn test_geom_accessors() { + with_feature("roads.geojson", 236194095, |feature| { + let geom = feature.geometry().unwrap(); + assert_eq!(geom.geometry_type(), OGRwkbGeometryType::wkbLineString); + let coords = geom.get_point_vec(); + assert_eq!( + coords, + [ + (26.1019276, 44.4302748, 0.0), + (26.1019382, 44.4303191, 0.0), + (26.1020002, 44.4304202, 0.0) + ] + ); + assert_eq!(geom.geometry_count(), 0); + + let geom = feature.geometry_by_index(0).unwrap(); + assert_eq!(geom.geometry_type(), OGRwkbGeometryType::wkbLineString); + assert!(feature.geometry_by_index(1).is_err()); + let geom = feature.geometry_by_name(""); + assert!(geom.is_ok()); + let geom = feature.geometry_by_name("").unwrap(); + assert_eq!(geom.geometry_type(), OGRwkbGeometryType::wkbLineString); + assert!(feature.geometry_by_name("FOO").is_err()); + }); + } + + #[test] + fn test_feature_wkt() { + with_feature("roads.geojson", 236194095, |feature| { + let wkt = feature.geometry().unwrap().wkt().unwrap(); + let wkt_ok = format!( + "{}{}", + "LINESTRING (26.1019276 44.4302748,", + "26.1019382 44.4303191,26.1020002 44.4304202)" + ); + assert_eq!(wkt, wkt_ok); + }); + } + + #[test] + fn test_feature_json() { + with_feature("roads.geojson", 236194095, |feature| { + let json = feature.geometry().unwrap().json(); + let json_ok = format!( + "{}{}{}{}", + "{ \"type\": \"LineString\", \"coordinates\": [ ", + "[ 26.1019276, 44.4302748 ], ", + "[ 26.1019382, 44.4303191 ], ", + "[ 26.1020002, 44.4304202 ] ] }" + ); + assert_eq!(json.unwrap(), json_ok); + }); + } + + #[test] + fn test_write_features() { + use std::fs; + + { + let driver = DriverManager::get_driver_by_name("GeoJSON").unwrap(); + let mut ds = driver + .create_vector_only(fixture("output.geojson")) + .unwrap(); + let mut layer = ds.create_layer(Default::default()).unwrap(); + layer + .create_defn_fields(&[ + ("Name", OGRFieldType::OFTString), + ("Value", OGRFieldType::OFTReal), + ("Int_value", OGRFieldType::OFTInteger), + ]) + .unwrap(); + layer + .create_feature_fields( + Geometry::from_wkt("POINT (1 2)").unwrap(), + &["Name", "Value", "Int_value"], + &[ + FieldValue::StringValue("Feature 1".to_string()), + FieldValue::RealValue(45.78), + FieldValue::IntegerValue(1), + ], + ) + .unwrap(); + // dataset is closed here + } + + { + let ds = Dataset::open(fixture("output.geojson")).unwrap(); + let mut layer = ds.layer(0).unwrap(); + let ft = layer.features().next().unwrap(); + assert_eq!(ft.geometry().unwrap().wkt().unwrap(), "POINT (1 2)"); + assert_eq!( + ft.field("Name").unwrap().unwrap().into_string(), + Some("Feature 1".to_string()) + ); + assert_eq!(ft.field("Value").unwrap().unwrap().into_real(), Some(45.78)); + assert_eq!(ft.field("Int_value").unwrap().unwrap().into_int(), Some(1)); + } + fs::remove_file(fixture("output.geojson")).unwrap(); + } + + #[test] + fn test_features_reset() { + with_layer("roads.geojson", |mut layer| { + assert_eq!(layer.features().count(), layer.features().count(),); + }); + } + + #[test] + fn test_set_attribute_filter() { + with_layer("roads.geojson", |mut layer| { + // check number without calling any function + assert_eq!(layer.features().count(), 21); + + // check if clearing does not corrupt anything + layer.clear_attribute_filter(); + assert_eq!(layer.features().count(), 21); + + // apply actual filter + layer.set_attribute_filter("highway = 'primary'").unwrap(); + + assert_eq!(layer.features().count(), 1); + assert_eq!( + layer + .features() + .next() + .unwrap() + .field_as_string_by_name("highway") + .unwrap() + .unwrap(), + "primary" + ); + + // clearing and check again + layer.clear_attribute_filter(); + + assert_eq!(layer.features().count(), 21); + + { + let _nolog = SuppressGDALErrorLog::new(); + // force error + assert!(matches!( + layer.set_attribute_filter("foo = bar").unwrap_err(), + GdalError::OgrError { + err: gdal_sys::OGRErr::OGRERR_CORRUPT_DATA, + method_name: "OGR_L_SetAttributeFilter", + } + )); + } + }); + } + + #[test] + fn test_set_feature() { + let ds_options = DatasetOptions { + open_flags: GdalOpenFlags::GDAL_OF_UPDATE, + ..DatasetOptions::default() + }; + let tmp_file = TempFixture::empty("test.s3db"); + std::fs::copy(fixture("three_layer_ds.s3db"), &tmp_file).unwrap(); + let ds = Dataset::open_ex(&tmp_file, ds_options).unwrap(); + let mut layer = ds.layer(0).unwrap(); + let fids: Vec = layer.features().map(|f| f.fid().unwrap()).collect(); + let feature = layer.feature(fids[0]).unwrap(); + // to original value of the id field in fid 0 is null; we will set it to 1. + feature.set_field_integer("id", 1).ok(); + layer.set_feature(feature).ok(); + + // now we check that the field is 1. + let ds = Dataset::open(&tmp_file).unwrap(); + let layer = ds.layer(0).unwrap(); + let feature = layer.feature(fids[0]).unwrap(); + let value = feature.field("id").unwrap().unwrap().into_int().unwrap(); + assert_eq!(value, 1); + } + #[test] + fn test_schema() { + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + let layer = ds.layer(0).unwrap(); + // The layer name is "roads" in GDAL 2.2 + assert!(layer.name() == "OGRGeoJSON" || layer.name() == "roads"); + let name_list = layer + .defn() + .fields() + .map(|f| (f.name(), f.field_type())) + .collect::>(); + let ok_names_types = vec![ + ("kind", OGRFieldType::OFTString), + ("sort_key", OGRFieldType::OFTReal), + ("is_link", OGRFieldType::OFTString), + ("is_tunnel", OGRFieldType::OFTString), + ("is_bridge", OGRFieldType::OFTString), + ("railway", OGRFieldType::OFTString), + ("highway", OGRFieldType::OFTString), + ] + .iter() + .map(|s| (s.0.to_string(), s.1)) + .collect::>(); + assert_eq!(name_list, ok_names_types); + } + + #[test] + fn test_geom_fields() { + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + let layer = ds.layer(0).unwrap(); + let name_list = layer + .defn() + .geom_fields() + .map(|f| (f.name(), f.field_type())) + .collect::>(); + let ok_names_types = vec![("", OGRwkbGeometryType::wkbLineString)] + .iter() + .map(|s| (s.0.to_string(), s.1)) + .collect::>(); + assert_eq!(name_list, ok_names_types); + + let geom_field = layer.defn().geom_fields().next().unwrap(); + let spatial_ref2 = SpatialRef::from_epsg(4326).unwrap(); + #[cfg(major_ge_3)] + spatial_ref2.set_axis_mapping_strategy(0); + + assert_eq!(geom_field.spatial_ref().unwrap(), spatial_ref2); + } + + #[test] + fn test_get_layer_by_name() { + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + // The layer name is "roads" in GDAL 2.2 + if let Ok(layer) = ds.layer_by_name("OGRGeoJSON") { + assert_eq!(layer.name(), "OGRGeoJSON"); + } + if let Ok(layer) = ds.layer_by_name("roads") { + assert_eq!(layer.name(), "roads"); + } + } + + #[test] + fn test_spatial_filter() { + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + let mut layer = ds.layer(0).unwrap(); + assert_eq!(layer.features().count(), 21); + + let bbox = Geometry::bbox(26.1017, 44.4297, 26.1025, 44.4303).unwrap(); + layer.set_spatial_filter(&bbox); + assert_eq!(layer.features().count(), 7); + + layer.clear_spatial_filter(); + assert_eq!(layer.features().count(), 21); + + // test filter as rectangle + layer.set_spatial_filter_rect(26.1017, 44.4297, 26.1025, 44.4303); + assert_eq!(layer.features().count(), 7); + } +} diff --git a/src/vector/mod.rs b/src/vector/mod.rs index 981ebdd09..a0077457c 100644 --- a/src/vector/mod.rs +++ b/src/vector/mod.rs @@ -66,8 +66,6 @@ mod defn; mod feature; -mod gdal_to_geo; -mod geo_to_gdal; mod geometry; mod layer; mod ops; @@ -80,14 +78,6 @@ pub use geometry::{geometry_type_to_name, Geometry}; pub use layer::{ FeatureIterator, FieldDefn, Layer, LayerAccess, LayerCaps, OwnedFeatureIterator, OwnedLayer, }; -pub use ops::GeometryIntersection; - -use crate::errors::Result; - -/// Convert object to a GDAL geometry. -pub trait ToGdal { - fn to_gdal(&self) -> Result; -} /// Axis aligned 2D bounding box. pub type Envelope = gdal_sys::OGREnvelope; @@ -95,5 +85,4 @@ pub type Envelope = gdal_sys::OGREnvelope; /// Axis aligned 3D bounding box. pub type Envelope3D = gdal_sys::OGREnvelope3D; -#[cfg(test)] -mod vector_tests; +pub use ops::ToGdal; diff --git a/src/vector/ops/conversions/formats.rs b/src/vector/ops/conversions/formats.rs new file mode 100644 index 000000000..a3a82db9f --- /dev/null +++ b/src/vector/ops/conversions/formats.rs @@ -0,0 +1,121 @@ +use crate::errors::GdalError; +use crate::errors::Result; +use crate::utils::{_last_null_pointer_err, _string}; +use crate::vector::Geometry; +use gdal_sys::OGRErr; +use libc::c_char; +use std::ffi::{c_void, CString}; +use std::ptr::null_mut; + +/// Methods supporting translation between GDAL [`Geometry`] and various text representations. +/// +/// These include: +/// * ["Well Known" representations of geometry][wikipedia]. +/// * [GeoJSON][geojson] +/// +/// [wikipedia]: https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry +/// [geojson]: https://geojson.org/ +/// +impl Geometry { + /// Create a geometry by parsing a + /// [WKT](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry) string. + pub fn from_wkt(wkt: &str) -> Result { + let c_wkt = CString::new(wkt)?; + // OGR_G_CreateFromWkt does not write to the pointed-to memory, but this is not reflected + // in its signature (`char**` instead of `char const**`), so we need a scary looking cast. + let mut c_wkt_ptr = c_wkt.as_ptr() as *mut c_char; + let mut c_geom = null_mut(); + let rv = unsafe { gdal_sys::OGR_G_CreateFromWkt(&mut c_wkt_ptr, null_mut(), &mut c_geom) }; + if rv != OGRErr::OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_CreateFromWkt", + }); + } + Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) + } + + /// Creates a geometry by parsing a slice of bytes in + /// [WKB](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Well-known_binary) + /// (Well-Known Binary) format. + pub fn from_wkb(wkb: &[u8]) -> Result { + let mut c_geom = null_mut(); + let rv = unsafe { + gdal_sys::OGR_G_CreateFromWkb( + wkb.as_ptr() as *const std::ffi::c_void, + null_mut(), + &mut c_geom, + wkb.len() as i32, + ) + }; + if rv != gdal_sys::OGRErr::OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_CreateFromWkb", + }); + } + Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) + } + + /// Serialize the geometry as WKT. + pub fn wkt(&self) -> Result { + let mut c_wkt = null_mut(); + let rv = unsafe { gdal_sys::OGR_G_ExportToWkt(self.c_geometry(), &mut c_wkt) }; + if rv != OGRErr::OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_ExportToWkt", + }); + } + let wkt = _string(c_wkt); + unsafe { gdal_sys::OGRFree(c_wkt as *mut c_void) }; + Ok(wkt) + } + + /// Serializes the geometry to + /// [WKB](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Well-known_binary) + /// (Well-Known Binary) format. + pub fn wkb(&self) -> Result> { + let wkb_size = unsafe { gdal_sys::OGR_G_WkbSize(self.c_geometry()) as usize }; + // We default to little-endian for now. A WKB string explicitly indicates the byte + // order, so this is not a problem for interoperability. + let byte_order = gdal_sys::OGRwkbByteOrder::wkbNDR; + let mut wkb = vec![0; wkb_size]; + let rv = + unsafe { gdal_sys::OGR_G_ExportToWkb(self.c_geometry(), byte_order, wkb.as_mut_ptr()) }; + if rv != gdal_sys::OGRErr::OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_ExportToWkb", + }); + } + Ok(wkb) + } + + /// Serialize the geometry as GeoJSON. + /// + /// See: [`OGR_G_ExportToJson`](https://gdal.org/api/vector_c_api.html#_CPPv418OGR_G_ExportToJson12OGRGeometryH) + pub fn json(&self) -> Result { + let c_json = unsafe { gdal_sys::OGR_G_ExportToJson(self.c_geometry()) }; + if c_json.is_null() { + return Err(_last_null_pointer_err("OGR_G_ExportToJson")); + }; + let rv = _string(c_json); + unsafe { gdal_sys::VSIFree(c_json as *mut c_void) }; + Ok(rv) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + pub fn test_wkb() { + let wkt = "POLYGON ((45.0 45.0, 45.0 50.0, 50.0 50.0, 50.0 45.0, 45.0 45.0))"; + let orig_geom = Geometry::from_wkt(wkt).unwrap(); + let wkb = orig_geom.wkb().unwrap(); + let new_geom = Geometry::from_wkb(&wkb).unwrap(); + assert_eq!(new_geom, orig_geom); + } +} diff --git a/src/vector/gdal_to_geo.rs b/src/vector/ops/conversions/gdal_to_geo.rs similarity index 100% rename from src/vector/gdal_to_geo.rs rename to src/vector/ops/conversions/gdal_to_geo.rs diff --git a/src/vector/geo_to_gdal.rs b/src/vector/ops/conversions/geo_to_gdal.rs similarity index 100% rename from src/vector/geo_to_gdal.rs rename to src/vector/ops/conversions/geo_to_gdal.rs diff --git a/src/vector/ops/conversions/mod.rs b/src/vector/ops/conversions/mod.rs new file mode 100644 index 000000000..1853c798b --- /dev/null +++ b/src/vector/ops/conversions/mod.rs @@ -0,0 +1,187 @@ +mod formats; +mod gdal_to_geo; +mod geo_to_gdal; + +use crate::errors::Result; +use crate::vector::Geometry; + +/// Convert object to a GDAL geometry. +pub trait ToGdal { + fn to_gdal(&self) -> Result; +} + +impl Geometry { + /// Create a copy of self as a `geo-types` geometry. + pub fn to_geo(&self) -> Result> { + self.try_into() + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use crate::vector::{Geometry, ToGdal}; + + #[test] + fn test_import_export_point() { + let wkt = "POINT (1 2)"; + let coord = geo_types::Coord { x: 1., y: 2. }; + let geo = geo_types::Geometry::Point(geo_types::Point(coord)); + + assert_eq!( + geo_types::Geometry::<_>::try_from(Geometry::from_wkt(wkt).unwrap()).unwrap(), + geo + ); + assert_eq!(geo.to_gdal().unwrap().wkt().unwrap(), wkt); + } + + #[test] + fn test_import_export_multipoint() { + let wkt = "MULTIPOINT (0 0,0 1,1 2)"; + let coord = vec![ + geo_types::Point(geo_types::Coord { x: 0., y: 0. }), + geo_types::Point(geo_types::Coord { x: 0., y: 1. }), + geo_types::Point(geo_types::Coord { x: 1., y: 2. }), + ]; + let geo = geo_types::Geometry::MultiPoint(geo_types::MultiPoint(coord)); + + assert_eq!( + geo_types::Geometry::<_>::try_from(Geometry::from_wkt(wkt).unwrap()).unwrap(), + geo + ); + assert_eq!(geo.to_gdal().unwrap().wkt().unwrap(), wkt); + } + + #[test] + fn test_import_export_linestring() { + let wkt = "LINESTRING (0 0,0 1,1 2)"; + let coord = vec![ + geo_types::Coord { x: 0., y: 0. }, + geo_types::Coord { x: 0., y: 1. }, + geo_types::Coord { x: 1., y: 2. }, + ]; + let geo = geo_types::Geometry::LineString(geo_types::LineString(coord)); + + assert_eq!( + geo_types::Geometry::<_>::try_from(Geometry::from_wkt(wkt).unwrap()).unwrap(), + geo + ); + assert_eq!(geo.to_gdal().unwrap().wkt().unwrap(), wkt); + } + + #[test] + fn test_import_export_multilinestring() { + let wkt = "MULTILINESTRING ((0 0,0 1,1 2),(3 3,3 4,4 5))"; + let strings = vec![ + geo_types::LineString(vec![ + geo_types::Coord { x: 0., y: 0. }, + geo_types::Coord { x: 0., y: 1. }, + geo_types::Coord { x: 1., y: 2. }, + ]), + geo_types::LineString(vec![ + geo_types::Coord { x: 3., y: 3. }, + geo_types::Coord { x: 3., y: 4. }, + geo_types::Coord { x: 4., y: 5. }, + ]), + ]; + let geo = geo_types::Geometry::MultiLineString(geo_types::MultiLineString(strings)); + + assert_eq!( + geo_types::Geometry::<_>::try_from(Geometry::from_wkt(wkt).unwrap()).unwrap(), + geo + ); + assert_eq!(geo.to_gdal().unwrap().wkt().unwrap(), wkt); + } + + fn square(x0: isize, y0: isize, x1: isize, y1: isize) -> geo_types::LineString { + geo_types::LineString(vec![ + geo_types::Coord { + x: x0 as f64, + y: y0 as f64, + }, + geo_types::Coord { + x: x0 as f64, + y: y1 as f64, + }, + geo_types::Coord { + x: x1 as f64, + y: y1 as f64, + }, + geo_types::Coord { + x: x1 as f64, + y: y0 as f64, + }, + geo_types::Coord { + x: x0 as f64, + y: y0 as f64, + }, + ]) + } + + #[test] + fn test_import_export_polygon() { + let wkt = "POLYGON ((0 0,0 5,5 5,5 0,0 0),\ + (1 1,1 2,2 2,2 1,1 1),\ + (3 3,3 4,4 4,4 3,3 3))"; + let outer = square(0, 0, 5, 5); + let holes = vec![square(1, 1, 2, 2), square(3, 3, 4, 4)]; + let geo = geo_types::Geometry::Polygon(geo_types::Polygon::new(outer, holes)); + + assert_eq!( + geo_types::Geometry::<_>::try_from(Geometry::from_wkt(wkt).unwrap()).unwrap(), + geo + ); + assert_eq!(geo.to_gdal().unwrap().wkt().unwrap(), wkt); + } + + #[test] + fn test_import_export_multipolygon() { + let wkt = "MULTIPOLYGON (\ + ((0 0,0 5,5 5,5 0,0 0),\ + (1 1,1 2,2 2,2 1,1 1),\ + (3 3,3 4,4 4,4 3,3 3)),\ + ((4 4,4 9,9 9,9 4,4 4),\ + (5 5,5 6,6 6,6 5,5 5),\ + (7 7,7 8,8 8,8 7,7 7))\ + )"; + let multipolygon = geo_types::MultiPolygon(vec![ + geo_types::Polygon::new( + square(0, 0, 5, 5), + vec![square(1, 1, 2, 2), square(3, 3, 4, 4)], + ), + geo_types::Polygon::new( + square(4, 4, 9, 9), + vec![square(5, 5, 6, 6), square(7, 7, 8, 8)], + ), + ]); + let geo = geo_types::Geometry::MultiPolygon(multipolygon); + + assert_eq!( + geo_types::Geometry::<_>::try_from(Geometry::from_wkt(wkt).unwrap()).unwrap(), + geo + ); + assert_eq!(geo.to_gdal().unwrap().wkt().unwrap(), wkt); + } + + #[test] + fn test_import_export_geometrycollection() { + let wkt = "GEOMETRYCOLLECTION (POINT (1 2),LINESTRING (0 0,0 1,1 2))"; + let coord = geo_types::Coord { x: 1., y: 2. }; + let point = geo_types::Geometry::Point(geo_types::Point(coord)); + let coords = vec![ + geo_types::Coord { x: 0., y: 0. }, + geo_types::Coord { x: 0., y: 1. }, + geo_types::Coord { x: 1., y: 2. }, + ]; + let linestring = geo_types::Geometry::LineString(geo_types::LineString(coords)); + let collection = geo_types::GeometryCollection(vec![point, linestring]); + let geo = geo_types::Geometry::GeometryCollection(collection); + + assert_eq!( + geo_types::Geometry::<_>::try_from(Geometry::from_wkt(wkt).unwrap()).unwrap(), + geo + ); + assert_eq!(geo.to_gdal().unwrap().wkt().unwrap(), wkt); + } +} diff --git a/src/vector/ops/geometry/mod.rs b/src/vector/ops/geometry/mod.rs deleted file mode 100644 index 8d646b654..000000000 --- a/src/vector/ops/geometry/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod intersection; diff --git a/src/vector/ops/mod.rs b/src/vector/ops/mod.rs index 6c8accfd8..385dcfa17 100644 --- a/src/vector/ops/mod.rs +++ b/src/vector/ops/mod.rs @@ -1,4 +1,6 @@ -mod geometry; +mod conversions; mod predicates; +mod set; +mod transformations; -pub use geometry::intersection::Intersection as GeometryIntersection; +pub use conversions::ToGdal; diff --git a/src/vector/ops/predicates.rs b/src/vector/ops/predicates.rs index 099c510ee..683f751a3 100644 --- a/src/vector/ops/predicates.rs +++ b/src/vector/ops/predicates.rs @@ -1,6 +1,8 @@ use crate::vector::Geometry; -/// These methods define common [spatial relations](https://en.wikipedia.org/wiki/DE-9IM#Spatial_predicates) between +/// # Geometric Predicates +/// +/// These methods provide common [spatial relations](https://en.wikipedia.org/wiki/DE-9IM#Spatial_predicates) between /// two geometries. impl Geometry { /// Tests if two geometries [_intersect_][DE-9IM]; diff --git a/src/vector/ops/geometry/intersection.rs b/src/vector/ops/set.rs similarity index 79% rename from src/vector/ops/geometry/intersection.rs rename to src/vector/ops/set.rs index a8334cdff..05250798d 100644 --- a/src/vector/ops/geometry/intersection.rs +++ b/src/vector/ops/set.rs @@ -1,11 +1,9 @@ use crate::vector::Geometry; -use gdal_sys::OGR_G_Intersection; -/// An intersection between Geometry/Geometry returning the same type. -pub trait Intersection -where - Self: Sized, -{ +/// # Set Operations +/// +/// These methods provide set operations over two geometries, producing a new geometry. +impl Geometry { /// Compute intersection. /// /// Generates a new geometry which is the region of intersection of @@ -18,24 +16,21 @@ where /// # Returns /// Some(Geometry) if both Geometries contain pointers /// None if either geometry is missing the gdal pointer, or there is an error. - fn intersection(&self, other: &Self) -> Option; -} - -impl Intersection for Geometry { - fn intersection(&self, other: &Self) -> Option { + /// + /// See: [`OGR_G_Intersection`](https://gdal.org/api/vector_c_api.html#_CPPv418OGR_G_Intersection12OGRGeometryH12OGRGeometryH) + pub fn intersection(&self, other: &Self) -> Option { if !self.has_gdal_ptr() { return None; } if !other.has_gdal_ptr() { return None; } - unsafe { - let ogr_geom = OGR_G_Intersection(self.c_geometry(), other.c_geometry()); - if ogr_geom.is_null() { - return None; - } - Some(Geometry::with_c_geometry(ogr_geom, true)) + let ogr_geom = + unsafe { gdal_sys::OGR_G_Intersection(self.c_geometry(), other.c_geometry()) }; + if ogr_geom.is_null() { + return None; } + Some(unsafe { Geometry::with_c_geometry(ogr_geom, true) }) } } diff --git a/src/vector/ops/transformations.rs b/src/vector/ops/transformations.rs new file mode 100644 index 000000000..1e68b6533 --- /dev/null +++ b/src/vector/ops/transformations.rs @@ -0,0 +1,313 @@ +use gdal_sys::OGRErr; + +use crate::cpl::CslStringList; +use crate::errors::{GdalError, Result}; +use crate::spatial_ref::{CoordTransform, SpatialRef}; +use crate::utils::_last_null_pointer_err; +use crate::vector::Geometry; + +/// # Geometry Transformations +/// +/// These methods provide geometric transformations on a `Geometry`. +impl Geometry { + /// Apply arbitrary coordinate transformation to geometry, mutating the [`Geometry`] in-place. + /// + /// See: [`OGR_G_Transform`](https://gdal.org/api/vector_c_api.html#_CPPv415OGR_G_Transform12OGRGeometryH28OGRCoordinateTransformationH) + pub fn transform_inplace(&mut self, htransform: &CoordTransform) -> Result<()> { + let rv = unsafe { gdal_sys::OGR_G_Transform(self.c_geometry(), htransform.to_c_hct()) }; + if rv != OGRErr::OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_Transform", + }); + } + Ok(()) + } + + /// Apply arbitrary coordinate transformation to geometry on a clone of `Self`. + /// + /// See: [`OGR_G_Transform`](https://gdal.org/api/vector_c_api.html#_CPPv415OGR_G_Transform12OGRGeometryH28OGRCoordinateTransformationH) + pub fn transform(&self, htransform: &CoordTransform) -> Result { + let new_c_geom = unsafe { gdal_sys::OGR_G_Clone(self.c_geometry()) }; + let rv = unsafe { gdal_sys::OGR_G_Transform(new_c_geom, htransform.to_c_hct()) }; + if rv != OGRErr::OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_Transform", + }); + } + Ok(unsafe { Geometry::with_c_geometry(new_c_geom, true) }) + } + + /// Transforms this geometry's coordinates into another [`SpatialRef`], mutating the [`Geometry`] in-place. + /// + /// See: [`OGR_G_TransformTo`](https://gdal.org/api/vector_c_api.html#_CPPv417OGR_G_TransformTo12OGRGeometryH20OGRSpatialReferenceH) + pub fn transform_to_inplace(&mut self, spatial_ref: &SpatialRef) -> Result<()> { + let rv = unsafe { gdal_sys::OGR_G_TransformTo(self.c_geometry(), spatial_ref.to_c_hsrs()) }; + if rv != OGRErr::OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_TransformTo", + }); + } + Ok(()) + } + + /// Transforms this geometry's coordinates into another [`SpatialRef`]. + /// + /// See: [`OGR_G_TransformTo`](https://gdal.org/api/vector_c_api.html#_CPPv417OGR_G_TransformTo12OGRGeometryH20OGRSpatialReferenceH) + pub fn transform_to(&self, spatial_ref: &SpatialRef) -> Result { + let new_c_geom = unsafe { gdal_sys::OGR_G_Clone(self.c_geometry()) }; + let rv = unsafe { gdal_sys::OGR_G_TransformTo(new_c_geom, spatial_ref.to_c_hsrs()) }; + if rv != OGRErr::OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_TransformTo", + }); + } + Ok(unsafe { Geometry::with_c_geometry(new_c_geom, true) }) + } + + /// Compute the convex hull of this geometry. + /// + /// See: [`OGR_G_ConvexHull`](https://gdal.org/api/vector_c_api.html#_CPPv416OGR_G_ConvexHull12OGRGeometryH) + pub fn convex_hull(&self) -> Result { + let c_geom = unsafe { gdal_sys::OGR_G_ConvexHull(self.c_geometry()) }; + if c_geom.is_null() { + return Err(_last_null_pointer_err("OGR_G_ConvexHull")); + }; + Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) + } + + #[cfg(any(all(major_is_2, minor_ge_1), major_ge_3))] + /// Return a [Delaunay triangulation of][dt] the vertices of the geometry. + /// + /// # Arguments + /// * `tolerance`: optional snapping tolerance to use for improved robustness + /// + /// # Notes + /// This function requires GEOS library, v3.4 or above. + /// If OGR is built without the GEOS library, this function will always fail. + /// Check with [`VersionInfo::has_geos`][has_geos]. + /// + /// See: [`OGR_G_DelaunayTriangulation`](https://gdal.org/api/vector_c_api.html#_CPPv427OGR_G_DelaunayTriangulation12OGRGeometryHdi) + /// + /// [dt]: https://en.wikipedia.org/wiki/Delaunay_triangulation + /// [has_geos]: crate::version::VersionInfo::has_geos + pub fn delaunay_triangulation(&self, tolerance: Option) -> Result { + let c_geom = unsafe { + gdal_sys::OGR_G_DelaunayTriangulation(self.c_geometry(), tolerance.unwrap_or(0.0), 0) + }; + if c_geom.is_null() { + return Err(_last_null_pointer_err("OGR_G_DelaunayTriangulation")); + }; + + Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) + } + + /// Compute a simplified geometry. + /// + /// # Arguments + /// * `tolerance`: the distance tolerance for the simplification. + /// + /// See: [`OGR_G_Simplify`](https://gdal.org/api/vector_c_api.html#_CPPv414OGR_G_Simplify12OGRGeometryHd) + pub fn simplify(&self, tolerance: f64) -> Result { + let c_geom = unsafe { gdal_sys::OGR_G_Simplify(self.c_geometry(), tolerance) }; + if c_geom.is_null() { + return Err(_last_null_pointer_err("OGR_G_Simplify")); + }; + + Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) + } + + /// Simplify the geometry while preserving topology. + /// + /// # Arguments + /// * `tolerance`: the distance tolerance for the simplification. + /// + /// See: [`OGR_G_SimplifyPreserveTopology`](https://gdal.org/api/vector_c_api.html#_CPPv430OGR_G_SimplifyPreserveTopology12OGRGeometryHd) + pub fn simplify_preserve_topology(&self, tolerance: f64) -> Result { + let c_geom = + unsafe { gdal_sys::OGR_G_SimplifyPreserveTopology(self.c_geometry(), tolerance) }; + if c_geom.is_null() { + return Err(_last_null_pointer_err("OGR_G_SimplifyPreserveTopology")); + }; + + Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) + } + + /// Compute buffer of geometry + /// + /// # Arguments + /// * `distance`: the buffer distance to be applied. Should be expressed in + /// the same unit as the coordinates of the geometry. + /// * `n_quad_segs` specifies the number of segments used to approximate a + /// 90 degree (quadrant) of curvature. + /// + /// See: [`OGR_G_Buffer`](https://gdal.org/api/vector_c_api.html#_CPPv412OGR_G_Buffer12OGRGeometryHdi) + pub fn buffer(&self, distance: f64, n_quad_segs: u32) -> Result { + let c_geom = + unsafe { gdal_sys::OGR_G_Buffer(self.c_geometry(), distance, n_quad_segs as i32) }; + if c_geom.is_null() { + return Err(_last_null_pointer_err("OGR_G_Buffer")); + }; + + Ok(unsafe { Geometry::with_c_geometry(c_geom, true) }) + } + + /// 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. + /// + /// See: [OGR_G_MakeValidEx](https://gdal.org/api/vector_c_api.html#_CPPv417OGR_G_MakeValidEx12OGRGeometryH12CSLConstList) + /// + /// # Example + /// ```rust, no_run + /// use gdal::cpl::CslStringList; + /// 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(&CslStringList::new())?; + /// 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(&self, opts: &CslStringList) -> Result { + #[cfg(all(major_ge_3, minor_ge_4))] + let c_geom = unsafe { gdal_sys::OGR_G_MakeValidEx(self.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(self.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) }) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::SuppressGDALErrorLog; + + #[test] + fn test_convex_hull() { + let star = "POLYGON ((0 1,3 1,1 3,1.5 0.0,2 3,0 1))"; + let hull = "POLYGON ((1.5 0.0,0 1,1 3,2 3,3 1,1.5 0.0))"; + assert_eq!( + Geometry::from_wkt(star) + .unwrap() + .convex_hull() + .unwrap() + .wkt() + .unwrap(), + hull + ); + } + + #[test] + #[cfg(any(all(major_is_2, minor_ge_1), major_ge_3))] + fn test_delaunay_triangulation() -> Result<()> { + let square = Geometry::from_wkt("POLYGON ((0 1,1 1,1 0,0 0,0 1))")?; + let triangles = Geometry::from_wkt( + "GEOMETRYCOLLECTION (POLYGON ((0 1,0 0,1 0,0 1)),POLYGON ((0 1,1 0,1 1,0 1)))", + )?; + assert_eq!(square.delaunay_triangulation(None)?, triangles); + Ok(()) + } + + #[test] + fn test_simplify() -> Result<()> { + let line = Geometry::from_wkt("LINESTRING(1.2 0.19,1.63 0.58,1.98 0.65,2.17 0.89)")?; + let triangles = Geometry::from_wkt("LINESTRING (1.2 0.19,2.17 0.89)")?; + assert_eq!(line.simplify(0.5)?, triangles); + Ok(()) + } + + #[test] + fn test_simplify_preserve_topology() -> Result<()> { + let donut = Geometry::from_wkt( + "POLYGON ((20 35,10 30,10 10,30 5,45 20,20 35),(30 20,20 15,20 25,30 20))", + )?; + let triangles = Geometry::from_wkt( + "POLYGON ((20 35,10 10,30 5,45 20,20 35),(30 20,20 15,20 25,30 20))", + )?; + assert_eq!(donut.simplify_preserve_topology(100.0)?, triangles); + Ok(()) + } + + #[test] + pub fn test_buffer() { + let geom = Geometry::from_wkt("POINT(0 0)").unwrap(); + let buffered = geom.buffer(10.0, 2).unwrap(); + assert_eq!( + buffered.geometry_type(), + ::gdal_sys::OGRwkbGeometryType::wkbPolygon + ); + assert!(buffered.area() > 10.0); + } + + #[test] + /// Simple clone case. + pub fn test_make_valid_clone() { + let src = Geometry::from_wkt("POINT (0 0)").unwrap(); + let dst = src.make_valid(&CslStringList::new()); + assert!(dst.is_ok()); + assert!(dst.unwrap().is_valid()); + } + + #[test] + /// Un-repairable geometry case + pub fn test_make_valid_invalid() { + let _nolog = SuppressGDALErrorLog::new(); + let src = Geometry::from_wkt("LINESTRING (0 0)").unwrap(); + assert!(!src.is_valid()); + let dst = src.make_valid(&CslStringList::new()); + 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(); + assert!(!src.is_valid()); + let dst = src.make_valid(&CslStringList::new()); + assert!(dst.is_ok()); + assert!(dst.unwrap().is_valid()); + } + + #[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 opts = CslStringList::try_from(&[("STRUCTURE", "LINEWORK")]).unwrap(); + let dst = src.make_valid(&opts); + assert!(dst.is_ok(), "{dst:?}"); + assert!(dst.unwrap().is_valid()); + } +} diff --git a/src/vector/sql.rs b/src/vector/sql.rs index 0d5351444..3906fa71c 100644 --- a/src/vector/sql.rs +++ b/src/vector/sql.rs @@ -51,3 +51,142 @@ pub enum Dialect { pub(crate) const OGRSQL: &[u8] = b"OGRSQL\0"; pub(crate) const SQLITE: &[u8] = b"SQLITE\0"; + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use crate::test_utils::SuppressGDALErrorLog; + use crate::{ + test_utils::fixture, + vector::{sql, Geometry, LayerAccess}, + Dataset, + }; + + #[test] + fn test_sql() { + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + let query = "SELECT kind, is_bridge, highway FROM roads WHERE highway = 'pedestrian'"; + let mut result_set = ds + .execute_sql(query, None, sql::Dialect::DEFAULT) + .unwrap() + .unwrap(); + + let field_names: HashSet<_> = result_set + .defn() + .fields() + .map(|field| field.name()) + .collect(); + + let mut correct_field_names = HashSet::new(); + correct_field_names.insert("kind".into()); + correct_field_names.insert("is_bridge".into()); + correct_field_names.insert("highway".into()); + + assert_eq!(correct_field_names, field_names); + assert_eq!(10, result_set.feature_count()); + + for feature in result_set.features() { + let highway = feature + .field("highway") + .unwrap() + .unwrap() + .into_string() + .unwrap(); + + assert_eq!("pedestrian", highway); + } + } + + #[test] + fn test_sql_with_spatial_filter() { + let query = "SELECT * FROM roads WHERE highway = 'pedestrian'"; + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + let bbox = Geometry::bbox(26.1017, 44.4297, 26.1025, 44.4303).unwrap(); + let mut result_set = ds + .execute_sql(query, Some(&bbox), sql::Dialect::DEFAULT) + .unwrap() + .unwrap(); + + assert_eq!(2, result_set.feature_count()); + let mut correct_fids = HashSet::new(); + correct_fids.insert(252725993); + correct_fids.insert(23489656); + + let mut fids = HashSet::new(); + for feature in result_set.features() { + let highway = feature + .field("highway") + .unwrap() + .unwrap() + .into_string() + .unwrap(); + + assert_eq!("pedestrian", highway); + fids.insert(feature.fid().unwrap()); + } + + assert_eq!(correct_fids, fids); + } + + #[test] + fn test_sql_with_dialect() { + let query = "SELECT * FROM roads WHERE highway = 'pedestrian' and NumPoints(GEOMETRY) = 3"; + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + let bbox = Geometry::bbox(26.1017, 44.4297, 26.1025, 44.4303).unwrap(); + let mut result_set = ds + .execute_sql(query, Some(&bbox), sql::Dialect::SQLITE) + .unwrap() + .unwrap(); + + assert_eq!(1, result_set.feature_count()); + let mut features: Vec<_> = result_set.features().collect(); + let feature = features.pop().unwrap(); + let highway = feature + .field("highway") + .unwrap() + .unwrap() + .into_string() + .unwrap(); + + assert_eq!("pedestrian", highway); + } + + #[test] + fn test_sql_empty_result() { + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + let query = "SELECT kind, is_bridge, highway FROM roads WHERE highway = 'jazz hands 👐'"; + let mut result_set = ds + .execute_sql(query, None, sql::Dialect::DEFAULT) + .unwrap() + .unwrap(); + assert_eq!(0, result_set.feature_count()); + assert_eq!(0, result_set.features().count()); + } + + #[test] + fn test_sql_no_result() { + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + let query = "ALTER TABLE roads ADD COLUMN fun integer"; + let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT).unwrap(); + assert!(result_set.is_none()); + } + + #[test] + fn test_sql_bad_query() { + let _nolog = SuppressGDALErrorLog::new(); + let ds = Dataset::open(fixture("roads.geojson")).unwrap(); + + let query = "SELECT nope FROM roads"; + let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT); + assert!(result_set.is_err()); + + let query = "SELECT nope FROM"; + let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT); + assert!(result_set.is_err()); + + let query = "SELECT ninetynineredballoons(highway) FROM roads"; + let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT); + assert!(result_set.is_err()); + } +} diff --git a/src/vector/vector_tests/convert_geo.rs b/src/vector/vector_tests/convert_geo.rs deleted file mode 100644 index 6b5c99e3b..000000000 --- a/src/vector/vector_tests/convert_geo.rs +++ /dev/null @@ -1,165 +0,0 @@ -use std::convert::TryFrom; - -use crate::vector::{Geometry, ToGdal}; - -#[test] -fn test_import_export_point() { - let wkt = "POINT (1 2)"; - let coord = geo_types::Coord { x: 1., y: 2. }; - let geo = geo_types::Geometry::Point(geo_types::Point(coord)); - - assert_eq!( - geo_types::Geometry::<_>::try_from(Geometry::from_wkt(wkt).unwrap()).unwrap(), - geo - ); - assert_eq!(geo.to_gdal().unwrap().wkt().unwrap(), wkt); -} - -#[test] -fn test_import_export_multipoint() { - let wkt = "MULTIPOINT (0 0,0 1,1 2)"; - let coord = vec![ - geo_types::Point(geo_types::Coord { x: 0., y: 0. }), - geo_types::Point(geo_types::Coord { x: 0., y: 1. }), - geo_types::Point(geo_types::Coord { x: 1., y: 2. }), - ]; - let geo = geo_types::Geometry::MultiPoint(geo_types::MultiPoint(coord)); - - assert_eq!( - geo_types::Geometry::<_>::try_from(Geometry::from_wkt(wkt).unwrap()).unwrap(), - geo - ); - assert_eq!(geo.to_gdal().unwrap().wkt().unwrap(), wkt); -} - -#[test] -fn test_import_export_linestring() { - let wkt = "LINESTRING (0 0,0 1,1 2)"; - let coord = vec![ - geo_types::Coord { x: 0., y: 0. }, - geo_types::Coord { x: 0., y: 1. }, - geo_types::Coord { x: 1., y: 2. }, - ]; - let geo = geo_types::Geometry::LineString(geo_types::LineString(coord)); - - assert_eq!( - geo_types::Geometry::<_>::try_from(Geometry::from_wkt(wkt).unwrap()).unwrap(), - geo - ); - assert_eq!(geo.to_gdal().unwrap().wkt().unwrap(), wkt); -} - -#[test] -fn test_import_export_multilinestring() { - let wkt = "MULTILINESTRING ((0 0,0 1,1 2),(3 3,3 4,4 5))"; - let strings = vec![ - geo_types::LineString(vec![ - geo_types::Coord { x: 0., y: 0. }, - geo_types::Coord { x: 0., y: 1. }, - geo_types::Coord { x: 1., y: 2. }, - ]), - geo_types::LineString(vec![ - geo_types::Coord { x: 3., y: 3. }, - geo_types::Coord { x: 3., y: 4. }, - geo_types::Coord { x: 4., y: 5. }, - ]), - ]; - let geo = geo_types::Geometry::MultiLineString(geo_types::MultiLineString(strings)); - - assert_eq!( - geo_types::Geometry::<_>::try_from(Geometry::from_wkt(wkt).unwrap()).unwrap(), - geo - ); - assert_eq!(geo.to_gdal().unwrap().wkt().unwrap(), wkt); -} - -fn square(x0: isize, y0: isize, x1: isize, y1: isize) -> geo_types::LineString { - geo_types::LineString(vec![ - geo_types::Coord { - x: x0 as f64, - y: y0 as f64, - }, - geo_types::Coord { - x: x0 as f64, - y: y1 as f64, - }, - geo_types::Coord { - x: x1 as f64, - y: y1 as f64, - }, - geo_types::Coord { - x: x1 as f64, - y: y0 as f64, - }, - geo_types::Coord { - x: x0 as f64, - y: y0 as f64, - }, - ]) -} - -#[test] -fn test_import_export_polygon() { - let wkt = "POLYGON ((0 0,0 5,5 5,5 0,0 0),\ - (1 1,1 2,2 2,2 1,1 1),\ - (3 3,3 4,4 4,4 3,3 3))"; - let outer = square(0, 0, 5, 5); - let holes = vec![square(1, 1, 2, 2), square(3, 3, 4, 4)]; - let geo = geo_types::Geometry::Polygon(geo_types::Polygon::new(outer, holes)); - - assert_eq!( - geo_types::Geometry::<_>::try_from(Geometry::from_wkt(wkt).unwrap()).unwrap(), - geo - ); - assert_eq!(geo.to_gdal().unwrap().wkt().unwrap(), wkt); -} - -#[test] -fn test_import_export_multipolygon() { - let wkt = "MULTIPOLYGON (\ - ((0 0,0 5,5 5,5 0,0 0),\ - (1 1,1 2,2 2,2 1,1 1),\ - (3 3,3 4,4 4,4 3,3 3)),\ - ((4 4,4 9,9 9,9 4,4 4),\ - (5 5,5 6,6 6,6 5,5 5),\ - (7 7,7 8,8 8,8 7,7 7))\ - )"; - let multipolygon = geo_types::MultiPolygon(vec![ - geo_types::Polygon::new( - square(0, 0, 5, 5), - vec![square(1, 1, 2, 2), square(3, 3, 4, 4)], - ), - geo_types::Polygon::new( - square(4, 4, 9, 9), - vec![square(5, 5, 6, 6), square(7, 7, 8, 8)], - ), - ]); - let geo = geo_types::Geometry::MultiPolygon(multipolygon); - - assert_eq!( - geo_types::Geometry::<_>::try_from(Geometry::from_wkt(wkt).unwrap()).unwrap(), - geo - ); - assert_eq!(geo.to_gdal().unwrap().wkt().unwrap(), wkt); -} - -#[test] -fn test_import_export_geometrycollection() { - let wkt = "GEOMETRYCOLLECTION (POINT (1 2),LINESTRING (0 0,0 1,1 2))"; - let coord = geo_types::Coord { x: 1., y: 2. }; - let point = geo_types::Geometry::Point(geo_types::Point(coord)); - let coords = vec![ - geo_types::Coord { x: 0., y: 0. }, - geo_types::Coord { x: 0., y: 1. }, - geo_types::Coord { x: 1., y: 2. }, - ]; - let linestring = geo_types::Geometry::LineString(geo_types::LineString(coords)); - let collection = geo_types::GeometryCollection(vec![point, linestring]); - let geo = geo_types::Geometry::GeometryCollection(collection); - - assert_eq!( - geo_types::Geometry::<_>::try_from(Geometry::from_wkt(wkt).unwrap()).unwrap(), - geo - ); - assert_eq!(geo.to_gdal().unwrap().wkt().unwrap(), wkt); -} diff --git a/src/vector/vector_tests/mod.rs b/src/vector/vector_tests/mod.rs deleted file mode 100644 index c7f1fcc03..000000000 --- a/src/vector/vector_tests/mod.rs +++ /dev/null @@ -1,867 +0,0 @@ -use crate::spatial_ref::SpatialRef; -use crate::test_utils::{fixture, TempFixture}; -use crate::{assert_almost_eq, Dataset, DatasetOptions, GdalOpenFlags}; - -use super::{ - Feature, FeatureIterator, FieldValue, Geometry, Layer, LayerAccess, LayerCaps::*, OGRFieldType, - OGRwkbGeometryType, OwnedLayer, -}; - -mod convert_geo; -mod sql; - -#[test] -fn test_layer_count() { - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - assert_eq!(ds.layer_count(), 1); -} - -#[test] -fn test_many_layer_count() { - let ds = Dataset::open(fixture("three_layer_ds.s3db")).unwrap(); - assert_eq!(ds.layer_count(), 3); -} - -#[test] -fn test_layer_get_extent() { - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - let layer = ds.layer(0).unwrap(); - let extent = layer.get_extent().unwrap(); - assert_almost_eq(extent.MinX, 26.100768); - assert_almost_eq(extent.MaxX, 26.103515); - assert_almost_eq(extent.MinY, 44.429858); - assert_almost_eq(extent.MaxY, 44.431818); -} - -#[test] -fn test_layer_try_get_extent() { - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - let layer = ds.layer(0).unwrap(); - assert!(layer.try_get_extent().unwrap().is_none()); -} - -#[test] -fn test_layer_spatial_ref() { - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - let layer = ds.layer(0).unwrap(); - let srs = layer.spatial_ref().unwrap(); - assert_eq!(srs.auth_code().unwrap(), 4326); -} - -#[test] -fn test_layer_capabilities() { - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - let layer = ds.layer(0).unwrap(); - - assert!(!layer.has_capability(OLCFastSpatialFilter)); - assert!(layer.has_capability(OLCFastFeatureCount)); - assert!(!layer.has_capability(OLCFastGetExtent)); - assert!(layer.has_capability(OLCRandomRead)); - assert!(layer.has_capability(OLCStringsAsUTF8)); -} - -fn ds_with_layer(ds_name: &str, layer_name: &str, f: F) -where - F: Fn(Layer), -{ - let ds = Dataset::open(fixture(ds_name)).unwrap(); - let layer = ds.layer_by_name(layer_name).unwrap(); - f(layer); -} - -fn with_layer(name: &str, f: F) -where - F: Fn(Layer), -{ - let ds = Dataset::open(fixture(name)).unwrap(); - let layer = ds.layer(0).unwrap(); - f(layer); -} - -fn with_owned_layer(name: &str, f: F) -where - F: Fn(OwnedLayer), -{ - let ds = Dataset::open(fixture(name)).unwrap(); - let layer = ds.into_layer(0).unwrap(); - f(layer); -} - -fn with_features(name: &str, f: F) -where - F: Fn(FeatureIterator), -{ - with_layer(name, |mut layer| f(layer.features())); -} - -fn with_feature(name: &str, fid: u64, f: F) -where - F: Fn(Feature), -{ - with_layer(name, |layer| f(layer.feature(fid).unwrap())); -} - -#[cfg(test)] -mod tests { - use gdal_sys::OGRwkbGeometryType::{wkbLineString, wkbLinearRing, wkbPolygon}; - - use crate::test_utils::SuppressGDALErrorLog; - use crate::{ - errors::{GdalError, Result}, - DriverManager, - }; - - use super::*; - - #[test] - fn test_feature_count() { - with_layer("roads.geojson", |layer| { - assert_eq!(layer.feature_count(), 21); - }); - } - - #[test] - fn test_many_feature_count() { - ds_with_layer("three_layer_ds.s3db", "layer_0", |layer| { - assert_eq!(layer.feature_count(), 3); - }); - } - - #[test] - fn test_try_feature_count() { - with_layer("roads.geojson", |layer| { - assert_eq!(layer.try_feature_count(), Some(21)); - }); - } - - #[test] - fn test_feature() { - with_layer("roads.geojson", |layer| { - assert!(layer.feature(236194095).is_some()); - assert!(layer.feature(23489660).is_some()); - assert!(layer.feature(0).is_none()); - assert!(layer.feature(404).is_none()); - }); - } - - #[test] - fn test_iterate_features() { - with_features("roads.geojson", |features| { - assert_eq!(features.size_hint(), (21, Some(21))); - assert_eq!(features.count(), 21); - }); - } - - #[test] - fn test_iterate_layers() { - let ds = Dataset::open(fixture("three_layer_ds.s3db")).unwrap(); - let layers = ds.layers(); - assert_eq!(layers.size_hint(), (3, Some(3))); - assert_eq!(layers.count(), 3); - } - - #[test] - fn test_owned_layers() { - let ds = Dataset::open(fixture("three_layer_ds.s3db")).unwrap(); - - assert_eq!(ds.layer_count(), 3); - - let mut layer = ds.into_layer(0).unwrap(); - - { - let feature = layer.features().next().unwrap(); - assert_eq!(feature.field("id").unwrap(), None); - } - - // convert back to dataset - - let ds = layer.into_dataset(); - assert_eq!(ds.layer_count(), 3); - } - - #[test] - fn test_iterate_owned_features() { - with_owned_layer("roads.geojson", |layer| { - let mut features = layer.owned_features(); - - assert_eq!(features.as_mut().size_hint(), (21, Some(21))); - assert_eq!(features.count(), 21); - - // get back layer - - let layer = features.into_layer(); - assert_eq!(layer.name(), "roads"); - }); - } - - #[test] - fn test_fid() { - with_feature("roads.geojson", 236194095, |feature| { - assert_eq!(feature.fid(), Some(236194095)); - }); - } - - #[test] - fn test_string_field() { - with_feature("roads.geojson", 236194095, |feature| { - assert_eq!( - feature.field("highway").unwrap().unwrap().into_string(), - Some("footway".to_string()) - ); - }); - with_features("roads.geojson", |features| { - assert_eq!( - features - .filter(|field| { - let highway = field.field("highway").unwrap().unwrap().into_string(); - highway == Some("residential".to_string()) - }) - .count(), - 2 - ); - }); - } - - #[test] - fn test_null_field() { - with_features("null_feature_fields.geojson", |mut features| { - let feature = features.next().unwrap(); - assert_eq!( - feature.field("some_int").unwrap(), - Some(FieldValue::IntegerValue(0)) - ); - assert_eq!(feature.field("some_string").unwrap(), None); - }); - } - - #[test] - fn test_string_list_field() { - with_features("soundg.json", |mut features| { - let feature = features.next().unwrap(); - assert_eq!( - feature.field("a_string_list").unwrap().unwrap(), - FieldValue::StringListValue(vec![ - String::from("a"), - String::from("list"), - String::from("of"), - String::from("strings") - ]) - ); - }); - } - - #[test] - fn test_set_string_list_field() { - with_features("soundg.json", |mut features| { - let feature = features.next().unwrap(); - let value = FieldValue::StringListValue(vec![ - String::from("the"), - String::from("new"), - String::from("strings"), - ]); - feature.set_field("a_string_list", &value).unwrap(); - assert_eq!(feature.field("a_string_list").unwrap().unwrap(), value); - }); - } - - #[test] - #[allow(clippy::float_cmp)] - fn test_get_field_as_x_by_name() { - with_features("roads.geojson", |mut features| { - let feature = features.next().unwrap(); - - assert_eq!( - feature.field_as_string_by_name("highway").unwrap(), - Some("footway".to_owned()) - ); - - assert_eq!( - feature.field_as_string_by_name("sort_key").unwrap(), - Some("-9".to_owned()) - ); - assert_eq!( - feature.field_as_integer_by_name("sort_key").unwrap(), - Some(-9) - ); - assert_eq!( - feature.field_as_integer64_by_name("sort_key").unwrap(), - Some(-9) - ); - assert_eq!( - feature.field_as_double_by_name("sort_key").unwrap(), - Some(-9.) - ); - - // test failed conversions - assert_eq!( - feature.field_as_integer_by_name("highway").unwrap(), - Some(0) - ); - assert_eq!( - feature.field_as_integer64_by_name("highway").unwrap(), - Some(0) - ); - assert_eq!( - feature.field_as_double_by_name("highway").unwrap(), - Some(0.) - ); - - // test nulls - assert_eq!(feature.field_as_string_by_name("railway").unwrap(), None); - assert_eq!(feature.field_as_integer_by_name("railway").unwrap(), None); - assert_eq!(feature.field_as_integer64_by_name("railway").unwrap(), None); - assert_eq!(feature.field_as_double_by_name("railway").unwrap(), None); - - assert!(matches!( - feature.field_as_string_by_name("not_a_field").unwrap_err(), - GdalError::InvalidFieldName { - field_name, - method_name: "OGR_F_GetFieldIndex", - } - if field_name == "not_a_field" - )); - }); - } - - #[test] - #[allow(clippy::float_cmp)] - fn test_get_field_as_x() { - with_features("roads.geojson", |mut features| { - let feature = features.next().unwrap(); - - let highway_field = 6; - let railway_field = 5; - let sort_key_field = 1; - - assert_eq!( - feature.field_as_string(highway_field).unwrap(), - Some("footway".to_owned()) - ); - - assert_eq!( - feature.field_as_string(sort_key_field).unwrap(), - Some("-9".to_owned()) - ); - assert_eq!(feature.field_as_integer(sort_key_field).unwrap(), Some(-9)); - assert_eq!( - feature.field_as_integer64(sort_key_field).unwrap(), - Some(-9) - ); - assert_eq!(feature.field_as_double(sort_key_field).unwrap(), Some(-9.)); - - // test failed conversions - assert_eq!(feature.field_as_integer(highway_field).unwrap(), Some(0)); - assert_eq!(feature.field_as_integer64(highway_field).unwrap(), Some(0)); - assert_eq!(feature.field_as_double(highway_field).unwrap(), Some(0.)); - - // test nulls - assert_eq!(feature.field_as_string(railway_field).unwrap(), None); - assert_eq!(feature.field_as_integer(railway_field).unwrap(), None); - assert_eq!(feature.field_as_integer64(railway_field).unwrap(), None); - assert_eq!(feature.field_as_double(railway_field).unwrap(), None); - - // test error - assert!(matches!( - feature.field_as_string(23).unwrap_err(), - GdalError::InvalidFieldIndex { - index: 23, - method_name: "field_as_string", - } - )); - }); - } - - #[test] - fn test_get_field_as_datetime() { - use chrono::{FixedOffset, TimeZone}; - - let hour_secs = 3600; - - with_features("points_with_datetime.json", |mut features| { - let feature = features.next().unwrap(); - - let dt = FixedOffset::east_opt(-5 * hour_secs) - .unwrap() - .with_ymd_and_hms(2011, 7, 14, 19, 43, 37) - .unwrap(); - - let d = FixedOffset::east_opt(0) - .unwrap() - .with_ymd_and_hms(2018, 1, 4, 0, 0, 0) - .unwrap(); - - assert_eq!(feature.field_as_datetime_by_name("dt").unwrap(), Some(dt)); - - assert_eq!(feature.field_as_datetime(0).unwrap(), Some(dt)); - - assert_eq!(feature.field_as_datetime_by_name("d").unwrap(), Some(d)); - - assert_eq!(feature.field_as_datetime(1).unwrap(), Some(d)); - }); - - with_features("roads.geojson", |mut features| { - let feature = features.next().unwrap(); - - let railway_field = 5; - - // test null - assert_eq!(feature.field_as_datetime_by_name("railway").unwrap(), None); - assert_eq!(feature.field_as_datetime(railway_field).unwrap(), None); - - // test error - assert!(matches!( - feature - .field_as_datetime_by_name("not_a_field") - .unwrap_err(), - GdalError::InvalidFieldName { - field_name, - method_name: "OGR_F_GetFieldIndex", - } if field_name == "not_a_field" - )); - assert!(matches!( - feature.field_as_datetime(23).unwrap_err(), - GdalError::InvalidFieldIndex { - index: 23, - method_name: "field_as_datetime", - } - )); - }); - } - - #[test] - fn test_field_in_layer() { - ds_with_layer("three_layer_ds.s3db", "layer_0", |mut layer| { - let feature = layer.features().next().unwrap(); - assert_eq!(feature.field("id").unwrap(), None); - }); - } - - #[test] - fn test_int_list_field() { - with_features("soundg.json", |mut features| { - let feature = features.next().unwrap(); - assert_eq!( - feature.field("an_int_list").unwrap().unwrap(), - FieldValue::IntegerListValue(vec![1, 2]) - ); - }); - } - - #[test] - fn test_set_int_list_field() { - with_features("soundg.json", |mut features| { - let feature = features.next().unwrap(); - let value = FieldValue::IntegerListValue(vec![3, 4, 5]); - feature.set_field("an_int_list", &value).unwrap(); - assert_eq!(feature.field("an_int_list").unwrap().unwrap(), value); - }); - } - - #[test] - fn test_real_list_field() { - with_features("soundg.json", |mut features| { - let feature = features.next().unwrap(); - assert_eq!( - feature.field("a_real_list").unwrap().unwrap(), - FieldValue::RealListValue(vec![0.1, 0.2]) - ); - }); - } - - #[test] - fn test_set_real_list_field() { - with_features("soundg.json", |mut features| { - let feature = features.next().unwrap(); - let value = FieldValue::RealListValue(vec![2.5, 3.0, 4.75]); - feature.set_field("a_real_list", &value).unwrap(); - assert_eq!(feature.field("a_real_list").unwrap().unwrap(), value); - }); - } - - #[test] - fn test_long_list_field() { - with_features("soundg.json", |mut features| { - let feature = features.next().unwrap(); - assert_eq!( - feature.field("a_long_list").unwrap().unwrap(), - FieldValue::Integer64ListValue(vec![5000000000, 6000000000]) - ); - }); - } - - #[test] - fn test_set_long_list_field() { - with_features("soundg.json", |mut features| { - let feature = features.next().unwrap(); - let value = FieldValue::Integer64ListValue(vec![7000000000, 8000000000]); - feature.set_field("a_long_list", &value).unwrap(); - assert_eq!(feature.field("a_long_list").unwrap().unwrap(), value); - }); - } - - #[test] - fn test_float_field() { - with_feature("roads.geojson", 236194095, |feature| { - assert_almost_eq( - feature - .field("sort_key") - .unwrap() - .unwrap() - .into_real() - .unwrap(), - -9.0, - ); - }); - } - - #[test] - fn test_missing_field() { - with_feature("roads.geojson", 236194095, |feature| { - assert!(feature.field("no such field").is_err()); - }); - } - - #[test] - fn test_geom_accessors() { - with_feature("roads.geojson", 236194095, |feature| { - let geom = feature.geometry().unwrap(); - assert_eq!(geom.geometry_type(), OGRwkbGeometryType::wkbLineString); - let coords = geom.get_point_vec(); - assert_eq!( - coords, - [ - (26.1019276, 44.4302748, 0.0), - (26.1019382, 44.4303191, 0.0), - (26.1020002, 44.4304202, 0.0) - ] - ); - assert_eq!(geom.geometry_count(), 0); - - let geom = feature.geometry_by_index(0).unwrap(); - assert_eq!(geom.geometry_type(), OGRwkbGeometryType::wkbLineString); - assert!(feature.geometry_by_index(1).is_err()); - let geom = feature.geometry_by_name(""); - assert!(geom.is_ok()); - let geom = feature.geometry_by_name("").unwrap(); - assert_eq!(geom.geometry_type(), OGRwkbGeometryType::wkbLineString); - assert!(feature.geometry_by_name("FOO").is_err()); - }); - } - - #[test] - fn test_ring_points() { - let mut ring = Geometry::empty(wkbLinearRing).unwrap(); - ring.add_point_2d((1179091.1646903288, 712782.8838459781)); - ring.add_point_2d((1161053.0218226474, 667456.2684348812)); - ring.add_point_2d((1214704.933941905, 641092.8288590391)); - ring.add_point_2d((1228580.428455506, 682719.3123998424)); - ring.add_point_2d((1218405.0658121984, 721108.1805541387)); - ring.add_point_2d((1179091.1646903288, 712782.8838459781)); - assert!(!ring.is_empty()); - assert_eq!(ring.get_point_vec().len(), 6); - let mut poly = Geometry::empty(wkbPolygon).unwrap(); - poly.add_geometry(ring.to_owned()).unwrap(); - // Points are in ring, not containing geometry. - // NB: In Python SWIG bindings, `GetPoints` is fallible. - assert!(poly.get_point_vec().is_empty()); - assert_eq!(poly.geometry_count(), 1); - let ring_out = poly.get_geometry(0); - // NB: `wkb()` shows it to be a `LINEARRING`, but returned type is LineString - assert_eq!(ring_out.geometry_type(), wkbLineString); - assert!(!&ring_out.is_empty()); - assert_eq!(ring.get_point_vec(), ring_out.get_point_vec()); - } - - #[test] - fn test_get_inner_points() { - let geom = Geometry::bbox(0., 0., 1., 1.).unwrap(); - assert!(!geom.is_empty()); - assert_eq!(geom.geometry_count(), 1); - assert!(geom.area() > 0.); - assert_eq!(geom.geometry_type(), OGRwkbGeometryType::wkbPolygon); - assert!(geom.json().unwrap().contains("Polygon")); - let inner = geom.get_geometry(0); - let points = inner.get_point_vec(); - assert!(!points.is_empty()); - } - - #[test] - fn test_wkt() { - with_feature("roads.geojson", 236194095, |feature| { - let wkt = feature.geometry().unwrap().wkt().unwrap(); - let wkt_ok = format!( - "{}{}", - "LINESTRING (26.1019276 44.4302748,", - "26.1019382 44.4303191,26.1020002 44.4304202)" - ); - assert_eq!(wkt, wkt_ok); - }); - } - - #[test] - fn test_json() { - with_feature("roads.geojson", 236194095, |feature| { - let json = feature.geometry().unwrap().json(); - let json_ok = format!( - "{}{}{}{}", - "{ \"type\": \"LineString\", \"coordinates\": [ ", - "[ 26.1019276, 44.4302748 ], ", - "[ 26.1019382, 44.4303191 ], ", - "[ 26.1020002, 44.4304202 ] ] }" - ); - assert_eq!(json.unwrap(), json_ok); - }); - } - - #[test] - fn test_schema() { - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - let layer = ds.layer(0).unwrap(); - // The layer name is "roads" in GDAL 2.2 - assert!(layer.name() == "OGRGeoJSON" || layer.name() == "roads"); - let name_list = layer - .defn() - .fields() - .map(|f| (f.name(), f.field_type())) - .collect::>(); - let ok_names_types = vec![ - ("kind", OGRFieldType::OFTString), - ("sort_key", OGRFieldType::OFTReal), - ("is_link", OGRFieldType::OFTString), - ("is_tunnel", OGRFieldType::OFTString), - ("is_bridge", OGRFieldType::OFTString), - ("railway", OGRFieldType::OFTString), - ("highway", OGRFieldType::OFTString), - ] - .iter() - .map(|s| (s.0.to_string(), s.1)) - .collect::>(); - assert_eq!(name_list, ok_names_types); - } - - #[test] - fn test_geom_fields() { - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - let layer = ds.layer(0).unwrap(); - let name_list = layer - .defn() - .geom_fields() - .map(|f| (f.name(), f.field_type())) - .collect::>(); - let ok_names_types = vec![("", OGRwkbGeometryType::wkbLineString)] - .iter() - .map(|s| (s.0.to_string(), s.1)) - .collect::>(); - assert_eq!(name_list, ok_names_types); - - let geom_field = layer.defn().geom_fields().next().unwrap(); - let spatial_ref2 = SpatialRef::from_epsg(4326).unwrap(); - #[cfg(major_ge_3)] - spatial_ref2.set_axis_mapping_strategy(0); - - assert!(geom_field.spatial_ref().unwrap() == spatial_ref2); - } - - #[test] - fn test_get_layer_by_name() { - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - // The layer name is "roads" in GDAL 2.2 - if let Ok(layer) = ds.layer_by_name("OGRGeoJSON") { - assert_eq!(layer.name(), "OGRGeoJSON"); - } - if let Ok(layer) = ds.layer_by_name("roads") { - assert_eq!(layer.name(), "roads"); - } - } - - #[test] - fn test_create_bbox() { - let bbox = Geometry::bbox(-27., 33., 52., 85.).unwrap(); - assert_eq!(bbox.json().unwrap(), "{ \"type\": \"Polygon\", \"coordinates\": [ [ [ -27.0, 85.0 ], [ 52.0, 85.0 ], [ 52.0, 33.0 ], [ -27.0, 33.0 ], [ -27.0, 85.0 ] ] ] }"); - } - - #[test] - fn test_spatial_filter() { - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - let mut layer = ds.layer(0).unwrap(); - assert_eq!(layer.features().count(), 21); - - let bbox = Geometry::bbox(26.1017, 44.4297, 26.1025, 44.4303).unwrap(); - layer.set_spatial_filter(&bbox); - assert_eq!(layer.features().count(), 7); - - layer.clear_spatial_filter(); - assert_eq!(layer.features().count(), 21); - - // test filter as rectangle - layer.set_spatial_filter_rect(26.1017, 44.4297, 26.1025, 44.4303); - assert_eq!(layer.features().count(), 7); - } - - #[test] - fn test_convex_hull() { - let star = "POLYGON ((0 1,3 1,1 3,1.5 0.0,2 3,0 1))"; - let hull = "POLYGON ((1.5 0.0,0 1,1 3,2 3,3 1,1.5 0.0))"; - assert_eq!( - Geometry::from_wkt(star) - .unwrap() - .convex_hull() - .unwrap() - .wkt() - .unwrap(), - hull - ); - } - - #[test] - #[cfg(any(all(major_is_2, minor_ge_1), major_ge_3))] - fn test_delaunay_triangulation() -> Result<()> { - let square = Geometry::from_wkt("POLYGON ((0 1,1 1,1 0,0 0,0 1))")?; - let triangles = Geometry::from_wkt( - "GEOMETRYCOLLECTION (POLYGON ((0 1,0 0,1 0,0 1)),POLYGON ((0 1,1 0,1 1,0 1)))", - )?; - assert_eq!(square.delaunay_triangulation(None)?, triangles); - Ok(()) - } - - #[test] - fn test_simplify() -> Result<()> { - let line = Geometry::from_wkt("LINESTRING(1.2 0.19,1.63 0.58,1.98 0.65,2.17 0.89)")?; - let triangles = Geometry::from_wkt("LINESTRING (1.2 0.19,2.17 0.89)")?; - assert_eq!(line.simplify(0.5)?, triangles); - Ok(()) - } - - #[test] - fn test_simplify_preserve_topology() -> Result<()> { - let donut = Geometry::from_wkt( - "POLYGON ((20 35,10 30,10 10,30 5,45 20,20 35),(30 20,20 15,20 25,30 20))", - )?; - let triangles = Geometry::from_wkt( - "POLYGON ((20 35,10 10,30 5,45 20,20 35),(30 20,20 15,20 25,30 20))", - )?; - assert_eq!(donut.simplify_preserve_topology(100.0)?, triangles); - Ok(()) - } - - #[test] - fn test_write_features() { - use std::fs; - - { - let driver = DriverManager::get_driver_by_name("GeoJSON").unwrap(); - let mut ds = driver - .create_vector_only(fixture("output.geojson")) - .unwrap(); - let mut layer = ds.create_layer(Default::default()).unwrap(); - layer - .create_defn_fields(&[ - ("Name", OGRFieldType::OFTString), - ("Value", OGRFieldType::OFTReal), - ("Int_value", OGRFieldType::OFTInteger), - ]) - .unwrap(); - layer - .create_feature_fields( - Geometry::from_wkt("POINT (1 2)").unwrap(), - &["Name", "Value", "Int_value"], - &[ - FieldValue::StringValue("Feature 1".to_string()), - FieldValue::RealValue(45.78), - FieldValue::IntegerValue(1), - ], - ) - .unwrap(); - // dataset is closed here - } - - { - let ds = Dataset::open(fixture("output.geojson")).unwrap(); - let mut layer = ds.layer(0).unwrap(); - let ft = layer.features().next().unwrap(); - assert_eq!(ft.geometry().unwrap().wkt().unwrap(), "POINT (1 2)"); - assert_eq!( - ft.field("Name").unwrap().unwrap().into_string(), - Some("Feature 1".to_string()) - ); - assert_eq!(ft.field("Value").unwrap().unwrap().into_real(), Some(45.78)); - assert_eq!(ft.field("Int_value").unwrap().unwrap().into_int(), Some(1)); - } - fs::remove_file(fixture("output.geojson")).unwrap(); - } - - #[test] - fn test_features_reset() { - with_layer("roads.geojson", |mut layer| { - assert_eq!(layer.features().count(), layer.features().count(),); - }); - } - - #[test] - fn test_set_attribute_filter() { - with_layer("roads.geojson", |mut layer| { - // check number without calling any function - assert_eq!(layer.features().count(), 21); - - // check if clearing does not corrupt anything - layer.clear_attribute_filter(); - assert_eq!(layer.features().count(), 21); - - // apply actual filter - layer.set_attribute_filter("highway = 'primary'").unwrap(); - - assert_eq!(layer.features().count(), 1); - assert_eq!( - layer - .features() - .next() - .unwrap() - .field_as_string_by_name("highway") - .unwrap() - .unwrap(), - "primary" - ); - - // clearing and check again - layer.clear_attribute_filter(); - - assert_eq!(layer.features().count(), 21); - - { - let _nolog = SuppressGDALErrorLog::new(); - // force error - assert!(matches!( - layer.set_attribute_filter("foo = bar").unwrap_err(), - GdalError::OgrError { - err: gdal_sys::OGRErr::OGRERR_CORRUPT_DATA, - method_name: "OGR_L_SetAttributeFilter", - } - )); - } - }); - } - - #[test] - fn test_set_feature() { - let ds_options = DatasetOptions { - open_flags: GdalOpenFlags::GDAL_OF_UPDATE, - ..DatasetOptions::default() - }; - let tmp_file = TempFixture::empty("test.s3db"); - std::fs::copy(fixture("three_layer_ds.s3db"), &tmp_file).unwrap(); - let ds = Dataset::open_ex(&tmp_file, ds_options).unwrap(); - let mut layer = ds.layer(0).unwrap(); - let fids: Vec = layer.features().map(|f| f.fid().unwrap()).collect(); - let feature = layer.feature(fids[0]).unwrap(); - // to original value of the id field in fid 0 is null; we will set it to 1. - feature.set_field_integer("id", 1).ok(); - layer.set_feature(feature).ok(); - - // now we check that the field is 1. - let ds = Dataset::open(&tmp_file).unwrap(); - let layer = ds.layer(0).unwrap(); - let feature = layer.feature(fids[0]).unwrap(); - let value = feature.field("id").unwrap().unwrap().into_int().unwrap(); - assert_eq!(value, 1); - } -} diff --git a/src/vector/vector_tests/sql.rs b/src/vector/vector_tests/sql.rs deleted file mode 100644 index faa2d2257..000000000 --- a/src/vector/vector_tests/sql.rs +++ /dev/null @@ -1,135 +0,0 @@ -use std::collections::HashSet; - -use crate::test_utils::SuppressGDALErrorLog; -use crate::{ - test_utils::fixture, - vector::{sql, Geometry, LayerAccess}, - Dataset, -}; - -#[test] -fn test_sql() { - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - let query = "SELECT kind, is_bridge, highway FROM roads WHERE highway = 'pedestrian'"; - let mut result_set = ds - .execute_sql(query, None, sql::Dialect::DEFAULT) - .unwrap() - .unwrap(); - - let field_names: HashSet<_> = result_set - .defn() - .fields() - .map(|field| field.name()) - .collect(); - - let mut correct_field_names = HashSet::new(); - correct_field_names.insert("kind".into()); - correct_field_names.insert("is_bridge".into()); - correct_field_names.insert("highway".into()); - - assert_eq!(correct_field_names, field_names); - assert_eq!(10, result_set.feature_count()); - - for feature in result_set.features() { - let highway = feature - .field("highway") - .unwrap() - .unwrap() - .into_string() - .unwrap(); - - assert_eq!("pedestrian", highway); - } -} - -#[test] -fn test_sql_with_spatial_filter() { - let query = "SELECT * FROM roads WHERE highway = 'pedestrian'"; - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - let bbox = Geometry::bbox(26.1017, 44.4297, 26.1025, 44.4303).unwrap(); - let mut result_set = ds - .execute_sql(query, Some(&bbox), sql::Dialect::DEFAULT) - .unwrap() - .unwrap(); - - assert_eq!(2, result_set.feature_count()); - let mut correct_fids = HashSet::new(); - correct_fids.insert(252725993); - correct_fids.insert(23489656); - - let mut fids = HashSet::new(); - for feature in result_set.features() { - let highway = feature - .field("highway") - .unwrap() - .unwrap() - .into_string() - .unwrap(); - - assert_eq!("pedestrian", highway); - fids.insert(feature.fid().unwrap()); - } - - assert_eq!(correct_fids, fids); -} - -#[test] -fn test_sql_with_dialect() { - let query = "SELECT * FROM roads WHERE highway = 'pedestrian' and NumPoints(GEOMETRY) = 3"; - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - let bbox = Geometry::bbox(26.1017, 44.4297, 26.1025, 44.4303).unwrap(); - let mut result_set = ds - .execute_sql(query, Some(&bbox), sql::Dialect::SQLITE) - .unwrap() - .unwrap(); - - assert_eq!(1, result_set.feature_count()); - let mut features: Vec<_> = result_set.features().collect(); - let feature = features.pop().unwrap(); - let highway = feature - .field("highway") - .unwrap() - .unwrap() - .into_string() - .unwrap(); - - assert_eq!("pedestrian", highway); -} - -#[test] -fn test_sql_empty_result() { - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - let query = "SELECT kind, is_bridge, highway FROM roads WHERE highway = 'jazz hands 👐'"; - let mut result_set = ds - .execute_sql(query, None, sql::Dialect::DEFAULT) - .unwrap() - .unwrap(); - assert_eq!(0, result_set.feature_count()); - assert_eq!(0, result_set.features().count()); -} - -#[test] -fn test_sql_no_result() { - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - let query = "ALTER TABLE roads ADD COLUMN fun integer"; - let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT).unwrap(); - assert!(result_set.is_none()); -} - -#[test] -fn test_sql_bad_query() { - let _nolog = SuppressGDALErrorLog::new(); - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - - let query = "SELECT nope FROM roads"; - let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT); - assert!(result_set.is_err()); - - let query = "SELECT nope FROM"; - let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT); - assert!(result_set.is_err()); - - let query = "SELECT ninetynineredballoons(highway) FROM roads"; - let result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT); - assert!(result_set.is_err()); -}