use std::cmp::Ordering;
use crate::{Error, Expr};
use geo::*;
use geo_types::Geometry as GGeom;
use geozero::{geojson::GeoJsonWriter, wkt::Wkt, CoordDimensions, GeozeroGeometry, ToWkt};
use serde::{Deserialize, Serialize, Serializer};
const DEFAULT_NDIM: usize = 2;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum Geometry {
GeoJSON(geojson::Geometry),
#[serde(skip_deserializing, serialize_with = "to_geojson")]
Wkt(String),
}
impl Geometry {
pub fn to_wkt(&self) -> Result<String, Error> {
match self {
Geometry::Wkt(wkt) if has_measure(wkt) => Ok(wkt.clone()),
Geometry::Wkt(wkt) => Ok(wkt_to_geojson(wkt)
.and_then(|geojson| Geometry::GeoJSON(geojson).to_wkt())
.unwrap_or_else(|_| wkt.clone())),
Geometry::GeoJSON(geojson) => {
let (dims, marker) = match geojson_ndims(geojson) {
n if n >= 3 => (CoordDimensions::xyz(), " Z"),
_ => (CoordDimensions::xy(), ""),
};
let json = geojson.to_string();
let wkt = geozero::geojson::GeoJson(&json).to_wkt_ndim(dims)?;
Ok(tag_dimensions(&wkt, marker))
}
}
}
}
fn tag_dimensions(wkt: &str, marker: &str) -> String {
const TAGS: [&str; 7] = [
"GEOMETRYCOLLECTION",
"MULTILINESTRING",
"MULTIPOLYGON",
"MULTIPOINT",
"LINESTRING",
"POLYGON",
"POINT",
];
if marker.is_empty() {
return wkt.to_string();
}
let mut out = String::with_capacity(wkt.len() + marker.len());
let mut rest = wkt;
'scan: while !rest.is_empty() {
for tag in TAGS {
if let Some(after) = rest.strip_prefix(tag) {
if after.starts_with('(') {
out.push_str(tag);
out.push_str(marker);
rest = after;
continue 'scan;
}
}
}
let next = rest.chars().next().expect("rest is non-empty");
out.push(next);
rest = &rest[next.len_utf8()..];
}
out
}
impl PartialEq for Geometry {
fn eq(&self, other: &Self) -> bool {
let left = Expr::Geometry(self.clone());
let right = Expr::Geometry(other.clone());
let v = spatial_op(left, right, "s_equals").unwrap_or(Expr::Bool(false));
match v {
Expr::Bool(v) => v,
_ => false,
}
}
}
impl PartialOrd for Geometry {
fn partial_cmp(&self, _other: &Self) -> Option<Ordering> {
None
}
}
fn has_measure(wkt: &str) -> bool {
wkt.match_indices('(').any(|(paren, _)| {
let head = wkt[..paren].trim_end();
let before_tag = head.trim_end_matches(|c: char| c.is_ascii_alphabetic());
head[before_tag.len()..].ends_with(['M', 'm'])
})
}
fn wkt_to_geojson(wkt: &str) -> Result<geojson::Geometry, Error> {
let mut out: Vec<u8> = Vec::new();
let mut writer = GeoJsonWriter::with_dims(&mut out, CoordDimensions::xyz());
Wkt(wkt).process_geom(&mut writer)?;
let json = String::from_utf8(out)
.map_err(|e| geozero::error::GeozeroError::Geometry(e.to_string()))?;
Ok(serde_json::from_str(&json)?)
}
#[allow(clippy::ptr_arg)]
fn to_geojson<S>(wkt: &String, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::Error as _;
wkt_to_geojson(wkt)
.map_err(S::Error::custom)?
.serialize(serializer)
}
fn geojson_ndims(geojson: &geojson::Geometry) -> usize {
use geojson::Value::*;
fn min_len(positions: impl IntoIterator<Item = usize>) -> usize {
positions.into_iter().min().unwrap_or(DEFAULT_NDIM)
}
match &geojson.value {
Point(coords) => coords.len(),
MultiPoint(v) | LineString(v) => min_len(v.iter().map(Vec::len)),
MultiLineString(v) | Polygon(v) => min_len(v.iter().flatten().map(Vec::len)),
MultiPolygon(v) => min_len(v.iter().flatten().flatten().map(Vec::len)),
GeometryCollection(v) => min_len(v.iter().map(geojson_ndims)),
}
}
pub fn spatial_op(left: Expr, right: Expr, op: &str) -> Result<Expr, Error> {
let op = crate::expr::canonical_op(op);
let left: GGeom = GGeom::try_from(left)?;
let right: GGeom = GGeom::try_from(right)?;
let rel = left.relate(&right);
let out = match op.as_str() {
"s_equals" => rel.is_equal_topo(),
"s_intersects" => rel.is_intersects(),
"s_disjoint" => rel.is_disjoint(),
"s_touches" => rel.is_touches(),
"s_within" => rel.is_within(),
"s_overlaps" => rel.is_overlaps(),
"s_crosses" => rel.is_crosses(),
"s_contains" => rel.is_contains(),
_ => return Err(Error::OpNotImplemented("spatial")),
};
Ok(Expr::Bool(out))
}