use crate::transaction::WriteTransaction;
use crate::{Error, GeoPackage, Result};
use geopackage_core::ident::quote;
use geopackage_core::srs::epsg_definition;
use rusqlite::{Connection, OptionalExtension};
const CRS_WKT_EXTENSION: &str = "gpkg_crs_wkt_1_1";
const CRS_WKT_DEFINITION: &str = "http://www.geopackage.org/spec/#extension_crs_wkt";
#[derive(Debug, Clone, PartialEq)]
pub struct Srs {
pub name: String,
pub srs_id: i32,
pub organization: String,
pub organization_coordsys_id: i32,
pub definition: String,
pub description: Option<String>,
pub definition_wkt2: Option<String>,
pub epoch: Option<f64>,
}
const BASE_COLUMNS: &str =
"srs_name, srs_id, organization, organization_coordsys_id, definition, description";
#[derive(Debug, Clone, Copy)]
struct CrsWktColumns {
definition_12_063: bool,
epoch: bool,
}
impl CrsWktColumns {
fn read(conn: &Connection) -> Result<Self> {
let mut stmt = conn.prepare("PRAGMA table_info(gpkg_spatial_ref_sys)")?;
let mut rows = stmt.query([])?;
let mut columns = Self {
definition_12_063: false,
epoch: false,
};
while let Some(row) = rows.next()? {
let name: String = row.get(1)?;
if name.eq_ignore_ascii_case("definition_12_063") {
columns.definition_12_063 = true;
} else if name.eq_ignore_ascii_case("epoch") {
columns.epoch = true;
}
}
Ok(columns)
}
fn select_list(self) -> String {
let mut list = BASE_COLUMNS.to_owned();
if self.definition_12_063 {
list.push_str(", definition_12_063");
}
if self.epoch {
list.push_str(", epoch");
}
list
}
}
fn row_to_srs(row: &rusqlite::Row<'_>, columns: CrsWktColumns) -> rusqlite::Result<Srs> {
let mut next = 6;
let mut take = |present: bool| {
present.then(|| {
let index = next;
next += 1;
index
})
};
let wkt2_index = take(columns.definition_12_063);
let epoch_index = take(columns.epoch);
Ok(Srs {
name: row.get(0)?,
srs_id: row.get(1)?,
organization: row.get(2)?,
organization_coordsys_id: row.get(3)?,
definition: row.get(4)?,
description: row.get(5)?,
definition_wkt2: match wkt2_index {
Some(index) => row.get::<_, Option<String>>(index)?.filter(is_defined),
None => None,
},
epoch: match epoch_index {
Some(index) => row.get(index)?,
None => None,
},
})
}
fn is_defined(definition: &String) -> bool {
definition != "undefined"
}
impl GeoPackage {
pub fn srs(&self, srs_id: i32) -> Result<Option<Srs>> {
let conn = self.connection();
let columns = CrsWktColumns::read(conn)?;
conn.query_row(
&format!(
"SELECT {} FROM gpkg_spatial_ref_sys WHERE srs_id = ?1",
columns.select_list()
),
[srs_id],
|r| row_to_srs(r, columns),
)
.optional()
.map_err(Error::from)
}
pub fn srs_list(&self) -> Result<Vec<Srs>> {
let conn = self.connection();
let columns = CrsWktColumns::read(conn)?;
let mut stmt = conn.prepare(&format!(
"SELECT {} FROM gpkg_spatial_ref_sys ORDER BY srs_id",
columns.select_list()
))?;
let rows = stmt.query_map([], |r| row_to_srs(r, columns))?;
Ok(rows.collect::<rusqlite::Result<_>>()?)
}
pub fn add_srs(&self, srs: &Srs) -> Result<bool> {
if self.srs(srs.srs_id)?.is_some() {
return Ok(false);
}
let conn = self.connection();
let tx = WriteTransaction::begin(conn)?;
if srs.definition_wkt2.is_some() || srs.epoch.is_some() {
enable_crs_wkt_extension(conn)?;
}
let columns = CrsWktColumns::read(conn)?;
let mut names = BASE_COLUMNS.to_owned();
let mut placeholders = "?1, ?2, ?3, ?4, ?5, ?6".to_owned();
let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![
Box::new(srs.name.clone()),
Box::new(srs.srs_id),
Box::new(srs.organization.clone()),
Box::new(srs.organization_coordsys_id),
Box::new(srs.definition.clone()),
Box::new(srs.description.clone()),
];
if columns.definition_12_063 {
names.push_str(", definition_12_063");
placeholders.push_str(", ?7");
params.push(Box::new(
srs.definition_wkt2
.clone()
.unwrap_or_else(|| "undefined".to_owned()),
));
}
if columns.epoch {
names.push_str(", epoch");
placeholders.push_str(if columns.definition_12_063 {
", ?8"
} else {
", ?7"
});
params.push(Box::new(srs.epoch));
}
conn.execute(
&format!("INSERT INTO gpkg_spatial_ref_sys ({names}) VALUES ({placeholders})"),
rusqlite::params_from_iter(params.iter()),
)?;
tx.commit()?;
Ok(true)
}
pub fn add_epsg_srs(&self, code: i32) -> Result<bool> {
if let Some(def) = epsg_definition(code) {
return self.add_srs(&Srs {
name: def.name.into_owned(),
srs_id: code,
organization: def.organization.into_owned(),
organization_coordsys_id: def.organization_coordsys_id,
definition: def.definition.into_owned(),
description: None,
definition_wkt2: None,
epoch: None,
});
}
self.add_epsg_srs_via_wkt2(code)
}
fn add_epsg_srs_via_wkt2(&self, code: i32) -> Result<bool> {
let wkt2 = epsg_utils::epsg_to_wkt2(code)
.ok()
.ok_or(Error::UnknownEpsgCode { code })?;
self.add_srs(&Srs {
name: srs_name_from_wkt2(wkt2, code),
srs_id: code,
organization: "EPSG".to_owned(),
organization_coordsys_id: code,
definition: "undefined".to_owned(),
description: None,
definition_wkt2: Some(wkt2.to_owned()),
epoch: None,
})
}
}
fn enable_crs_wkt_extension(conn: &Connection) -> Result<()> {
if column_exists(conn, "gpkg_spatial_ref_sys", "definition_12_063")? {
return Ok(());
}
conn.execute_batch(
"ALTER TABLE gpkg_spatial_ref_sys \
ADD COLUMN definition_12_063 TEXT NOT NULL DEFAULT 'undefined'; \
ALTER TABLE gpkg_spatial_ref_sys ADD COLUMN epoch DOUBLE;",
)?;
backfill_wkt2(conn)?;
for column in ["definition_12_063", "epoch"] {
crate::extensions::register(
conn,
Some("gpkg_spatial_ref_sys"),
Some(column),
CRS_WKT_EXTENSION,
CRS_WKT_DEFINITION,
"read-write",
)?;
}
Ok(())
}
fn backfill_wkt2(conn: &Connection) -> Result<()> {
let codes: Vec<i32> = conn
.prepare(
"SELECT srs_id FROM gpkg_spatial_ref_sys \
WHERE organization_coordsys_id > 0 AND organization LIKE 'EPSG'",
)?
.query_map([], |r| r.get(0))?
.collect::<rusqlite::Result<_>>()?;
for code in codes {
if let Ok(wkt2) = epsg_utils::epsg_to_wkt2(code) {
conn.execute(
"UPDATE gpkg_spatial_ref_sys SET definition_12_063 = ?1 WHERE srs_id = ?2",
rusqlite::params![wkt2, code],
)?;
}
}
Ok(())
}
fn column_exists(conn: &Connection, table: &str, column: &str) -> Result<bool> {
let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", quote(table)?))?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
if row.get::<_, String>(1)?.eq_ignore_ascii_case(column) {
return Ok(true);
}
}
Ok(false)
}
fn srs_name_from_wkt2(wkt2: &str, code: i32) -> String {
wkt2.split_once('"')
.and_then(|(_, rest)| rest.split_once('"'))
.map(|(name, _)| name)
.filter(|name| !name.is_empty())
.map_or_else(|| format!("EPSG:{code}"), ToOwned::to_owned)
}
#[cfg(test)]
mod crs_wkt_tests {
use super::*;
use crate::GeoPackage;
#[test]
fn geographic_3d_code_lands_in_the_extension_column() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("g.gpkg");
let gpkg = GeoPackage::create(&path).unwrap();
assert!(gpkg.add_epsg_srs(4979).unwrap());
let (definition, wkt2): (String, String) = gpkg
.connection()
.query_row(
"SELECT definition, definition_12_063 FROM gpkg_spatial_ref_sys \
WHERE srs_id = 4979",
[],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.unwrap();
assert_eq!(definition, "undefined");
assert!(wkt2.contains("CS[ellipsoidal,3"), "{wkt2}");
assert!(wkt2.contains(r#"ID["EPSG",4979]"#), "{wkt2}");
assert_eq!(gpkg.srs(4979).unwrap().unwrap().name, "WGS 84");
let rows: i64 = gpkg
.connection()
.query_row(
"SELECT count(*) FROM gpkg_extensions \
WHERE extension_name = 'gpkg_crs_wkt_1_1'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(rows, 2, "one row each for definition_12_063 and epoch");
}
#[test]
fn enabling_the_extension_backfills_rows_already_present() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("g.gpkg");
let gpkg = GeoPackage::create(&path).unwrap();
assert_eq!(gpkg.srs(4326).unwrap().unwrap().srs_id, 4326);
gpkg.add_epsg_srs(4979).unwrap();
let wkt2: String = gpkg
.connection()
.query_row(
"SELECT definition_12_063 FROM gpkg_spatial_ref_sys WHERE srs_id = 4326",
[],
|r| r.get(0),
)
.unwrap();
assert!(
wkt2.contains(r#"ID["EPSG",4326]"#),
"4326 should have gained WKT2, got {wkt2}"
);
}
#[test]
fn adding_a_second_extension_code_is_idempotent() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("g.gpkg");
let gpkg = GeoPackage::create(&path).unwrap();
gpkg.add_epsg_srs(4979).unwrap();
gpkg.add_epsg_srs(4937).unwrap();
assert!(!gpkg.add_epsg_srs(4979).unwrap(), "already present");
let rows: i64 = gpkg
.connection()
.query_row(
"SELECT count(*) FROM gpkg_extensions \
WHERE extension_name = 'gpkg_crs_wkt_1_1'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(rows, 2, "the extension is registered once, not per code");
}
#[test]
fn a_code_in_neither_the_subset_nor_the_registry_is_still_an_error() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("g.gpkg");
let gpkg = GeoPackage::create(&path).unwrap();
assert!(matches!(
gpkg.add_epsg_srs(999_999),
Err(Error::UnknownEpsgCode { code: 999_999 })
));
}
}