use std::fmt;
use std::fs;
use std::io::{self, Write};
use std::path::Path;
use std::str::FromStr;
use geo_types::{Coord, Geometry};
use serde::Deserialize;
use serde_json_lenient::{Map, Value};
use super::args::OutputFormat;
pub type Members = Map<String, Value>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InputKind {
Geometry,
Feature,
FeatureCollection,
}
#[derive(Debug)]
pub struct InputRecord {
pub geometry: Option<Geometry<f64>>,
pub meta: Option<Members>,
}
#[derive(Debug)]
pub struct Input {
pub kind: InputKind,
pub records: Vec<InputRecord>,
}
#[derive(Debug)]
pub struct InputError(pub String);
impl fmt::Display for InputError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for InputError {}
const GEOMETRY_TYPES: [&str; 7] = [
"Point",
"MultiPoint",
"LineString",
"MultiLineString",
"Polygon",
"MultiPolygon",
"GeometryCollection",
];
pub fn read_input(
input_arg: Option<&str>,
read_stdin: &mut dyn FnMut() -> io::Result<String>,
) -> Result<Input, InputError> {
let text = match input_arg {
None => read_stdin().map_err(|e| InputError(e.to_string()))?,
Some(arg) if is_geojson(arg) => arg.to_string(),
Some(arg) if Path::new(arg).is_file() => {
fs::read_to_string(arg).map_err(|e| InputError(format!("{arg}: {e}")))?
}
Some(arg) => arg.to_string(),
};
if is_geojson(&text) {
parse_geojson(&text)
} else {
parse_wkt(&text)
}
}
fn is_geojson(text: &str) -> bool {
text.trim_start().starts_with('{')
}
fn parse_wkt(text: &str) -> Result<Input, InputError> {
let parsed = wkt::Wkt::<f64>::from_str(text.trim()).map_err(|e| InputError(e.to_string()))?;
let geometry: Geometry<f64> = parsed.try_into().map_err(|e| InputError(format!("{e}")))?;
Ok(Input {
kind: InputKind::Geometry,
records: vec![InputRecord {
geometry: Some(geometry),
meta: None,
}],
})
}
fn parse_json(text: &str) -> Result<Value, serde_json_lenient::Error> {
let mut deserializer = serde_json_lenient::Deserializer::from_str(text);
deserializer.set_ignore_trailing_commas(false);
deserializer.set_allow_comments(false);
let value = Value::deserialize(&mut deserializer)?;
deserializer.end()?;
Ok(value)
}
fn parse_geojson(text: &str) -> Result<Input, InputError> {
let value = parse_json(text).map_err(|e| InputError(format!("Invalid JSON: {e}")))?;
match type_member(&value) {
Some("FeatureCollection") => {
let features = value
.get("features")
.and_then(Value::as_array)
.ok_or_else(|| InputError("FeatureCollection has no features array".to_string()))?;
Ok(Input {
kind: InputKind::FeatureCollection,
records: features
.iter()
.map(split_feature)
.collect::<Result<Vec<_>, _>>()?,
})
}
Some("Feature") => Ok(Input {
kind: InputKind::Feature,
records: vec![split_feature(&value)?],
}),
Some(name) if GEOMETRY_TYPES.contains(&name) => Ok(Input {
kind: InputKind::Geometry,
records: vec![InputRecord {
geometry: Some(parse_geometry(&value)?),
meta: None,
}],
}),
other => Err(InputError(format!(
"Unsupported GeoJSON type '{}'",
other.unwrap_or("undefined")
))),
}
}
fn type_member(value: &Value) -> Option<&str> {
value.get("type").and_then(Value::as_str)
}
fn parse_geometry(value: &Value) -> Result<Geometry<f64>, InputError> {
let text = serde_json_lenient::to_string(value).map_err(|e| InputError(e.to_string()))?;
let parsed = geojson::Geometry::from_str(&text).map_err(|e| InputError(e.to_string()))?;
Geometry::<f64>::try_from(parsed).map_err(|e| InputError(e.to_string()))
}
fn split_feature(value: &Value) -> Result<InputRecord, InputError> {
let members = match (type_member(value), value.as_object()) {
(Some("Feature"), Some(members)) => members,
_ => return Err(InputError("Expected a Feature".to_string())),
};
let mut meta = Members::new();
let mut geometry = None;
for (key, member) in members {
match key.as_str() {
"geometry" => {
if !member.is_null() {
geometry = Some(parse_geometry(member)?);
}
}
"type" | "bbox" => {}
_ => {
meta.insert(key.clone(), member.clone());
}
}
}
Ok(InputRecord {
geometry,
meta: Some(meta),
})
}
#[derive(Debug)]
pub struct OutputRecord {
pub point: Option<Coord<f64>>,
pub meta: Option<Members>,
}
pub fn serialize(kind: InputKind, records: Vec<OutputRecord>, format: OutputFormat) -> String {
match format {
OutputFormat::Wkt => records
.iter()
.map(|r| format!("{}\n", point_wkt(r.point)))
.collect(),
OutputFormat::Geojson => {
let value = match kind {
InputKind::Geometry => {
point_geometry(records.first().and_then(|record| record.point))
}
InputKind::Feature => {
let record = records.into_iter().next().unwrap_or(OutputRecord {
point: None,
meta: None,
});
feature_for(record)
}
InputKind::FeatureCollection => {
let mut collection = Members::new();
collection.insert("type".to_string(), Value::from("FeatureCollection"));
collection.insert(
"features".to_string(),
Value::Array(records.into_iter().map(feature_for).collect()),
);
Value::Object(collection)
}
};
let mut text = String::new();
write_json(&value, &mut text);
text.push('\n');
text
}
}
}
fn write_json(value: &Value, out: &mut String) {
match value {
Value::Number(number) => out.push_str(&json_number(number.as_f64().unwrap_or(f64::NAN))),
Value::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_json(item, out);
}
out.push(']');
}
Value::Object(members) => {
out.push('{');
for (i, (key, member)) in members.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_json(&Value::from(key.as_str()), out);
out.push(':');
write_json(member, out);
}
out.push('}');
}
scalar => out.push_str(&scalar.to_string()),
}
}
fn json_number(v: f64) -> String {
if !v.is_finite() {
return "null".to_string();
}
if v == 0.0 {
return "0".to_string();
}
if v < 0.0 {
return format!("-{}", json_number(-v));
}
let mut buffer = ryu::Buffer::new();
let shortest = buffer.format_finite(v);
let (mantissa, exponent) = match shortest.split_once('e') {
Some((mantissa, exponent)) => (
mantissa,
exponent
.parse::<i32>()
.expect("ryu writes a decimal exponent"),
),
None => (shortest, 0),
};
let (whole, fraction) = mantissa.split_once('.').unwrap_or((mantissa, ""));
let mut digits = format!("{whole}{fraction}");
let mut n = whole.len() as i32 + exponent;
let leading = digits.len() - digits.trim_start_matches('0').len();
digits.drain(..leading);
n -= leading as i32;
digits.truncate(digits.trim_end_matches('0').len());
let k = digits.len() as i32;
if n > 21 || n <= -6 {
let sign = if n > 1 { "+" } else { "" };
return match k {
1 => format!("{digits}e{sign}{}", n - 1),
_ => format!("{}.{}e{sign}{}", &digits[..1], &digits[1..], n - 1),
};
}
if k <= n {
return format!("{digits}{}", "0".repeat((n - k) as usize));
}
if n > 0 {
return format!("{}.{}", &digits[..n as usize], &digits[n as usize..]);
}
format!("0.{}{digits}", "0".repeat(-n as usize))
}
fn feature_for(record: OutputRecord) -> Value {
let mut members = Members::new();
members.insert("type".to_string(), Value::from("Feature"));
if let Some(meta) = record.meta {
for (key, value) in meta {
members.insert(key, value);
}
}
members.insert("geometry".to_string(), point_geometry(record.point));
Value::Object(members)
}
fn point_geometry(point: Option<Coord<f64>>) -> Value {
match point {
None => Value::Null,
Some(p) => {
let mut members = Members::new();
members.insert("type".to_string(), Value::from("Point"));
members.insert(
"coordinates".to_string(),
Value::Array(vec![Value::from(p.x), Value::from(p.y)]),
);
Value::Object(members)
}
}
}
fn point_wkt(point: Option<Coord<f64>>) -> String {
match point {
Some(p) => format!("POINT ({} {})", p.x, p.y),
None => "POINT EMPTY".to_string(),
}
}
pub fn write_output(text: &str, output_path: Option<&str>, out: &mut dyn Write) -> io::Result<()> {
match output_path {
Some(path) => fs::write(path, text),
None => out.write_all(text.as_bytes()),
}
}