jsonfg 0.1.0

Types for OGC Features and Geometries JSON (JSON-FG), a GeoJSON superset with support for arbitrary coordinate reference systems, solids, and curved geometries.
Documentation
//! JSON-FG geometry types.
//!
//! The [`Geometry`] enum covers the seven GeoJSON geometry types plus the JSON-FG
//! extensions: 3D solids ([`Geometry::Polyhedron`], [`Geometry::Prism`], …) and curved
//! geometries ([`Geometry::CircularString`], [`Geometry::CurvePolygon`], …).
//!
//! The shapes follow the normative JSON Schema `geometry-object.json` of JSON-FG 1.0.

use serde::{Deserialize, Serialize};

use crate::crs::CoordRefSys;
use crate::measures::Measures;

/// A single coordinate: `[x, y]`, `[x, y, z]`, or `[x, y, z, m]` (the trailing `m` value
/// requires the *Measures* conformance class).
pub type Position = Vec<f64>;

/// A bounding box: four numbers (2D) or six numbers (3D).
pub type Bbox = Vec<f64>;

/// A JSON-FG geometry.
///
/// Serialized as a GeoJSON-style object internally tagged on the `type` member. In
/// addition to the seven GeoJSON types this includes the JSON-FG extensions for solids
/// and curves. Every geometry may optionally carry `coordRefSys`, `measures` and `bbox`
/// members (see [`GeometryMeta`]); these are omitted when empty.
///
/// Note the two sub-geometry members allowed by the schema are modelled loosely as
/// [`Geometry`] for ergonomics: the schema restricts a [`Geometry::Prism`] `base` and the
/// members of the curve/surface collections to specific sub-types. Constructing an
/// out-of-spec combination is possible in Rust but will not validate against the JSON-FG
/// schema.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Geometry {
    // -- GeoJSON geometry types ------------------------------------------------------
    Point {
        coordinates: Position,
        #[serde(flatten)]
        meta: GeometryMeta,
    },
    MultiPoint {
        coordinates: Vec<Position>,
        #[serde(flatten)]
        meta: GeometryMeta,
    },
    LineString {
        coordinates: Vec<Position>,
        #[serde(flatten)]
        meta: GeometryMeta,
    },
    MultiLineString {
        coordinates: Vec<Vec<Position>>,
        #[serde(flatten)]
        meta: GeometryMeta,
    },
    Polygon {
        coordinates: Vec<Vec<Position>>,
        #[serde(flatten)]
        meta: GeometryMeta,
    },
    MultiPolygon {
        coordinates: Vec<Vec<Vec<Position>>>,
        #[serde(flatten)]
        meta: GeometryMeta,
    },
    GeometryCollection {
        geometries: Vec<Geometry>,
        #[serde(flatten)]
        meta: GeometryMeta,
    },

    // -- JSON-FG solids --------------------------------------------------------------
    /// A solid, given as an array of shells; each shell has the structure of a
    /// `MultiPolygon`'s coordinates (an array of polygons). Coordinates are 3D.
    Polyhedron {
        coordinates: Vec<Vec<Vec<Vec<Position>>>>,
        #[serde(flatten)]
        meta: GeometryMeta,
    },
    MultiPolyhedron {
        coordinates: Vec<Vec<Vec<Vec<Vec<Position>>>>>,
        #[serde(flatten)]
        meta: GeometryMeta,
    },
    /// A prism: a base geometry extruded between the `lower` and `upper` heights.
    Prism {
        base: Box<Geometry>,
        #[serde(skip_serializing_if = "Option::is_none", default)]
        lower: Option<f64>,
        upper: f64,
        #[serde(flatten)]
        meta: GeometryMeta,
    },
    MultiPrism {
        prisms: Vec<Geometry>,
        #[serde(flatten)]
        meta: GeometryMeta,
    },

    // -- JSON-FG curves --------------------------------------------------------------
    /// A sequence of circular arc segments; `coordinates` has an odd number of positions
    /// (3, 5, 7, …), each consecutive triple defining one arc.
    CircularString {
        coordinates: Vec<Position>,
        #[serde(flatten)]
        meta: GeometryMeta,
    },
    /// A continuous curve made of `LineString` and `CircularString` segments.
    CompoundCurve {
        geometries: Vec<Geometry>,
        #[serde(flatten)]
        meta: GeometryMeta,
    },
    /// A polygon whose rings may be curves (`LineString`, `CircularString`, `CompoundCurve`).
    CurvePolygon {
        geometries: Vec<Geometry>,
        #[serde(flatten)]
        meta: GeometryMeta,
    },
    MultiCurve {
        geometries: Vec<Geometry>,
        #[serde(flatten)]
        meta: GeometryMeta,
    },
    MultiSurface {
        geometries: Vec<Geometry>,
        #[serde(flatten)]
        meta: GeometryMeta,
    },
}

