Skip to content

Commit

Permalink
nusamai-gpkg: クレートの作成、空のGeoPackageファイル作成 (#89)
Browse files Browse the repository at this point in the history
#8

まず、ベースの部分を作ってみました。

このPR後に、ジオメトリデータをバイナリ化したりしたものなど、各情報をSQLiteテーブルへ入れていく。

## 変更

- [x] `nusamai-gpkg` クレートの新規作成
- [x] SQLxによる、空のGeoPackageファイル作成
- [x] example: CLIからのテスト実行
  • Loading branch information
sorami authored Dec 25, 2023
1 parent 0bed124 commit a894113
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"nusamai-geometry",
"nusamai-gltf",
"nusamai-geojson",
"nusamai-gpkg",
"nusamai-plateau",
"nusamai-mvt",
"nusamai",
Expand Down
3 changes: 3 additions & 0 deletions nusamai-gpkg/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.gpkg
*.gpkg-shm
*.gpkg-wal
15 changes: 15 additions & 0 deletions nusamai-gpkg/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "nusamai-gpkg"
version = "0.1.0"
edition = "2021"

[dependencies]
sqlx = { version = "0.7.3", features = ["sqlite", "runtime-tokio"] }
tokio = { version = "1.35.1", features = ["full"] }
nusamai-plateau = { path = "../nusamai-plateau" }
thiserror = "1.0.51"

[dev-dependencies]
citygml = {path = "../nusamai-plateau/citygml" }
clap = { version = "4.4.8", features = ["derive"] }
quick-xml = "0.31.0"
68 changes: 68 additions & 0 deletions nusamai-gpkg/examples/gml2gpkg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use citygml::{CityGMLElement, CityGMLReader, ParseError, SubTreeReader};
use clap::Parser;
use nusamai_plateau::TopLevelCityObject;
use std::io::BufRead;

#[derive(Parser)]
struct Args {
#[clap(required = true)]
filename: String,
}

fn toplevel_dispatcher<R: BufRead>(
st: &mut SubTreeReader<R>,
) -> Result<Vec<TopLevelCityObject>, ParseError> {
let mut cityobjs: Vec<TopLevelCityObject> = vec![];

match st.parse_children(|st| match st.current_path() {
b"core:cityObjectMember" => {
let mut cityobj: nusamai_plateau::models::TopLevelCityObject = Default::default();
cityobj.parse(st)?;
let geometries = st.collect_geometries();

if let Some(root) = cityobj.into_object() {
let obj = TopLevelCityObject { root, geometries };
cityobjs.push(obj);
}

Ok(())
}
b"gml:boundedBy" | b"app:appearanceMember" => {
st.skip_current_element()?;
Ok(())
}
other => Err(ParseError::SchemaViolation(format!(
"Unrecognized element {}",
String::from_utf8_lossy(other)
))),
}) {
Ok(_) => Ok(cityobjs),
Err(e) => {
println!("Err: {:?}", e);
Err(e)
}
}
}

#[tokio::main]
async fn main() {
// Parse CityGML

let args = Args::parse();

let reader = std::io::BufReader::new(std::fs::File::open(args.filename).unwrap());
let mut xml_reader = quick_xml::NsReader::from_reader(reader);
let _cityobjs = match CityGMLReader::new().start_root(&mut xml_reader) {
Ok(mut st) => match toplevel_dispatcher(&mut st) {
Ok(items) => items,
Err(e) => panic!("Err: {:?}", e),
},
Err(e) => panic!("Err: {:?}", e),
};

// GeoPackage

let output_path = "output.gpkg";
let _handler = nusamai_gpkg::GpkgHandler::init(output_path).await.unwrap();
// TODO: handler.add_objects(&cityobjs).await;
}
98 changes: 98 additions & 0 deletions nusamai-gpkg/src/handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use nusamai_plateau::TopLevelCityObject;
use sqlx::Row;
use sqlx::{migrate::MigrateDatabase, Pool, Sqlite, SqlitePool};
use std::path::Path;
use thiserror::Error;

pub struct GpkgHandler {
pool: Pool<Sqlite>,
}

#[derive(Error, Debug)]
pub enum GpkgError {
#[error("Database file already exists: {0}")]
DatabaseExists(String),
#[error("SQLx error: {0}")]
SqlxError(#[from] sqlx::Error),
}

impl GpkgHandler {
/// Create and initialize new GeoPackage database
pub async fn init(path: &str) -> Result<Self, GpkgError> {
if Path::new(path).exists() {
return Err(GpkgError::DatabaseExists(path.to_string()));
}

let db_url = format!("sqlite://{}", path);

Sqlite::create_database(&db_url).await?;
let pool = SqlitePool::connect(&db_url).await?;

// Initialize the database with minimum GeoPackage schema
let create_query = include_str!("sql/gpkg_template.sql");
sqlx::query(create_query).execute(&pool).await?;

Ok(Self { pool })
}

/// Connect to an existing GeoPackage database
pub async fn connect(path: &str) -> Result<Self, GpkgError> {
let db_url = format!("sqlite://{}", path);
let pool = SqlitePool::connect(&db_url).await?;
Ok(Self { pool })
}

/// Get the names of all tables in the GeoPackage database
pub async fn table_names(&self) -> Vec<String> {
let result = sqlx::query(
"SELECT name
FROM sqlite_schema
WHERE type ='table'
AND name NOT LIKE 'sqlite_%';",
)
.fetch_all(&self.pool)
.await
.unwrap();

let mut table_names: Vec<String> = result
.iter()
.map(|row| row.get::<String, &str>("name"))
.collect();
table_names.sort();
table_names
}

/// Add a TopLevelCityObjects to the GeoPackage database
pub async fn add_object(&self, _object: &TopLevelCityObject) {
todo!();
}

/// Add TopLevelCityObjects to the GeoPackage database
pub async fn add_objects(&self, _objects: &[TopLevelCityObject]) {
todo!();
}
}

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn test_init_connect() {
let handler = GpkgHandler::init("sqlite::memory:").await.unwrap();
let _handler2 = GpkgHandler::connect("sqlite::memory:").await.unwrap();

let table_names = handler.table_names().await;
assert_eq!(
table_names,
vec![
"gpkg_contents",
"gpkg_extensions",
"gpkg_geometry_columns",
"gpkg_spatial_ref_sys",
"gpkg_tile_matrix",
"gpkg_tile_matrix_set"
]
);
}
}
3 changes: 3 additions & 0 deletions nusamai-gpkg/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod handler;

pub use handler::GpkgHandler;
67 changes: 67 additions & 0 deletions nusamai-gpkg/src/sql/gpkg_template.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
CREATE TABLE gpkg_contents (
table_name TEXT NOT NULL PRIMARY KEY,
data_type TEXT NOT NULL,
identifier TEXT UNIQUE,
description TEXT DEFAULT '',
last_change DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
min_x DOUBLE,
min_y DOUBLE,
max_x DOUBLE,
max_y DOUBLE,
srs_id INTEGER,
CONSTRAINT fk_gc_r_srs_id FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)
);

