Skip to content

Commit

Permalink
feat: geojson allow reprojection
Browse files Browse the repository at this point in the history
  • Loading branch information
jjcfrancisco committed Sep 7, 2024
1 parent 0340f21 commit 020b2a0
Show file tree
Hide file tree
Showing 9 changed files with 446 additions and 49 deletions.
353 changes: 343 additions & 10 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ bytes = "1.6.0"
geojson = "0.24.1"
serde_json = "1.0.120"
derive_more = "0.99.18"
proj = "0.27.2"
7 changes: 1 addition & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,10 @@ Although non extensive, the benchmarking shows **Popgis is twice faster than ogr

> The file used for this test can be found [here](https://data.cityofnewyork.us/City-Government/NYC-Street-Centerline-CSCL-/exjm-f27b).
## Future implementation
The list below contains the upcoming implementations.
## Future implementations

* Project from 4326 to 3857 (and viceversa).
* Allow nested GeoJSON properties.
* Reduce precision of a GeoJSON file.
* New validate command to validate files.
* Merge two columns of different types.


## License
See [`LICENSE`](./LICENSE)
9 changes: 9 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,13 @@
--schema geojson \
--table spain

@try-geojson-to-webmercator:
cargo build --release
cd ./target/release/ && ./popgis --input ../../examples/geojson/spain.geojson \
--uri postgresql://pio:password@localhost:25432/popgis \
--schema geojson \
--table spain3857 \
--srid=4326 \
--reproject=3857

@set-tags:
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ pub enum Error {
Pg(postgres::Error),
#[from]
Shapefile(shapefile::Error),
#[from]
Proj(proj::ProjCreateError),
#[from]
ProjTransform(proj::ProjError),
}

// region: --- Error Boilerplate
Expand Down
18 changes: 15 additions & 3 deletions src/format/common.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{Result, Error};
use crate::{Error, Result};

use postgres::types::Type;
use proj::{Proj, Transform};
use std::path::Path;

use crate::pg::binary_copy::Wkb;
Expand All @@ -23,7 +24,9 @@ pub struct Rows {

impl Row {
pub fn new() -> Self {
Row { columns: Vec::new() }
Row {
columns: Vec::new(),
}
}
pub fn add(&mut self, column: AcceptedTypes) {
self.columns.push(column);
Expand Down Expand Up @@ -67,10 +70,19 @@ pub fn determine_file_type(input_file: &str) -> Result<FileType> {
match file_extension_str {
"shp" => Ok(FileType::Shapefile),
"geojson" => Ok(FileType::GeoJson),
_ => Err(Error::UnsupportedFileExtension("Unsupported file type ✘".into())),
_ => Err(Error::UnsupportedFileExtension(
"Unsupported file type ✘".into(),
)),
}
}

fn reproject(from: &str, to: &str, geom: &mut geo::Geometry) -> Result<geo::Geometry> {
let proj = Proj::new_known_crs(&from, &to, None)?;
geom.transform(&proj)?;

Ok(geom.clone())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
34 changes: 26 additions & 8 deletions src/format/geojson.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use crate::{Result, Error};
use crate::utils::cli::Cli;
use crate::{Error, Result};

use std::collections::HashMap;
use geojson::GeoJson;
use postgres::types::Type;
use proj::{Proj, Transform};
use serde_json;
use std::collections::HashMap;
use wkb::geom_to_wkb;

use crate::format::common::{AcceptedTypes, Row, Rows};
Expand Down Expand Up @@ -41,7 +43,9 @@ pub fn determine_data_types(file_path: &str) -> Result<Vec<NewTableTypes>> {
} else if table_config.contains_key(&key)
&& table_config[&key] != Type::INT8
{
return Err(Error::MixedDataTypes("Column contains mixed data types ✘".to_string()));
return Err(Error::MixedDataTypes(
"Column contains mixed data types ✘".to_string(),
));
} else {
table_config.insert(key, Type::FLOAT8);
}
Expand All @@ -54,7 +58,9 @@ pub fn determine_data_types(file_path: &str) -> Result<Vec<NewTableTypes>> {
} else if table_config.contains_key(&key)
&& table_config[&key] != Type::INT8
{
return Err(Error::MixedDataTypes("Column contains mixed data types ✘".to_string()));
return Err(Error::MixedDataTypes(
"Column contains mixed data types ✘".to_string(),
));
} else {
table_config.insert(key, Type::TEXT);
}
Expand All @@ -67,7 +73,9 @@ pub fn determine_data_types(file_path: &str) -> Result<Vec<NewTableTypes>> {
} else if table_config.contains_key(&key)
&& table_config[&key] != Type::INT8
{
return Err(Error::MixedDataTypes("Column contains mixed data types ✘".to_string()));
return Err(Error::MixedDataTypes(
"Column contains mixed data types ✘".to_string(),
));
} else {
table_config.insert(key, Type::BOOL);
}
Expand All @@ -94,9 +102,9 @@ pub fn determine_data_types(file_path: &str) -> Result<Vec<NewTableTypes>> {
Ok(data_types)
}

pub fn read_geojson(file_path: &str) -> Result<Rows> {
pub fn read_geojson(args: &Cli) -> Result<Rows> {
let mut rows = Rows::new();
let geojson_str = std::fs::read_to_string(file_path)?;
let geojson_str = std::fs::read_to_string(&args.input)?;
let geojson = geojson_str.parse::<GeoJson>().unwrap();

match geojson {
Expand Down Expand Up @@ -131,10 +139,20 @@ pub fn read_geojson(file_path: &str) -> Result<Rows> {
}
}
let gj_geom = feature.geometry.unwrap();
let geom: geo::Geometry<f64> = gj_geom
let mut geom: geo::Geometry<f64> = gj_geom
.value
.try_into()
.expect("Failed to convert geojson::Geometry to geo::Geometry ✘");
// If reprojecting
geom = if args.reproject.is_some() {
let from = format!("EPSG:{}", args.srid.unwrap());
let to = format!("EPSG:{}", args.reproject.unwrap());
let proj = Proj::new_known_crs(&from, &to, None)?;
geom.transform(&proj)?;
geom
} else {
geom
};
let wkb = geom_to_wkb(&geom).expect("Could not convert geometry to WKB ✘");
// Check length of row
row.add(AcceptedTypes::Geometry(Some(Wkb { geometry: wkb })));
Expand Down
63 changes: 43 additions & 20 deletions src/utils/cli.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
use crate::{Result, Error};
use crate::format::common::{FileType, determine_file_type};
use crate::format::shapefile;
use std::i32;

use crate::format::common::{determine_file_type, FileType};
use crate::format::geojson;
use crate::utils::validate::validate_args;
use crate::pg::crud::{create_table, create_schema, check_table_exists, drop_table, get_stmt, can_append};
use crate::format::shapefile;
use crate::pg::binary_copy::{infer_geom_type, insert_rows};
use crate::pg::crud::{
can_append, check_table_exists, create_schema, create_table, drop_table, get_stmt,
};
use crate::utils::validate::validate_args;
use crate::{Error, Result};

use clap::Parser;

/// A blazing fast way to insert GeoJSON & ShapeFiles into a PostGIS database
/// A blazing fast way to insert GeoJSON & ShapeFiles into a PostGIS database
#[derive(Parser, Debug)]
#[command(about, version)]
pub struct Cli {

/// Input file path, either shapefile or geojson
#[arg(short, long)]
pub input: String,
Expand All @@ -36,6 +39,10 @@ pub struct Cli {
/// Mode: overwrite, append, fail. Optional.
#[arg(short, long)]
pub mode: Option<String>,

/// Reproject: reproject to 4326 or 3857. Optional.
#[arg(short, long)]
pub reproject: Option<i32>,
}

pub fn run() -> Result<()> {
Expand All @@ -49,12 +56,14 @@ pub fn run() -> Result<()> {

let file_type = determine_file_type(&args.input)?;
let (rows, config) = match file_type {
FileType::Shapefile => {
(shapefile::read_shapefile(&args.input)?, shapefile::determine_data_types(&args.input)?)
}
FileType::GeoJson => {
(geojson::read_geojson(&args.input)?, geojson::determine_data_types(&args.input)?)
}
FileType::Shapefile => (
shapefile::read_shapefile(&args.input)?,
shapefile::determine_data_types(&args.input)?,
),
FileType::GeoJson => (
geojson::read_geojson(&args)?,
geojson::determine_data_types(&args.input)?,
),
};

// If mode not present, check if table exists
Expand Down Expand Up @@ -89,16 +98,30 @@ pub fn run() -> Result<()> {
if let Some(schema) = &args.schema {
create_schema(schema, &args.uri)?;
}
if let Some(srid) = args.srid {
create_table(&args.table, &args.schema, &config, &args.uri, srid)?
} else {
create_table(&args.table, &args.schema, &config, &args.uri, 4326)?
};
//
if let Some(reproject) = args.reproject {
create_table(&args.table, &args.schema, &config, &args.uri, reproject)?
} else if None == args.reproject {
create_table(
&args.table,
&args.schema,
&config,
&args.uri,
args.srid.unwrap(),
)?
}
}

let stmt = get_stmt(&args.table, &args.schema, &args.uri)?;
let stmt = get_stmt(&args.table, &args.schema, &args.uri)?;
let geom_type = infer_geom_type(stmt)?;
insert_rows(&rows, &config, geom_type, &args.uri, &args.schema, &args.table)?;
insert_rows(
&rows,
&config,
geom_type,
&args.uri,
&args.schema,
&args.table,
)?;

Ok(())
}
6 changes: 4 additions & 2 deletions src/utils/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ mod tests {
table: "points".to_string(),
schema: None,
srid: None,
mode: None
mode: None,
reproject: None
};
assert!(validate_args(&args).is_ok());
}
Expand All @@ -58,7 +59,8 @@ mod tests {
table: "points".to_string(),
schema: Some("gis".to_string()),
srid: Some(4326),
mode: None
mode: None,
reproject: None
};
assert!(validate_args(&args).is_ok());
}
Expand Down

0 comments on commit 020b2a0

Please sign in to comment.