use std::collections::HashMap;
use std::path::Path;
use crate::error::{Error, Result};
use geo_types::{Coord, Geometry, LineString, Point, Polygon};
use super::{AttributeValue, Feature, FeatureCollection};
pub fn read_gpkg(path: &Path, layer: Option<&str>) -> Result<FeatureCollection> {
let conn = rusqlite::Connection::open(path).map_err(|e| {
Error::Other(format!(
"Cannot open GeoPackage '{}': {}",
path.display(),
e
))
})?;
let table_name = if let Some(name) = layer {
name.to_string()
} else {
conn.query_row(
"SELECT table_name FROM gpkg_contents WHERE data_type = 'features' LIMIT 1",
[],
|row| row.get::<_, String>(0),
)
.map_err(|e| {
Error::Other(format!(
"No feature tables in GeoPackage '{}': {}",
path.display(),
e
))
})?
};
let geom_col: String = conn
.query_row(
"SELECT column_name FROM gpkg_geometry_columns WHERE table_name = ?1",
[&table_name],
|row| row.get(0),
)
.map_err(|e| {
Error::Other(format!(
"No geometry column found for table '{}': {}",
table_name, e
))
})?;
let pragma_query = format!("PRAGMA table_info(\"{}\")", table_name);
let mut col_stmt = conn
.prepare(&pragma_query)
.map_err(|e| Error::Other(format!("Cannot query table info: {}", e)))?;
let columns: Vec<String> = col_stmt
.query_map([], |row| row.get::<_, String>(1))
.map_err(|e| Error::Other(format!("Cannot read columns: {}", e)))?
.filter_map(|r| r.ok())
.filter(|name| name != &geom_col)
.collect();
let all_cols: Vec<String> = std::iter::once(format!("\"{}\"", geom_col))
.chain(columns.iter().map(|c| format!("\"{}\"", c)))
.collect();
let select_query = format!("SELECT {} FROM \"{}\"", all_cols.join(", "), table_name);
let mut stmt = conn
.prepare(&select_query)
.map_err(|e| Error::Other(format!("Cannot prepare query: {}", e)))?;
let mut result_rows = stmt
.query([])
.map_err(|e| Error::Other(format!("Query failed: {}", e)))?;
let mut features = FeatureCollection::new();
while let Some(row) = result_rows
.next()
.map_err(|e| Error::Other(format!("Row error: {}", e)))?
{
let geometry = if let Ok(blob) = row.get_ref(0) {
match blob {
rusqlite::types::ValueRef::Blob(bytes) => parse_gpkg_wkb(bytes),
_ => None,
}
} else {
None
};
let mut properties = HashMap::new();
for (i, col_name) in columns.iter().enumerate() {
let col_idx = i + 1; let value = if let Ok(val_ref) = row.get_ref(col_idx) {
match val_ref {
rusqlite::types::ValueRef::Null => AttributeValue::Null,
rusqlite::types::ValueRef::Integer(v) => AttributeValue::Int(v),
rusqlite::types::ValueRef::Real(v) => AttributeValue::Float(v),
rusqlite::types::ValueRef::Text(s) => {
AttributeValue::String(String::from_utf8_lossy(s).to_string())
}
rusqlite::types::ValueRef::Blob(_) => AttributeValue::Null,
}
} else {
AttributeValue::Null
};
properties.insert(col_name.clone(), value);
}
let mut feature = match geometry {
Some(geom) => Feature::new(geom),
None => Feature::empty(),
};
feature.properties = properties;
features.push(feature);
}
Ok(features)
}
pub fn list_gpkg_layers(path: &Path) -> Result<Vec<String>> {
let conn = rusqlite::Connection::open(path)
.map_err(|e| Error::Other(format!("Cannot open GeoPackage: {}", e)))?;
let mut stmt = conn
.prepare("SELECT table_name FROM gpkg_contents WHERE data_type = 'features'")
.map_err(|e| Error::Other(format!("Cannot query gpkg_contents: {}", e)))?;
let names: Vec<String> = stmt
.query_map([], |row| row.get(0))
.map_err(|e| Error::Other(format!("Query failed: {}", e)))?
.filter_map(|r| r.ok())
.collect();
Ok(names)
}
fn parse_gpkg_wkb(bytes: &[u8]) -> Option<Geometry<f64>> {
if bytes.len() < 8 {
return None;
}
if bytes[0] != 0x47 || bytes[1] != 0x50 {
return parse_wkb(bytes);
}
let flags = bytes[3];
let envelope_type = (flags >> 1) & 0x07;
let envelope_size = match envelope_type {
0 => 0, 1 => 32, 2 => 48, 3 => 48, 4 => 64, _ => return None,
};
let wkb_start = 8 + envelope_size;
if wkb_start > bytes.len() {
return None;
}
parse_wkb(&bytes[wkb_start..])
}
fn parse_wkb(bytes: &[u8]) -> Option<Geometry<f64>> {
if bytes.len() < 5 {
return None;
}
let is_le = bytes[0] == 1;
let geom_type = read_u32(bytes, &mut 1, is_le)?;
let base_type = geom_type % 1000;
let has_z = matches!(geom_type / 1000, 1 | 3);
let has_m = matches!(geom_type / 1000, 2 | 3);
let coord_size = 2 + if has_z { 1 } else { 0 } + if has_m { 1 } else { 0 };
let mut cursor = 5;
match base_type {
1 => parse_wkb_point(bytes, &mut cursor, is_le, coord_size),
2 => parse_wkb_linestring(bytes, &mut cursor, is_le, coord_size),
3 => parse_wkb_polygon(bytes, &mut cursor, is_le, coord_size),
4 => parse_wkb_multi(bytes, &mut cursor, is_le, 1), 5 => parse_wkb_multi(bytes, &mut cursor, is_le, 2), 6 => parse_wkb_multi(bytes, &mut cursor, is_le, 3), _ => None,
}
}
fn read_f64(bytes: &[u8], cursor: &mut usize, is_le: bool) -> Option<f64> {
if *cursor + 8 > bytes.len() {
return None;
}
let val = if is_le {
f64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().ok()?)
} else {
f64::from_be_bytes(bytes[*cursor..*cursor + 8].try_into().ok()?)
};
*cursor += 8;
Some(val)
}
fn read_u32(bytes: &[u8], cursor: &mut usize, is_le: bool) -> Option<u32> {
if *cursor + 4 > bytes.len() {
return None;
}
let val = if is_le {
u32::from_le_bytes(bytes[*cursor..*cursor + 4].try_into().ok()?)
} else {
u32::from_be_bytes(bytes[*cursor..*cursor + 4].try_into().ok()?)
};
*cursor += 4;
Some(val)
}
fn read_coord(bytes: &[u8], cursor: &mut usize, is_le: bool, coord_size: usize) -> Option<Coord> {
let x = read_f64(bytes, cursor, is_le)?;
let y = read_f64(bytes, cursor, is_le)?;
let extra = coord_size.saturating_sub(2);
for _ in 0..extra {
read_f64(bytes, cursor, is_le)?;
}
Some(Coord { x, y })
}
fn parse_wkb_point(
bytes: &[u8],
cursor: &mut usize,
is_le: bool,
coord_size: usize,
) -> Option<Geometry<f64>> {
let c = read_coord(bytes, cursor, is_le, coord_size)?;
Some(Geometry::Point(Point::new(c.x, c.y)))
}
fn parse_wkb_linestring(
bytes: &[u8],
cursor: &mut usize,
is_le: bool,
coord_size: usize,
) -> Option<Geometry<f64>> {
let num_points = read_u32(bytes, cursor, is_le)? as usize;
let mut coords = Vec::with_capacity(num_points);
for _ in 0..num_points {
coords.push(read_coord(bytes, cursor, is_le, coord_size)?);
}
Some(Geometry::LineString(LineString::new(coords)))
}
fn parse_wkb_polygon(
bytes: &[u8],
cursor: &mut usize,
is_le: bool,
coord_size: usize,
) -> Option<Geometry<f64>> {
let num_rings = read_u32(bytes, cursor, is_le)? as usize;
let mut rings = Vec::with_capacity(num_rings);
for _ in 0..num_rings {
let num_points = read_u32(bytes, cursor, is_le)? as usize;
let mut coords = Vec::with_capacity(num_points);
for _ in 0..num_points {
coords.push(read_coord(bytes, cursor, is_le, coord_size)?);
}
rings.push(LineString::new(coords));
}
if rings.is_empty() {
return None;
}
let exterior = rings.remove(0);
Some(Geometry::Polygon(Polygon::new(exterior, rings)))
}
fn parse_wkb_multi(
bytes: &[u8],
cursor: &mut usize,
_is_le: bool,
expected_base_type: u32,
) -> Option<Geometry<f64>> {
let num_geoms = read_u32(bytes, cursor, _is_le)? as usize;
let mut sub_geoms = Vec::with_capacity(num_geoms);
for _ in 0..num_geoms {
if *cursor >= bytes.len() {
break;
}
let sub_le = bytes[*cursor] == 1;
*cursor += 1;
let sub_type = read_u32(bytes, cursor, sub_le)?;
let sub_base = sub_type % 1000;
let has_z = matches!(sub_type / 1000, 1 | 3);
let has_m = matches!(sub_type / 1000, 2 | 3);
let coord_size = 2 + if has_z { 1 } else { 0 } + if has_m { 1 } else { 0 };
if sub_base != expected_base_type {
return None; }
let geom = match sub_base {
1 => parse_wkb_point(bytes, cursor, sub_le, coord_size)?,
2 => parse_wkb_linestring(bytes, cursor, sub_le, coord_size)?,
3 => parse_wkb_polygon(bytes, cursor, sub_le, coord_size)?,
_ => return None,
};
sub_geoms.push(geom);
}
match expected_base_type {
1 => {
let points: Vec<geo_types::Point<f64>> = sub_geoms
.into_iter()
.filter_map(|g| match g {
Geometry::Point(p) => Some(p),
_ => None,
})
.collect();
Some(Geometry::MultiPoint(geo_types::MultiPoint::new(points)))
}
2 => {
let lines: Vec<LineString<f64>> = sub_geoms
.into_iter()
.filter_map(|g| match g {
Geometry::LineString(l) => Some(l),
_ => None,
})
.collect();
Some(Geometry::MultiLineString(geo_types::MultiLineString::new(
lines,
)))
}
3 => {
let polygons: Vec<Polygon<f64>> = sub_geoms
.into_iter()
.filter_map(|g| match g {
Geometry::Polygon(p) => Some(p),
_ => None,
})
.collect();
Some(Geometry::MultiPolygon(geo_types::MultiPolygon::new(
polygons,
)))
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_read_nonexistent_gpkg() {
let result = read_gpkg(Path::new("/tmp/nonexistent_surtgis_test.gpkg"), None);
assert!(result.is_err());
}
#[test]
fn test_parse_wkb_point() {
let mut wkb = Vec::new();
wkb.push(1u8); wkb.extend_from_slice(&1u32.to_le_bytes()); wkb.extend_from_slice(&1.5f64.to_le_bytes());
wkb.extend_from_slice(&2.5f64.to_le_bytes());
let geom = parse_wkb(&wkb).unwrap();
match geom {
Geometry::Point(p) => {
assert!((p.x() - 1.5).abs() < 1e-10);
assert!((p.y() - 2.5).abs() < 1e-10);
}
other => panic!("Expected Point, got {:?}", other),
}
}
#[test]
fn test_parse_wkb_polygon() {
let mut wkb = Vec::new();
wkb.push(1u8); wkb.extend_from_slice(&3u32.to_le_bytes()); wkb.extend_from_slice(&1u32.to_le_bytes()); wkb.extend_from_slice(&5u32.to_le_bytes()); for &(x, y) in &[
(0.0f64, 0.0f64),
(10.0, 0.0),
(10.0, 10.0),
(0.0, 10.0),
(0.0, 0.0),
] {
wkb.extend_from_slice(&x.to_le_bytes());
wkb.extend_from_slice(&y.to_le_bytes());
}
let geom = parse_wkb(&wkb).unwrap();
match geom {
Geometry::Polygon(p) => {
assert_eq!(p.exterior().0.len(), 5);
assert_eq!(p.interiors().len(), 0);
}
other => panic!("Expected Polygon, got {:?}", other),
}
}
#[test]
fn test_parse_gpkg_wkb_with_header() {
let mut gpkg_wkb = Vec::new();
gpkg_wkb.extend_from_slice(&[0x47, 0x50]); gpkg_wkb.push(0); gpkg_wkb.push(0b00000001); gpkg_wkb.extend_from_slice(&4326u32.to_le_bytes());
gpkg_wkb.push(1u8); gpkg_wkb.extend_from_slice(&1u32.to_le_bytes()); gpkg_wkb.extend_from_slice(&(-70.5f64).to_le_bytes());
gpkg_wkb.extend_from_slice(&(-33.4f64).to_le_bytes());
let geom = parse_gpkg_wkb(&gpkg_wkb).unwrap();
match geom {
Geometry::Point(p) => {
assert!((p.x() - (-70.5)).abs() < 1e-10);
assert!((p.y() - (-33.4)).abs() < 1e-10);
}
other => panic!("Expected Point, got {:?}", other),
}
}
#[test]
fn test_parse_gpkg_wkb_with_envelope() {
let mut gpkg_wkb = Vec::new();
gpkg_wkb.extend_from_slice(&[0x47, 0x50]); gpkg_wkb.push(0); gpkg_wkb.push(0b00000011); gpkg_wkb.extend_from_slice(&4326u32.to_le_bytes());
for &v in &[0.0f64, 10.0, 0.0, 10.0] {
gpkg_wkb.extend_from_slice(&v.to_le_bytes());
}
gpkg_wkb.push(1u8);
gpkg_wkb.extend_from_slice(&1u32.to_le_bytes());
gpkg_wkb.extend_from_slice(&5.0f64.to_le_bytes());
gpkg_wkb.extend_from_slice(&5.0f64.to_le_bytes());
let geom = parse_gpkg_wkb(&gpkg_wkb).unwrap();
match geom {
Geometry::Point(p) => {
assert!((p.x() - 5.0).abs() < 1e-10);
assert!((p.y() - 5.0).abs() < 1e-10);
}
other => panic!("Expected Point, got {:?}", other),
}
}
#[test]
fn test_gpkg_roundtrip() {
let dir = std::env::temp_dir().join("surtgis_gpkg_test");
std::fs::create_dir_all(&dir).unwrap();
let gpkg_path = dir.join("test_roundtrip.gpkg");
let _ = std::fs::remove_file(&gpkg_path);
let conn = rusqlite::Connection::open(&gpkg_path).unwrap();
conn.execute_batch(
"
CREATE TABLE gpkg_contents (
table_name TEXT NOT NULL PRIMARY KEY,
data_type TEXT NOT NULL,
identifier TEXT,
description TEXT DEFAULT '',
last_change TEXT,
min_x DOUBLE, min_y DOUBLE, max_x DOUBLE, max_y DOUBLE,
srs_id INTEGER
);
CREATE TABLE gpkg_geometry_columns (
table_name TEXT NOT NULL,
column_name TEXT NOT NULL,
geometry_type_name TEXT NOT NULL,
srs_id INTEGER NOT NULL,
z TINYINT NOT NULL,
m TINYINT NOT NULL
);
INSERT INTO gpkg_contents VALUES (
'test_layer', 'features', 'test', '', '', 0, 0, 10, 10, 4326
);
INSERT INTO gpkg_geometry_columns VALUES (
'test_layer', 'geom', 'POLYGON', 4326, 0, 0
);
CREATE TABLE test_layer (
fid INTEGER PRIMARY KEY,
geom BLOB,
name TEXT,
value REAL
);
",
)
.unwrap();
let mut wkb = Vec::new();
wkb.push(1u8); wkb.extend_from_slice(&3u32.to_le_bytes()); wkb.extend_from_slice(&1u32.to_le_bytes()); wkb.extend_from_slice(&5u32.to_le_bytes()); for &(x, y) in &[
(0.0f64, 0.0f64),
(10.0, 0.0),
(10.0, 10.0),
(0.0, 10.0),
(0.0, 0.0),
] {
wkb.extend_from_slice(&x.to_le_bytes());
wkb.extend_from_slice(&y.to_le_bytes());
}
conn.execute(
"INSERT INTO test_layer (fid, geom, name, value) VALUES (1, ?1, 'Basin1', 42.5)",
[&wkb],
)
.unwrap();
drop(conn);
let fc = read_gpkg(&gpkg_path, None).unwrap();
assert_eq!(fc.len(), 1);
let feature = &fc.features[0];
assert!(feature.geometry.is_some());
match feature.get_property("name") {
Some(AttributeValue::String(s)) => assert_eq!(s, "Basin1"),
other => panic!("Expected String 'Basin1', got {:?}", other),
}
match feature.get_property("value") {
Some(AttributeValue::Float(v)) => assert!((*v - 42.5).abs() < 1e-10),
other => panic!("Expected Float 42.5, got {:?}", other),
}
let layers = list_gpkg_layers(&gpkg_path).unwrap();
assert_eq!(layers, vec!["test_layer"]);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_gpkg_specific_layer() {
let dir = std::env::temp_dir().join("surtgis_gpkg_layer_test");
std::fs::create_dir_all(&dir).unwrap();
let gpkg_path = dir.join("multi_layer.gpkg");
let _ = std::fs::remove_file(&gpkg_path);
let conn = rusqlite::Connection::open(&gpkg_path).unwrap();
conn.execute_batch(
"
CREATE TABLE gpkg_contents (
table_name TEXT NOT NULL PRIMARY KEY,
data_type TEXT NOT NULL,
identifier TEXT,
description TEXT DEFAULT '',
last_change TEXT,
min_x DOUBLE, min_y DOUBLE, max_x DOUBLE, max_y DOUBLE,
srs_id INTEGER
);
CREATE TABLE gpkg_geometry_columns (
table_name TEXT NOT NULL,
column_name TEXT NOT NULL,
geometry_type_name TEXT NOT NULL,
srs_id INTEGER NOT NULL,
z TINYINT NOT NULL,
m TINYINT NOT NULL
);
INSERT INTO gpkg_contents VALUES ('layer_a', 'features', 'A', '', '', 0, 0, 10, 10, 4326);
INSERT INTO gpkg_contents VALUES ('layer_b', 'features', 'B', '', '', 0, 0, 10, 10, 4326);
INSERT INTO gpkg_geometry_columns VALUES ('layer_a', 'geom', 'POINT', 4326, 0, 0);
INSERT INTO gpkg_geometry_columns VALUES ('layer_b', 'geom', 'POINT', 4326, 0, 0);
CREATE TABLE layer_a (fid INTEGER PRIMARY KEY, geom BLOB, label TEXT);
CREATE TABLE layer_b (fid INTEGER PRIMARY KEY, geom BLOB, label TEXT);
INSERT INTO layer_a (fid, label) VALUES (1, 'from_a');
INSERT INTO layer_b (fid, label) VALUES (1, 'from_b');
INSERT INTO layer_b (fid, label) VALUES (2, 'from_b_2');
",
)
.unwrap();
drop(conn);
let fc_b = read_gpkg(&gpkg_path, Some("layer_b")).unwrap();
assert_eq!(fc_b.len(), 2);
match fc_b.features[0].get_property("label") {
Some(AttributeValue::String(s)) => assert_eq!(s, "from_b"),
other => panic!("Expected 'from_b', got {:?}", other),
}
let fc_default = read_gpkg(&gpkg_path, None).unwrap();
assert_eq!(fc_default.len(), 1);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_parse_wkb_z_point() {
let mut wkb = Vec::new();
wkb.push(1u8);
wkb.extend_from_slice(&1001u32.to_le_bytes()); wkb.extend_from_slice(&1.0f64.to_le_bytes()); wkb.extend_from_slice(&2.0f64.to_le_bytes()); wkb.extend_from_slice(&3.0f64.to_le_bytes());
let geom = parse_wkb(&wkb).unwrap();
match geom {
Geometry::Point(p) => {
assert!((p.x() - 1.0).abs() < 1e-10);
assert!((p.y() - 2.0).abs() < 1e-10);
}
other => panic!("Expected Point, got {:?}", other),
}
}
}