cityjson-lib 0.6.0

High-level CityJSON 2.0 read/write facade integrating JSON I/O
Documentation
# Writing Data

The shared FFI write path now centers on typed values, typed resource ids, and
draft objects for nested geometry authoring.

The published C++ and Python surfaces expose that authoring model directly.

## Create A Model

=== "Rust"
    ```rust
    use cityjson_lib::cityjson::{self, v2_0::OwnedCityModel};

    let model = OwnedCityModel::new(cityjson::CityModelType::CityJSON);
    # let _ = model;
    ```

=== "C++"
    ```cpp
    #include <cityjson_lib/cityjson_lib.hpp>

    auto model = cityjson_lib::Model::create(CJ_MODEL_TYPE_CITY_JSON);
    ```

=== "Python"
    ```python
    from cityjson_lib import CityModel, ModelType

    model = CityModel.create(model_type=ModelType.CITY_JSON)
    ```

## Set Metadata

=== "Rust"
    ```rust
    use cityjson_lib::cityjson::{self, v2_0::OwnedCityModel};

    let mut model = OwnedCityModel::new(cityjson::CityModelType::CityJSON);
    model.metadata_mut().set_title("My Dataset".to_string());
    # let _ = model;
    ```

=== "C++"
    ```cpp
    model.set_metadata_title("My Dataset");
    model.set_metadata_identifier("my-dataset-001");
    model.set_metadata_geographical_extent({
        .min_x = 0.0,
        .min_y = 0.0,
        .min_z = 0.0,
        .max_x = 1.0,
        .max_y = 1.0,
        .max_z = 1.0,
    });
    model.set_metadata_contact(
        cityjson_lib::Contact{}
            .set_name("Author")
            .set_email("author@example.com")
            .set_role(CJ_CONTACT_ROLE_AUTHOR));
    ```

=== "Python"
    ```python
    from cityjson_lib import BBox, Contact, ContactRole

    model.set_metadata_title("My Dataset")
    model.set_metadata_identifier("my-dataset-001")
    model.set_metadata_geographical_extent(
        BBox(min_x=0.0, min_y=0.0, min_z=0.0, max_x=1.0, max_y=1.0, max_z=1.0)
    )
    model.set_metadata_contact(
        Contact()
        .set_name("Author")
        .set_email("author@example.com")
        .set_role(ContactRole.AUTHOR)
    )
    ```

## Add Geometry Through Drafts

=== "C++"
    ```cpp
    const auto v0 = model.add_vertex({10.0, 20.0, 0.0});
    const auto v1 = model.add_vertex({11.0, 20.0, 0.0});
    const auto v2 = model.add_vertex({11.0, 21.0, 0.0});
    const auto v3 = model.add_vertex({10.0, 21.0, 0.0});

    auto ring = cityjson_lib::RingDraft{};
    ring.push_vertex_index(v0)
        .push_vertex_index(v1)
        .push_vertex_index(v2)
        .push_vertex_index(v3);

    auto draft = cityjson_lib::GeometryDraft::multi_surface("2.2");
    draft.add_surface(cityjson_lib::SurfaceDraft(std::move(ring)));

    auto building = cityjson_lib::CityObjectDraft("building-1", "Building");
    building.set_attribute("height", cityjson_lib::Value::number(12.5));

    const auto geometry_id = model.add_geometry(std::move(draft));
    const auto building_id = model.add_cityobject(std::move(building));
    model.add_cityobject_geometry(building_id, geometry_id);
    ```

=== "Python"
    ```python
    from cityjson_lib import CityObjectDraft, GeometryDraft, RingDraft, SurfaceDraft, Value, Vertex

    v0 = model.add_vertex(Vertex(10.0, 20.0, 0.0))
    v1 = model.add_vertex(Vertex(11.0, 20.0, 0.0))
    v2 = model.add_vertex(Vertex(11.0, 21.0, 0.0))
    v3 = model.add_vertex(Vertex(10.0, 21.0, 0.0))

    ring = RingDraft().push_vertex_index(v0).push_vertex_index(v1).push_vertex_index(v2).push_vertex_index(v3)
    draft = GeometryDraft.multi_surface("2.2").add_surface(SurfaceDraft(ring))

    building = CityObjectDraft("building-1", "Building")
    building.set_attribute("height", Value.number(12.5))

    geometry_id = model.add_geometry(draft)
    building_id = model.add_cityobject(building)
    model.add_cityobject_geometry(building_id, geometry_id)
    ```

