Skip to content

gltf_to_cj

Scripts to load custom geometry (building shells and rooms) and export them to CityJSON after adding custom attributes.

Functions:

Name Description
full_building_from_gltf

Load and structure the building shell/parts/storeys/rooms from a glTF path based on the IDs of the objects.

load_units_from_csv

Load the units from the given CSV file and attach them to their geometry in the given CityJSONFile.

full_building_from_gltf(gltf_path)

Load and structure the building shell/parts/storeys/rooms from a glTF path based on the IDs of the objects.

Parameters:

Name Type Description Default
gltf_path pathlib.Path

The glTF path containing the building shell and rooms.

required

Returns:

Type Description
data_pipeline.cj_helpers.cj_objects.CityJSONFile

All the geometry formatted and structured properly.

Raises:

Type Description
RuntimeError

If the space ID has an unexpected format.

Source code in python/src/data_pipeline/cj_writing/gltf_to_cj.py
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
def full_building_from_gltf(gltf_path: Path) -> CityJSONFile:
    """
    Load and structure the building shell/parts/storeys/rooms from a glTF path based on the IDs of the objects.

    Parameters
    ----------
    gltf_path : Path
        The glTF path containing the building shell and rooms.

    Returns
    -------
    CityJSONFile
        All the geometry formatted and structured properly.

    Raises
    ------
    RuntimeError
        If the space ID has an unexpected format.
    """
    # Load the scene
    scene = trimesh.load_scene(gltf_path)

    graph: nx.DiGraph = scene.graph.to_networkx()  # type: ignore

    # Start from the root
    root_id = "world"

    # Get the scene collection
    all_objects_ids = list(graph.successors(root_id))
    all_objects_geoms: dict[str, list[Geometry]] = defaultdict(lambda: [])

    # Process all the objects and their geometries
    for obj_key in tqdm(all_objects_ids, desc="Process all geometries"):
        geom, name = _geom_and_name_from_scene_id(scene=scene, cj_key=obj_key)
        all_objects_geoms[name].append(geom)

    # # Add LoD 0 geometry to all objects that only have higher geometries
    # for object_geoms in tqdm(
    #     all_objects_geoms.values(), desc="Build missing LoD 0 geometries"
    # ):
    #     lods_to_geoms = {geom.lod: geom for geom in object_geoms}
    #     if len(lods_to_geoms) > 0 and not 0 in lods_to_geoms.keys():
    #         smallest_lod = min(lods_to_geoms.keys())
    #         base_mesh = lods_to_geoms[smallest_lod].to_trimesh()
    #         lod_0_mesh = flatten_trimesh(base_mesh)
    #         object_geoms.append(MultiSurface.from_mesh(lod=0, mesh=lod_0_mesh))

    logging.info("Add the missing hierarchy.")

    # Add the missing hierarchy without geometry
    current_objects = list(all_objects_geoms.keys())
    for space_id in current_objects:
        last_dot_position = space_id.rfind(".")
        while last_dot_position != -1:
            parent_space_id = space_id[:last_dot_position]
            if parent_space_id not in all_objects_geoms.keys():
                all_objects_geoms[parent_space_id] = []
            last_dot_position = parent_space_id.rfind(".")

    logging.info("Transform into actual CityJSON objects.")

    # Store the geometry into actual objects
    all_objects_cj: dict[str, CityJSONObjectSubclass] = {}
    for space_id, geoms in all_objects_geoms.items():
        hierarchy_level = space_id.count(".")
        if hierarchy_level == 0:
            obj_func = Building
        elif hierarchy_level == 1:
            obj_func = BuildingPart
        elif hierarchy_level == 2:
            obj_func = BuildingStorey
        elif hierarchy_level == 3:
            obj_func = BuildingRoom
        else:
            raise RuntimeError(
                f"Unexpected format for an object space id: '{space_id}'"
            )

        obj_key = obj_func.key_to_cj_key(key=space_id)

        all_objects_cj[space_id] = obj_func(
            cj_key=obj_key, space_id=space_id, geometries=geoms
        )

    logging.info("Apply the parent-child relationships.")

    # Apply the parent-child relationships
    for obj_name in all_objects_cj.keys():
        last_dot_position = obj_name.rfind(".")
        if last_dot_position != -1:
            obj_parent_name = obj_name[:last_dot_position]
            CityJSONObject.add_parent_child(
                parent=all_objects_cj[obj_parent_name], child=all_objects_cj[obj_name]
            )

    cj_file = CityJSONFile(
        scale=np.array([0.00001, 0.00001, 0.00001], dtype=np.float64),
        translate=np.array([0, 0, 0], dtype=np.float64),
    )
    cj_file.add_cityjson_objects(list(all_objects_cj.values()))

    logging.info("Done processing the full building.")

    return cj_file

load_units_from_csv(cj_file, csv_path, gltf_path)

Load the units from the given CSV file and attach them to their geometry in the given CityJSONFile. Also accepts an optional glTF path containing the geometry of the units that have geometry.

Parameters:

Name Type Description Default
cj_file data_pipeline.cj_helpers.cj_objects.CityJSONFile

The CityJSON objects, already containing the spaces.

required
csv_path pathlib.Path

The CSV path to the units attributes.

required
gltf_path pathlib.Path | None

