Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend the existing possibilities of writing ogr datasets. #31

Merged
merged 11 commits into from
Jan 17, 2017
Merged

Extend the existing possibilities of writing ogr datasets. #31

merged 11 commits into from
Jan 17, 2017

Conversation

mthh
Copy link
Member

@mthh mthh commented Jan 12, 2017

This PR (related to #30) aims to extend the existing possibilities of writing ogr datasets.

  • It allows to set a definition schema (name, type, precision and width for each field) on a layer.
  • It still allows to create a feature directly from geometry but now also with the various values to set on that feature.
  • It adds three functions on the Field struct, allowing to retrieve the width, type and precision when reading a layer.
  • It adds an IntergerValue to the FieldValue enum (allowing to get/set integer values too).
  • It implements the Clone trait on the Geometry struct

I honestly don't know if I did it the right way or if there is too much redundancy in the way i implemented that (there is two commented examples (read_write_ogr.rs and write_ogr.rs) to facilitate your potential reviews).
Let me know if you have advices/requirements on how it should be designed (I should have asked for some guidance before doing it..!) and/or if it combines badly with the existing functionalities allowing to create/write ogr features.

For example in src/vector/test/mod.rs a dataset is currently written like this :

{
	let driver = Driver::get("GeoJSON").unwrap();
	let mut ds = driver.create(fixture!("output.geojson")).unwrap();
	let mut layer = ds.create_layer().unwrap();
	layer.create_feature(Geometry::from_wkt("POINT (1 2)").unwrap()).unwrap();
	// dataset is closed here
}

Now it could be possible to write :

{
	let driver = Driver::get("GeoJSON").unwrap();
	let mut ds = driver.create(fixture!("output.geojson")).unwrap();
	let mut layer = ds.create_layer().unwrap();
	layer.create_defn_fields(&[("Name", OFT_STRING), ("Value", OFT_REAL)]);
	layer.create_feature_fields(
		Geometry::from_wkt("POINT (1 2)").unwrap(),
		&["Name", "Value"],
		&[FieldValue::StringValue("Feature 1".to_string()), FieldValue::RealValue(45.78)]
		);
	// dataset is closed here
}

pub fn OGRFree(ptr: *mut c_void);
pub fn VSIFree(ptr: *mut c_void);
}

pub const OFT_INTEGER: c_int = 0;
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be nice to change the OFT types into an enum and move it into ogr_enums.rs. This way all the FFI methods could take OgrFieldType instead of c_int as parameter. (link to the OGR enum)

Copy link
Member Author

@mthh mthh Jan 15, 2017

Choose a reason for hiding this comment

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

I was thinking too about moving OFT types into an enum, so no problem, I will do it.

}

pub fn set_field_string(&self, field_name: &str, value: &str){
let c_str_field_name = CString::new(field_name).unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

If it is possible please try not to use unwrap in library methods. Methods like this should return Result<()>.
I think we should add the returned error in errors.rs if we don't handle it already.

Copy link
Member Author

Choose a reason for hiding this comment

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

Alright for returning Result on these methods.
However is it possible here to not use unwrap() ?

Copy link
Member

@jdroenner jdroenner Jan 15, 2017

Choose a reason for hiding this comment

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

Error handling in rust is a bit difficult. Where is a lot about it in the book.

To make this easier rust-gdal uses the error-chain crate. We already have some wrapping errors in errors.rs:

FfiNulError(::std::ffi::NulError);
StrUtf8Error(::std::str::Utf8Error);

To handle the Ok or return the (wrapped) Err you can use the try!{} macro or ?. In this case all you need to do is to replace unwrap() with ?: let c_str_field_name = CString::new(field_name)?;
CString::new returns Result<CString, ::std::ffi::NulError>. In the error case ? will convert NulError into the wrapping error (FfiNulError) and return it.

@jdroenner
Copy link
Member

@mthh this looks really nice :) I can have a closer look this weekend.

Copy link
Member

@frewsxcv frewsxcv left a comment

Choose a reason for hiding this comment

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

I added a bunch of nits related to naming, they aren't big issues.

@@ -26,6 +29,11 @@ impl Defn {
total: total
};
}

