use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use crate::btree::CellValue;
use crate::error::GpkgError;
use crate::gpkg::GeoPackage;
use crate::vector::feature::FeatureRow;
use crate::vector::types::GpkgGeometry;
use crate::vector::wkb::GpkgBinaryParser;
use crate::writer::GeoPackageBuilder;
struct PendingMutations {
inserts: Vec<FeatureRow>,
updates: HashMap<i64, FeatureRow>,
deletes: HashSet<i64>,
}
impl PendingMutations {
fn new() -> Self {
Self {
inserts: Vec::new(),
updates: HashMap::new(),
deletes: HashSet::new(),
}
}
}
struct FeatureTableSnapshot {
features: Vec<FeatureRow>,
geometry_type: String,
}
#[derive(Debug, Clone)]
pub struct MutationStats {
pub inserts_applied: usize,
pub updates_applied: usize,
pub deletes_applied: usize,
pub features_after: usize,
pub features_before: usize,
}
pub struct GeoPackageEditor {
source_path: PathBuf,
feature_table: String,
pending: PendingMutations,
snapshot: FeatureTableSnapshot,
next_fid: i64,
srs_id: i32,
}
impl GeoPackageEditor {
pub fn open<P: AsRef<Path>>(path: P, feature_table: &str) -> Result<Self, GpkgError> {
let path = path.as_ref().to_path_buf();
let bytes = fs::read(&path)?;
let gpkg = GeoPackage::from_bytes(bytes)?;
let srs_id = read_srs_id_from_contents(&gpkg, feature_table).unwrap_or(4326);
let geom_type_info = read_geometry_column_info(&gpkg, feature_table)
.unwrap_or_else(|| ("geom".to_string(), "POINT".to_string()));
let (_geometry_column, geometry_type) = geom_type_info;
let mut features: Vec<FeatureRow> = Vec::new();
let mut max_fid: i64 = 0;
if let Some(rows) = gpkg.scan_table_by_name(feature_table)? {
for (_rowid, values) in rows {
if values.is_empty() {
continue;
}
let fid = match &values[0] {
CellValue::Integer(i) => *i,
CellValue::Float(f) => *f as i64,
_ => continue,
};
if fid > max_fid {
max_fid = fid;
}
let geometry = if values.len() > 1 {
match &values[1] {
CellValue::Blob(blob) => GpkgBinaryParser::parse(blob).ok(),
_ => None,
}
} else {
None
};
let fields: HashMap<String, crate::vector::types::FieldValue> = HashMap::new();
features.push(FeatureRow {
fid,
geometry,
fields,
});
}
}
Ok(Self {
source_path: path,
feature_table: feature_table.to_string(),
pending: PendingMutations::new(),
snapshot: FeatureTableSnapshot {
features,
geometry_type,
},
next_fid: max_fid + 1,
srs_id,
})
}
pub fn insert_feature(&mut self, mut row: FeatureRow) -> i64 {
let fid = self.next_fid;
self.next_fid += 1;
row.fid = fid;
self.pending.inserts.push(row);
fid
}
pub fn update_feature(&mut self, fid: i64, mut row: FeatureRow) -> Result<(), GpkgError> {
let exists_in_snapshot = self.snapshot.features.iter().any(|f| f.fid == fid);
if !exists_in_snapshot {
return Err(GpkgError::FeatureNotFound(fid));
}
row.fid = fid;
self.pending.updates.insert(fid, row);
Ok(())
}
pub fn delete_feature(&mut self, fid: i64) -> Result<(), GpkgError> {
let exists_in_snapshot = self.snapshot.features.iter().any(|f| f.fid == fid);
if !exists_in_snapshot {
return Err(GpkgError::FeatureNotFound(fid));
}
self.pending.deletes.insert(fid);
Ok(())
}
pub fn rollback(&mut self) {
self.pending = PendingMutations::new();
}
pub fn pending_inserts(&self) -> usize {
self.pending.inserts.len()
}
pub fn pending_updates(&self) -> usize {
self.pending.updates.len()
}
pub fn pending_deletes(&self) -> usize {
self.pending.deletes.len()
}
pub fn snapshot_feature_count(&self) -> usize {
self.snapshot.features.len()
}
pub fn commit_to_path<P: AsRef<Path>>(
&self,
output_path: P,
) -> Result<MutationStats, GpkgError> {
let (bytes, stats) = self.build_output_bytes()?;
fs::write(output_path.as_ref(), &bytes)?;
Ok(stats)
}
pub fn commit_in_place(&self) -> Result<MutationStats, GpkgError> {
let tmp_path = self.source_path.with_extension("gpkg.tmp");
let (bytes, stats) = self.build_output_bytes()?;
fs::write(&tmp_path, &bytes)?;
fs::rename(&tmp_path, &self.source_path)?;
Ok(stats)
}
fn build_output_bytes(&self) -> Result<(Vec<u8>, MutationStats), GpkgError> {
let features_before = self.snapshot.features.len();
let inserts_applied = self.pending.inserts.len();
let updates_applied = self.pending.updates.len();
let deletes_applied = self.pending.deletes.len();
let mut merged: Vec<(i64, f64, f64)> = Vec::new();
for feature in &self.snapshot.features {
if self.pending.deletes.contains(&feature.fid) {
continue;
}
let row = self.pending.updates.get(&feature.fid).unwrap_or(feature);
if let Some((x, y)) = extract_point_xy_from_row(row) {
merged.push((row.fid, x, y));
}
}
for insert in &self.pending.inserts {
if let Some((x, y)) = extract_point_xy_from_row(insert) {
merged.push((insert.fid, x, y));
}
}
let features_after = merged.len();
let bytes = GeoPackageBuilder::new(self.srs_id)
.add_feature_table(&self.feature_table, &self.snapshot.geometry_type, merged)
.build()?;
let stats = MutationStats {
inserts_applied,
updates_applied,
deletes_applied,
features_after,
features_before,
};
Ok((bytes, stats))
}
}
fn extract_point_xy_from_row(row: &FeatureRow) -> Option<(f64, f64)> {
row.geometry.as_ref().and_then(extract_point_xy)
}
fn extract_point_xy(geom: &GpkgGeometry) -> Option<(f64, f64)> {
match geom {
GpkgGeometry::Point { x, y } => Some((*x, *y)),
GpkgGeometry::PointZ { x, y, .. } => Some((*x, *y)),
GpkgGeometry::PointM { x, y, .. } => Some((*x, *y)),
GpkgGeometry::PointZM(p) => Some((p.x, p.y)),
GpkgGeometry::LineString { coords } => coords.first().copied(),
GpkgGeometry::LineStringZ { coords } => coords.first().map(|(x, y, _)| (*x, *y)),
GpkgGeometry::LineStringM { coords } => coords.first().map(|(x, y, _)| (*x, *y)),
GpkgGeometry::LineStringZM { coords } => coords.first().map(|p| (p.x, p.y)),
GpkgGeometry::Polygon { rings } => rings.first().and_then(|r| r.first()).copied(),
GpkgGeometry::PolygonZ { rings } => rings
.first()
.and_then(|r| r.first())
.map(|(x, y, _)| (*x, *y)),
GpkgGeometry::PolygonM { rings } => rings
.first()
.and_then(|r| r.first())
.map(|(x, y, _)| (*x, *y)),
GpkgGeometry::PolygonZM { rings } => {
rings.first().and_then(|r| r.first()).map(|p| (p.x, p.y))
}
GpkgGeometry::MultiPoint { points } => points.first().copied(),
GpkgGeometry::MultiPointZ { points } => points.first().map(|(x, y, _)| (*x, *y)),
GpkgGeometry::MultiPointM { points } => points.first().map(|(x, y, _)| (*x, *y)),
GpkgGeometry::MultiPointZM { points } => points.first().map(|p| (p.x, p.y)),
GpkgGeometry::MultiLineString { lines } => lines.first().and_then(|l| l.first()).copied(),
GpkgGeometry::MultiLineStringZ { lines } => lines
.first()
.and_then(|l| l.first())
.map(|(x, y, _)| (*x, *y)),
GpkgGeometry::MultiLineStringM { lines } => lines
.first()
.and_then(|l| l.first())
.map(|(x, y, _)| (*x, *y)),
GpkgGeometry::MultiLineStringZM { lines } => {
lines.first().and_then(|l| l.first()).map(|p| (p.x, p.y))
}
GpkgGeometry::MultiPolygon { polygons } => polygons
.first()
.and_then(|poly| poly.first())
.and_then(|ring| ring.first())
.copied(),
GpkgGeometry::MultiPolygonZ { polygons } => polygons
.first()
.and_then(|poly| poly.first())
.and_then(|ring| ring.first())
.map(|(x, y, _)| (*x, *y)),
GpkgGeometry::MultiPolygonM { polygons } => polygons
.first()
.and_then(|poly| poly.first())
.and_then(|ring| ring.first())
.map(|(x, y, _)| (*x, *y)),
GpkgGeometry::MultiPolygonZM { polygons } => polygons
.first()
.and_then(|poly| poly.first())
.and_then(|ring| ring.first())
.map(|p| (p.x, p.y)),
GpkgGeometry::GeometryCollection(geoms)
| GpkgGeometry::GeometryCollectionZ(geoms)
| GpkgGeometry::GeometryCollectionM(geoms)
| GpkgGeometry::GeometryCollectionZM(geoms) => geoms.first().and_then(extract_point_xy),
GpkgGeometry::Empty => None,
}
}
fn read_srs_id_from_contents(gpkg: &GeoPackage, table_name: &str) -> Option<i32> {
let rows = gpkg.scan_table_by_name("gpkg_contents").ok()??;
for (_rowid, values) in rows {
if values.len() < 10 {
continue;
}
let name = cell_as_str(&values[0]);
if name == table_name {
return Some(cell_as_i32(&values[9]));
}
}
None
}
fn read_geometry_column_info(gpkg: &GeoPackage, table_name: &str) -> Option<(String, String)> {
let rows = gpkg.scan_table_by_name("gpkg_geometry_columns").ok()??;
for (_rowid, values) in rows {
if values.len() < 3 {
continue;
}
let name = cell_as_str(&values[0]);
if name == table_name {
let column_name = cell_as_str(&values[1]).to_string();
let geom_type = cell_as_str(&values[2]).to_string();
return Some((column_name, geom_type));
}
}
None
}
fn cell_as_str(v: &CellValue) -> &str {
match v {
CellValue::Text(s) => s.as_str(),
_ => "",
}
}
fn cell_as_i32(v: &CellValue) -> i32 {
match v {
CellValue::Integer(i) => {
if *i > i32::MAX as i64 {
i32::MAX
} else if *i < i32::MIN as i64 {
i32::MIN
} else {
*i as i32
}
}
CellValue::Float(f) => *f as i32,
_ => 0,
}
}