From a8941131f0af2725a67ca723a8d4d549182105c2 Mon Sep 17 00:00:00 2001 From: Sorami Hisamoto Date: Mon, 25 Dec 2023 19:07:48 +0900 Subject: [PATCH] =?UTF-8?q?nusamai-gpkg:=20=E3=82=AF=E3=83=AC=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=AE=E4=BD=9C=E6=88=90=E3=80=81=E7=A9=BA=E3=81=AE?= =?UTF-8?q?GeoPackage=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E4=BD=9C?= =?UTF-8?q?=E6=88=90=20(#89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #8 まず、ベースの部分を作ってみました。 このPR後に、ジオメトリデータをバイナリ化したりしたものなど、各情報をSQLiteテーブルへ入れていく。 ## 変更 - [x] `nusamai-gpkg` クレートの新規作成 - [x] SQLxによる、空のGeoPackageファイル作成 - [x] example: CLIからのテスト実行 --- Cargo.toml | 1 + nusamai-gpkg/.gitignore | 3 + nusamai-gpkg/Cargo.toml | 15 ++++ nusamai-gpkg/examples/gml2gpkg.rs | 68 ++++++++++++++++++ nusamai-gpkg/src/handler.rs | 98 ++++++++++++++++++++++++++ nusamai-gpkg/src/lib.rs | 3 + nusamai-gpkg/src/sql/gpkg_template.sql | 67 ++++++++++++++++++ 7 files changed, 255 insertions(+) create mode 100644 nusamai-gpkg/.gitignore create mode 100644 nusamai-gpkg/Cargo.toml create mode 100644 nusamai-gpkg/examples/gml2gpkg.rs create mode 100644 nusamai-gpkg/src/handler.rs create mode 100644 nusamai-gpkg/src/lib.rs create mode 100644 nusamai-gpkg/src/sql/gpkg_template.sql diff --git a/Cargo.toml b/Cargo.toml index 48160def8..4101b2c39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "nusamai-geometry", "nusamai-gltf", "nusamai-geojson", + "nusamai-gpkg", "nusamai-plateau", "nusamai-mvt", "nusamai", diff --git a/nusamai-gpkg/.gitignore b/nusamai-gpkg/.gitignore new file mode 100644 index 000000000..f62bf7911 --- /dev/null +++ b/nusamai-gpkg/.gitignore @@ -0,0 +1,3 @@ +*.gpkg +*.gpkg-shm +*.gpkg-wal \ No newline at end of file diff --git a/nusamai-gpkg/Cargo.toml b/nusamai-gpkg/Cargo.toml new file mode 100644 index 000000000..b07b832af --- /dev/null +++ b/nusamai-gpkg/Cargo.toml @@ -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" diff --git a/nusamai-gpkg/examples/gml2gpkg.rs b/nusamai-gpkg/examples/gml2gpkg.rs new file mode 100644 index 000000000..9a879d232 --- /dev/null +++ b/nusamai-gpkg/examples/gml2gpkg.rs @@ -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( + st: &mut SubTreeReader, +) -> Result, ParseError> { + let mut cityobjs: Vec = 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; +} diff --git a/nusamai-gpkg/src/handler.rs b/nusamai-gpkg/src/handler.rs new file mode 100644 index 000000000..a8af3dae9 --- /dev/null +++ b/nusamai-gpkg/src/handler.rs @@ -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, +} + +#[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 { + 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 { + 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 { + 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 = result + .iter() + .map(|row| row.get::("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" + ] + ); + } +} diff --git a/nusamai-gpkg/src/lib.rs b/nusamai-gpkg/src/lib.rs new file mode 100644 index 000000000..2ef5b8a30 --- /dev/null +++ b/nusamai-gpkg/src/lib.rs @@ -0,0 +1,3 @@ +mod handler; + +pub use handler::GpkgHandler; diff --git a/nusamai-gpkg/src/sql/gpkg_template.sql b/nusamai-gpkg/src/sql/gpkg_template.sql new file mode 100644 index 000000000..b4f751749 --- /dev/null +++ b/nusamai-gpkg/src/sql/gpkg_template.sql @@ -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) +); \ No newline at end of file