Skip to main content

oxigdal_gpkg/
vector.rs

1//! GeoPackage vector feature table types.
2//!
3//! Provides pure-Rust data structures for GeoPackage geometry (GeoPackageBinary /
4//! WKB), field definitions, feature rows, and feature tables.  No SQLite
5//! library is required — this module is a binary parser.
6
7use std::collections::HashMap;
8
9use crate::error::GpkgError;
10
11// ─────────────────────────────────────────────────────────────────────────────
12// FieldType
13// ─────────────────────────────────────────────────────────────────────────────
14
15/// Column type categories used in GeoPackage / SQLite schemas.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum FieldType {
18    /// Signed integer (SQLite INTEGER affinity).
19    Integer,
20    /// IEEE-754 double (SQLite REAL affinity).
21    Real,
22    /// UTF-8 text (SQLite TEXT affinity).
23    Text,
24    /// Raw binary (SQLite BLOB affinity).
25    Blob,
26    /// Boolean stored as INTEGER 0/1.
27    Boolean,
28    /// Calendar date stored as TEXT `"YYYY-MM-DD"`.
29    Date,
30    /// Date+time stored as TEXT `"YYYY-MM-DDTHH:MM:SS.sssZ"`.
31    DateTime,
32    /// SQL NULL / unknown type.
33    Null,
34}
35
36impl FieldType {
37    /// Derive a [`FieldType`] from a SQLite type-name string (case-insensitive).
38    ///
39    /// Unrecognised strings map to [`FieldType::Text`] following SQLite type
40    /// affinity rules.
41    pub fn from_sql_type(type_str: &str) -> Self {
42        match type_str.to_ascii_uppercase().trim() {
43            "INTEGER" | "INT" | "TINYINT" | "SMALLINT" | "MEDIUMINT" | "BIGINT"
44            | "UNSIGNED BIG INT" | "INT2" | "INT8" => Self::Integer,
45            "REAL" | "DOUBLE" | "DOUBLE PRECISION" | "FLOAT" | "NUMERIC" | "DECIMAL" => Self::Real,
46            "BLOB" => Self::Blob,
47            "BOOLEAN" | "BOOL" => Self::Boolean,
48            "DATE" => Self::Date,
49            "DATETIME" | "TIMESTAMP" => Self::DateTime,
50            "NULL" => Self::Null,
51            _ => Self::Text,
52        }
53    }
54
55    /// Return the canonical SQL type name string for this field type.
56    pub fn as_str(self) -> &'static str {
57        match self {
58            Self::Integer => "INTEGER",
59            Self::Real => "REAL",
60            Self::Text => "TEXT",
61            Self::Blob => "BLOB",
62            Self::Boolean => "BOOLEAN",
63            Self::Date => "DATE",
64            Self::DateTime => "DATETIME",
65            Self::Null => "NULL",
66        }
67    }
68}
69
70// ─────────────────────────────────────────────────────────────────────────────
71// FieldValue
72// ─────────────────────────────────────────────────────────────────────────────
73
74/// A runtime value read from a GeoPackage feature-table column.
75#[derive(Debug, Clone, PartialEq)]
76pub enum FieldValue {
77    /// Signed 64-bit integer.
78    Integer(i64),
79    /// IEEE-754 double-precision float.
80    Real(f64),
81    /// UTF-8 text.
82    Text(String),
83    /// Raw binary data.
84    Blob(Vec<u8>),
85    /// Boolean value.
86    Boolean(bool),
87    /// SQL NULL.
88    Null,
89}
90
91impl FieldValue {
92    /// Return the contained integer, or `None` for other variants.
93    pub fn as_integer(&self) -> Option<i64> {
94        match self {
95            Self::Integer(v) => Some(*v),
96            _ => None,
97        }
98    }
99
100    /// Return the contained float, or `None` for other variants.
101    pub fn as_real(&self) -> Option<f64> {
102        match self {
103            Self::Real(v) => Some(*v),
104            _ => None,
105        }
106    }
107
108    /// Return a reference to the contained text, or `None` for other variants.
109    pub fn as_text(&self) -> Option<&str> {
110        match self {
111            Self::Text(s) => Some(s.as_str()),
112            _ => None,
113        }
114    }
115
116    /// Return the contained boolean, or `None` for other variants.
117    pub fn as_bool(&self) -> Option<bool> {
118        match self {
119            Self::Boolean(b) => Some(*b),
120            _ => None,
121        }
122    }
123
124    /// Return `true` if this is the SQL NULL variant.
125    pub fn is_null(&self) -> bool {
126        matches!(self, Self::Null)
127    }
128
129    /// Return the [`FieldType`] that corresponds to this value's variant.
130    pub fn field_type(&self) -> FieldType {
131        match self {
132            Self::Integer(_) => FieldType::Integer,
133            Self::Real(_) => FieldType::Real,
134            Self::Text(_) => FieldType::Text,
135            Self::Blob(_) => FieldType::Blob,
136            Self::Boolean(_) => FieldType::Boolean,
137            Self::Null => FieldType::Null,
138        }
139    }
140
141    /// Serialise this value as a JSON fragment (no trailing newline).
142    fn to_json(&self) -> String {
143        match self {
144            Self::Integer(v) => v.to_string(),
145            Self::Real(v) => {
146                if v.is_finite() {
147                    format!("{v}")
148                } else {
149                    "null".into()
150                }
151            }
152            Self::Text(s) => json_string_escape(s),
153            Self::Blob(b) => {
154                // Encode as a hex string prefixed with "0x"
155                let hex: String = b.iter().map(|byte| format!("{byte:02x}")).collect();
156                json_string_escape(&format!("0x{hex}"))
157            }
158            Self::Boolean(b) => if *b { "true" } else { "false" }.into(),
159            Self::Null => "null".into(),
160        }
161    }
162}
163
164// ─────────────────────────────────────────────────────────────────────────────
165// FieldDefinition
166// ─────────────────────────────────────────────────────────────────────────────
167
168/// Schema description of a single column in a feature table.
169#[derive(Debug, Clone, PartialEq)]
170pub struct FieldDefinition {
171    /// Column name.
172    pub name: String,
173    /// Declared column type.
174    pub field_type: FieldType,
175    /// `true` when a NOT NULL constraint is present.
176    pub not_null: bool,
177    /// `true` when this column is (part of) the primary key.
178    pub primary_key: bool,
179    /// Optional DEFAULT expression as a raw SQL string.
180    pub default_value: Option<String>,
181}
182
183// ─────────────────────────────────────────────────────────────────────────────
184// GpkgGeometry
185// ─────────────────────────────────────────────────────────────────────────────
186
187/// A decoded GeoPackage geometry value.
188///
189/// Coordinates are always (x, y) pairs — typically (longitude, latitude) for
190/// geographic SRSs or (easting, northing) for projected ones.
191#[derive(Debug, Clone, PartialEq)]
192pub enum GpkgGeometry {
193    /// A single point.
194    Point {
195        /// X coordinate (longitude / easting).
196        x: f64,
197        /// Y coordinate (latitude / northing).
198        y: f64,
199    },
200    /// An ordered sequence of points forming a line.
201    LineString {
202        /// Coordinate pairs along the line.
203        coords: Vec<(f64, f64)>,
204    },
205    /// A polygon defined by one exterior ring and zero or more interior rings.
206    Polygon {
207        /// Rings: index 0 is the exterior ring; subsequent entries are holes.
208        rings: Vec<Vec<(f64, f64)>>,
209    },
210    /// A collection of points.
211    MultiPoint {
212        /// Individual point coordinates.
213        points: Vec<(f64, f64)>,
214    },
215    /// A collection of line strings.
216    MultiLineString {
217        /// Individual line strings, each as a coordinate sequence.
218        lines: Vec<Vec<(f64, f64)>>,
219    },
220    /// A collection of polygons.
221    MultiPolygon {
222        /// Individual polygons, each as a list of rings.
223        polygons: Vec<Vec<Vec<(f64, f64)>>>,
224    },
225    /// A heterogeneous collection of geometries.
226    GeometryCollection(Vec<GpkgGeometry>),
227    /// An explicitly empty geometry (GeoPackage envelope-indicator = 0, empty flag set).
228    Empty,
229}
230
231impl GpkgGeometry {
232    /// Return the OGC geometry-type name (uppercase).
233    pub fn geometry_type(&self) -> &'static str {
234        match self {
235            Self::Point { .. } => "Point",
236            Self::LineString { .. } => "LineString",
237            Self::Polygon { .. } => "Polygon",
238            Self::MultiPoint { .. } => "MultiPoint",
239            Self::MultiLineString { .. } => "MultiLineString",
240            Self::MultiPolygon { .. } => "MultiPolygon",
241            Self::GeometryCollection(_) => "GeometryCollection",
242            Self::Empty => "Empty",
243        }
244    }
245
246    /// Return the total number of coordinate points in this geometry.
247    pub fn point_count(&self) -> usize {
248        match self {
249            Self::Point { .. } => 1,
250            Self::LineString { coords } => coords.len(),
251            Self::Polygon { rings } => rings.iter().map(|r| r.len()).sum(),
252            Self::MultiPoint { points } => points.len(),
253            Self::MultiLineString { lines } => lines.iter().map(|l| l.len()).sum(),
254            Self::MultiPolygon { polygons } => polygons
255                .iter()
256                .flat_map(|poly| poly.iter())
257                .map(|ring| ring.len())
258                .sum(),
259            Self::GeometryCollection(geoms) => geoms.iter().map(|g| g.point_count()).sum(),
260            Self::Empty => 0,
261        }
262    }
263
264    /// Return the axis-aligned bounding box `(min_x, min_y, max_x, max_y)`, or
265    /// `None` for empty / zero-point geometries.
266    pub fn bbox(&self) -> Option<(f64, f64, f64, f64)> {
267        let coords: Vec<(f64, f64)> = self.collect_coords();
268        if coords.is_empty() {
269            return None;
270        }
271        let mut min_x = f64::INFINITY;
272        let mut min_y = f64::INFINITY;
273        let mut max_x = f64::NEG_INFINITY;
274        let mut max_y = f64::NEG_INFINITY;
275        for (x, y) in &coords {
276            if *x < min_x {
277                min_x = *x;
278            }
279            if *y < min_y {
280                min_y = *y;
281            }
282            if *x > max_x {
283                max_x = *x;
284            }
285            if *y > max_y {
286                max_y = *y;
287            }
288        }
289        if min_x.is_finite() {
290            Some((min_x, min_y, max_x, max_y))
291        } else {
292            None
293        }
294    }
295
296    /// Collect all coordinate pairs depth-first.
297    fn collect_coords(&self) -> Vec<(f64, f64)> {
298        match self {
299            Self::Point { x, y } => vec![(*x, *y)],
300            Self::LineString { coords } => coords.clone(),
301            Self::Polygon { rings } => rings.iter().flatten().copied().collect(),
302            Self::MultiPoint { points } => points.clone(),
303            Self::MultiLineString { lines } => lines.iter().flatten().copied().collect(),
304            Self::MultiPolygon { polygons } => polygons
305                .iter()
306                .flat_map(|poly| poly.iter().flatten())
307                .copied()
308                .collect(),
309            Self::GeometryCollection(geoms) => {
310                geoms.iter().flat_map(|g| g.collect_coords()).collect()
311            }
312            Self::Empty => vec![],
313        }
314    }
315
316    /// Serialise this geometry as a GeoJSON geometry object string.
317    pub(crate) fn to_geojson_geometry(&self) -> String {
318        match self {
319            Self::Point { x, y } => {
320                format!(r#"{{"type":"Point","coordinates":[{x},{y}]}}"#)
321            }
322            Self::LineString { coords } => {
323                let pts = coords_to_json_array(coords);
324                format!(r#"{{"type":"LineString","coordinates":{pts}}}"#)
325            }
326            Self::Polygon { rings } => {
327                let rings_json = rings
328                    .iter()
329                    .map(|r| coords_to_json_array(r))
330                    .collect::<Vec<_>>()
331                    .join(",");
332                format!(r#"{{"type":"Polygon","coordinates":[{rings_json}]}}"#)
333            }
334            Self::MultiPoint { points } => {
335                let pts = coords_to_json_array(points);
336                format!(r#"{{"type":"MultiPoint","coordinates":{pts}}}"#)
337            }
338            Self::MultiLineString { lines } => {
339                let lines_json = lines
340                    .iter()
341                    .map(|l| coords_to_json_array(l))
342                    .collect::<Vec<_>>()
343                    .join(",");
344                format!(r#"{{"type":"MultiLineString","coordinates":[{lines_json}]}}"#)
345            }
346            Self::MultiPolygon { polygons } => {
347                let polys_json = polygons
348                    .iter()
349                    .map(|poly| {
350                        let rings_json = poly
351                            .iter()
352                            .map(|r| coords_to_json_array(r))
353                            .collect::<Vec<_>>()
354                            .join(",");
355                        format!("[{rings_json}]")
356                    })
357                    .collect::<Vec<_>>()
358                    .join(",");
359                format!(r#"{{"type":"MultiPolygon","coordinates":[{polys_json}]}}"#)
360            }
361            Self::GeometryCollection(geoms) => {
362                let geom_json = geoms
363                    .iter()
364                    .map(|g| g.to_geojson_geometry())
365                    .collect::<Vec<_>>()
366                    .join(",");
367                format!(r#"{{"type":"GeometryCollection","geometries":[{geom_json}]}}"#)
368            }
369            Self::Empty => "null".into(),
370        }
371    }
372}
373
374// ─────────────────────────────────────────────────────────────────────────────
375// GpkgBinaryParser
376// ─────────────────────────────────────────────────────────────────────────────
377
378/// Parser and encoder for the GeoPackageBinary (GPB) and WKB geometry formats.
379///
380/// The GeoPackageBinary layout is:
381///
382/// ```text
383/// magic[2]     = 0x47 0x50  ("GP")
384/// version[1]
385/// flags[1]     bits 0-2: envelope indicator
386///              bit  3:   empty-geometry flag
387///              bit  5:   byte order (0=BE, 1=LE)
388/// srs_id[4]    i32, same byte order as flags bit 5
389/// envelope     0/32/48/64 bytes depending on flags bits 0-2
390/// WKB          remainder of the blob
391/// ```
392pub struct GpkgBinaryParser;
393
394impl GpkgBinaryParser {
395    /// Parse a GeoPackageBinary blob into a [`GpkgGeometry`].
396    ///
397    /// # Errors
398    /// - [`GpkgError::InvalidGeometryMagic`] — first two bytes are not `GP`
399    /// - [`GpkgError::InsufficientData`] — blob too short
400    /// - [`GpkgError::WkbParseError`] / [`GpkgError::UnknownWkbType`] — WKB invalid
401    pub fn parse(data: &[u8]) -> Result<GpkgGeometry, GpkgError> {
402        if data.len() < 8 {
403            return Err(GpkgError::InsufficientData {
404                needed: 8,
405                available: data.len(),
406            });
407        }
408        if data[0] != 0x47 || data[1] != 0x50 {
409            return Err(GpkgError::InvalidGeometryMagic);
410        }
411
412        let flags = data[3];
413        let is_little_endian = (flags >> 5) & 1 == 1;
414        let envelope_indicator = flags & 0b0000_0111;
415        let empty_flag = (flags >> 3) & 1 == 1;
416
417        // srs_id at bytes 4..8 (not used for geometry parsing, but we consume it)
418        let _srs_id: i32 = if is_little_endian {
419            i32::from_le_bytes([data[4], data[5], data[6], data[7]])
420        } else {
421            i32::from_be_bytes([data[4], data[5], data[6], data[7]])
422        };
423
424        // Envelope size in bytes: 0, 32, 48, 48, or 64
425        let envelope_bytes: usize = match envelope_indicator {
426            0 => 0,
427            1 => 32,
428            2 | 3 => 48,
429            4 => 64,
430            _ => {
431                return Err(GpkgError::WkbParseError(format!(
432                    "Unknown envelope indicator {envelope_indicator}"
433                )));
434            }
435        };
436
437        let header_size = 8 + envelope_bytes;
438        if data.len() < header_size {
439            return Err(GpkgError::InsufficientData {
440                needed: header_size,
441                available: data.len(),
442            });
443        }
444
445        if empty_flag {
446            return Ok(GpkgGeometry::Empty);
447        }
448
449        let wkb = &data[header_size..];
450        if wkb.is_empty() {
451            return Ok(GpkgGeometry::Empty);
452        }
453        Self::parse_wkb(wkb)
454    }
455
456    /// Parse a WKB (Well-Known Binary) blob into a [`GpkgGeometry`].
457    ///
458    /// Both big-endian (`byte_order = 0`) and little-endian (`byte_order = 1`)
459    /// WKB are supported.
460    pub fn parse_wkb(data: &[u8]) -> Result<GpkgGeometry, GpkgError> {
461        let (geom, _consumed) = parse_wkb_inner(data, 0)?;
462        Ok(geom)
463    }
464
465    /// Encode a [`GpkgGeometry`] as little-endian WKB.
466    pub fn to_wkb(geom: &GpkgGeometry) -> Vec<u8> {
467        let mut buf = Vec::new();
468        write_wkb(geom, &mut buf);
469        buf
470    }
471
472    /// Encode a [`GpkgGeometry`] as a GeoPackageBinary blob (no envelope, LE byte order).
473    ///
474    /// The resulting bytes begin with the magic `GP` (`0x47 0x50`).
475    pub fn to_gpb(geom: &GpkgGeometry, srs_id: i32) -> Vec<u8> {
476        let mut buf = Vec::new();
477        // magic
478        buf.push(0x47); // 'G'
479        buf.push(0x50); // 'P'
480        // version
481        buf.push(0);
482        // flags: no envelope (bits 0-2 = 0), not empty (bit 3 = 0), LE (bit 5 = 1)
483        let is_empty = matches!(geom, GpkgGeometry::Empty);
484        let empty_bit: u8 = if is_empty { 1 << 3 } else { 0 };
485        let flags: u8 = empty_bit | (1 << 5); // LE, no envelope
486        buf.push(flags);
487        // srs_id (LE i32)
488        buf.extend_from_slice(&srs_id.to_le_bytes());
489        // WKB body
490        if !is_empty {
491            write_wkb(geom, &mut buf);
492        }
493        buf
494    }
495}
496
497// ─────────────────────────────────────────────────────────────────────────────
498// WKB internal helpers
499// ─────────────────────────────────────────────────────────────────────────────
500
501/// WKB type constants.
502const WKB_POINT: u32 = 1;
503const WKB_LINESTRING: u32 = 2;
504const WKB_POLYGON: u32 = 3;
505const WKB_MULTIPOINT: u32 = 4;
506const WKB_MULTILINESTRING: u32 = 5;
507const WKB_MULTIPOLYGON: u32 = 6;
508const WKB_GEOMETRYCOLLECTION: u32 = 7;
509
510/// Parse one WKB geometry starting at `data[offset]`.
511/// Returns `(geometry, new_offset)`.
512fn parse_wkb_inner(data: &[u8], offset: usize) -> Result<(GpkgGeometry, usize), GpkgError> {
513    if data.len() < offset + 5 {
514        return Err(GpkgError::InsufficientData {
515            needed: offset + 5,
516            available: data.len(),
517        });
518    }
519    let byte_order = data[offset];
520    let le = byte_order == 1;
521    let mut pos = offset + 1;
522
523    let wkb_type = read_u32(data, pos, le)?;
524    pos += 4;
525
526    match wkb_type {
527        WKB_POINT => {
528            let (x, y, new_pos) = read_point_coords(data, pos, le)?;
529            Ok((GpkgGeometry::Point { x, y }, new_pos))
530        }
531        WKB_LINESTRING => {
532            let (coords, new_pos) = read_coord_sequence(data, pos, le)?;
533            Ok((GpkgGeometry::LineString { coords }, new_pos))
534        }
535        WKB_POLYGON => {
536            let (rings, new_pos) = read_rings(data, pos, le)?;
537            Ok((GpkgGeometry::Polygon { rings }, new_pos))
538        }
539        WKB_MULTIPOINT => {
540            let (n, mut pos2) = read_u32_pos(data, pos, le)?;
541            let mut points = Vec::with_capacity(n as usize);
542            for _ in 0..n {
543                // Each sub-geometry is a complete WKB Point
544                let (sub, new_pos) = parse_wkb_inner(data, pos2)?;
545                pos2 = new_pos;
546                match sub {
547                    GpkgGeometry::Point { x, y } => points.push((x, y)),
548                    other => {
549                        return Err(GpkgError::WkbParseError(format!(
550                            "Expected Point in MultiPoint, got {}",
551                            other.geometry_type()
552                        )));
553                    }
554                }
555            }
556            Ok((GpkgGeometry::MultiPoint { points }, pos2))
557        }
558        WKB_MULTILINESTRING => {
559            let (n, mut pos2) = read_u32_pos(data, pos, le)?;
560            let mut lines = Vec::with_capacity(n as usize);
561            for _ in 0..n {
562                let (sub, new_pos) = parse_wkb_inner(data, pos2)?;
563                pos2 = new_pos;
564                match sub {
565                    GpkgGeometry::LineString { coords } => lines.push(coords),
566                    other => {
567                        return Err(GpkgError::WkbParseError(format!(
568                            "Expected LineString in MultiLineString, got {}",
569                            other.geometry_type()
570                        )));
571                    }
572                }
573            }
574            Ok((GpkgGeometry::MultiLineString { lines }, pos2))
575        }
576        WKB_MULTIPOLYGON => {
577            let (n, mut pos2) = read_u32_pos(data, pos, le)?;
578            let mut polygons = Vec::with_capacity(n as usize);
579            for _ in 0..n {
580                let (sub, new_pos) = parse_wkb_inner(data, pos2)?;
581                pos2 = new_pos;
582                match sub {
583                    GpkgGeometry::Polygon { rings } => polygons.push(rings),
584                    other => {
585                        return Err(GpkgError::WkbParseError(format!(
586                            "Expected Polygon in MultiPolygon, got {}",
587                            other.geometry_type()
588                        )));
589                    }
590                }
591            }
592            Ok((GpkgGeometry::MultiPolygon { polygons }, pos2))
593        }
594        WKB_GEOMETRYCOLLECTION => {
595            let (n, mut pos2) = read_u32_pos(data, pos, le)?;
596            let mut geoms = Vec::with_capacity(n as usize);
597            for _ in 0..n {
598                let (sub, new_pos) = parse_wkb_inner(data, pos2)?;
599                pos2 = new_pos;
600                geoms.push(sub);
601            }
602            Ok((GpkgGeometry::GeometryCollection(geoms), pos2))
603        }
604        other => Err(GpkgError::UnknownWkbType(other)),
605    }
606}
607
608/// Read a u32 from `data[pos]` with the given byte order, returning (value, pos+4).
609fn read_u32_pos(data: &[u8], pos: usize, le: bool) -> Result<(u32, usize), GpkgError> {
610    Ok((read_u32(data, pos, le)?, pos + 4))
611}
612
613fn read_u32(data: &[u8], pos: usize, le: bool) -> Result<u32, GpkgError> {
614    if data.len() < pos + 4 {
615        return Err(GpkgError::InsufficientData {
616            needed: pos + 4,
617            available: data.len(),
618        });
619    }
620    let bytes = [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]];
621    Ok(if le {
622        u32::from_le_bytes(bytes)
623    } else {
624        u32::from_be_bytes(bytes)
625    })
626}
627
628fn read_f64(data: &[u8], pos: usize, le: bool) -> Result<f64, GpkgError> {
629    if data.len() < pos + 8 {
630        return Err(GpkgError::InsufficientData {
631            needed: pos + 8,
632            available: data.len(),
633        });
634    }
635    let bytes = [
636        data[pos],
637        data[pos + 1],
638        data[pos + 2],
639        data[pos + 3],
640        data[pos + 4],
641        data[pos + 5],
642        data[pos + 6],
643        data[pos + 7],
644    ];
645    Ok(if le {
646        f64::from_le_bytes(bytes)
647    } else {
648        f64::from_be_bytes(bytes)
649    })
650}
651
652fn read_point_coords(data: &[u8], pos: usize, le: bool) -> Result<(f64, f64, usize), GpkgError> {
653    let x = read_f64(data, pos, le)?;
654    let y = read_f64(data, pos + 8, le)?;
655    Ok((x, y, pos + 16))
656}
657
658fn read_coord_sequence(
659    data: &[u8],
660    pos: usize,
661    le: bool,
662) -> Result<(Vec<(f64, f64)>, usize), GpkgError> {
663    let (n, mut cur) = read_u32_pos(data, pos, le)?;
664    let mut coords = Vec::with_capacity(n as usize);
665    for _ in 0..n {
666        let (x, y, new_cur) = read_point_coords(data, cur, le)?;
667        cur = new_cur;
668        coords.push((x, y));
669    }
670    Ok((coords, cur))
671}
672
673type RingsResult = Result<(Vec<Vec<(f64, f64)>>, usize), GpkgError>;
674
675fn read_rings(data: &[u8], pos: usize, le: bool) -> RingsResult {
676    let (n_rings, mut cur) = read_u32_pos(data, pos, le)?;
677    let mut rings = Vec::with_capacity(n_rings as usize);
678    for _ in 0..n_rings {
679        let (coords, new_cur) = read_coord_sequence(data, cur, le)?;
680        cur = new_cur;
681        rings.push(coords);
682    }
683    Ok((rings, cur))
684}
685
686/// Write a little-endian WKB representation of `geom` into `buf`.
687fn write_wkb(geom: &GpkgGeometry, buf: &mut Vec<u8>) {
688    buf.push(1); // LE
689    match geom {
690        GpkgGeometry::Point { x, y } => {
691            buf.extend_from_slice(&WKB_POINT.to_le_bytes());
692            buf.extend_from_slice(&x.to_le_bytes());
693            buf.extend_from_slice(&y.to_le_bytes());
694        }
695        GpkgGeometry::LineString { coords } => {
696            buf.extend_from_slice(&WKB_LINESTRING.to_le_bytes());
697            buf.extend_from_slice(&(coords.len() as u32).to_le_bytes());
698            for (x, y) in coords {
699                buf.extend_from_slice(&x.to_le_bytes());
700                buf.extend_from_slice(&y.to_le_bytes());
701            }
702        }
703        GpkgGeometry::Polygon { rings } => {
704            buf.extend_from_slice(&WKB_POLYGON.to_le_bytes());
705            buf.extend_from_slice(&(rings.len() as u32).to_le_bytes());
706            for ring in rings {
707                buf.extend_from_slice(&(ring.len() as u32).to_le_bytes());
708                for (x, y) in ring {
709                    buf.extend_from_slice(&x.to_le_bytes());
710                    buf.extend_from_slice(&y.to_le_bytes());
711                }
712            }
713        }
714        GpkgGeometry::MultiPoint { points } => {
715            buf.extend_from_slice(&WKB_MULTIPOINT.to_le_bytes());
716            buf.extend_from_slice(&(points.len() as u32).to_le_bytes());
717            for (x, y) in points {
718                write_wkb(&GpkgGeometry::Point { x: *x, y: *y }, buf);
719            }
720        }
721        GpkgGeometry::MultiLineString { lines } => {
722            buf.extend_from_slice(&WKB_MULTILINESTRING.to_le_bytes());
723            buf.extend_from_slice(&(lines.len() as u32).to_le_bytes());
724            for line in lines {
725                write_wkb(
726                    &GpkgGeometry::LineString {
727                        coords: line.clone(),
728                    },
729                    buf,
730                );
731            }
732        }
733        GpkgGeometry::MultiPolygon { polygons } => {
734            buf.extend_from_slice(&WKB_MULTIPOLYGON.to_le_bytes());
735            buf.extend_from_slice(&(polygons.len() as u32).to_le_bytes());
736            for poly in polygons {
737                write_wkb(
738                    &GpkgGeometry::Polygon {
739                        rings: poly.clone(),
740                    },
741                    buf,
742                );
743            }
744        }
745        GpkgGeometry::GeometryCollection(geoms) => {
746            buf.extend_from_slice(&WKB_GEOMETRYCOLLECTION.to_le_bytes());
747            buf.extend_from_slice(&(geoms.len() as u32).to_le_bytes());
748            for g in geoms {
749                write_wkb(g, buf);
750            }
751        }
752        GpkgGeometry::Empty => {
753            // Encode as an empty GeometryCollection
754            buf.extend_from_slice(&WKB_GEOMETRYCOLLECTION.to_le_bytes());
755            buf.extend_from_slice(&0u32.to_le_bytes());
756        }
757    }
758}
759
760// ─────────────────────────────────────────────────────────────────────────────
761// FeatureRow
762// ─────────────────────────────────────────────────────────────────────────────
763
764/// A single feature (row) read from a GeoPackage feature table.
765#[derive(Debug, Clone)]
766pub struct FeatureRow {
767    /// Feature identifier (primary key value).
768    pub fid: i64,
769    /// Decoded geometry, or `None` when the geometry column is NULL.
770    pub geometry: Option<GpkgGeometry>,
771    /// Non-geometry attribute values, keyed by column name.
772    pub fields: HashMap<String, FieldValue>,
773}
774
775impl FeatureRow {
776    /// Look up a field by name.
777    pub fn get_field(&self, name: &str) -> Option<&FieldValue> {
778        self.fields.get(name)
779    }
780
781    /// Convenience: return the integer value of a field, or `None`.
782    pub fn get_integer(&self, name: &str) -> Option<i64> {
783        self.fields.get(name)?.as_integer()
784    }
785
786    /// Convenience: return the real value of a field, or `None`.
787    pub fn get_real(&self, name: &str) -> Option<f64> {
788        self.fields.get(name)?.as_real()
789    }
790
791    /// Convenience: return the text value of a field, or `None`.
792    pub fn get_text(&self, name: &str) -> Option<&str> {
793        self.fields.get(name)?.as_text()
794    }
795}
796
797// ─────────────────────────────────────────────────────────────────────────────
798// FeatureTable
799// ─────────────────────────────────────────────────────────────────────────────
800
801/// An in-memory representation of a GeoPackage feature table.
802///
803/// Holds the table schema and all feature rows that have been loaded.
804#[derive(Debug, Clone)]
805pub struct FeatureTable {
806    /// Name of the feature table (matches `gpkg_contents.table_name`).
807    pub name: String,
808    /// Name of the geometry column.
809    pub geometry_column: String,
810    /// Spatial reference system ID, or `None` when unknown.
811    pub srs_id: Option<i32>,
812    /// Column definitions (excludes the geometry column and FID).
813    pub schema: Vec<FieldDefinition>,
814    /// Loaded feature rows.
815    pub features: Vec<FeatureRow>,
816}
817
818impl FeatureTable {
819    /// Create a new, empty feature table with the given name and geometry column.
820    pub fn new(name: impl Into<String>, geometry_column: impl Into<String>) -> Self {
821        Self {
822            name: name.into(),
823            geometry_column: geometry_column.into(),
824            srs_id: None,
825            schema: Vec::new(),
826            features: Vec::new(),
827        }
828    }
829
830    /// Return the number of loaded feature rows.
831    pub fn feature_count(&self) -> usize {
832        self.features.len()
833    }
834
835    /// Append a feature row to the table.
836    pub fn add_feature(&mut self, row: FeatureRow) {
837        self.features.push(row);
838    }
839
840    /// Find a feature by its FID, or return `None`.
841    pub fn get_feature(&self, fid: i64) -> Option<&FeatureRow> {
842        self.features.iter().find(|r| r.fid == fid)
843    }
844
845    /// Return the union bounding box of all feature geometries, or `None` when
846    /// there are no geometries.
847    pub fn bbox(&self) -> Option<(f64, f64, f64, f64)> {
848        let mut min_x = f64::INFINITY;
849        let mut min_y = f64::INFINITY;
850        let mut max_x = f64::NEG_INFINITY;
851        let mut max_y = f64::NEG_INFINITY;
852        let mut found = false;
853
854        for row in &self.features {
855            if let Some(geom) = &row.geometry {
856                if let Some((gx0, gy0, gx1, gy1)) = geom.bbox() {
857                    found = true;
858                    if gx0 < min_x {
859                        min_x = gx0;
860                    }
861                    if gy0 < min_y {
862                        min_y = gy0;
863                    }
864                    if gx1 > max_x {
865                        max_x = gx1;
866                    }
867                    if gy1 > max_y {
868                        max_y = gy1;
869                    }
870                }
871            }
872        }
873
874        if found {
875            Some((min_x, min_y, max_x, max_y))
876        } else {
877            None
878        }
879    }
880
881    /// Return all features whose geometry bounding box intersects the query bbox.
882    ///
883    /// Features with `None` geometry are excluded.  The check is a simple AABB
884    /// intersection test (not precise polygon intersection).
885    pub fn features_in_bbox(
886        &self,
887        min_x: f64,
888        min_y: f64,
889        max_x: f64,
890        max_y: f64,
891    ) -> Vec<&FeatureRow> {
892        self.features
893            .iter()
894            .filter(|row| {
895                if let Some(geom) = &row.geometry {
896                    if let Some((gx0, gy0, gx1, gy1)) = geom.bbox() {
897                        // AABB intersects when not separated on either axis
898                        return gx0 <= max_x && gx1 >= min_x && gy0 <= max_y && gy1 >= min_y;
899                    }
900                }
901                false
902            })
903            .collect()
904    }
905
906    /// Collect all distinct (non-Null) values for a named field across all features.
907    ///
908    /// The returned vec is deduplicated by equality.
909    pub fn distinct_values(&self, field_name: &str) -> Vec<FieldValue> {
910        let mut seen: Vec<FieldValue> = Vec::new();
911        for row in &self.features {
912            if let Some(val) = row.fields.get(field_name) {
913                if !val.is_null() && !seen.contains(val) {
914                    seen.push(val.clone());
915                }
916            }
917        }
918        seen
919    }
920
921    /// Serialise the feature table as a GeoJSON FeatureCollection string.
922    ///
923    /// Geometry `None` is encoded as `"geometry":null`.
924    pub fn to_geojson(&self) -> String {
925        let features_json: String = self
926            .features
927            .iter()
928            .map(|row| {
929                let geom_json = match &row.geometry {
930                    Some(g) => g.to_geojson_geometry(),
931                    None => "null".into(),
932                };
933                let props_json = build_properties_json(&row.fields);
934                format!(r#"{{"type":"Feature","geometry":{geom_json},"properties":{props_json}}}"#)
935            })
936            .collect::<Vec<_>>()
937            .join(",");
938
939        format!(r#"{{"type":"FeatureCollection","features":[{features_json}]}}"#)
940    }
941}
942
943// ─────────────────────────────────────────────────────────────────────────────
944// SrsInfo
945// ─────────────────────────────────────────────────────────────────────────────
946
947/// Spatial reference system metadata (from `gpkg_spatial_ref_sys`).
948#[derive(Debug, Clone, PartialEq)]
949pub struct SrsInfo {
950    /// Human-readable name for this SRS.
951    pub srs_name: String,
952    /// Numeric SRS identifier (primary key in `gpkg_spatial_ref_sys`).
953    pub srs_id: i32,
954    /// Defining organisation (e.g. `"EPSG"`).
955    pub organization: String,
956    /// Organisation-assigned CRS code.
957    pub org_coord_sys_id: i32,
958    /// WKT or PROJ definition of the SRS.
959    pub definition: String,
960    /// Optional free-text description.
961    pub description: Option<String>,
962}
963
964impl SrsInfo {
965    /// Return the standard WGS 84 geographic SRS (EPSG:4326).
966    pub fn wgs84() -> Self {
967        Self {
968            srs_name: "WGS 84".into(),
969            srs_id: 4326,
970            organization: "EPSG".into(),
971            org_coord_sys_id: 4326,
972            definition: concat!(
973                "GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",",
974                "SPHEROID[\"WGS 84\",6378137,298.257223563]],",
975                "PRIMEM[\"Greenwich\",0],",
976                "UNIT[\"degree\",0.0174532925199433]]"
977            )
978            .into(),
979            description: Some("World Geodetic System 1984".into()),
980        }
981    }
982
983    /// Return the Web Mercator (Pseudo-Mercator) projected SRS (EPSG:3857).
984    pub fn web_mercator() -> Self {
985        Self {
986            srs_name: "WGS 84 / Pseudo-Mercator".into(),
987            srs_id: 3857,
988            organization: "EPSG".into(),
989            org_coord_sys_id: 3857,
990            definition: concat!(
991                "PROJCS[\"WGS 84 / Pseudo-Mercator\",",
992                "GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",",
993                "SPHEROID[\"WGS 84\",6378137,298.257223563]],",
994                "PRIMEM[\"Greenwich\",0],",
995                "UNIT[\"degree\",0.0174532925199433]],",
996                "PROJECTION[\"Mercator_1SP\"],",
997                "PARAMETER[\"central_meridian\",0],",
998                "PARAMETER[\"scale_factor\",1],",
999                "PARAMETER[\"false_easting\",0],",
1000                "PARAMETER[\"false_northing\",0],",
1001                "UNIT[\"metre\",1]]"
1002            )
1003            .into(),
1004            description: Some("Web Mercator projection used by many web mapping services".into()),
1005        }
1006    }
1007
1008    /// Return `true` when this SRS uses geographic (lat/lon) coordinates.
1009    ///
1010    /// Heuristic: considers `srs_id` values in the range 4000–4999 as geographic.
1011    pub fn is_geographic(&self) -> bool {
1012        (4000..5000).contains(&self.srs_id)
1013    }
1014
1015    /// Return the EPSG code when the defining organisation is `"EPSG"`.
1016    pub fn epsg_code(&self) -> Option<i32> {
1017        if self.organization.eq_ignore_ascii_case("EPSG") {
1018            Some(self.org_coord_sys_id)
1019        } else {
1020            None
1021        }
1022    }
1023}
1024
1025// ─────────────────────────────────────────────────────────────────────────────
1026// JSON helper utilities
1027// ─────────────────────────────────────────────────────────────────────────────
1028
1029/// Escape a string for use as a JSON string value (including the surrounding quotes).
1030fn json_string_escape(s: &str) -> String {
1031    let mut out = String::with_capacity(s.len() + 2);
1032    out.push('"');
1033    for ch in s.chars() {
1034        match ch {
1035            '"' => out.push_str("\\\""),
1036            '\\' => out.push_str("\\\\"),
1037            '\n' => out.push_str("\\n"),
1038            '\r' => out.push_str("\\r"),
1039            '\t' => out.push_str("\\t"),
1040            c if (c as u32) < 0x20 => {
1041                out.push_str(&format!("\\u{:04x}", c as u32));
1042            }
1043            c => out.push(c),
1044        }
1045    }
1046    out.push('"');
1047    out
1048}
1049
1050/// Render a coordinate sequence as a JSON array of `[x,y]` arrays.
1051fn coords_to_json_array(coords: &[(f64, f64)]) -> String {
1052    let inner: String = coords
1053        .iter()
1054        .map(|(x, y)| format!("[{x},{y}]"))
1055        .collect::<Vec<_>>()
1056        .join(",");
1057    format!("[{inner}]")
1058}
1059
1060/// Render a `HashMap<String, FieldValue>` as a JSON object.
1061fn build_properties_json(fields: &HashMap<String, FieldValue>) -> String {
1062    if fields.is_empty() {
1063        return "{}".into();
1064    }
1065    // Sort keys for deterministic output
1066    let mut pairs: Vec<(&String, &FieldValue)> = fields.iter().collect();
1067    pairs.sort_by_key(|(k, _)| k.as_str());
1068    let members: String = pairs
1069        .iter()
1070        .map(|(k, v)| format!("{}:{}", json_string_escape(k), v.to_json()))
1071        .collect::<Vec<_>>()
1072        .join(",");
1073    format!("{{{members}}}")
1074}