Skip to main content

wbvector/geojson/
mod.rs

1//! GeoJSON (RFC 7946) reader and writer.
2//!
3//! The parser is hand-rolled with no external JSON library.  It covers the
4//! GeoJSON subset of JSON precisely — numbers, strings (including `\uXXXX`),
5//! arrays, and objects.
6//!
7//! ## Schema inference
8//! Property keys are collected across all features.  Column types are inferred
9//! by scanning every value:
10//! - All integers → `Integer`
11//! - Mix of integer + float → `Float`
12//! - Any object or array → `Json`
13//! - YYYY-MM-DD strings → `Date`
14//! - Otherwise → `Text`
15
16use std::collections::{HashMap, HashSet};
17use std::path::Path;
18use crate::error::{GeoError, Result};
19use crate::feature::{FieldDef, FieldType, FieldValue, Feature, Layer, Schema};
20use crate::geometry::{Coord, Geometry, Ring};
21use crate::reproject;
22
23// ══════════════════════════════════════════════════════════════════════════════
24// Public API
25// ══════════════════════════════════════════════════════════════════════════════
26
27/// Read a GeoJSON file into a [`Layer`].
28pub fn read<P: AsRef<Path>>(path: P) -> Result<Layer> {
29    let text = std::fs::read_to_string(path).map_err(GeoError::Io)?;
30    parse_str(&text)
31}
32
33/// Parse a GeoJSON string into a [`Layer`].
34pub fn parse_str(text: &str) -> Result<Layer> {
35    let val = Parser::new(text).parse_value()?;
36    layer_from_value(val, "layer")
37}
38
39/// Write a [`Layer`] as a GeoJSON `FeatureCollection` to a file.
40pub fn write<P: AsRef<Path>>(layer: &Layer, path: P) -> Result<()> {
41    let out_layer = prepare_rfc7946_layer(layer)?;
42    std::fs::write(path, to_string(&out_layer).as_bytes()).map_err(GeoError::Io)
43}
44
45/// Serialise a [`Layer`] as a compact GeoJSON string.
46pub fn to_string(layer: &Layer) -> String {
47    let mut s = String::new();
48    write_feature_collection(&mut s, layer);
49    s
50}
51
52fn prepare_rfc7946_layer(layer: &Layer) -> Result<Layer> {
53    // RFC 7946 requires WGS 84 lon/lat coordinates. If CRS metadata is
54    // present and not already EPSG:4326, reproject on write.
55    if layer.crs_epsg() == Some(4326) {
56        return Ok(layer.clone());
57    }
58
59    if layer.crs_epsg().is_some() || layer.crs_wkt().is_some() {
60        return reproject::layer_to_epsg(layer, 4326);
61    }
62
63    Ok(layer.clone())
64}
65
66// ══════════════════════════════════════════════════════════════════════════════
67// Internal JSON value
68// ══════════════════════════════════════════════════════════════════════════════
69
70#[derive(Debug, Clone)]
71enum Jv {
72    Null,
73    Bool(bool),
74    Num(f64),
75    Str(String),
76    Arr(Vec<Jv>),
77    Obj(Vec<(String, Jv)>),  // preserve insertion order
78}
79
80impl Jv {
81    fn get(&self, key: &str) -> Option<&Jv> {
82        if let Jv::Obj(pairs) = self { pairs.iter().find(|(k,_)| k == key).map(|(_,v)| v) }
83        else { None }
84    }
85    fn as_str(&self) -> Option<&str>  { if let Jv::Str(s) = self { Some(s) } else { None } }
86    fn as_f64(&self) -> Option<f64>   { if let Jv::Num(n) = self { Some(*n) } else { None } }
87    fn as_arr(&self) -> Option<&[Jv]> { if let Jv::Arr(a) = self { Some(a) } else { None } }
88}
89
90// ══════════════════════════════════════════════════════════════════════════════
91// Minimal JSON parser
92// ══════════════════════════════════════════════════════════════════════════════
93
94struct Parser<'a> {
95    src: &'a [u8],
96    pos: usize,
97}
98
99impl<'a> Parser<'a> {
100    fn new(s: &'a str) -> Self { Self { src: s.as_bytes(), pos: 0 } }
101
102    fn err(&self, msg: &str) -> GeoError {
103        GeoError::GeoJsonParse { offset: self.pos, msg: msg.to_owned() }
104    }
105
106    fn peek(&self) -> Option<u8> { self.src.get(self.pos).copied() }
107
108    fn skip_ws(&mut self) {
109        while matches!(self.peek(), Some(b' '|b'\t'|b'\n'|b'\r')) { self.pos += 1; }
110    }
111
112    fn eat(&mut self, b: u8) -> Result<()> {
113        self.skip_ws();
114        if self.peek() == Some(b) { self.pos += 1; Ok(()) }
115        else { Err(self.err(&format!("expected '{}' got {:?}", b as char, self.peek().map(|b| b as char)))) }
116    }
117
118    pub fn parse_value(&mut self) -> Result<Jv> {
119        self.skip_ws();
120        match self.peek() {
121            Some(b'"')                   => self.parse_string().map(Jv::Str),
122            Some(b'{')                   => self.parse_object(),
123            Some(b'[')                   => self.parse_array(),
124            Some(b't')                   => { self.pos += 4; Ok(Jv::Bool(true))  }
125            Some(b'f')                   => { self.pos += 5; Ok(Jv::Bool(false)) }
126            Some(b'n')                   => { self.pos += 4; Ok(Jv::Null)        }
127            Some(b'-') | Some(b'0'..=b'9') => self.parse_number(),
128            Some(b)                      => Err(self.err(&format!("unexpected byte 0x{b:02X}"))),
129            None                         => Err(self.err("unexpected end of input")),
130        }
131    }
132
133    fn parse_string(&mut self) -> Result<String> {
134        self.eat(b'"')?;
135        let mut s = String::new();
136        loop {
137            match self.peek() {
138                None        => return Err(self.err("unterminated string")),
139                Some(b'"')  => { self.pos += 1; break; }
140                Some(b'\\') => {
141                    self.pos += 1;
142                    match self.peek() {
143                        Some(b'"')  => { s.push('"');   self.pos += 1; }
144                        Some(b'\\') => { s.push('\\');  self.pos += 1; }
145                        Some(b'/')  => { s.push('/');   self.pos += 1; }
146                        Some(b'n')  => { s.push('\n');  self.pos += 1; }
147                        Some(b'r')  => { s.push('\r');  self.pos += 1; }
148                        Some(b't')  => { s.push('\t');  self.pos += 1; }
149                        Some(b'b')  => { s.push('\x08'); self.pos += 1; }
150                        Some(b'f')  => { s.push('\x0C'); self.pos += 1; }
151                        Some(b'u')  => {
152                            self.pos += 1;
153                            if self.pos + 4 > self.src.len() {
154                                return Err(self.err("truncated \\u escape"));
155                            }
156                            let hex = std::str::from_utf8(&self.src[self.pos..self.pos+4])
157                                .map_err(|_| self.err("invalid \\u escape"))?;
158                            let cp = u32::from_str_radix(hex, 16)
159                                .map_err(|_| self.err("invalid \\u codepoint"))?;
160                            if let Some(ch) = char::from_u32(cp) { s.push(ch); }
161                            self.pos += 4;
162                        }
163                        _ => s.push('\\'),
164                    }
165                }
166                Some(b) => { s.push(b as char); self.pos += 1; }
167            }
168        }
169        Ok(s)
170    }
171
172    fn parse_number(&mut self) -> Result<Jv> {
173        let start = self.pos;
174        if self.peek() == Some(b'-') { self.pos += 1; }
175        while matches!(self.peek(), Some(b'0'..=b'9')) { self.pos += 1; }
176        if self.peek() == Some(b'.') {
177            self.pos += 1;
178            while matches!(self.peek(), Some(b'0'..=b'9')) { self.pos += 1; }
179        }
180        if matches!(self.peek(), Some(b'e'|b'E')) {
181            self.pos += 1;
182            if matches!(self.peek(), Some(b'+'|b'-')) { self.pos += 1; }
183            while matches!(self.peek(), Some(b'0'..=b'9')) { self.pos += 1; }
184        }
185        let s = std::str::from_utf8(&self.src[start..self.pos])
186            .map_err(|_| self.err("invalid number bytes"))?;
187        let n: f64 = s.parse().map_err(|_| self.err("invalid number"))?;
188        Ok(Jv::Num(n))
189    }
190
191    fn parse_array(&mut self) -> Result<Jv> {
192        self.eat(b'[')?;
193        let mut arr = Vec::new();
194        self.skip_ws();
195        if self.peek() == Some(b']') { self.pos += 1; return Ok(Jv::Arr(arr)); }
196        loop {
197            arr.push(self.parse_value()?);
198            self.skip_ws();
199            match self.peek() {
200                Some(b',') => { self.pos += 1; }
201                Some(b']') => { self.pos += 1; break; }
202                _          => return Err(self.err("expected ',' or ']'")),
203            }
204        }
205        Ok(Jv::Arr(arr))
206    }
207
208    fn parse_object(&mut self) -> Result<Jv> {
209        self.eat(b'{')?;
210        let mut pairs: Vec<(String, Jv)> = Vec::new();
211        self.skip_ws();
212        if self.peek() == Some(b'}') { self.pos += 1; return Ok(Jv::Obj(pairs)); }
213        loop {
214            self.skip_ws();
215            let key = self.parse_string()?;
216            self.eat(b':')?;
217            let val = self.parse_value()?;
218            pairs.push((key, val));
219            self.skip_ws();
220            match self.peek() {
221                Some(b',') => { self.pos += 1; }
222                Some(b'}') => { self.pos += 1; break; }
223                _          => return Err(self.err("expected ',' or '}'")),
224            }
225        }
226        Ok(Jv::Obj(pairs))
227    }
228}
229
230// ══════════════════════════════════════════════════════════════════════════════
231// JSON value → Layer
232// ══════════════════════════════════════════════════════════════════════════════
233
234fn layer_from_value(val: Jv, name: &str) -> Result<Layer> {
235    let type_s = val.get("type").and_then(|v| v.as_str()).unwrap_or("").to_owned();
236    match type_s.as_str() {
237        "FeatureCollection" => parse_feature_collection(val, name),
238        "Feature" => {
239            let mut layer = Layer::new(name);
240            if let Some(f) = build_feature(&val, &layer.schema, 0)? {
241                if let Some(geom) = &f.geometry {
242                    layer.geom_type = Some(geom.geom_type());
243                }
244                layer.push(f);
245            }
246            Ok(layer)
247        }
248        _ => {
249            // Bare geometry
250            let geom = parse_geometry(&val)?;
251            let mut layer = Layer::new(name);
252            layer.geom_type = Some(geom.geom_type());
253            layer.push(Feature { fid: 0, geometry: Some(geom), attributes: vec![] });
254            Ok(layer)
255        }
256    }
257}
258
259fn parse_feature_collection(val: Jv, name: &str) -> Result<Layer> {
260    let features_arr = val.get("features").and_then(|v| v.as_arr())
261        .ok_or_else(|| GeoError::GeoJsonMissing("features".into()))?;
262
263    // ── two-pass schema inference ─────────────────────────────────────────────
264    let mut key_order: Vec<String>       = Vec::new();
265    let mut key_seen:  HashSet<String>   = HashSet::new();
266    let mut key_type:  HashMap<String, FieldType> = HashMap::new();
267
268    for feat in features_arr {
269        if let Some(Jv::Obj(props)) = feat.get("properties") {
270            for (k, v) in props {
271                if key_seen.insert(k.clone()) { key_order.push(k.clone()); }
272                if matches!(v, Jv::Null) { continue; }
273                let inferred = infer_type(v);
274                let entry = key_type.entry(k.clone()).or_insert(inferred);
275                *entry = FieldValue::widen_type(*entry, inferred);
276            }
277        }
278    }
279
280    let mut layer = Layer::new(name);
281    for k in &key_order {
282        let ft = key_type.get(k).copied().unwrap_or(FieldType::Text);
283        layer.add_field(FieldDef::new(k, ft));
284    }
285
286    for (idx, feat_val) in features_arr.iter().enumerate() {
287        if let Some(f) = build_feature(feat_val, &layer.schema, idx as u64)? {
288            // Infer layer geometry type from first feature with geometry
289            if layer.geom_type.is_none() {
290                if let Some(geom) = &f.geometry {
291                    layer.geom_type = Some(geom.geom_type());
292                }
293            }
294            layer.push(f);
295        }
296    }
297
298    Ok(layer)
299}
300
301fn infer_type(v: &Jv) -> FieldType {
302    match v {
303        Jv::Bool(_)   => FieldType::Boolean,
304        Jv::Num(n)    => if n.fract() == 0.0 { FieldType::Integer } else { FieldType::Float },
305        Jv::Null      => FieldType::Text,           // conservative
306        Jv::Arr(_) | Jv::Obj(_) => FieldType::Json,
307        Jv::Str(s)    => if looks_like_date(s) { FieldType::Date } else { FieldType::Text },
308    }
309}
310
311fn looks_like_date(s: &str) -> bool {
312    let b = s.as_bytes();
313    b.len() == 10 && b[4] == b'-' && b[7] == b'-'
314}
315
316fn build_feature(val: &Jv, schema: &Schema, fid: u64) -> Result<Option<Feature>> {
317    let type_s = val.get("type").and_then(|v| v.as_str()).unwrap_or("");
318    if type_s != "Feature" {
319        return Err(GeoError::GeoJsonType(type_s.to_owned()));
320    }
321
322    let geom = match val.get("geometry") {
323        Some(Jv::Null) | None => None,
324        Some(g) => Some(parse_geometry(g)?),
325    };
326
327    let mut attrs = vec![FieldValue::Null; schema.len()];
328    if let Some(Jv::Obj(props)) = val.get("properties") {
329        for (k, v) in props {
330            if let Some(idx) = schema.field_index(k) {
331                let ft = schema.fields()[idx].field_type;
332                attrs[idx] = jv_to_field(v, ft);
333            }
334        }
335    }
336
337    Ok(Some(Feature { fid, geometry: geom, attributes: attrs }))
338}
339
340fn jv_to_field(v: &Jv, ft: FieldType) -> FieldValue {
341    match (v, ft) {
342        (Jv::Null, _)                       => FieldValue::Null,
343        (Jv::Bool(b), _)                    => FieldValue::Boolean(*b),
344        (Jv::Num(n), FieldType::Integer)    => FieldValue::Integer(*n as i64),
345        (Jv::Num(n), _)                     => FieldValue::Float(*n),
346        (Jv::Str(s), FieldType::Date)       => FieldValue::Date(s.clone()),
347        (Jv::Str(s), FieldType::DateTime)   => FieldValue::DateTime(s.clone()),
348        (Jv::Str(s), _)                     => FieldValue::Text(s.clone()),
349        (Jv::Arr(_), _) | (Jv::Obj(_), _)  => FieldValue::Text(jv_to_json_str(v)),
350    }
351}
352
353fn jv_to_json_str(v: &Jv) -> String {
354    match v {
355        Jv::Null      => "null".into(),
356        Jv::Bool(b)   => b.to_string(),
357        Jv::Num(n)    => fmt_number(*n),
358        Jv::Str(s)    => format!("\"{}\"", s.replace('"', "\\\"")),
359        Jv::Arr(a)    => format!("[{}]", a.iter().map(jv_to_json_str).collect::<Vec<_>>().join(",")),
360        Jv::Obj(o)    => {
361            let pairs: Vec<String> = o.iter().map(|(k,v)| format!("\"{}\":{}", k, jv_to_json_str(v))).collect();
362            format!("{{{}}}", pairs.join(","))
363        }
364    }
365}
366
367// ══════════════════════════════════════════════════════════════════════════════
368// GeoJSON geometry parsing
369// ══════════════════════════════════════════════════════════════════════════════
370
371fn parse_geometry(val: &Jv) -> Result<Geometry> {
372    let type_s = val.get("type").and_then(|v| v.as_str())
373        .ok_or_else(|| GeoError::GeoJsonMissing("geometry.type".into()))?;
374    let coords = val.get("coordinates");
375
376    match type_s {
377        "Point" => {
378            let c = parse_one_coord(coords.ok_or_else(|| GeoError::GeoJsonMissing("coordinates".into()))?)?;
379            Ok(Geometry::Point(c))
380        }
381        "LineString" => {
382            let cs = parse_coord_ring(coords.ok_or_else(|| GeoError::GeoJsonMissing("coordinates".into()))?)?;
383            Ok(Geometry::LineString(cs))
384        }
385        "Polygon" => {
386            let rings = coords.and_then(|v| v.as_arr())
387                .ok_or_else(|| GeoError::GeoJsonMissing("polygon coordinates".into()))?;
388            let mut parsed: Vec<Vec<Coord>> = rings.iter()
389                .map(|r| parse_coord_ring(r).map(strip_closed_ring))
390                .collect::<Result<_>>()?;
391            let exterior = parsed.drain(..1).next().unwrap_or_default();
392            Ok(Geometry::polygon(exterior, parsed))
393        }
394        "MultiPoint" => {
395            let cs = parse_coord_ring(coords.ok_or_else(|| GeoError::GeoJsonMissing("coordinates".into()))?)?;
396            Ok(Geometry::MultiPoint(cs))
397        }
398        "MultiLineString" => {
399            let lines = coords.and_then(|v| v.as_arr())
400                .ok_or_else(|| GeoError::GeoJsonMissing("MultiLineString coordinates".into()))?;
401            let ls: Vec<Vec<Coord>> = lines.iter().map(|l| parse_coord_ring(l)).collect::<Result<_>>()?;
402            Ok(Geometry::MultiLineString(ls))
403        }
404        "MultiPolygon" => {
405            let polys = coords.and_then(|v| v.as_arr())
406                .ok_or_else(|| GeoError::GeoJsonMissing("MultiPolygon coordinates".into()))?;
407            let ps: Vec<(Vec<Coord>, Vec<Vec<Coord>>)> = polys.iter().map(|poly| {
408                let rings = poly.as_arr().ok_or_else(|| GeoError::GeoJsonMissing("polygon rings".into()))?;
409                let mut parsed: Vec<Vec<Coord>> = rings.iter()
410                    .map(|r| parse_coord_ring(r).map(strip_closed_ring))
411                    .collect::<Result<_>>()?;
412                let ext = parsed.drain(..1).next().unwrap_or_default();
413                Ok((ext, parsed))
414            }).collect::<Result<_>>()?;
415            Ok(Geometry::multi_polygon(ps))
416        }
417        "GeometryCollection" => {
418            let geoms = val.get("geometries").and_then(|v| v.as_arr())
419                .ok_or_else(|| GeoError::GeoJsonMissing("geometries".into()))?;
420            let gs: Vec<Geometry> = geoms.iter().map(|g| parse_geometry(g)).collect::<Result<_>>()?;
421            Ok(Geometry::GeometryCollection(gs))
422        }
423        other => Err(GeoError::GeoJsonType(other.to_owned())),
424    }
425}
426
427fn parse_one_coord(v: &Jv) -> Result<Coord> {
428    let a = v.as_arr().ok_or_else(|| GeoError::GeoJsonParse { offset: 0, msg: "coordinate must be array".into() })?;
429    let x = a.get(0).and_then(|v| v.as_f64()).ok_or_else(|| GeoError::GeoJsonParse { offset: 0, msg: "missing x".into() })?;
430    let y = a.get(1).and_then(|v| v.as_f64()).ok_or_else(|| GeoError::GeoJsonParse { offset: 0, msg: "missing y".into() })?;
431    let z = a.get(2).and_then(|v| v.as_f64());
432    Ok(Coord { x, y, z, m: None })
433}
434
435fn parse_coord_ring(v: &Jv) -> Result<Vec<Coord>> {
436    let arr = v.as_arr().ok_or_else(|| GeoError::GeoJsonParse { offset: 0, msg: "expected coord array".into() })?;
437    arr.iter().map(|c| parse_one_coord(c)).collect()
438}
439
440fn strip_closed_ring(mut coords: Vec<Coord>) -> Vec<Coord> {
441    if coords.len() > 1 {
442        let first = coords.first().cloned();
443        let last = coords.last().cloned();
444        if first == last {
445            coords.pop();
446        }
447    }
448    coords
449}
450
451// ══════════════════════════════════════════════════════════════════════════════
452// Layer → GeoJSON string
453// ══════════════════════════════════════════════════════════════════════════════
454
455fn write_feature_collection(s: &mut String, layer: &Layer) {
456    s.push_str(r#"{"type":"FeatureCollection","features":["#);
457    for (i, f) in layer.features.iter().enumerate() {
458        if i > 0 { s.push(','); }
459        write_feature(s, f, &layer.schema);
460    }
461    s.push_str("]}");
462}
463
464fn write_feature(s: &mut String, f: &Feature, schema: &Schema) {
465    s.push_str(r#"{"type":"Feature","geometry":"#);
466    match &f.geometry { None => s.push_str("null"), Some(g) => write_geom(s, g) }
467    s.push_str(r#","properties":"#);
468    write_props(s, f, schema);
469    s.push('}');
470}
471
472fn write_geom(s: &mut String, g: &Geometry) {
473    match g {
474        Geometry::Point(c) => {
475            s.push_str(r#"{"type":"Point","coordinates":"#);
476            write_coord(s, c); s.push('}');
477        }
478        Geometry::LineString(cs) => {
479            s.push_str(r#"{"type":"LineString","coordinates":"#);
480            write_coord_arr(s, cs); s.push('}');
481        }
482        Geometry::Polygon { exterior, interiors } => {
483            s.push_str(r#"{"type":"Polygon","coordinates":["#);
484            write_ring_arr(s, exterior);
485            for r in interiors { s.push(','); write_ring_arr(s, r); }
486            s.push_str("]}");
487        }
488        Geometry::MultiPoint(cs) => {
489            s.push_str(r#"{"type":"MultiPoint","coordinates":"#);
490            write_coord_arr(s, cs); s.push('}');
491        }
492        Geometry::MultiLineString(ls) => {
493            s.push_str(r#"{"type":"MultiLineString","coordinates":["#);
494            for (i, l) in ls.iter().enumerate() { if i>0 {s.push(',');} write_coord_arr(s, l); }
495            s.push_str("]}");
496        }
497        Geometry::MultiPolygon(ps) => {
498            s.push_str(r#"{"type":"MultiPolygon","coordinates":["#);
499            for (i, (e, hs)) in ps.iter().enumerate() {
500                if i>0 {s.push(',');} s.push('[');
501                write_ring_arr(s, e);
502                for h in hs { s.push(','); write_ring_arr(s, h); }
503                s.push(']');
504            }
505            s.push_str("]}");
506        }
507        Geometry::GeometryCollection(gs) => {
508            s.push_str(r#"{"type":"GeometryCollection","geometries":["#);
509            for (i, g) in gs.iter().enumerate() { if i>0 {s.push(',');} write_geom(s, g); }
510            s.push_str("]}");
511        }
512    }
513}
514
515fn write_coord(s: &mut String, c: &Coord) {
516    s.push('[');
517    s.push_str(&fmt_number(c.x)); s.push(','); s.push_str(&fmt_number(c.y));
518    if let Some(z) = c.z { s.push(','); s.push_str(&fmt_number(z)); }
519    s.push(']');
520}
521
522fn write_coord_arr(s: &mut String, cs: &[Coord]) {
523    s.push('[');
524    for (i, c) in cs.iter().enumerate() { if i>0 {s.push(',');} write_coord(s, c); }
525    s.push(']');
526}
527
528fn write_ring_arr(s: &mut String, ring: &Ring) {
529    s.push('[');
530    for (i, c) in ring.0.iter().enumerate() { if i>0 {s.push(',');} write_coord(s, c); }
531    // close ring
532    if !ring.0.is_empty() { s.push(','); write_coord(s, &ring.0[0]); }
533    s.push(']');
534}
535
536fn write_props(s: &mut String, f: &Feature, schema: &Schema) {
537    if schema.is_empty() { s.push_str("null"); return; }
538    s.push('{');
539    let mut first = true;
540    for (i, fd) in schema.fields().iter().enumerate() {
541        if !first { s.push(','); }
542        first = false;
543        write_json_str(s, &fd.name);
544        s.push(':');
545        let val = f.attributes.get(i).unwrap_or(&FieldValue::Null);
546        write_field_value(s, val);
547    }
548    s.push('}');
549}
550
551fn write_field_value(s: &mut String, val: &FieldValue) {
552    match val {
553        FieldValue::Null         => s.push_str("null"),
554        FieldValue::Integer(v)   => s.push_str(&v.to_string()),
555        FieldValue::Float(v)     => s.push_str(&fmt_number(*v)),
556        FieldValue::Boolean(v)   => s.push_str(if *v { "true" } else { "false" }),
557        FieldValue::Text(v) | FieldValue::Date(v) | FieldValue::DateTime(v) => write_json_str(s, v),
558        FieldValue::Blob(b)      => {
559            // Encode as hex string
560            s.push('"');
561            for byte in b { s.push_str(&format!("{byte:02X}")); }
562            s.push('"');
563        }
564    }
565}
566
567fn write_json_str(s: &mut String, v: &str) {
568    s.push('"');
569    for ch in v.chars() {
570        match ch {
571            '"'  => s.push_str("\\\""),
572            '\\' => s.push_str("\\\\"),
573            '\n' => s.push_str("\\n"),
574            '\r' => s.push_str("\\r"),
575            '\t' => s.push_str("\\t"),
576            c    => s.push(c),
577        }
578    }
579    s.push('"');
580}
581
582fn fmt_number(n: f64) -> String {
583    if n.fract() == 0.0 && n.abs() < 1e15 { format!("{}", n as i64) }
584    else { format!("{n}") }
585}
586
587// ══════════════════════════════════════════════════════════════════════════════
588// Tests
589// ══════════════════════════════════════════════════════════════════════════════
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594
595    const SAMPLE: &str = r#"{
596        "type": "FeatureCollection",
597        "features": [
598            {"type":"Feature",
599             "geometry":{"type":"Point","coordinates":[10.5,20.0]},
600             "properties":{"name":"alpha","count":7,"score":3.14}},
601            {"type":"Feature",
602             "geometry":{"type":"Polygon","coordinates":[[[0,0],[1,0],[1,1],[0,1],[0,0]]]},
603             "properties":{"name":"beta","count":2,"score":null}}
604        ]
605    }"#;
606
607    #[test]
608    fn parse_fc() {
609        let l = parse_str(SAMPLE).unwrap();
610        assert_eq!(l.len(), 2);
611        assert_eq!(l.schema.len(), 3);
612    }
613
614    #[test]
615    fn parse_point() {
616        let l = parse_str(SAMPLE).unwrap();
617        if let Some(Geometry::Point(c)) = &l[0].geometry {
618            assert!((c.x - 10.5).abs() < 1e-9);
619            assert!((c.y - 20.0).abs() < 1e-9);
620        } else { panic!("expected Point"); }
621    }
622
623    #[test]
624    fn parse_polygon() {
625        let l = parse_str(SAMPLE).unwrap();
626        if let Some(Geometry::Polygon { exterior, interiors }) = &l[1].geometry {
627            assert_eq!(exterior.len(), 4); // closing point stripped
628            assert!(interiors.is_empty());
629        } else { panic!("expected Polygon"); }
630    }
631
632    #[test]
633    fn field_types() {
634        let l = parse_str(SAMPLE).unwrap();
635        let f = l.schema.field("name").unwrap();
636        assert_eq!(f.field_type, FieldType::Text);
637        let f = l.schema.field("count").unwrap();
638        assert_eq!(f.field_type, FieldType::Integer);
639        let f = l.schema.field("score").unwrap();
640        // null in one row widens Integer→Float here because 3.14 has fract
641        assert_eq!(f.field_type, FieldType::Float);
642    }
643
644    #[test]
645    fn roundtrip() {
646        let l1 = parse_str(SAMPLE).unwrap();
647        let json = to_string(&l1);
648        let l2 = parse_str(&json).unwrap();
649        assert_eq!(l1.len(), l2.len());
650        assert_eq!(l1.schema.len(), l2.schema.len());
651    }
652
653    #[test]
654    fn null_geometry() {
655        let text = r#"{"type":"FeatureCollection","features":[
656            {"type":"Feature","geometry":null,"properties":{"id":1}}]}"#;
657        let l = parse_str(text).unwrap();
658        assert!(l[0].geometry.is_none());
659    }
660
661    #[test]
662    fn geometry_collection() {
663        let text = r#"{"type":"GeometryCollection","geometries":[
664            {"type":"Point","coordinates":[0,0]},
665            {"type":"LineString","coordinates":[[0,0],[1,1]]}]}"#;
666        let l = parse_str(text).unwrap();
667        assert!(matches!(l[0].geometry, Some(Geometry::GeometryCollection(_))));
668    }
669
670    #[test]
671    fn file_roundtrip() {
672        let dir = tempfile::tempdir().unwrap();
673        let path = dir.path().join("test.geojson");
674        let l1 = parse_str(SAMPLE).unwrap();
675        write(&l1, &path).unwrap();
676        let l2 = read(&path).unwrap();
677        assert_eq!(l2.len(), 2);
678    }
679
680    #[test]
681    fn write_reprojects_projected_layer_to_epsg4326() {
682        let dir = tempfile::tempdir().unwrap();
683        let path = dir.path().join("reprojected.geojson");
684
685        let mut layer = Layer::new("mercator").with_crs_epsg(3857);
686        layer.push(Feature {
687            fid: 0,
688            geometry: Some(Geometry::point(111319.49079327357, 0.0)),
689            attributes: vec![],
690        });
691
692        write(&layer, &path).unwrap();
693        let out = read(&path).unwrap();
694
695        if let Some(Geometry::Point(c)) = &out[0].geometry {
696            assert!((c.x - 1.0).abs() < 1.0e-5);
697            assert!(c.y.abs() < 1.0e-9);
698        } else {
699            panic!("expected Point geometry");
700        }
701    }
702}