## Full Fixture Example

The full reference examples live in `ffi/cpp/examples/fake_complete.cpp` and
`ffi/python/examples/fake_complete.py`. Both build the equivalent of the
complete fake CityJSON fixture through the typed authoring API and are
exercised in the automated test suite.

## Serialize A Document

=== "Rust"
    ```rust
    use cityjson_lib::json::{self, WriteOptions};

    let model = json::from_file("amsterdam.city.json")?;
    let compact = json::to_string(&model)?;
    let pretty = json::to_string_with_options(
        &model,
        WriteOptions { pretty: true, ..Default::default() },
    )?;
    # let _ = (compact, pretty);
    # Ok::<(), cityjson_lib::Error>(())
    ```

=== "C++"
    ```cpp
    model.cleanup();
    const auto text = model.serialize_document(cityjson_lib::WriteOptions{.pretty = true});
    ```

## Serialize A Feature Stream

=== "Rust"
    ```rust
    use std::fs::File;
    use std::io::BufReader;

    use cityjson_lib::json;

    let reader = BufReader::new(File::open("tests/data/v2_0/stream.city.jsonl")?);
    let models = json::read_feature_stream(reader)?
        .collect::<cityjson_lib::Result<Vec<_>>>()?;

    let mut out = Vec::new();
    json::write_feature_stream(&mut out, models)?;
    # let _ = out;
    # Ok::<(), cityjson_lib::Error>(())
    ```

=== "Python"
    ```python
    from cityjson_lib import CityModel, serialize_feature_stream_bytes

    feature_a = CityModel.parse_feature_bytes(open("feature-a.city.json", "rb").read())
    feature_b = CityModel.parse_feature_bytes(open("feature-b.city.json", "rb").read())
    payload = serialize_feature_stream_bytes([feature_a, feature_b])
    ```

=== "C++"
    ```cpp
    std::array<const cityjson_lib::Model*, 2> models{&feature_a, &feature_b};
    const auto payload = cityjson_lib::Model::serialize_feature_stream(models);
    ```

## Serialize To Arrow

Arrow I/O requires the `arrow` feature (`features = ["arrow"]` in `Cargo.toml`).
The Python and C++ bindings always include it.

=== "Rust"
    ```rust
    # #[cfg(feature = "arrow")]
    # {
    use cityjson_lib::{arrow, json};

    let model = json::from_file("amsterdam.city.json")?;
    let bytes = arrow::to_vec(&model)?;
    arrow::to_file("model.cjarrow", &model)?;
    # let _ = bytes;
    # }
    # Ok::<(), cityjson_lib::Error>(())
    ```

=== "Python"
    ```python
    arrow_bytes = model.serialize_arrow_bytes()
    model.serialize_parquet_file("model.cjarrow")  # via file path
    ```

=== "C++"
    ```cpp
    const auto arrow_bytes = model.serialize_arrow_bytes();
    model.serialize_parquet_file("model.cjarrow");
    ```

## Serialize To Parquet

Parquet I/O requires the `parquet` feature.
Two layouts are supported: a self-contained package file and a bare dataset
directory.

=== "Rust"
    ```rust
    # #[cfg(feature = "parquet")]
    # {
    use cityjson_lib::{json, parquet};

    let model = json::from_file("amsterdam.city.json")?;
    let manifest = parquet::to_file("city.cityjson-parquet", &model)?;
    let dataset_manifest = parquet::to_dir("city.dataset", &model)?;
    # let _ = (manifest, dataset_manifest);
    # }
    # Ok::<(), cityjson_lib::Error>(())
    ```

=== "Python"
    ```python
    model.serialize_parquet_file("city.cityjson-parquet")
    model.serialize_parquet_dataset_dir("city.dataset")
    ```

=== "C++"
    ```cpp
    model.serialize_parquet_file("city.cityjson-parquet");
    model.serialize_parquet_dataset_dir("city.dataset");
    ```

The wasm adapter remains work in progress and is intentionally omitted from the
published write guide.