use std::path::Path;
use std::process::ExitCode;
use geopackage::core::gpb;
use geopackage::core::types::ColumnType;
use geopackage::{
ColumnSpec, ContentsDataType, GeoPackage, GeometrySpec, Layer, TableSchemaBuilder,
};
use crate::error::Result;
const BATCH: usize = 10_000;
pub fn run(src_path: &Path, dst_path: &Path) -> Result<ExitCode> {
if dst_path.exists() {
eprintln!("gpkg: {} already exists", dst_path.display());
return Ok(ExitCode::FAILURE);
}
let src = GeoPackage::open_read_only_lenient(src_path)?;
let dst = GeoPackage::create(dst_path)?;
for warning in src.open_warnings() {
println!("source warning: {warning}");
}
let layers = src.layers()?;
for layer in &layers {
copy_layer(&src, &dst, layer)?;
}
report_what_was_left(&src, &dst, &layers)?;
dst.close()?;
Ok(ExitCode::SUCCESS)
}
fn copy_layer(src: &GeoPackage, dst: &GeoPackage, layer: &Layer<'_>) -> Result<()> {
let name = layer.table_name();
let geometry_name = layer.geometry_column().map(|geom| &geom.column_name);
let pk_name = layer.primary_key_column();
let mut builder = TableSchemaBuilder::new(name);
if let Some(pk) = pk_name {
builder = builder.primary_key(pk);
}
let mut untyped = Vec::new();
for column in &layer.schema().columns {
if Some(&column.name) == geometry_name || Some(column.name.as_str()) == pk_name {
continue;
}
let column_type = match &column.column_type {
Some(column_type) => column_type.clone(),
None => {
untyped.push(column.name.clone());
ColumnType::Blob(None)
}
};
let mut spec = ColumnSpec::new(column.name.clone(), column_type);
if column.not_null {
spec = spec.not_null();
}
if let Some(default) = &column.default_value {
spec = spec.default_value(default.clone());
}
builder = builder.column(spec);
}
if !untyped.is_empty() {
println!(
"{name}: {} column(s) had a type outside the spec vocabulary, carried as BLOB: {}",
untyped.len(),
untyped.join(", ")
);
}
let indexed = match layer.geometry_column() {
Some(geom) => {
if let Some(srs) = src.srs(geom.srs_id)? {
dst.add_srs(&srs)?;
}
let indexed = layer.has_spatial_index()?;
builder = builder
.geometry(
GeometrySpec::new(geom.geometry_type, geom.srs_id)
.column_name(geom.column_name.clone())
.z(geom.z)
.m(geom.m),
)
.spatial_index(indexed);
dst.create_layer(&builder)?;
indexed
}
None => {
dst.create_attributes_table(&builder)?;
false
}
};
let target = if layer.geometry_column().is_some() {
dst.layer(name)?
} else {
dst.attributes(name)?
};
let mut copied = 0u64;
let mut writer = target.writer()?;
let mut cursor = layer.cursor()?;
let mut rows = cursor.features()?;
let mut in_batch = 0usize;
for feature in rows.by_ref() {
let feature = feature?;
let values: Vec<_> = feature.values().collect();
match feature.geometry_bytes() {
Some(blob) => {
let offset =
gpb::body_offset(blob).map_err(|e| geopackage::Error::Core(e.into()))?;
let body = blob.get(offset..).unwrap_or_default();
writer.insert_wkb(Some(feature.fid()), body, &values)?;
}
None => {
writer.insert_row(Some(feature.fid()), &values)?;
}
}
copied += 1;
in_batch += 1;
if in_batch >= BATCH {
writer.commit()?;
writer = target.writer()?;
in_batch = 0;
}
}
writer.commit()?;
let index_note = if indexed { ", indexed" } else { "" };
println!("{name}: {copied} rows{index_note}");
Ok(())
}
fn report_what_was_left(src: &GeoPackage, dst: &GeoPackage, copied: &[Layer<'_>]) -> Result<()> {
let mut left = Vec::new();
let tile_tables = src
.contents()?
.into_iter()
.filter(|entry| entry.data_type == ContentsDataType::Tiles)
.count();
if tile_tables > 0 {
left.push(format!("{tile_tables} tile pyramid(s)"));
}
let arrived: std::collections::BTreeSet<String> =
dst.extensions()?.into_iter().map(|row| row.name).collect();
let mut missing: Vec<String> = src
.extensions()?
.into_iter()
.map(|row| row.name)
.filter(|name| !arrived.contains(name))
.collect();
missing.sort();
missing.dedup();
if !missing.is_empty() {
left.push(format!("extensions: {}", missing.join(", ")));
}
if left.is_empty() {
return Ok(());
}
println!(
"\nnot copied ({} layers were): {}",
copied.len(),
left.join("; ")
);
println!("`gpkg copy` copies feature and attribute layers only.");
Ok(())
}