The glTF path to the units geometry.

required

Raises:

Type Description
RuntimeError

If the root of cj_file is not a Building.

Source code in python/src/data_pipeline/cj_writing/gltf_to_cj.py
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
def load_units_from_csv(
    cj_file: CityJSONFile,
    csv_path: Path,
    gltf_path: Path | None,
) -> None:
    """
    Load the units from the given CSV file and attach them to their geometry in the given CityJSONFile.
    Also accepts an optional glTF path containing the geometry of the units that have geometry.

    Parameters
    ----------
    cj_file : CityJSONFile
        The CityJSON objects, already containing the spaces.
    csv_path : Path
        The CSV path to the units attributes.
    gltf_path : Path | None
        The glTF path to the units geometry.

    Raises
    ------
    RuntimeError
        If the root of `cj_file` is not a Building.
    """
    root_pos = cj_file.get_root_position()
    root = cj_file.city_objects[root_pos]
    if not isinstance(root, Building):
        raise RuntimeError(
            f"The root of the `cj_file` should be a Building, not a {type(root)}"
        )
    prefix = CityJSONSpace.key_to_prefix(key=root.space_id)
    unit_main_container = BuildingUnitObject(prefix=prefix)
    CityJSONObject.add_parent_child(parent=root, child=unit_main_container)

    # Read the units geometry
    if gltf_path is not None:
        scene = trimesh.load_scene(gltf_path)

    all_units: dict[str, list[BuildingUnit]] = defaultdict(lambda: [])

    # Process the CSV file to find all the units
    unit_to_spaces: dict[str, list[str]] = {}
    units_attributes_all = BdgUnitAttrReader(csv_path=csv_path)
    units_attributes_iterator = units_attributes_all.iterator()
    for cj_key, units_attributes in units_attributes_iterator:
        unit_code = units_attributes.code

        # Get the potential geometry
        unit_geometry = None
        if gltf_path is not None:
            unit_gltf = units_attributes.unit_gltf
            if unit_gltf is not None:
                unit_geometry = _get_unit_geometry_from_id(
                    scene=scene, cj_key=unit_gltf
                )

        current_units_same_code = len(all_units[unit_code])
        unit_id = BuildingUnit.unit_code_to_cj_key(
            code=unit_code, prefix=prefix, index=current_units_same_code
        )

        unit = BuildingUnit(
            cj_key=unit_id,
            unit_code=unit_code,
            unit_storeys=units_attributes.unit_storeys,
            geometry=unit_geometry,
            attributes=units_attributes.attributes,
            icon_position=units_attributes.icon_position,
        )
        unit_to_spaces[unit.id] = units_attributes.unit_spaces
        all_units[unit_code].append(unit)

    unit_containers: list[BuildingUnitContainer] = []
    for code, units in all_units.items():
        unit_container_id = BuildingUnitContainer.unit_code_to_cj_key(
            code=code, prefix=prefix
        )
        unit_container = BuildingUnitContainer(
            cj_key=unit_container_id, unit_code=code, attributes={}
        )
        unit_containers.append(unit_container)

        CityJSONObject.add_parent_child(
            parent=unit_main_container, child=unit_container
        )
        for unit in units:
            CityJSONObject.add_parent_child(parent=unit_container, child=unit)

    # Extract all the spaces from the given CityJSON file
    spaces_ids_to_pos = {}
    for i, cj_obj in enumerate(cj_file.city_objects):
        # We search for the actual spaces
        if isinstance(cj_obj, CityJSONSpaceSubclass):
            spaces_ids_to_pos[cj_obj.space_id] = i

    # Add the links from spaces to the units they belong in
    all_units_flattened = [unit for units in all_units.values() for unit in units]
    for unit in all_units_flattened:
        for space_id in unit_to_spaces[unit.id]:
            cj_file_pos = spaces_ids_to_pos[space_id]
            space = cj_file.city_objects[cj_file_pos]
            assert isinstance(space, CityJSONSpaceSubclass)
            CityJSONObject.add_unit_space(unit=unit, space=space)

    # Compute the icon positions of the units
    for unit in all_units_flattened:
        meshes: list[trimesh.Trimesh] = []
        for space_id in unit_to_spaces[unit.id]:
            cj_file_pos = spaces_ids_to_pos[space_id]
            space = cj_file.city_objects[cj_file_pos]
            assert isinstance(space, CityJSONSpaceSubclass)
            if space.geometries is None or len(space.geometries) == 0:
                continue
            best_idx = -1
            best_lod = -1
            for idx in range(0, len(space.geometries)):
                if space.geometries[idx].lod > best_lod:
                    best_idx = idx
                    best_lod = space.geometries[best_idx].lod
            if best_idx >= 0:
                meshes.append(space.geometries[best_idx].to_trimesh())

        if len(meshes) == 0:
            continue

        merged_mesh = merge_trimeshes(meshes=meshes, fix_geometry=False)
        icon_position = IconPosition.from_mesh(
            merged_mesh, z_offset=BuildingUnit.icon_z_offset
        )
        unit.set_icon(icon_position=icon_position)

    cj_file.add_cityjson_objects([unit_main_container])
    cj_file.add_cityjson_objects(unit_containers)
    cj_file.add_cityjson_objects(
        [unit for units in all_units.values() for unit in units]
    )