busstoptaktik / geodesy

Rust geodesy
Apache License 2.0
66 stars 6 forks source link

CoordinateTuple should be a trait, rather than a type #25

Closed busstoptaktik closed 1 year ago

busstoptaktik commented 2 years ago

Problem

Currently, Rust Geodesy forces input data to be in the form of a slice of CoordinateTuples, which really is forcing the coordinate representation conventions of the library on to the application code calling it.

In PROJ, I eliminated this unfortunate characteristic in 2017, by introducing the proj_trans_generic(...) function in the 4D API (cf https://github.com/OSGeo/PROJ/pull/530).

With Rust having much better support for generic programming than C, one would think that it would be possible to do something better in the case of Rust Geodesy. I believe the demo code below shows that this is actually the case.

Solution

Essentially, we do four things:

  1. Introduce the internal coordinate representation Coord, formerly known as CoordinateTuple:
    pub struct Coord(pub [f64; 4]);
  2. Introduce the minimal CoordinateTuple trait, describing anything that can be both constructed from a Coord, turned into a Coord, and copied:
    pub trait CoordinateTuple: Copy + From<Coord> + Into<Coord> {}
    impl<T: Copy + From<Coord> + Into<Coord>> CoordinateTuple for T {}
  3. Introduce the CoordinateSet trait, describing a set of any kind of CoordinateTuples. Here CoordinateSet refers to the ISO19100 concept of coordinatesets, i.e. a collection of CoordinateTuples, all of the same base type.
    pub trait CoordinateSet {
       fn len(&self) -> usize;
       fn into_coord(&self, index: usize) -> Coord;
       fn from_coord(&mut self, index: usize, coord: Coord);
    }
  4. Switch the internals to work on generic CoordinateSets, rather than slices of CoordinateTuples

Conclusion

With this done, users of Rust geodesy may implement the two tiny traits above, for their particular data type, and RG will automagically support that type.

The code below includes two examples:

  1. A generic plain coordinate set type, demoed as container for a 2-D single precision coordinate type.
  2. A generic "Point Feature" coordinate set type, combining coordinates and attributes.

I believe the demo suggests that this will make Rust Geodesy much more generally useful.

Demo

/// Demo implementation of `CoordinateTuple` as trait. Makes it possible to
/// support transformations *directly on generic user-defined data types*,
/// as in the PROJ function `proj_trans_generic(...)`.
///
/// Compared to `proj_trans_generic` (introduced by yours truly on 2017-07-07
/// in https://github.com/OSGeo/PROJ/pull/530), `CoordinateTuple` as trait
/// is cleaner, clearer, and much safer.
///
/// Thomas Knudsen, 2022-01-02

// ----------------------------------------------------------------------------

/// `Coord` is the generic 4-D, RG-internal, double precision, coordinate
/// representation formerly known as `CoordinateTuple`
#[derive(Clone, Copy, Debug, Default)]
pub struct Coord(pub [f64; 4]);

/// `coord[0]` is a bit more readable than `coord.0[0]`, so we implement
/// the indexing traits for `Coord`
impl std::ops::Index<usize> for Coord {
    type Output = f64;
    fn index(&self, i: usize) -> &Self::Output {
        &self.0[i]
    }
}

impl std::ops::IndexMut<usize> for Coord {
    fn index_mut(&mut self, i: usize) -> &mut Self::Output {
        &mut self.0[i]
    }
}

// ----------------------------------------------------------------------------

/// A `CoordinateTuple` in the new trait sense, is anything that can be both
/// 1: copied, 2: constructed from a `Coord`, and 3: turned into a `Coord`
pub trait CoordinateTuple: Copy + From<Coord> + Into<Coord> {}
impl<T: Copy + From<Coord> + Into<Coord>> CoordinateTuple for T {}

// ----------------------------------------------------------------------------

/// `Xy` is an example of a user-defined coordinate representation.
/// `Xy` implements the CoordinateTuple trait by deriving `Copy`, and
/// providing `Into<Coord>` and `From<Coord>`.
/// `Xy` is 2-D, single precision, as may make sense for e.g. small
/// scale map data, where it will provide ample resolution, while
/// weighing only 1/4 of a `Coord`
#[derive(Clone, Copy, Debug, Default)]
pub struct Xy(pub [f32; 2]);

impl Into<Coord> for Xy {
    fn into(self) -> Coord {
        Coord([self.0[0] as f64, self.0[1] as f64, 0., f64::NAN])
    }
}

impl From<Coord> for Xy {
    fn from(coord: Coord) -> Xy {
        Xy([coord.0[0] as f32, coord.0[1] as f32])
    }
}

// ----------------------------------------------------------------------------

/// In the ISO-19100 standards series sense, a `CoordinateSet` is a set of
/// `CoordinateTuple`s, all of the same type.
/// The trait implementation is surprisingly simple: We just need to be able
/// to iterate over the contents, to extract each element as a `Coord`, and
/// to re-introduce the contents of the transformed `Coord` into the original
/// data element
pub trait CoordinateSet {
    fn len(&self) -> usize;
    fn into_coord(&self, index: usize) -> Coord;
    fn from_coord(&mut self, index: usize, coord: Coord);
}

// ----------------------------------------------------------------------------

/// In the simplest case, our CoordinateSet consists of a slice of raw
/// `CoordinateTuple`s
#[derive(Debug, Default)]
struct PlainCoordinateSet<'a, C: CoordinateTuple> (
    &'a mut [C]
);

