1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
// Copyright 2023 Balázs Dukai, Ravi Peters
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::path::{Path, PathBuf};
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about)]
pub struct Cli {
/// Input dataset root. Must be a cjindex-compatible dataset in NDJSON,
/// CityJSON, or feature-files layout.
#[arg(value_parser = existing_canonical_path)]
pub input: PathBuf,
/// Directory for the output.
#[arg(short, long)]
pub output: PathBuf,
// /// Output format.
// #[arg(long, value_enum)]
// pub format: crate::Formats,
/// The CityObject type to include
/// (https://www.cityjson.org/specs/2.0.1/#city-objects).
/// You can specify it multiple times.
#[arg(long, value_enum)]
pub object_type: Option<Vec<crate::parser::CityObjectType>>,
/// The CityObject attribute name and value type to include as feature attribute when the
/// output is 3D Tiles. Format: <attribute_name>:<attribute_type>,<attribute_name>:<attribute_type>
/// eg: 'name1:string,name2:bool'.
/// Possible value types are, 'bool', 'int', 'float', 'string'.
#[arg(long, value_delimiter = ',')]
pub object_attributes: Option<Vec<String>>,
/// The metadata class to assign to the property table when the output is
/// 3D Tiles (https://github.com/CesiumGS/glTF/tree/3d-tiles-next/extensions/2.0/Vendor/EXT_structural_metadata#class).
#[arg(long = "3dtiles-metadata-class", default_value = "citymodel")]
pub cesium3dtiles_metadata_class: String,
/// Create implicit tiling when the output format is 3D Tiles (https://docs.ogc.org/cs/22-025r4/22-025r4.html#toc31).
/// By default, explicit tiling is created for the 3D Tiles output.
#[arg(long = "3dtiles-implicit")]
pub cesium3dtiles_implicit: bool,
/// Generate and write the Tileset only, without exporting the glTF tiles, when the output format is 3D Tiles (https://docs.ogc.org/cs/22-025r4/22-025r4.html#toc31).
#[arg(long = "debug-3dtiles-tileset-only")]
pub debug_cesium3dtiles_tileset_only: bool,
/// Use the tile boundingVolume as the content boundingVolume, instead of calculating the content boundingVolume from the data.
#[arg(long = "3dtiles-content-bv-from-tile")]
pub cesium3dtiles_content_bv_from_tile: bool,
/// Clip tile content geometry to the tile bounding box before writing the GLB.
#[arg(long = "3dtiles-content-clip-to-tile-bounds")]
pub cesium3dtiles_content_clip_to_tile_bounds: bool,
/// Add the boundingVolume of the content for the the tiles that have content.
#[arg(long = "3dtiles-content-add-bv")]
pub cesium3dtiles_content_add_bv: bool,
/// Set the geometric error factor (see 3D Tiles specification) for internal tiles.
/// Internal tile geometricError is computed as tile width multiplied by this factor.
/// Higher values make detailed content visible earlier when zooming in.
#[arg(long = "3dtiles-geometric-error-factor", default_value = "0.024")]
pub cesium3dtiles_geometric_error_factor: f64,
/// Set the 2D cell size for the grid that is used for constructing the quadtree.
/// In input units (eg. meters). Note that the cell size will be adjusted so that it is
/// possible to construct a tightly fit square, containing 4^n cells. The final cell size will
/// larger than this value.
#[arg(long, default_value = "250")]
pub grid_cellsize: Option<u32>,
/// Load grid.tsv debug output and use it for quadtree construction. If a sibling
/// features.tsv exists, it is also loaded.
#[arg(long = "debug-load-grid", value_parser = existing_canonical_path)]
pub debug_load_grid: Option<PathBuf>,
/// Limit the minimum z coordinate for the bounding box that is computed from the
/// features. Useful if the features contain errors with extremely small z
/// coordinates. In input units (eg. meters).
#[arg(long)]
pub grid_minz: Option<i32>,
/// Limit the maximum z coordinate for the bounding box that is computed from the
/// features. Useful if the features contain errors with extremely large z
/// coordinates. In input units (eg. meters).
#[arg(long)]
pub grid_maxz: Option<i32>,
/// Export the grid into .tsv files in the debug output directory.
#[arg(long = "debug-dump-grid")]
pub debug_dump_grid: bool,
/// Export the grid, and also the feature centroids into .tsv files in the working
/// directory. Used for debugging.
#[arg(long = "debug-dump-grid-features")]
pub debug_dump_grid_features: bool,
/// Load instances from this directory.
/// In debug mode, tyler writes the generated world, quadtree etc. instances to .bincode files, which later can be used for debugging.
/// When this argument is specified, tyler will load the instances from the .bincode files that are available in the directory.
#[arg(long, value_parser = existing_canonical_path)]
pub debug_load_data: Option<PathBuf>,
/// The maximum number of vertices in a leaf of the quadtree.
#[arg(long, default_value = "42000")]
pub qtree_capacity: Option<usize>,
/// Dump debug data for the current run, including `.bincode` snapshots and
/// per-tile intermediary CityJSONFeature streams.
#[arg(long = "debug-dump-data")]
pub debug_dump_data: bool,
/// Copy inherited parent attributes onto geometry-bearing CityObjects before GLB conversion.
/// This is useful for 3DBAG/roofer-style parent-child models where attributes live on the parent.
#[arg(long)]
pub include_parent_attributes: bool,
/// Compute smooth vertex normals.
#[arg(long)]
pub smooth_normals: bool,
/// LoD to use in output for Building features
#[arg(long)]
pub lod_building: Option<String>,
/// LoD to use in output for building_part features
#[arg(long)]
pub lod_building_part: Option<String>,
/// LoD to use in output for building_installation features
#[arg(long)]
pub lod_building_installation: Option<String>,
/// LoD to use in output for tin_relief features
#[arg(long)]
pub lod_tin_relief: Option<String>,
/// LoD to use in output for road features
#[arg(long)]
pub lod_road: Option<String>,
/// LoD to use in output for railway features
#[arg(long)]
pub lod_railway: Option<String>,
/// LoD to use in output for transport_square features
#[arg(long)]
pub lod_transport_square: Option<String>,
/// LoD to use in output for water_body features
#[arg(long)]
pub lod_water_body: Option<String>,
/// LoD to use in output for plant_cover features
#[arg(long)]
pub lod_plant_cover: Option<String>,
/// LoD to use in output for solitary_vegetation_object features
#[arg(long)]
pub lod_solitary_vegetation_object: Option<String>,
/// LoD to use in output for land_use features
#[arg(long)]
pub lod_land_use: Option<String>,
/// LoD to use in output for city_furniture features
#[arg(long)]
pub lod_city_furniture: Option<String>,
/// LoD to use in output for bridge features
#[arg(long)]
pub lod_bridge: Option<String>,
/// LoD to use in output for bridge_part features
#[arg(long)]
pub lod_bridge_part: Option<String>,
/// LoD to use in output for bridge_installation features
#[arg(long)]
pub lod_bridge_installation: Option<String>,
/// LoD to use in output for bridge_construction_element features
#[arg(long)]
pub lod_bridge_construction_element: Option<String>,
/// LoD to use in output for tunnel features
#[arg(long)]
pub lod_tunnel: Option<String>,
/// LoD to use in output for tunnel_part features
#[arg(long)]
pub lod_tunnel_part: Option<String>,
/// LoD to use in output for tunnel_installation features
#[arg(long)]
pub lod_tunnel_installation: Option<String>,
/// LoD to use in output for lod_generic_city_object features
#[arg(long)]
pub lod_generic_city_object: Option<String>,
/// Color for Building features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_building: Option<String>,
/// Color for BuildingPart features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_building_part: Option<String>,
/// Color for BuildingInstallation features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_building_installation: Option<String>,
/// Color for TINRelief features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_tin_relief: Option<String>,
/// Color for Road features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_road: Option<String>,
/// Color for Railway features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_railway: Option<String>,
/// Color for TransportSquare features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_transport_square: Option<String>,
/// Color for WaterBody features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_water_body: Option<String>,
/// Color for PlantCover features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_plant_cover: Option<String>,
/// Color for SolitaryVegetationObject features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_solitary_vegetation_object: Option<String>,
/// Color for LandUse features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_land_use: Option<String>,
/// Color for CityFurniture features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_city_furniture: Option<String>,
/// Color for Bridge features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_bridge: Option<String>,
/// Color for BridgePart features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_bridge_part: Option<String>,
/// Color for BridgeInstallation features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_bridge_installation: Option<String>,
/// Color for BridgeConstructionElement features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_bridge_construction_element: Option<String>,
/// Color for Tunnel features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_tunnel: Option<String>,
/// Color for TunnelPart features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_tunnel_part: Option<String>,
/// Color for TunnelInstallation features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_tunnel_installation: Option<String>,
/// Color for GenericCityObject features specified as a hex rgb-color value, eg. #FF0000 is red.
#[arg(long, value_parser = hex_color)]
pub color_generic_city_object: Option<String>,
// The number of levels to export as content from the quadtree.
// Counted from the leaves.
// #[arg(long, default_value = "0")]
// pub qtree_export_levels: Option<u16>,
// /// The criteria to check for the quadtree leaf capacity.
// #[arg(long, value_enum, default_value = "vertices")]
// pub qtree_criteria: Option<crate::spatial_structs::QuadTreeCriteria>,
// /// Path to the python interpreter (>=3.8) to use for generating CityJSON tiles.
// /// The interpreter must have a recent cjio (https://github.com/cityjson/cjio)
// /// installed.
// #[arg(long, value_parser = existing_path)]
// pub exe_python: Option<PathBuf>,
}
fn existing_canonical_path(s: &str) -> Result<PathBuf, String> {
if let Ok(c) = Path::new(s).canonicalize() {
if c.exists() {
Ok(c)
} else {
Err(format!("path {:?} does not exist", &c))
}
} else {
Err(format!("could not resolve the path {:?}", s))
}
}
/// Checks is `s` constains a 6 digit hexadecimal value preceded by a '#', eg. #FF0000
fn hex_color(s: &str) -> Result<String, String> {
if s.len() != 7 || !s.starts_with('#') {
return Err(String::from(
"Input must be a 6-digit hexadecimal value preceded by a '#'",
));
}
let hex_digits = &s[1..];
if !hex_digits.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(String::from(
"Input must be a 6-digit hexadecimal value preceded by a '#'",
));
}
Ok(String::from(s))
}
#[cfg(test)]
mod tests {
use super::Cli;
use clap::{CommandFactory, Parser};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
fn resource_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources")
.join("data")
.join(name)
}
fn unique_test_dir(prefix: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time")
.as_nanos();
let path = std::env::temp_dir().join(format!("tyler-{prefix}-{unique}"));
fs::create_dir_all(&path).expect("create test dir");
path
}
fn legacy_dataset_dir() -> PathBuf {
let dataset_dir = unique_test_dir("cli-legacy");
let features_dir = dataset_dir.join("features");
fs::create_dir_all(&features_dir).expect("create features dir");
fs::copy(
resource_path("3dbag_x00.city.json"),
dataset_dir.join("metadata.city.json"),
)
.expect("copy metadata");
fs::copy(
resource_path("3dbag_feature_x71.city.jsonl"),
features_dir.join("sample.city.jsonl"),
)
.expect("copy feature");
dataset_dir
}
fn required_args(input_dir: &Path) -> Vec<String> {
vec![
"tyler".to_string(),
input_dir.display().to_string(),
"-o".to_string(),
env!("CARGO_MANIFEST_DIR").to_string(),
]
}
fn dataset_args() -> Vec<String> {
vec![
"tyler".to_string(),
concat!(env!("CARGO_MANIFEST_DIR"), "/resources/data").to_string(),
"-o".to_string(),
env!("CARGO_MANIFEST_DIR").to_string(),
]
}
#[test]
fn verify_cli() {
Cli::command().debug_assert()
}
/// CLI arguments review item 7: the CityJSON v2.0 `--object-type` parser
/// still accepts repeated occurrences and stores all requested types.
#[test]
fn verify_object_types() {
let dataset_dir = legacy_dataset_dir();
let types: Vec<String> = vec![
"--object-type".to_string(),
"Building".to_string(),
"--object-type".to_string(),
"PlantCover".to_string(),
];
let mut args = required_args(&dataset_dir);
args.extend(types);
let cli = Cli::try_parse_from(args).unwrap();
let otypes = &cli.object_type.unwrap();
assert!(otypes.contains(&crate::parser::CityObjectType::Building));
assert!(otypes.contains(&crate::parser::CityObjectType::PlantCover));
}
/// CLI arguments review item 10: `--include-parent-attributes` remains the
/// supported flag after removing `--bag3d-attributes-per-part`.
#[test]
fn verify_include_parent_attributes_flag() {
let dataset_dir = legacy_dataset_dir();
let mut args = required_args(&dataset_dir);
args.push("--include-parent-attributes".to_string());
let cli = Cli::try_parse_from(args).unwrap();
assert!(cli.include_parent_attributes);
}
/// CLI arguments review item 12: `--3dtiles-geometric-error-factor`
/// replaces the old flag name and still parses the configured float value.
#[test]
fn verify_geometric_error_factor() {
let dataset_dir = legacy_dataset_dir();
let mut args = required_args(&dataset_dir);
args.extend([
"--3dtiles-geometric-error-factor".to_string(),
"0.05".to_string(),
]);
let cli = Cli::try_parse_from(args).unwrap();
assert!((cli.cesium3dtiles_geometric_error_factor - 0.05).abs() < f64::EPSILON);
}
/// CLI arguments review item 8: `--object-attributes` now accepts a
/// comma-separated `name:type` mapping list and splits it into entries.
#[test]
fn verify_object_attributes_csv_mapping() {
let dataset_dir = legacy_dataset_dir();
let mut args = required_args(&dataset_dir);
args.extend([
"--object-attributes".to_string(),
"name:string,levels:int,occupied:bool".to_string(),
]);
let cli = Cli::try_parse_from(args).unwrap();
assert_eq!(
cli.object_attributes,
Some(vec![
"name:string".to_string(),
"levels:int".to_string(),
"occupied:bool".to_string()
])
);
}
#[test]
fn verify_single_input_arg_for_dataset_root() {
let cli = Cli::try_parse_from(dataset_args()).unwrap();
assert_eq!(
cli.input,
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources")
.join("data")
.canonicalize()
.unwrap()
);
}
#[test]
fn reject_legacy_input_flags() {
let dataset_dir = legacy_dataset_dir();
let args = vec![
"tyler".to_string(),
"--metadata".to_string(),
dataset_dir.join("metadata.city.json").display().to_string(),
"--features".to_string(),
dataset_dir.join("features").display().to_string(),
"-o".to_string(),
env!("CARGO_MANIFEST_DIR").to_string(),
];
assert!(Cli::try_parse_from(args).is_err());
}
}