/// Optional members common to every JSON-FG geometry object.
///
/// All members are omitted from the serialized form when unset, so an empty
/// `GeometryMeta` adds nothing to the geometry object.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct GeometryMeta {
    /// The coordinate reference system of this geometry, when it differs from the
    /// in-scope default. Note the JSON-FG schema forbids this member on the `place` value
    /// of a feature (the CRS is taken from the feature or collection instead).
    #[serde(
        rename = "coordRefSys",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub coord_ref_sys: Option<CoordRefSys>,
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub measures: Option<Measures>,
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub bbox: Option<Bbox>,
    /// Any additional members (e.g. `conformsTo` when a geometry is the top-level object
    /// of a document), preserved verbatim.
    #[serde(flatten)]
    pub foreign_members: crate::JsonObject,
}

impl Geometry {
    /// The value of the geometry's `type` member, e.g. `"Point"` or `"CurvePolygon"`.
    pub fn geometry_type(&self) -> &'static str {
        match self {
            Geometry::Point { .. } => "Point",
            Geometry::MultiPoint { .. } => "MultiPoint",
            Geometry::LineString { .. } => "LineString",
            Geometry::MultiLineString { .. } => "MultiLineString",
            Geometry::Polygon { .. } => "Polygon",
            Geometry::MultiPolygon { .. } => "MultiPolygon",
            Geometry::GeometryCollection { .. } => "GeometryCollection",
            Geometry::Polyhedron { .. } => "Polyhedron",
            Geometry::MultiPolyhedron { .. } => "MultiPolyhedron",
            Geometry::Prism { .. } => "Prism",
            Geometry::MultiPrism { .. } => "MultiPrism",
            Geometry::CircularString { .. } => "CircularString",
            Geometry::CompoundCurve { .. } => "CompoundCurve",
            Geometry::CurvePolygon { .. } => "CurvePolygon",
            Geometry::MultiCurve { .. } => "MultiCurve",
            Geometry::MultiSurface { .. } => "MultiSurface",
        }
    }

    /// `true` for the geometry types that are also valid GeoJSON (and therefore allowed
    /// in a feature's `geometry` member): the seven GeoJSON types.
    pub fn is_geojson(&self) -> bool {
        matches!(
            self,
            Geometry::Point { .. }
                | Geometry::MultiPoint { .. }
                | Geometry::LineString { .. }
                | Geometry::MultiLineString { .. }
                | Geometry::Polygon { .. }
                | Geometry::MultiPolygon { .. }
                | Geometry::GeometryCollection { .. }
        )
    }

    /// A shared reference to the geometry's optional [`GeometryMeta`].
    pub fn meta(&self) -> &GeometryMeta {
        match self {
            Geometry::Point { meta, .. }
            | Geometry::MultiPoint { meta, .. }
            | Geometry::LineString { meta, .. }
            | Geometry::MultiLineString { meta, .. }
            | Geometry::Polygon { meta, .. }
            | Geometry::MultiPolygon { meta, .. }
            | Geometry::GeometryCollection { meta, .. }
            | Geometry::Polyhedron { meta, .. }
            | Geometry::MultiPolyhedron { meta, .. }
            | Geometry::Prism { meta, .. }
            | Geometry::MultiPrism { meta, .. }
            | Geometry::CircularString { meta, .. }
            | Geometry::CompoundCurve { meta, .. }
            | Geometry::CurvePolygon { meta, .. }
            | Geometry::MultiCurve { meta, .. }
            | Geometry::MultiSurface { meta, .. } => meta,
        }
    }

    /// Convenience constructor for a 2D or 3D point.
    pub fn point(coordinates: impl Into<Position>) -> Self {
        Geometry::Point {
            coordinates: coordinates.into(),
            meta: GeometryMeta::default(),
        }
    }
}

#[cfg(feature = "geojson")]
mod geojson_interop {
    use super::{Geometry, GeometryMeta};