CREATE TABLE gpkg_extensions (
table_name TEXT,
column_name TEXT,
extension_name TEXT NOT NULL,
definition TEXT NOT NULL,
scope TEXT NOT NULL,
CONSTRAINT ge_tce UNIQUE (table_name, column_name, extension_name)
);

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,
CONSTRAINT pk_geom_cols PRIMARY KEY (table_name, column_name),
CONSTRAINT fk_gc_tn FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name),
CONSTRAINT fk_gc_srs FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys (srs_id)
);

CREATE TABLE gpkg_spatial_ref_sys (
srs_name TEXT NOT NULL,
srs_id INTEGER NOT NULL PRIMARY KEY,
organization TEXT NOT NULL,
organization_coordsys_id INTEGER NOT NULL,
definition TEXT NOT NULL,
description TEXT
);

CREATE TABLE gpkg_tile_matrix (
table_name TEXT NOT NULL,
zoom_level INTEGER NOT NULL,
matrix_width INTEGER NOT NULL,
matrix_height INTEGER NOT NULL,
tile_width INTEGER NOT NULL,
tile_height INTEGER NOT NULL,
pixel_x_size DOUBLE NOT NULL,
pixel_y_size DOUBLE NOT NULL,
CONSTRAINT pk_ttm PRIMARY KEY (table_name, zoom_level),
CONSTRAINT fk_tmm_table_name FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name)
);

CREATE TABLE gpkg_tile_matrix_set (
table_name TEXT NOT NULL PRIMARY KEY,
srs_id INTEGER NOT NULL,
min_x DOUBLE NOT NULL,
min_y DOUBLE NOT NULL,
max_x DOUBLE NOT NULL,
max_y DOUBLE NOT NULL,
CONSTRAINT fk_gtms_table_name FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name),
CONSTRAINT fk_gtms_srs FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys (srs_id)
);

0 comments on commit a894113

Please sign in to comment.