pub fn new_from_layer(lyr: &Layer) -> Defn {
Copy link
Member

Choose a reason for hiding this comment

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

This should be just from_layer if you wanted to match conventions in std.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, I will change it.

@@ -66,4 +74,16 @@ impl<'a> Field<'a> {
let rv = unsafe { ogr::OGR_Fld_GetNameRef(self.c_field_defn) };
return _string(rv);
}

pub fn get_type(&'a self) -> i32 {
Copy link
Member

Choose a reason for hiding this comment

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

This should be just type if you wanted to match conventions in std.

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess I can't use type as a function name as it is keyword in rust (that's why I chose get_type in replacement and thus to be consistent over the methods of this object I also used get_width and get_precision). Any suggestions ?

Copy link
Member

Choose a reason for hiding this comment

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

As this will return a OGRFieldType you could change the method name to field_type.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok!

unsafe { ogr::OGR_Fld_GetType(self.c_field_defn) }
}

pub fn get_width(&'a self) -> i32 {
Copy link
Member

Choose a reason for hiding this comment

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

This should be just width if you wanted to match conventions in std.

unsafe { ogr::OGR_Fld_GetWidth(self.c_field_defn) }
}

pub fn get_precision(&'a self) -> i32 {
Copy link
Member

Choose a reason for hiding this comment

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

This should be just precision if you wanted to match conventions in std.

@@ -87,4 +150,12 @@ impl FieldValue {
_ => panic!("not a RealValue")
}
}