/// Since the data type `T` implements the `CoordinateTuple` trait,
/// it is extremely simple to implement the `CoordinateSet` trait,
/// as we already have the necessary `From<Coord>` and `Into<Coord>`
/// implementations.
///
/// Note that due to the from/into duality, `From<Coord>` is not
/// explicitly used: The compiler auto-derives `Into<T> for Coord`
/// when it sees `From<Coord> for T`
impl<'a, T: CoordinateTuple> CoordinateSet for PlainCoordinateSet<'a, T> {
    fn len(&self) -> usize {
        self.0.len()
    }

    fn into_coord(&self, index: usize) -> Coord {
        self.0[index].into()
    }

    fn from_coord(&mut self, index: usize, coord: Coord) {
        self.0[index] = coord.into()
    }
}

// ----------------------------------------------------------------------------

/// This is a more realistic case of implementing `CoordinateSet` for a rather
/// generic class of potential user defined types, composed from a payload and
/// a coordinate tuple.

/// The element `PointFeature`
pub struct PointFeature<T: CoordinateTuple, P> {
    pub coordinatetuple: T,
    pub payload: P
}

/// ...a set of `PointFeature`s
struct PointFeatureSet<'a, T: CoordinateTuple, P> (
    &'a mut [PointFeature<T, P>]
);

/// And the implementation of the `CoordinateSet` trait for generic `PointFeatureSet`s
impl<'a, T: CoordinateTuple, P> CoordinateSet for PointFeatureSet<'a, T, P> {
    fn len(&self) -> usize {
        self.0.len()
    }

    fn into_coord(&self, index: usize) -> Coord {
        self.0[index].coordinatetuple.into()
    }

    fn from_coord(&mut self, index: usize, coord: Coord) {
        self.0[index].coordinatetuple = coord.into()
    }
}

// ----------------------------------------------------------------------------

/// Code taking a generic `CoordinateTuple` as arg
fn print_coordinatetuple<T: CoordinateTuple>(ct: T) {
    let coord: Coord = ct.into();
    println!("{:#?}", coord);
}

/// Dummy transformation, here using a vtable
fn transformation_of_a_coordinate_set(cs: &mut dyn CoordinateSet) {
    for i in 0..cs.len() {
        let mut co = cs.into_coord(i);
        co[0] += 2.;
        cs.from_coord(i, co);
    }
}

fn main() {
    // A vector of `CoordinateTuple`s
    let mut xxxx = vec![Xy([1.,2.]), Xy([3.,4.])];
    print_coordinatetuple(xxxx[1]);

    // Now turn the vector into a `PlainCoordinateSet` (or perhaps any other
    // type, T, implementing the `CoordinateSet` trait), in order to be able
    // to transform it.
    //
    // Note that the life time of the `PlainCoordinateSet` struct need not be
    // any longer than the run time of the transformation, so we generate it
    // inline with the function call.
    transformation_of_a_coordinate_set(&mut PlainCoordinateSet(&mut xxxx));
    dbg!(&xxxx);

    // But of course we may also generate it prior to the call
    let mut xyxy = PlainCoordinateSet(&mut xxxx);
    transformation_of_a_coordinate_set(&mut xyxy);
    dbg!(&xyxy);
}
pka commented 2 years ago

I would expect a basic API like

for coord in mygeom {
    let (x, y) = transformer.transform((coord.0, coord.1));
    // ...
}

which returns transformed coordinates as a copy. Would the trait API be more efficient in transforming a list of coordinates, or is the goal to support transforming geometry coordinates in-place?

The latter case would probably look like

transformer.transform_all_coords(mygeom);

Fore me personally, the single coord API would be good enough.

busstoptaktik commented 1 year ago

Time to reconsider this, given https://github.com/georust/geo/issues/901 and https://github.com/georust/geo/issues/898

busstoptaktik commented 1 year ago

Closed with the merging of CoordinateSet #40