Skip to main content

oxigdal_gpkg/vector/
types.rs

1//! Core GeoPackage data types: field types/values, coordinate structures, and geometry.
2
3// ─────────────────────────────────────────────────────────────────────────────
4// FieldType
5// ─────────────────────────────────────────────────────────────────────────────
6
7/// Column type categories used in GeoPackage / SQLite schemas.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum FieldType {
10    /// Signed integer (SQLite INTEGER affinity).
11    Integer,
12    /// IEEE-754 double (SQLite REAL affinity).
13    Real,
14    /// UTF-8 text (SQLite TEXT affinity).
15    Text,
16    /// Raw binary (SQLite BLOB affinity).
17    Blob,
18    /// Boolean stored as INTEGER 0/1.
19    Boolean,
20    /// Calendar date stored as TEXT `"YYYY-MM-DD"`.
21    Date,
22    /// Date+time stored as TEXT `"YYYY-MM-DDTHH:MM:SS.sssZ"`.
23    DateTime,
24    /// SQL NULL / unknown type.
25    Null,
26}
27
28impl FieldType {
29    /// Derive a [`FieldType`] from a SQLite type-name string (case-insensitive).
30    ///
31    /// Unrecognised strings map to [`FieldType::Text`] following SQLite type
32    /// affinity rules.
33    pub fn from_sql_type(type_str: &str) -> Self {
34        match type_str.to_ascii_uppercase().trim() {
35            "INTEGER" | "INT" | "TINYINT" | "SMALLINT" | "MEDIUMINT" | "BIGINT"
36            | "UNSIGNED BIG INT" | "INT2" | "INT8" => Self::Integer,
37            "REAL" | "DOUBLE" | "DOUBLE PRECISION" | "FLOAT" | "NUMERIC" | "DECIMAL" => Self::Real,
38            "BLOB" => Self::Blob,
39            "BOOLEAN" | "BOOL" => Self::Boolean,
40            "DATE" => Self::Date,
41            "DATETIME" | "TIMESTAMP" => Self::DateTime,
42            "NULL" => Self::Null,
43            _ => Self::Text,
44        }
45    }
46
47    /// Return the canonical SQL type name string for this field type.
48    pub fn as_str(self) -> &'static str {
49        match self {
50            Self::Integer => "INTEGER",
51            Self::Real => "REAL",
52            Self::Text => "TEXT",
53            Self::Blob => "BLOB",
54            Self::Boolean => "BOOLEAN",
55            Self::Date => "DATE",
56            Self::DateTime => "DATETIME",
57            Self::Null => "NULL",
58        }
59    }
60}
61
62// ─────────────────────────────────────────────────────────────────────────────
63// FieldValue
64// ─────────────────────────────────────────────────────────────────────────────
65
66/// A runtime value read from a GeoPackage feature-table column.
67#[derive(Debug, Clone, PartialEq)]
68pub enum FieldValue {
69    /// Signed 64-bit integer.
70    Integer(i64),
71    /// IEEE-754 double-precision float.
72    Real(f64),
73    /// UTF-8 text.
74    Text(String),
75    /// Raw binary data.
76    Blob(Vec<u8>),
77    /// Boolean value.
78    Boolean(bool),
79    /// SQL NULL.
80    Null,
81}
82
83impl FieldValue {
84    /// Return the contained integer, or `None` for other variants.
85    pub fn as_integer(&self) -> Option<i64> {
86        match self {
87            Self::Integer(v) => Some(*v),
88            _ => None,
89        }
90    }
91
92    /// Return the contained float, or `None` for other variants.
93    pub fn as_real(&self) -> Option<f64> {
94        match self {
95            Self::Real(v) => Some(*v),
96            _ => None,
97        }
98    }
99
100    /// Return a reference to the contained text, or `None` for other variants.
101    pub fn as_text(&self) -> Option<&str> {
102        match self {
103            Self::Text(s) => Some(s.as_str()),
104            _ => None,
105        }
106    }
107
108    /// Return the contained boolean, or `None` for other variants.
109    pub fn as_bool(&self) -> Option<bool> {
110        match self {
111            Self::Boolean(b) => Some(*b),
112            _ => None,
113        }
114    }
115
116    /// Return `true` if this is the SQL NULL variant.
117    pub fn is_null(&self) -> bool {
118        matches!(self, Self::Null)
119    }
120
121    /// Return the [`FieldType`] that corresponds to this value's variant.
122    pub fn field_type(&self) -> FieldType {
123        match self {
124            Self::Integer(_) => FieldType::Integer,
125            Self::Real(_) => FieldType::Real,
126            Self::Text(_) => FieldType::Text,
127            Self::Blob(_) => FieldType::Blob,
128            Self::Boolean(_) => FieldType::Boolean,
129            Self::Null => FieldType::Null,
130        }
131    }
132
133    /// Serialise this value as a JSON fragment (no trailing newline).
134    pub(crate) fn to_json(&self) -> String {
135        match self {
136            Self::Integer(v) => v.to_string(),
137            Self::Real(v) => {
138                if v.is_finite() {
139                    format!("{v}")
140                } else {
141                    "null".into()
142                }
143            }
144            Self::Text(s) => json_string_escape(s),
145            Self::Blob(b) => {
146                // Encode as a hex string prefixed with "0x"
147                let hex: String = b.iter().map(|byte| format!("{byte:02x}")).collect();
148                json_string_escape(&format!("0x{hex}"))
149            }
150            Self::Boolean(b) => if *b { "true" } else { "false" }.into(),
151            Self::Null => "null".into(),
152        }
153    }
154}
155
156// ─────────────────────────────────────────────────────────────────────────────
157// FieldDefinition
158// ─────────────────────────────────────────────────────────────────────────────
159
160/// Schema description of a single column in a feature table.
161#[derive(Debug, Clone, PartialEq)]
162pub struct FieldDefinition {
163    /// Column name.
164    pub name: String,
165    /// Declared column type.
166    pub field_type: FieldType,
167    /// `true` when a NOT NULL constraint is present.
168    pub not_null: bool,
169    /// `true` when this column is (part of) the primary key.
170    pub primary_key: bool,
171    /// Optional DEFAULT expression as a raw SQL string.
172    pub default_value: Option<String>,
173}
174
175// ─────────────────────────────────────────────────────────────────────────────
176// CoordDim and Point4D
177// ─────────────────────────────────────────────────────────────────────────────
178
179/// Coordinate dimensionality derived from a WKB type code.
180///
181/// Used internally to select the correct coordinate reader during WKB parsing.
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum CoordDim {
184    /// 2D: X and Y only.
185    XY,
186    /// 3D: X, Y, and Z (elevation / height).
187    XYZ,
188    /// 3D: X, Y, and M (measure / linear reference).
189    XYM,
190    /// 4D: X, Y, Z (elevation), and M (measure).
191    XYZM,
192}
193
194/// A 4D coordinate carrying optional Z and M values.
195///
196/// Created by the WKB parser for ZM geometries; Z and M are `Some` only when
197/// the source WKB type includes those dimensions.
198#[derive(Debug, Clone, PartialEq)]
199pub struct Point4D {
200    /// X coordinate (longitude / easting).
201    pub x: f64,
202    /// Y coordinate (latitude / northing).
203    pub y: f64,
204    /// Optional Z coordinate (elevation / height).
205    pub z: Option<f64>,
206    /// Optional M coordinate (measure / linear reference value).
207    pub m: Option<f64>,
208}
209
210impl Point4D {
211    /// Project to a 2D (x, y) tuple, discarding Z and M.
212    pub fn to_xy(&self) -> (f64, f64) {
213        (self.x, self.y)
214    }
215
216    /// Project to a 3D (x, y, z) tuple, substituting 0.0 when Z is absent.
217    pub fn to_xyz(&self) -> (f64, f64, f64) {
218        (self.x, self.y, self.z.unwrap_or(0.0))
219    }
220}
221
222// ─────────────────────────────────────────────────────────────────────────────
223// GpkgGeometry
224// ─────────────────────────────────────────────────────────────────────────────
225
226/// A decoded GeoPackage geometry value.
227///
228/// Coordinates are always (x, y) pairs — typically (longitude, latitude) for
229/// geographic SRSs or (easting, northing) for projected ones.
230/// Z variants carry an additional elevation / height coordinate per vertex.
231#[derive(Debug, Clone, PartialEq)]
232pub enum GpkgGeometry {
233    /// A single point.
234    Point {
235        /// X coordinate (longitude / easting).
236        x: f64,
237        /// Y coordinate (latitude / northing).
238        y: f64,
239    },
240    /// An ordered sequence of points forming a line.
241    LineString {
242        /// Coordinate pairs along the line.
243        coords: Vec<(f64, f64)>,
244    },
245    /// A polygon defined by one exterior ring and zero or more interior rings.
246    Polygon {
247        /// Rings: index 0 is the exterior ring; subsequent entries are holes.
248        rings: Vec<Vec<(f64, f64)>>,
249    },
250    /// A collection of points.
251    MultiPoint {
252        /// Individual point coordinates.
253        points: Vec<(f64, f64)>,
254    },
255    /// A collection of line strings.
256    MultiLineString {
257        /// Individual line strings, each as a coordinate sequence.
258        lines: Vec<Vec<(f64, f64)>>,
259    },
260    /// A collection of polygons.
261    MultiPolygon {
262        /// Individual polygons, each as a list of rings.
263        polygons: Vec<Vec<Vec<(f64, f64)>>>,
264    },
265    /// A heterogeneous collection of geometries.
266    GeometryCollection(Vec<GpkgGeometry>),
267    /// A single 3D point with Z coordinate.
268    PointZ {
269        /// X coordinate (longitude / easting).
270        x: f64,
271        /// Y coordinate (latitude / northing).
272        y: f64,
273        /// Z coordinate (elevation / height).
274        z: f64,
275    },
276    /// An ordered sequence of 3D points forming a line.
277    LineStringZ {
278        /// (x, y, z) coordinate triples along the line.
279        coords: Vec<(f64, f64, f64)>,
280    },
281    /// A 3D polygon defined by one exterior ring and zero or more interior rings.
282    PolygonZ {
283        /// Rings of (x, y, z) triples; index 0 is the exterior ring.
284        rings: Vec<Vec<(f64, f64, f64)>>,
285    },
286    /// A collection of 3D points.
287    MultiPointZ {
288        /// Individual (x, y, z) point coordinates.
289        points: Vec<(f64, f64, f64)>,
290    },
291    /// A collection of 3D line strings.
292    MultiLineStringZ {
293        /// Individual line strings, each as a (x, y, z) sequence.
294        lines: Vec<Vec<(f64, f64, f64)>>,
295    },
296    /// A collection of 3D polygons.
297    MultiPolygonZ {
298        /// Individual 3D polygons, each as a list of (x, y, z) rings.
299        polygons: Vec<Vec<Vec<(f64, f64, f64)>>>,
300    },
301    /// A heterogeneous collection of geometries (may include Z variants).
302    GeometryCollectionZ(Vec<GpkgGeometry>),
303    /// A single XYM point (x, y, m — measure coordinate).
304    PointM {
305        /// X coordinate.
306        x: f64,
307        /// Y coordinate.
308        y: f64,
309        /// M (measure / linear reference) coordinate.
310        m: f64,
311    },
312    /// An ordered sequence of XYM points forming a line.
313    LineStringM {
314        /// (x, y, m) coordinate triples along the line.
315        coords: Vec<(f64, f64, f64)>,
316    },
317    /// An XYM polygon (exterior ring + optional interior rings).
318    PolygonM {
319        /// Rings of (x, y, m) triples; index 0 is the exterior ring.
320        rings: Vec<Vec<(f64, f64, f64)>>,
321    },
322    /// A collection of XYM points.
323    MultiPointM {
324        /// Individual (x, y, m) point coordinates.
325        points: Vec<(f64, f64, f64)>,
326    },
327    /// A collection of XYM line strings.
328    MultiLineStringM {
329        /// Individual line strings, each as an (x, y, m) sequence.
330        lines: Vec<Vec<(f64, f64, f64)>>,
331    },
332    /// A collection of XYM polygons.
333    MultiPolygonM {
334        /// Individual XYM polygons, each as a list of (x, y, m) rings.
335        polygons: Vec<Vec<Vec<(f64, f64, f64)>>>,
336    },
337    /// A heterogeneous collection of geometries that may include M variants.
338    GeometryCollectionM(Vec<GpkgGeometry>),
339    /// A single XYZM point (x, y, z, m).
340    PointZM(Point4D),
341    /// An ordered sequence of XYZM points forming a line.
342    LineStringZM {
343        /// [`Point4D`] vertices along the line.
344        coords: Vec<Point4D>,
345    },
346    /// An XYZM polygon (exterior ring + optional interior rings).
347    PolygonZM {
348        /// Rings of [`Point4D`] vertices; index 0 is the exterior ring.
349        rings: Vec<Vec<Point4D>>,
350    },
351    /// A collection of XYZM points.
352    MultiPointZM {
353        /// Individual [`Point4D`] coordinates.
354        points: Vec<Point4D>,
355    },
356    /// A collection of XYZM line strings.
357    MultiLineStringZM {
358        /// Individual line strings, each as a [`Point4D`] sequence.
359        lines: Vec<Vec<Point4D>>,
360    },
361    /// A collection of XYZM polygons.
362    MultiPolygonZM {
363        /// Individual XYZM polygons, each as a list of [`Point4D`] rings.
364        polygons: Vec<Vec<Vec<Point4D>>>,
365    },
366    /// A heterogeneous collection of geometries that may include ZM variants.
367    GeometryCollectionZM(Vec<GpkgGeometry>),
368    /// An explicitly empty geometry (GeoPackage envelope-indicator = 0, empty flag set).
369    Empty,
370}
371
372impl GpkgGeometry {
373    /// Return the OGC geometry-type name.
374    pub fn geometry_type(&self) -> &'static str {
375        match self {
376            Self::Point { .. } => "Point",
377            Self::LineString { .. } => "LineString",
378            Self::Polygon { .. } => "Polygon",
379            Self::MultiPoint { .. } => "MultiPoint",
380            Self::MultiLineString { .. } => "MultiLineString",
381            Self::MultiPolygon { .. } => "MultiPolygon",
382            Self::GeometryCollection(_) => "GeometryCollection",
383            Self::PointZ { .. } => "PointZ",
384            Self::LineStringZ { .. } => "LineStringZ",
385            Self::PolygonZ { .. } => "PolygonZ",
386            Self::MultiPointZ { .. } => "MultiPointZ",
387            Self::MultiLineStringZ { .. } => "MultiLineStringZ",
388            Self::MultiPolygonZ { .. } => "MultiPolygonZ",
389            Self::GeometryCollectionZ(_) => "GeometryCollectionZ",
390            Self::PointM { .. } => "PointM",
391            Self::LineStringM { .. } => "LineStringM",
392            Self::PolygonM { .. } => "PolygonM",
393            Self::MultiPointM { .. } => "MultiPointM",
394            Self::MultiLineStringM { .. } => "MultiLineStringM",
395            Self::MultiPolygonM { .. } => "MultiPolygonM",
396            Self::GeometryCollectionM(_) => "GeometryCollectionM",
397            Self::PointZM(_) => "PointZM",
398            Self::LineStringZM { .. } => "LineStringZM",
399            Self::PolygonZM { .. } => "PolygonZM",
400            Self::MultiPointZM { .. } => "MultiPointZM",
401            Self::MultiLineStringZM { .. } => "MultiLineStringZM",
402            Self::MultiPolygonZM { .. } => "MultiPolygonZM",
403            Self::GeometryCollectionZM(_) => "GeometryCollectionZM",
404            Self::Empty => "Empty",
405        }
406    }
407
408    /// Return the total number of coordinate points in this geometry.
409    pub fn point_count(&self) -> usize {
410        match self {
411            Self::Point { .. } | Self::PointZ { .. } | Self::PointM { .. } | Self::PointZM(_) => 1,
412            Self::LineString { coords } => coords.len(),
413            Self::LineStringZ { coords } => coords.len(),
414            Self::LineStringM { coords } => coords.len(),
415            Self::LineStringZM { coords } => coords.len(),
416            Self::Polygon { rings } => rings.iter().map(|r| r.len()).sum(),
417            Self::PolygonZ { rings } => rings.iter().map(|r| r.len()).sum(),
418            Self::PolygonM { rings } => rings.iter().map(|r| r.len()).sum(),
419            Self::PolygonZM { rings } => rings.iter().map(|r| r.len()).sum(),
420            Self::MultiPoint { points } => points.len(),
421            Self::MultiPointZ { points } => points.len(),
422            Self::MultiPointM { points } => points.len(),
423            Self::MultiPointZM { points } => points.len(),
424            Self::MultiLineString { lines } => lines.iter().map(|l| l.len()).sum(),
425            Self::MultiLineStringZ { lines } => lines.iter().map(|l| l.len()).sum(),
426            Self::MultiLineStringM { lines } => lines.iter().map(|l| l.len()).sum(),
427            Self::MultiLineStringZM { lines } => lines.iter().map(|l| l.len()).sum(),
428            Self::MultiPolygon { polygons } => polygons
429                .iter()
430                .flat_map(|poly| poly.iter())
431                .map(|ring| ring.len())
432                .sum(),
433            Self::MultiPolygonZ { polygons } => polygons
434                .iter()
435                .flat_map(|poly| poly.iter())
436                .map(|ring| ring.len())
437                .sum(),
438            Self::MultiPolygonM { polygons } => polygons
439                .iter()
440                .flat_map(|poly| poly.iter())
441                .map(|ring| ring.len())
442                .sum(),
443            Self::MultiPolygonZM { polygons } => polygons
444                .iter()
445                .flat_map(|poly| poly.iter())
446                .map(|ring| ring.len())
447                .sum(),
448            Self::GeometryCollection(geoms)
449            | Self::GeometryCollectionZ(geoms)
450            | Self::GeometryCollectionM(geoms)
451            | Self::GeometryCollectionZM(geoms) => geoms.iter().map(|g| g.point_count()).sum(),
452            Self::Empty => 0,
453        }
454    }
455
456    /// Return the axis-aligned bounding box `(min_x, min_y, max_x, max_y)`, or
457    /// `None` for empty / zero-point geometries.
458    pub fn bbox(&self) -> Option<(f64, f64, f64, f64)> {
459        let coords: Vec<(f64, f64)> = self.collect_coords();
460        if coords.is_empty() {
461            return None;
462        }
463        let mut min_x = f64::INFINITY;
464        let mut min_y = f64::INFINITY;
465        let mut max_x = f64::NEG_INFINITY;
466        let mut max_y = f64::NEG_INFINITY;
467        for (x, y) in &coords {
468            if *x < min_x {
469                min_x = *x;
470            }
471            if *y < min_y {
472                min_y = *y;
473            }
474            if *x > max_x {
475                max_x = *x;
476            }
477            if *y > max_y {
478                max_y = *y;
479            }
480        }
481        if min_x.is_finite() {
482            Some((min_x, min_y, max_x, max_y))
483        } else {
484            None
485        }
486    }
487
488    /// Collect all coordinate pairs depth-first (Z/M variants project to 2D).
489    fn collect_coords(&self) -> Vec<(f64, f64)> {
490        match self {
491            Self::Point { x, y } => vec![(*x, *y)],
492            Self::PointZ { x, y, .. } => vec![(*x, *y)],
493            Self::PointM { x, y, .. } => vec![(*x, *y)],
494            Self::PointZM(p) => vec![(p.x, p.y)],
495            Self::LineString { coords } => coords.clone(),
496            Self::LineStringZ { coords } => coords.iter().map(|(x, y, _)| (*x, *y)).collect(),
497            Self::LineStringM { coords } => coords.iter().map(|(x, y, _)| (*x, *y)).collect(),
498            Self::LineStringZM { coords } => coords.iter().map(|p| (p.x, p.y)).collect(),
499            Self::Polygon { rings } => rings.iter().flatten().copied().collect(),
500            Self::PolygonZ { rings } => rings.iter().flatten().map(|(x, y, _)| (*x, *y)).collect(),
501            Self::PolygonM { rings } => rings.iter().flatten().map(|(x, y, _)| (*x, *y)).collect(),
502            Self::PolygonZM { rings } => rings.iter().flatten().map(|p| (p.x, p.y)).collect(),
503            Self::MultiPoint { points } => points.clone(),
504            Self::MultiPointZ { points } => points.iter().map(|(x, y, _)| (*x, *y)).collect(),
505            Self::MultiPointM { points } => points.iter().map(|(x, y, _)| (*x, *y)).collect(),
506            Self::MultiPointZM { points } => points.iter().map(|p| (p.x, p.y)).collect(),
507            Self::MultiLineString { lines } => lines.iter().flatten().copied().collect(),
508            Self::MultiLineStringZ { lines } => {
509                lines.iter().flatten().map(|(x, y, _)| (*x, *y)).collect()
510            }
511            Self::MultiLineStringM { lines } => {
512                lines.iter().flatten().map(|(x, y, _)| (*x, *y)).collect()
513            }
514            Self::MultiLineStringZM { lines } => {
515                lines.iter().flatten().map(|p| (p.x, p.y)).collect()
516            }
517            Self::MultiPolygon { polygons } => polygons
518                .iter()
519                .flat_map(|poly| poly.iter().flatten())
520                .copied()
521                .collect(),
522            Self::MultiPolygonZ { polygons } => polygons
523                .iter()
524                .flat_map(|poly| poly.iter().flatten())
525                .map(|(x, y, _)| (*x, *y))
526                .collect(),
527            Self::MultiPolygonM { polygons } => polygons
528                .iter()
529                .flat_map(|poly| poly.iter().flatten())
530                .map(|(x, y, _)| (*x, *y))
531                .collect(),
532            Self::MultiPolygonZM { polygons } => polygons
533                .iter()
534                .flat_map(|poly| poly.iter().flatten())
535                .map(|p| (p.x, p.y))
536                .collect(),
537            Self::GeometryCollection(geoms)
538            | Self::GeometryCollectionZ(geoms)
539            | Self::GeometryCollectionM(geoms)
540            | Self::GeometryCollectionZM(geoms) => {
541                geoms.iter().flat_map(|g| g.collect_coords()).collect()
542            }
543            Self::Empty => vec![],
544        }
545    }
546
547    /// Serialise this geometry as a GeoJSON geometry object string.
548    ///
549    /// Z variants emit `[x,y,z]` coordinate arrays per RFC 7946.
550    pub(crate) fn to_geojson_geometry(&self) -> String {
551        match self {
552            Self::Point { x, y } => {
553                format!(r#"{{"type":"Point","coordinates":[{x},{y}]}}"#)
554            }
555            Self::PointZ { x, y, z } => {
556                format!(r#"{{"type":"Point","coordinates":[{x},{y},{z}]}}"#)
557            }
558            Self::LineString { coords } => {
559                let pts = coords_to_json_array(coords);
560                format!(r#"{{"type":"LineString","coordinates":{pts}}}"#)
561            }
562            Self::LineStringZ { coords } => {
563                let pts = coords_z_to_json_array(coords);
564                format!(r#"{{"type":"LineString","coordinates":{pts}}}"#)
565            }
566            Self::Polygon { rings } => {
567                let rings_json = rings
568                    .iter()
569                    .map(|r| coords_to_json_array(r))
570                    .collect::<Vec<_>>()
571                    .join(",");
572                format!(r#"{{"type":"Polygon","coordinates":[{rings_json}]}}"#)
573            }
574            Self::PolygonZ { rings } => {
575                let rings_json = rings
576                    .iter()
577                    .map(|r| coords_z_to_json_array(r))
578                    .collect::<Vec<_>>()
579                    .join(",");
580                format!(r#"{{"type":"Polygon","coordinates":[{rings_json}]}}"#)
581            }
582            Self::MultiPoint { points } => {
583                let pts = coords_to_json_array(points);
584                format!(r#"{{"type":"MultiPoint","coordinates":{pts}}}"#)
585            }
586            Self::MultiPointZ { points } => {
587                let pts = coords_z_to_json_array(points);
588                format!(r#"{{"type":"MultiPoint","coordinates":{pts}}}"#)
589            }
590            Self::MultiLineString { lines } => {
591                let lines_json = lines
592                    .iter()
593                    .map(|l| coords_to_json_array(l))
594                    .collect::<Vec<_>>()
595                    .join(",");
596                format!(r#"{{"type":"MultiLineString","coordinates":[{lines_json}]}}"#)
597            }
598            Self::MultiLineStringZ { lines } => {
599                let lines_json = lines
600                    .iter()
601                    .map(|l| coords_z_to_json_array(l))
602                    .collect::<Vec<_>>()
603                    .join(",");
604                format!(r#"{{"type":"MultiLineString","coordinates":[{lines_json}]}}"#)
605            }
606            Self::MultiPolygon { polygons } => {
607                let polys_json = polygons
608                    .iter()
609                    .map(|poly| {
610                        let rings_json = poly
611                            .iter()
612                            .map(|r| coords_to_json_array(r))
613                            .collect::<Vec<_>>()
614                            .join(",");
615                        format!("[{rings_json}]")
616                    })
617                    .collect::<Vec<_>>()
618                    .join(",");
619                format!(r#"{{"type":"MultiPolygon","coordinates":[{polys_json}]}}"#)
620            }
621            Self::MultiPolygonZ { polygons } => {
622                let polys_json = polygons
623                    .iter()
624                    .map(|poly| {
625                        let rings_json = poly
626                            .iter()
627                            .map(|r| coords_z_to_json_array(r))
628                            .collect::<Vec<_>>()
629                            .join(",");
630                        format!("[{rings_json}]")
631                    })
632                    .collect::<Vec<_>>()
633                    .join(",");
634                format!(r#"{{"type":"MultiPolygon","coordinates":[{polys_json}]}}"#)
635            }
636            Self::GeometryCollection(geoms)
637            | Self::GeometryCollectionZ(geoms)
638            | Self::GeometryCollectionM(geoms)
639            | Self::GeometryCollectionZM(geoms) => {
640                let geom_json = geoms
641                    .iter()
642                    .map(|g| g.to_geojson_geometry())
643                    .collect::<Vec<_>>()
644                    .join(",");
645                format!(r#"{{"type":"GeometryCollection","geometries":[{geom_json}]}}"#)
646            }
647            // M variants: GeoJSON does not have an M dimension, so we emit XY only.
648            Self::PointM { x, y, .. } => {
649                format!(r#"{{"type":"Point","coordinates":[{x},{y}]}}"#)
650            }
651            Self::LineStringM { coords } => {
652                let pts = coords_to_json_array(
653                    &coords.iter().map(|(x, y, _)| (*x, *y)).collect::<Vec<_>>(),
654                );
655                format!(r#"{{"type":"LineString","coordinates":{pts}}}"#)
656            }
657            Self::PolygonM { rings } => {
658                let rings_json = rings
659                    .iter()
660                    .map(|r| {
661                        coords_to_json_array(
662                            &r.iter().map(|(x, y, _)| (*x, *y)).collect::<Vec<_>>(),
663                        )
664                    })
665                    .collect::<Vec<_>>()
666                    .join(",");
667                format!(r#"{{"type":"Polygon","coordinates":[{rings_json}]}}"#)
668            }
669            Self::MultiPointM { points } => {
670                let pts = coords_to_json_array(
671                    &points.iter().map(|(x, y, _)| (*x, *y)).collect::<Vec<_>>(),
672                );
673                format!(r#"{{"type":"MultiPoint","coordinates":{pts}}}"#)
674            }
675            Self::MultiLineStringM { lines } => {
676                let lines_json = lines
677                    .iter()
678                    .map(|l| {
679                        coords_to_json_array(
680                            &l.iter().map(|(x, y, _)| (*x, *y)).collect::<Vec<_>>(),
681                        )
682                    })
683                    .collect::<Vec<_>>()
684                    .join(",");
685                format!(r#"{{"type":"MultiLineString","coordinates":[{lines_json}]}}"#)
686            }
687            Self::MultiPolygonM { polygons } => {
688                let polys_json = polygons
689                    .iter()
690                    .map(|poly| {
691                        let rings_json = poly
692                            .iter()
693                            .map(|r| {
694                                coords_to_json_array(
695                                    &r.iter().map(|(x, y, _)| (*x, *y)).collect::<Vec<_>>(),
696                                )
697                            })
698                            .collect::<Vec<_>>()
699                            .join(",");
700                        format!("[{rings_json}]")
701                    })
702                    .collect::<Vec<_>>()
703                    .join(",");
704                format!(r#"{{"type":"MultiPolygon","coordinates":[{polys_json}]}}"#)
705            }
706            // ZM variants: GeoJSON emits [x,y,z], M is dropped per RFC 7946.
707            Self::PointZM(p) => {
708                let (x, y, z) = p.to_xyz();
709                format!(r#"{{"type":"Point","coordinates":[{x},{y},{z}]}}"#)
710            }
711            Self::LineStringZM { coords } => {
712                let pts =
713                    coords_z_to_json_array(&coords.iter().map(|p| p.to_xyz()).collect::<Vec<_>>());
714                format!(r#"{{"type":"LineString","coordinates":{pts}}}"#)
715            }
716            Self::PolygonZM { rings } => {
717                let rings_json = rings
718                    .iter()
719                    .map(|r| {
720                        coords_z_to_json_array(&r.iter().map(|p| p.to_xyz()).collect::<Vec<_>>())
721                    })
722                    .collect::<Vec<_>>()
723                    .join(",");
724                format!(r#"{{"type":"Polygon","coordinates":[{rings_json}]}}"#)
725            }
726            Self::MultiPointZM { points } => {
727                let pts =
728                    coords_z_to_json_array(&points.iter().map(|p| p.to_xyz()).collect::<Vec<_>>());
729                format!(r#"{{"type":"MultiPoint","coordinates":{pts}}}"#)
730            }
731            Self::MultiLineStringZM { lines } => {
732                let lines_json = lines
733                    .iter()
734                    .map(|l| {
735                        coords_z_to_json_array(&l.iter().map(|p| p.to_xyz()).collect::<Vec<_>>())
736                    })
737                    .collect::<Vec<_>>()
738                    .join(",");
739                format!(r#"{{"type":"MultiLineString","coordinates":[{lines_json}]}}"#)
740            }
741            Self::MultiPolygonZM { polygons } => {
742                let polys_json = polygons
743                    .iter()
744                    .map(|poly| {
745                        let rings_json = poly
746                            .iter()
747                            .map(|r| {
748                                coords_z_to_json_array(
749                                    &r.iter().map(|p| p.to_xyz()).collect::<Vec<_>>(),
750                                )
751                            })
752                            .collect::<Vec<_>>()
753                            .join(",");
754                        format!("[{rings_json}]")
755                    })
756                    .collect::<Vec<_>>()
757                    .join(",");
758                format!(r#"{{"type":"MultiPolygon","coordinates":[{polys_json}]}}"#)
759            }
760            Self::Empty => "null".into(),
761        }
762    }
763}
764
765// ─────────────────────────────────────────────────────────────────────────────
766// JSON helper utilities (used by GpkgGeometry and FeatureTable)
767// ─────────────────────────────────────────────────────────────────────────────
768
769/// Escape a string for use as a JSON string value (including the surrounding quotes).
770pub(crate) fn json_string_escape(s: &str) -> String {
771    let mut out = String::with_capacity(s.len() + 2);
772    out.push('"');
773    for ch in s.chars() {
774        match ch {
775            '"' => out.push_str("\\\""),
776            '\\' => out.push_str("\\\\"),
777            '\n' => out.push_str("\\n"),
778            '\r' => out.push_str("\\r"),
779            '\t' => out.push_str("\\t"),
780            c if (c as u32) < 0x20 => {
781                out.push_str(&format!("\\u{:04x}", c as u32));
782            }
783            c => out.push(c),
784        }
785    }
786    out.push('"');
787    out
788}
789
790/// Render a coordinate sequence as a JSON array of `[x,y]` arrays.
791pub(crate) fn coords_to_json_array(coords: &[(f64, f64)]) -> String {
792    let inner: String = coords
793        .iter()
794        .map(|(x, y)| format!("[{x},{y}]"))
795        .collect::<Vec<_>>()
796        .join(",");
797    format!("[{inner}]")
798}
799
800/// Render a 3D coordinate sequence as a JSON array of `[x,y,z]` arrays.
801pub(crate) fn coords_z_to_json_array(coords: &[(f64, f64, f64)]) -> String {
802    let inner: String = coords
803        .iter()
804        .map(|(x, y, z)| format!("[{x},{y},{z}]"))
805        .collect::<Vec<_>>()
806        .join(",");
807    format!("[{inner}]")
808}