/// Interpret the value as `i32`. Panics if the value is something else.
pub fn as_int(self) -> i32 {
Copy link
Member

Choose a reason for hiding this comment

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

Methods that consume self are usually named into_<type>, so in this case, it would be into_int, but I notice above that the other methods on this structure use as_<type>.

Copy link
Member Author

Choose a reason for hiding this comment

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

Should I also change the name of the two previous methods (as_real -> to_real and as_string to_string ?)

Copy link
Member

Choose a reason for hiding this comment

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

i guess this would be the right thing to do. Also i think these methods should return an Option instead of panicking...

Copy link
Member Author

@mthh mthh Jan 15, 2017

Choose a reason for hiding this comment

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

I made the other changes but not this last one (returning an Option instead of panicking), because I didn't know how to handle this when using a FieldValue to set a field :

pub fn set_field(&self, field_name: &str, type_value: OGRFieldType, value: FieldValue) -> Result<()> {
    match type_value {
        OGRFieldType::OFTReal => self.set_field_double(field_name, value.to_real()),

like here, when using value.to_real(). Is the use of is_some ok ? Or should I look for a way to handle this nicely without using is_some then unwrap ? (i will look into the error_chain crate documentation then make the changes).

Copy link
Member

@jdroenner jdroenner left a comment

Choose a reason for hiding this comment

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

I really appreciate this contribution. It adds so much functionality to rust-gdal.
There are some unwraps left and i think we need more error types :)


pub fn set_field_string(&self, field_name: &str, value: &str) -> Result<()> {
let c_str_field_name = CString::new(field_name)?;
let c_str_value = CString::new(value).unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

unwrap to ? -> CString::new(value)?;

pub fn set_field_string(&self, field_name: &str, value: &str) -> Result<()> {
let c_str_field_name = CString::new(field_name)?;
let c_str_value = CString::new(value).unwrap();
let idx = unsafe { ogr::OGR_F_GetFieldIndex(self.c_feature, c_str_field_name.as_ptr())};
Copy link
Member

Choose a reason for hiding this comment

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

OGR_F_GetFieldIndex will return -1 if a field name does not exist. I guess we need a new error. Maybe InvalidFieldName ?


pub fn set_field(&self, field_name: &str, type_value: OGRFieldType, value: FieldValue) -> Result<()> {
match type_value {
OGRFieldType::OFTReal => self.set_field_double(field_name, value.to_real()),
Copy link
Member

Choose a reason for hiding this comment

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

Regarding the conversion of FieldValue to the matching OGRFieldType:
As i see it this method fails if FieldValue and OGRFieldType don't match. So at this point we don't need the OGRFieldType at all... We can match based on the FieldValue:

 pub enum FieldValue {
    IntegerValue(i32),
     StringValue(String),
     RealValue(f64),
 }

pub fn set_field(&self, field_name: &str,  value: FieldValue) -> Result<()> {
      match value {
         FieldValue::RealValue(value) => self.set_field_double(field_name, value)?,
         ...

Also the set_field_double method returns a result which should be used/handled here (using the ? operator).

Copy link
Member Author

Choose a reason for hiding this comment

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

Shouldn't the Result from set_field_double (and others) just be passed to the caller of set_field like this?

pub fn set_field(&self, field_name: &str,  value: FieldValue) -> Result<()> {
      match value {
         FieldValue::RealValue(value) => self.set_field_double(field_name, value),
         FieldValue::StringValue(value) => self.set_field_string(field_name, value.as_str()),
         FieldValue::IntegerValue(value) => self.set_field_integer(field_name, value)
     }
 }

This way, with an invalid field name we get the ErrKind::InvalidFieldName error from the set_field_double/string/.. function, and we can't enter an invalid FieldValue due to the type checking.

Copy link
Member

Choose a reason for hiding this comment

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

Yes you are right this should be perfectly fine. I was thinking to complicated.

OGRFieldType::OFTReal => self.set_field_double(field_name, value.to_real()),
OGRFieldType::OFTString => self.set_field_string(field_name, value.to_string().as_str()),
OGRFieldType::OFTInteger => self.set_field_integer(field_name, value.to_int()),
_ => Err(ErrorKind::InvalidInput("set_field").into())
Copy link
Member

Choose a reason for hiding this comment

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

Maybe a better name for the error is InvalidFieldType or 'UnhandledFieldType'.

}

pub fn set_geometry(&mut self, geom: Geometry) -> Result<()> {
self.geometry = geom;
Copy link
Member

Choose a reason for hiding this comment

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

What happens if the following method call fails? Wouldn't it be more safe if this is done after the GDAL call and before Ok(()) ?

let val = &values[i];
match val {
&FieldValue::StringValue(ref v) => {
unsafe { ogr::OGR_F_SetFieldString(c_feature, idx, CString::new(v.as_str()).unwrap().as_ptr()) };
Copy link
Member

Choose a reason for hiding this comment

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

unwrap to ?


impl FieldDefn {
pub fn new(name: &str, field_type: ogr_enums::OGRFieldType) -> FieldDefn {
let c_str = CString::new(name).unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

Let the method return a Result and change unwrap to ?

}
pub fn add_to_layer(&self, layer: &Layer) {
let rv = unsafe { ogr::OGR_L_CreateField(layer.gdal_object_ptr(), self.c_obj, 1) };
assert_eq!(rv, ogr_enums::OGRErr::OGRERR_NONE);
Copy link
Member

Choose a reason for hiding this comment

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

the last unwrap/assert :)

InvalidInput(method_name: &'static str) {
description("Invalid input")
display("Invalid input : {}", method_name)
}
Copy link
Member

Choose a reason for hiding this comment

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

Is there a more descriptive name for this error (maybe InvalidFieldType)?
Also i added the method name to the errors related to the GDAL FFI calls to enable users to look up methods in the GDAL API. I'm not sure if it is needed here.

let rv = unsafe { ogr::OGR_F_GetFieldAsInteger(self.c_feature, field_id) };
return Some(FieldValue::IntegerValue(rv as i32));
},
_ => panic!("Unknown field type {:?}", field_type)
Copy link
Member

Choose a reason for hiding this comment

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

There should be an error for this :) . Maybe UnhandledFieldType ?

@mthh
Copy link
Member Author

mthh commented Jan 16, 2017

Many thanks for the detailed and instructive review!! I made most of the changes but i'm still unsure about 1 or 2 things.
I'll push it on github it in the day.

@mthh
Copy link
Member Author

mthh commented Jan 16, 2017

I guess I modified the various things discussed, except the clone method for which I did not know how to handle errors that might occur.

@jdroenner
Copy link
Member

This is great!
I think we could merge this and create an issue for Geometry::clone. We may need to change some things in the Geometry implementation to solve this.

@mthh
Copy link
Member Author

mthh commented Jan 17, 2017

Great that it can be merged !
As I plan to dig a bit into rust (and to use rust-gdal) I will keep an eye on this future issue and see if can figure something out to improve some points of the Geometry implementation.

@jdroenner jdroenner merged commit 2f4e9e7 into georust:master Jan 17, 2017
@jdroenner
Copy link
Member

Merged.
Actually there already is an issue for Geometry handling: #4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants