Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ __pycache__/
*.pytest_cache/

temp/
data/

benchmark_data/
src/rust/fcb_core/tests/data/*.fcb
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@
"python.defaultInterpreterPath": "${workspaceFolder}/src/rust/fcb_py/.venv/bin/python",
"python.analysis.extraPaths": [
"${workspaceFolder}/src/rust/fcb_py/python"
],
"cursorpyright.analysis.extraPaths": [
"${workspaceFolder}/src/rust/fcb_py/python"
]
}
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,22 +146,29 @@ cd wasm && wasm-pack build --target web --release --out-dir ../../ts

### 🛠️ CLI Usage

#### Convert CityJSONSeq to FlatCityBuf
#### Convert CityJSON/CityJSONSeq to FlatCityBuf

replace `cargo run -p fcb_cli` with `fcb` in the following commands if you want to use the binary directly.
Replace `cargo run -p fcb_cli --` with `fcb` in the following commands if you want to use the installed binary directly.

```bash
# Basic conversion
fcb fcb_cli ser -i input.city.jsonl -o output.fcb
# Basic conversion from CityJSONSeq
fcb ser -i input.city.jsonl -o output.fcb

# With compression and indexing options
fcb fcb_cli ser -i data.city.jsonl -o data.fcb
# Convert standard CityJSON file
fcb ser -i city.city.json -o output.fcb

# Multiple input files
fcb ser -i file1.city.jsonl file2.city.jsonl -o merged.fcb

# Glob patterns to process all matching files
fcb ser -i 'data/*.city.jsonl' -o output.fcb
fcb ser -i 'cities/**/*.city.json' -o all_cities.fcb

# With spatial index and attribute index
fcb fcb_cli ser -i data.city.jsonl -o data.fcb --attr-index attribute_name,attribute_name2 --attr-branching-factor 256
fcb ser -i data.city.jsonl -o data.fcb --attr-index attribute_name,attribute_name2 --attr-branching-factor 256

# Show information about the file
fcb fcb_cli info -i data.fcb
fcb info -i data.fcb
```

### 🧪 Run Benchmarks
Expand Down
8 changes: 8 additions & 0 deletions src/rust/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ description = "FlatCityBuf is a library for reading and writing CityJSON with Fl
name = "fcb"
path = "src/main.rs"

[lib]
name = "fcb_cli"
path = "src/lib.rs"

[dependencies]
fcb_core = { workspace = true, features = ["http"] }
cjseq = { workspace = true }
Expand All @@ -23,3 +27,7 @@ serde_cbor = { workspace = true }
thiserror = { workspace = true }
indicatif = { workspace = true }
console = { workspace = true }
glob = "0.3"

[dev-dependencies]
tempfile = { workspace = true }
17 changes: 15 additions & 2 deletions src/rust/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ fcb ser -i INPUT -o OUTPUT [OPTIONS]

**Options:**

- `-i, --input INPUT` - Input file (use '-' for stdin)
- `-i, --input INPUT...` - Input file(s) or glob patterns (supports multiple files, use '-' for stdin)
- `-o, --output OUTPUT` - Output file (use '-' for stdout)
- `-a, --attr-index ATTRIBUTES` - Comma-separated list of attributes to create index for
- `-A, --index-all-attributes` - Index all attributes found in the dataset
Expand All @@ -64,9 +64,19 @@ fcb ser -i INPUT -o OUTPUT [OPTIONS]
**Examples:**

```bash
# basic conversion
# basic conversion from CityJSONSeq
fcb ser -i input.city.jsonl -o output.fcb

# convert CityJSON file (standard .json format)
fcb ser -i city.city.json -o output.fcb

# multiple input files
fcb ser -i file1.city.jsonl file2.city.jsonl -o merged.fcb

# glob patterns to process all matching files
fcb ser -i 'data/*.city.jsonl' -o output.fcb
fcb ser -i 'cities/**/*.city.json' -o all_cities.fcb

# with attribute indexing
fcb ser -i delft.city.jsonl -o delft_attr.fcb \
--attr-index identificatie,tijdstipregistratie,b3_is_glas_dak,b3_h_dak_50p \
Expand Down Expand Up @@ -150,9 +160,12 @@ fcb bson -i INPUT -o OUTPUT

### Input Formats

- **CityJSON** (`.city.json`) - Standard CityJSON files
- **CityJSON Text Sequences** (`.city.jsonl`) - Line-delimited CityJSON features
- **FCB** (`.fcb`) - FlatCityBuf binary format

> **Multi-file Support:** The `ser` command accepts multiple input files and glob patterns. When merging files with different coordinate transforms, vertices are automatically aligned to the first file's transform.

### Output Formats

- **FCB** (`.fcb`) - FlatCityBuf binary format with optional indexing
Expand Down
38 changes: 38 additions & 0 deletions src/rust/cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! FCB CLI Library
//!
//! This library exposes the merger and reader modules for integration testing.
//! The main CLI binary is in main.rs.

pub mod merger;
pub mod reader;

use fcb_core::error::Error;
use thiserror::Error;

/// CLI-specific error type
#[derive(Error, Debug)]
pub enum CliError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),

#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),

#[error("Glob pattern error: {0}")]
GlobPattern(#[from] glob::PatternError),

#[error("Glob error: {0}")]
Glob(#[from] glob::GlobError),

#[error("Unsupported file format for '{0}': {1}")]
UnsupportedFormat(String, String),

#[error("Empty file: {0}")]
EmptyFile(String),

#[error("No input files specified or matched")]
NoInputFiles,

#[error("FCB core error: {0}")]
FcbCore(#[from] Error),
}
86 changes: 54 additions & 32 deletions src/rust/cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use cjseq::{CityJSON, CityJSONFeature, Transform as CjTransform};
use clap::{ArgAction, Parser, Subcommand};
use console::{style, Term};
use fcb_cli::CliError;
use fcb_core::error::Error;
use fcb_core::{
attribute::{AttributeSchema, AttributeSchemaMethods},
deserializer,
header_writer::HeaderWriterOptions,
read_cityjson_from_reader, CJType, CJTypeKind, CityJSONSeq, FcbReader, FcbWriter,
FcbReader, FcbWriter,
};
use glob::glob;
use indicatif::{ProgressBar, ProgressStyle};
use std::{
fs::File,
Expand All @@ -30,9 +32,9 @@ struct Cli {
enum Commands {
/// Convert CityJSON to FCB
Ser {
/// Input file (use '-' for stdin)
#[arg(short = 'i', long)]
input: String,
/// Input files (glob patterns supported, e.g., "cities/*/*.jsonl")
#[arg(short = 'i', long, required = true, num_args = 1..)]
input: Vec<String>,

/// Output file (use '-' for stdout)
#[arg(short = 'o', long)]
Expand Down Expand Up @@ -125,7 +127,7 @@ struct SerializeOptions {
ge: bool,
}

fn serialize(input: &str, output: &str, options: SerializeOptions) -> Result<(), Error> {
fn serialize(inputs: &[String], output: &str, options: SerializeOptions) -> Result<(), CliError> {
let term = Term::stderr();
let is_stdout = output == "-";

Expand All @@ -145,16 +147,29 @@ fn serialize(input: &str, output: &str, options: SerializeOptions) -> Result<(),
.ok();
}

let reader = get_reader(input)?;
let writer = get_writer(output)?;
// Expand glob patterns and collect all input files
let mut input_paths: Vec<PathBuf> = Vec::new();
for pattern in inputs {
let paths: Vec<PathBuf> = glob(pattern)?.filter_map(|entry| entry.ok()).collect();
if paths.is_empty() {
// If no glob match, treat as literal path
input_paths.push(PathBuf::from(pattern));
} else {
input_paths.extend(paths);
}
}

let reader = BufReader::new(reader);
if input_paths.is_empty() {
return Err(CliError::NoInputFiles);
}

let writer = get_writer(output)?;
let writer = BufWriter::new(writer);

// Parse the bbox if provided
let bbox_parsed = if let Some(bbox_str) = &options.bbox {
Some(parse_bbox(bbox_str).map_err(|e| {
Error::IoError(std::io::Error::new(
CliError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("failed to parse bbox: {e}"),
))
Expand All @@ -169,15 +184,27 @@ fn serialize(input: &str, output: &str, options: SerializeOptions) -> Result<(),
term.write_line(&format!("{} Configuration", style("▶").bold().green()))
.ok();
term.write_line(&format!(
" {} {}",
" {} {} file(s)",
style("Input:").dim(),
if input == "-" {
style("stdin").yellow()
} else {
style(input).yellow()
}
style(input_paths.len()).yellow()
))
.ok();
for (i, path) in input_paths.iter().enumerate().take(5) {
term.write_line(&format!(
" {}. {}",
style(i + 1).dim(),
style(path.display()).yellow()
))
.ok();
}
if input_paths.len() > 5 {
term.write_line(&format!(
" {} {} more files...",
style("...").dim(),
style(input_paths.len() - 5).dim()
))
.ok();
}
term.write_line(&format!(
" {} {}",
style("Output:").dim(),
Expand Down Expand Up @@ -245,7 +272,7 @@ fn serialize(input: &str, output: &str, options: SerializeOptions) -> Result<(),
term.write_line("").ok();
}

// Create a CityJSONSeq reader
// Read and merge input files
if !is_stdout {
term.write_line(&format!(
"{} Reading CityJSON...",
Expand All @@ -254,16 +281,9 @@ fn serialize(input: &str, output: &str, options: SerializeOptions) -> Result<(),
.ok();
}

let cj_seq = read_cityjson_from_reader(reader, CJTypeKind::Seq)?;

let CityJSONSeq { cj, features } = match cj_seq {
CJType::Seq(cj_seq) => cj_seq,
_ => {
return Err(Error::IoError(std::io::Error::other(
"failed to read CityJSON Feature",
)))
}
};
let merge_result = fcb_cli::merger::merge_files(input_paths)?;
let cj = merge_result.metadata;
let features = merge_result.features;

if !is_stdout {
term.write_line(&format!(
Expand Down Expand Up @@ -954,7 +974,7 @@ fn show_info(input: PathBuf) -> Result<(), Error> {
Ok(())
}

fn main() -> Result<(), Error> {
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();

match cli.command {
Expand All @@ -978,12 +998,14 @@ fn main() -> Result<(), Error> {
bbox,
ge,
},
),
Commands::Deser { input, output } => deserialize(&input, &output),
Commands::Cbor { input, output } => encode_cbor(&input, &output),
Commands::Bson { input, output } => encode_bson(&input, &output),
Commands::Info { input } => show_info(input),
)?,
Commands::Deser { input, output } => deserialize(&input, &output)?,
Commands::Cbor { input, output } => encode_cbor(&input, &output)?,
Commands::Bson { input, output } => encode_bson(&input, &output)?,
Commands::Info { input } => show_info(input)?,
}

Ok(())
}

#[cfg(test)]
Expand Down
Loading
Loading