Skip to content

Commit

Permalink
add geoarrow GeoTable conversion. Derive default in most cases
Browse files Browse the repository at this point in the history
  • Loading branch information
JosiahParry committed Apr 11, 2024
1 parent 197a159 commit 05440b0
Show file tree
Hide file tree
Showing 12 changed files with 81 additions and 70 deletions.
1 change: 0 additions & 1 deletion .github/workflows/cargo-doc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ jobs:
uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@nightly
if: matrix.config.rust == 'nightly'

- name: Docs
run: cargo doc --no-deps --all-features
Expand Down
6 changes: 0 additions & 6 deletions .zed/settings.json

This file was deleted.

2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "serde_esri"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/josiahparry/serde_esri"
Expand Down
95 changes: 50 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,73 @@ This crate provides representations of Esri JSON objects with [`serde::Deseriali
`serde_esri` has two additional features `geo` and `geoarrow`.

- `geo` implements `From` for the Esri JSON objects.
- `geoarrow` provides compatibility with arrow and geoarrow by implementing geoarrow geometry traits as well as providing a utility function `featureset_to_arrow()` which converts a `FeatureSet` to an arrow `RecordBatch`.
- `geoarrow` provides compatibility with arrow and geoarrow by implementing geoarrow geometry traits as well as providing a utility function `featureset_to_geoarrow()` which converts a `FeatureSet` to an arrow `GeoTable`.


## Example usage:

In this example, we query a feature service and convert the response to an Arrow `RecordBatch`. This requires the `geoarrow` feature to be enabled.
This example reads a few features from a feature service and returns a `FeatureSet` struct. It illustrates the use of the geo and geoarrow features.

```toml
[dependencies]
geo = "0.28.0"
geoarrow = "0.2.0"
reqwest = { version = "0.12.3", features = ["blocking"] }
serde_esri = { version = "0.2.0", features = ["geo", "geoarrow"] }
serde_json = "1.0.115"
```

```rust
use geo::{GeodesicArea, Polygon};
use serde_esri::arrow_compat::featureset_to_geoarrow;
use serde_esri::features::FeatureSet;
use serde_esri::arrow_compat::featureset_to_arrow;

#[tokio::main]
async fn main() {

// query url
let furl = "https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Counties_Generalized_Boundaries/FeatureServer/0/query?where=1=1&outFields=*&f=json&resultRecordCount=10";

// make the request
let resp = reqwest::get(furl)
.await.unwrap()
.text()
.await.unwrap();

// parse the response into a FeatureSet
let fset: FeatureSet<2> = serde_json::from_str(&resp).unwrap();

// convert the FeatureSet to an Arrow RecordBatch
let rb = featureset_to_arrow(fset).unwrap();

println!("{:#?}", rb.schema());
use std::io::Read;

fn main() {
let flayer_url = "https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Counties_Generalized_Boundaries/FeatureServer/0/query?where=1%3D1&outFields=*&returnGeometry=true&resultRecordCount=5&f=json";
let mut res = reqwest::blocking::get(flayer_url).unwrap();
let mut body = String::new();

// Read the request into a String
res.read_to_string(&mut body).unwrap();

// Parse into a 2D FeatureSet
let fset: FeatureSet<2> = serde_json::from_str(&body).unwrap();

// Utilize the `geo` feature by converting to Polygon
// and calculate geodesic area
// iterate through the features
let area = fset
.features
.clone()
.into_iter()
.map(|feat| {
// convert to a geo_types::Polygon
let poly = Polygon::from(feat.geometry.unwrap().as_polygon().unwrap());
// calculate geodesic area
poly.geodesic_area_unsigned()
})
.collect::<Vec<_>>();

// print areas
println!("{:?}", area);

// convert to a geoarrow GeoTable
println!("{:?}", featureset_to_geoarrow(fset).unwrap());
}
```

## Supported Esri JSON objects:

### Geometry

[Esri Geometries Objects](https://developers.arcgis.com/documentation/common-data-types/geometry-objects.htm#CURVE) are encoded by the following structs:
[Esri Geometries Objects](https://developers.arcgis.com/documentation/common-data-types/geometry-objects.htm) are encoded by the following structs:

- `EsriPoint`
- `EsriMultiPoint<N>`
- `EsriPolyline<N>`
- `EsriPolygon<N>`
- `EsriEnvelope`

They are encapsulated by the `EsriGeometry` enum:

Expand All @@ -58,6 +83,7 @@ enum EsriGeometry<const N: usize> {
MultiPoint(EsriMultiPoint<N>),
Polygon(EsriPolygon<N>),
Polyline(EsriPolyline<N>),
Envelope(EsriEnvelope),
}
```
The parameter `N` is used to specify the dimension of the geometries. Use `<2>` for 2 dimensional data, `<3>` for Z values and `<4>` when `M` and `Z` are present.
Expand Down Expand Up @@ -99,24 +125,3 @@ struct SpatialReference {
wkt: Option<String>,
}
```

## Example usage:

This example reads a single feature from a feature service and returns a `FeatureSet` struct.

```rust
use serde_esri::features::FeatureSet;
use reqwest::Error;
use std::io::Read;

fn main() -> Result<(), Error> {
let flayer_url = "https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Counties_Generalized_Boundaries/FeatureServer/0/query?where=1%3D1&outFields=*&returnGeometry=true&resultRecordCount=1&f=json";
let mut res = reqwest::blocking::get(flayer_url)?;
let mut body = String::new();
res.read_to_string(&mut body).unwrap();

let fset: FeatureSet<2> = serde_json::from_str(&body).unwrap();
println!("{:#?}", fset);
Ok(())
}
```
16 changes: 15 additions & 1 deletion src/arrow_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::{
geometry::EsriGeometry,
};

use std::sync::Arc;
use std::{result::Result, sync::Arc};

use geoarrow::GeometryArrayTrait;
use serde_json::Value;
Expand Down Expand Up @@ -71,6 +71,20 @@ pub fn featureset_to_arrow<const N: usize>(
}
}

use geoarrow::table::GeoTable;

// TODO create my own error types
/// Given a `FeatureSet`, create a geoarrow `GeoTable`
pub fn featureset_to_geoarrow<const N: usize>(
x: FeatureSet<N>,
) -> Result<GeoTable, geoarrow::error::GeoArrowError> {
let arrow_res = featureset_to_arrow(x)?;
let schema_ref = arrow_res.schema_ref().clone();
let geometry_index = arrow_res.schema().fields.len();

GeoTable::try_new(schema_ref, vec![arrow_res], geometry_index)
}

// convert an esri field to a new arrow field
impl From<Field> for AField {
fn from(value: Field) -> Self {
Expand Down
2 changes: 0 additions & 2 deletions src/de_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,3 @@ pub mod arrays {
deserializer.deserialize_tuple(N, ArrayVisitor::<T, N>(PhantomData))
}
}

// pub struct ArrayVisitor<T, const N: usize>(PhantomData<T>);
6 changes: 3 additions & 3 deletions src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ use serde_with::{serde_as, DisplayFromStr};
/// Note that both geometry and attributes are optional. This is because
/// we can anticipate receiving _only_ geometries, or _only_ attributes
/// or both together.
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct Feature<const N: usize> {
pub geometry: Option<EsriGeometry<N>>,
pub attributes: Option<Map<String, Value>>,
}

/// A set of geometries and their attributes
#[allow(non_snake_case)]
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct FeatureSet<const N: usize> {
pub objectIdFieldName: Option<String>,
pub globalIdFieldName: Option<String>,
Expand All @@ -44,7 +44,7 @@ pub struct FeatureSet<const N: usize> {
// TODO sqlType, field_type need to be Enums
#[serde_as]
#[allow(non_snake_case)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
pub struct Field {
pub name: String,
#[serde(rename = "type")]
Expand Down
5 changes: 3 additions & 2 deletions src/field_type.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
//! Enumeration of valid esri field types
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
pub enum FieldType {
EsriFieldTypeSmallInteger = 0,
EsriFieldTypeInteger = 1,
EsriFieldTypeSingle = 2,
EsriFieldTypeDouble = 3,
#[default]
EsriFieldTypeString = 4,
EsriFieldTypeDate = 5,
EsriFieldTypeOid = 6,
Expand Down
12 changes: 6 additions & 6 deletions src/geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub struct EsriCoord<const N: usize>(#[serde(with = "arrays")] pub [f64; N]);
/// An `esriGeometryPoint` with fields x, y, z, and m. x and y are both required.
#[skip_serializing_none]
#[allow(non_snake_case)]
#[derive(Clone, Deserialize, Serialize, Debug)]
#[derive(Clone, Deserialize, Serialize, Debug, Default)]
pub struct EsriPoint {
pub x: f64,
pub y: f64,
Expand All @@ -38,7 +38,7 @@ pub struct EsriPoint {
/// a `panic!`.
#[skip_serializing_none]
#[allow(non_snake_case)]
#[derive(Clone, Deserialize, Serialize, Debug)]
#[derive(Clone, Deserialize, Serialize, Debug, Default)]
pub struct EsriMultiPoint<const N: usize> {
pub hasZ: Option<bool>,
pub hasM: Option<bool>,
Expand Down Expand Up @@ -91,7 +91,7 @@ impl<'a, const N: usize> ExactSizeIterator for EsriMultiPointIterator<'a, N> {
/// This struct is used strictly for representing the internal LineStrings
/// for the `EsriPolygon` and `EsriPolyline` structs. They do not represent
/// any Esri JSON geometry objects.
#[derive(Clone, Deserialize, Serialize, Debug)]
#[derive(Clone, Deserialize, Serialize, Debug, Default)]
pub struct EsriLineString<const N: usize>(pub Vec<EsriCoord<N>>);

pub struct EsriLineStringIterator<'a, const N: usize> {
Expand Down Expand Up @@ -137,7 +137,7 @@ impl<const N: usize> EsriLineString<N> {
/// a `panic!`.
#[skip_serializing_none]
#[allow(non_snake_case)]
#[derive(Clone, Deserialize, Serialize, Debug)]
#[derive(Clone, Deserialize, Serialize, Debug, Default)]
pub struct EsriPolyline<const N: usize> {
pub hasZ: Option<bool>,
pub hasM: Option<bool>,
Expand Down Expand Up @@ -174,7 +174,7 @@ impl<'a, const N: usize> ExactSizeIterator for EsriPolylineIterator<'a, N> {
/// a `panic!`.
#[skip_serializing_none]
#[allow(non_snake_case)]
#[derive(Clone, Deserialize, Serialize, Debug)]
#[derive(Clone, Deserialize, Serialize, Debug, Default)]
pub struct EsriPolygon<const N: usize> {
pub hasZ: Option<bool>,
pub hasM: Option<bool>,
Expand Down Expand Up @@ -249,7 +249,7 @@ impl<const N: usize> EsriGeometry<N> {
// TODO: esriGeometryEnvelope.

#[allow(non_snake_case)]
#[derive(Clone, Deserialize, Serialize, Debug)]
#[derive(Clone, Deserialize, Serialize, Debug, Default)]
#[skip_serializing_none]
pub struct EsriEnvelope {
xmin: f64,
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//!
#![doc = include_str!("../README.md")]
//!
//! Example usage:
//!
Expand Down
2 changes: 1 addition & 1 deletion src/spatial_reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use serde_with::skip_serializing_none;

/// Read more on [Esri docs site](https://developers.arcgis.com/documentation/common-data-types/geometry-objects.htm#GUID-DFF0E738-5A42-40BC-A811-ACCB5814BABC)
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct SpatialReference {
pub wkid: Option<u32>,
pub latest_wkid: Option<u32>,
Expand Down

0 comments on commit 05440b0

Please sign in to comment.