    /// Error returned when a JSON-FG geometry has no GeoJSON equivalent (a solid or a
    /// curved geometry).
    #[derive(Debug, Clone, PartialEq, Eq)]
    pub struct NotGeoJson(pub &'static str);

    impl std::fmt::Display for NotGeoJson {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(
                f,
                "geometry type `{}` cannot be represented as GeoJSON",
                self.0
            )
        }
    }

    impl std::error::Error for NotGeoJson {}

    // The `geojson` crate models a position as its own `geojson::Position` newtype, so
    // coordinates are converted position-by-position at every nesting level.
    fn pos_in(p: geojson::Position) -> super::Position {
        p.as_slice().to_vec()
    }
    fn line_in(v: Vec<geojson::Position>) -> Vec<super::Position> {
        v.into_iter().map(pos_in).collect()
    }
    fn poly_in(v: Vec<Vec<geojson::Position>>) -> Vec<Vec<super::Position>> {
        v.into_iter().map(line_in).collect()
    }
    fn line_out(v: Vec<super::Position>) -> Vec<geojson::Position> {
        v.into_iter().map(Into::into).collect()
    }
    fn poly_out(v: Vec<Vec<super::Position>>) -> Vec<Vec<geojson::Position>> {
        v.into_iter().map(line_out).collect()
    }

    impl From<geojson::Geometry> for Geometry {
        fn from(g: geojson::Geometry) -> Self {
            let meta = GeometryMeta {
                bbox: g.bbox,
                ..Default::default()
            };
            match g.value {
                geojson::GeometryValue::Point { coordinates } => Geometry::Point {
                    coordinates: pos_in(coordinates),
                    meta,
                },
                geojson::GeometryValue::MultiPoint { coordinates } => Geometry::MultiPoint {
                    coordinates: line_in(coordinates),
                    meta,
                },
                geojson::GeometryValue::LineString { coordinates } => Geometry::LineString {
                    coordinates: line_in(coordinates),
                    meta,
                },
                geojson::GeometryValue::MultiLineString { coordinates } => {
                    Geometry::MultiLineString {
                        coordinates: coordinates.into_iter().map(line_in).collect(),
                        meta,
                    }
                }
                geojson::GeometryValue::Polygon { coordinates } => Geometry::Polygon {
                    coordinates: poly_in(coordinates),
                    meta,
                },
                geojson::GeometryValue::MultiPolygon { coordinates } => Geometry::MultiPolygon {
                    coordinates: coordinates.into_iter().map(poly_in).collect(),
                    meta,
                },
                geojson::GeometryValue::GeometryCollection { geometries } => {
                    Geometry::GeometryCollection {
                        geometries: geometries.into_iter().map(Geometry::from).collect(),
                        meta,
                    }
                }
            }
        }
    }

    impl TryFrom<Geometry> for geojson::Geometry {
        type Error = NotGeoJson;

        fn try_from(g: Geometry) -> Result<Self, Self::Error> {
            let bbox = g.meta().bbox.clone();
            let value = match g {
                Geometry::Point { coordinates, .. } => geojson::GeometryValue::Point {
                    coordinates: coordinates.into(),
                },
                Geometry::MultiPoint { coordinates, .. } => geojson::GeometryValue::MultiPoint {
                    coordinates: line_out(coordinates),
                },
                Geometry::LineString { coordinates, .. } => geojson::GeometryValue::LineString {
                    coordinates: line_out(coordinates),
                },
                Geometry::MultiLineString { coordinates, .. } => {
                    geojson::GeometryValue::MultiLineString {
                        coordinates: coordinates.into_iter().map(line_out).collect(),
                    }
                }
                Geometry::Polygon { coordinates, .. } => geojson::GeometryValue::Polygon {
                    coordinates: poly_out(coordinates),
                },
                Geometry::MultiPolygon { coordinates, .. } => {
                    geojson::GeometryValue::MultiPolygon {
                        coordinates: coordinates.into_iter().map(poly_out).collect(),
                    }
                }
                Geometry::GeometryCollection { geometries, .. } => {
                    geojson::GeometryValue::GeometryCollection {
                        geometries: geometries
                            .into_iter()
                            .map(geojson::Geometry::try_from)
                            .collect::<Result<Vec<_>, _>>()?,
                    }
                }
                other => return Err(NotGeoJson(other.geometry_type())),
            };
            Ok(geojson::Geometry {
                bbox,
                value,
                foreign_members: None,
            })
        }
    }
}

#[cfg(feature = "geojson")]
pub use geojson_interop::NotGeoJson;