use crate::error::GpkgError;
use crate::multi_geom::GeometryColumnDef;
use crate::sqlite_writer::{
PAGE_SIZE,
btree_writer::write_table,
file_header::make_gpkg_header,
page_allocator::PageAllocator,
record::{RecordValue, encode_record},
};
use crate::writer::{
feature_writer::{compute_bbox, encode_gpkg_point},
schema_emitter::{
DDL_GPKG_CONTENTS, DDL_GPKG_GEOMETRY_COLUMNS, DDL_GPKG_SPATIAL_REF_SYS, ddl_feature_table,
default_srs_rows,
},
};
type FeatureTableEntry = (String, String, u32, Vec<(i64, f64, f64)>);
pub struct FeatureTableSpec {
pub name: String,
pub geometry_type: String,
pub points: Vec<(i64, f64, f64)>,
}
pub struct GeoPackageBuilder {
srs_id: i32,
feature_tables: Vec<FeatureTableSpec>,
extra_geometry_columns: Vec<GeometryColumnDef>,
}
impl GeoPackageBuilder {
pub fn new(srs_id: i32) -> Self {
Self {
srs_id,
feature_tables: Vec::new(),
extra_geometry_columns: Vec::new(),
}
}
pub fn add_feature_table(
mut self,
name: impl Into<String>,
geometry_type: impl Into<String>,
points: Vec<(i64, f64, f64)>,
) -> Self {
self.feature_tables.push(FeatureTableSpec {
name: name.into(),
geometry_type: geometry_type.into(),
points,
});
self
}
pub fn add_feature_table_mut(
&mut self,
name: impl Into<String>,
geometry_type: impl Into<String>,
points: Vec<(i64, f64, f64)>,
) -> &mut Self {
self.feature_tables.push(FeatureTableSpec {
name: name.into(),
geometry_type: geometry_type.into(),
points,
});
self
}
pub fn build_from_ref(&self) -> Result<Vec<u8>, GpkgError> {
let cloned = GeoPackageBuilder {
srs_id: self.srs_id,
feature_tables: self
.feature_tables
.iter()
.map(|s| FeatureTableSpec {
name: s.name.clone(),
geometry_type: s.geometry_type.clone(),
points: s.points.clone(),
})
.collect(),
extra_geometry_columns: self.extra_geometry_columns.clone(),
};
cloned.build()
}
pub fn add_geometry_column_def(
&mut self,
table_name: &str,
column_def: &GeometryColumnDef,
) -> Result<&mut Self, GpkgError> {
let table_known = self.feature_tables.iter().any(|t| t.name == table_name);
if !table_known {
return Err(GpkgError::TableNotFound(table_name.to_owned()));
}
let mut def = column_def.clone();
def.table_name = table_name.to_owned();
self.extra_geometry_columns.push(def);
Ok(self)
}
pub fn build(self) -> Result<Vec<u8>, GpkgError> {
let mut allocator = PageAllocator::new();
let sqlite_master_page = allocator.alloc();
debug_assert_eq!(sqlite_master_page, 1);
let srs_rows = build_srs_rows();
let srs_root = write_table(&mut allocator, &srs_rows, 0)?;
let mut feature_root_pages: Vec<FeatureTableEntry> =
Vec::with_capacity(self.feature_tables.len());
for spec in &self.feature_tables {
let feat_rows = build_feature_rows(&spec.points, self.srs_id);
let root = write_table(&mut allocator, &feat_rows, 0)?;
feature_root_pages.push((
spec.name.clone(),
spec.geometry_type.clone(),
root,
spec.points.clone(),
));
}
let contents_rows = build_contents_rows(&feature_root_pages, self.srs_id);
let contents_root = write_table(&mut allocator, &contents_rows, 0)?;
let geom_col_rows = build_geometry_columns_rows(
&feature_root_pages,
self.srs_id,
&self.extra_geometry_columns,
);
let geom_col_root = write_table(&mut allocator, &geom_col_rows, 0)?;
let master_rows =
build_sqlite_master_rows(srs_root, contents_root, geom_col_root, &feature_root_pages);
let master_page = emit_master_page(&master_rows);
allocator.write(sqlite_master_page, master_page);
let page_count = allocator.page_count();
let raw_pages = allocator.finalize();
let header_bytes = make_gpkg_header(page_count);
let mut out = Vec::with_capacity(raw_pages.len());
out.extend_from_slice(&header_bytes);
out.extend_from_slice(&raw_pages[100..PAGE_SIZE]);
out.extend_from_slice(&raw_pages[PAGE_SIZE..]);
Ok(out)
}
}
fn build_srs_rows() -> Vec<(i64, Vec<u8>)> {
default_srs_rows()
.iter()
.enumerate()
.map(|(idx, srs)| {
let rowid = (idx + 1) as i64;
let payload = encode_record(&[
RecordValue::Text(srs.srs_name),
RecordValue::Int(srs.srs_id),
RecordValue::Text(srs.organization),
RecordValue::Int(srs.organization_coordsys_id),
RecordValue::Text(srs.definition),
RecordValue::Text(srs.description),
]);
(rowid, payload)
})
.collect()
}
fn build_feature_rows(points: &[(i64, f64, f64)], srs_id: i32) -> Vec<(i64, Vec<u8>)> {
points
.iter()
.map(|(fid, x, y)| {
let gpb = encode_gpkg_point(*x, *y, srs_id);
let payload = encode_record(&[RecordValue::Int(*fid), RecordValue::Blob(&gpb)]);
(*fid, payload)
})
.collect()
}
fn build_contents_rows(tables: &[FeatureTableEntry], srs_id: i32) -> Vec<(i64, Vec<u8>)> {
tables
.iter()
.enumerate()
.map(|(idx, (name, _geom_type, _root, points))| {
let rowid = (idx + 1) as i64;
let bbox = compute_bbox(points);
let last_change = "2026-01-01T00:00:00.000Z";
let payload = match bbox {
Some((min_x, min_y, max_x, max_y)) => encode_record(&[
RecordValue::Text(name),
RecordValue::Text("features"),
RecordValue::Text(name), RecordValue::Text(""), RecordValue::Text(last_change),
RecordValue::Float(min_x),
RecordValue::Float(min_y),
RecordValue::Float(max_x),
RecordValue::Float(max_y),
RecordValue::Int(srs_id as i64),
]),
None => encode_record(&[
RecordValue::Text(name),
RecordValue::Text("features"),
RecordValue::Text(name),
RecordValue::Text(""),
RecordValue::Text(last_change),
RecordValue::Null,
RecordValue::Null,
RecordValue::Null,
RecordValue::Null,
RecordValue::Int(srs_id as i64),
]),
};
(rowid, payload)
})
.collect()
}
fn build_geometry_columns_rows(
tables: &[FeatureTableEntry],
srs_id: i32,
extra_cols: &[GeometryColumnDef],
) -> Vec<(i64, Vec<u8>)> {
let mut rows: Vec<(i64, Vec<u8>)> = Vec::with_capacity(tables.len() + extra_cols.len());
for (idx, (name, geom_type, _root, _points)) in tables.iter().enumerate() {
let rowid = (idx + 1) as i64;
let payload = encode_record(&[
RecordValue::Text(name),
RecordValue::Text("geom"),
RecordValue::Text(geom_type),
RecordValue::Int(srs_id as i64),
RecordValue::Int(0), RecordValue::Int(0), ]);
rows.push((rowid, payload));
}
let base_rowid = (tables.len() + 1) as i64;
for (extra_idx, col) in extra_cols.iter().enumerate() {
let rowid = base_rowid + extra_idx as i64;
let payload = encode_record(&[
RecordValue::Text(&col.table_name),
RecordValue::Text(&col.column_name),
RecordValue::Text(&col.geometry_type_name),
RecordValue::Int(col.srs_id as i64),
RecordValue::Int(col.z_flag() as i64),
RecordValue::Int(col.m_flag() as i64),
]);
rows.push((rowid, payload));
}
rows
}
type MasterRowSpec = (i64, Vec<u8>, String, String, u32);
fn build_sqlite_master_rows(
srs_root: u32,
contents_root: u32,
geom_col_root: u32,
feature_tables: &[FeatureTableEntry],
) -> Vec<MasterRowSpec> {
let mut rows: Vec<MasterRowSpec> = Vec::new();
let mut rowid = 1i64;
rows.push((
rowid,
vec![],
"gpkg_spatial_ref_sys".to_string(),
DDL_GPKG_SPATIAL_REF_SYS.to_string(),
srs_root,
));
rowid += 1;
rows.push((
rowid,
vec![],
"gpkg_contents".to_string(),
DDL_GPKG_CONTENTS.to_string(),
contents_root,
));
rowid += 1;
rows.push((
rowid,
vec![],
"gpkg_geometry_columns".to_string(),
DDL_GPKG_GEOMETRY_COLUMNS.to_string(),
geom_col_root,
));
rowid += 1;
for (name, _geom_type, root, _points) in feature_tables {
let ddl = ddl_feature_table(name);
rows.push((rowid, vec![], name.clone(), ddl, *root));
rowid += 1;
}
rows
}
fn emit_master_page(rows: &[MasterRowSpec]) -> Vec<u8> {
use crate::sqlite_writer::btree_writer::LeafPageBuilder;
let mut builder = LeafPageBuilder::new();
for (rowid, _unused, name, sql, rootpage) in rows {
let payload = encode_record(&[
RecordValue::Text("table"),
RecordValue::Text(name),
RecordValue::Text(name),
RecordValue::Int(*rootpage as i64),
RecordValue::Text(sql),
]);
let ok = builder.try_add(*rowid, payload);
debug_assert!(ok, "sqlite_master row {rowid} did not fit on page 1");
}
builder.emit(100)
}