Skip to content

cj_objects

Classes to format and normalize CityJSON objects and files.

Classes:

Name Description
Building

Class to store Building objects.

BuildingPart

Class to store BuildingPart objects.

BuildingRoom

Class to store BuildingRoom objects.

BuildingStorey

Class to store BuildingStorey objects.

BuildingUnit

Class used to store a single BuildingUnit object.

BuildingUnitContainer

Class used to group together all the BuildingUnit objects per code.

BuildingUnitObject

Class used to represent the object, child of the Building, that will be the parent of all the BuildingUnitContainer objects.

CityJSONFile

Main CityJSON file handler, allowing to store CityJSON objects and write them to a file by checking the correctness of the hierarchy.

CityJSONObject

Abstract base class to handle CityJSON objects.

CityJSONSpace

Abstract base class to handle CityJSON objects in the room hierarchy (i.e. Building,

OutdoorObject

Class used to represent the object, root of the file, that will be the parent of all the OutdoorUnitContainer objects.

OutdoorUnit

Class used to store a single OutdoorUnit object.

OutdoorUnitContainer

Class used to group together all the OutdoorUnit objects per code.

Building

Bases: cj_helpers.cj_objects.CityJSONSpace

Class to store Building objects.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
class Building(CityJSONSpace):
    """
    Class to store Building objects.
    """

    type_name = "Building"
    icon_z_offset = 2

    def __init__(
        self,
        cj_key: str,
        space_id: str,
        attributes: dict[str, Any] | None = None,
        geometries: Sequence[Geometry] | None = None,
        icon_position: IconPosition | None = None,
    ) -> None:
        super().__init__(
            cj_key=cj_key,
            space_id=space_id,
            attributes=attributes,
            geometries=geometries,
            icon_position=icon_position,
        )

    def apply_attr(self, attr: BdgAttr, overwrite: bool) -> None:
        self.add_attributes(new_attributes=attr.attributes)
        if attr.icon_position is not None:
            self.set_icon(attr.icon_position, overwrite=overwrite)

BuildingPart

Bases: cj_helpers.cj_objects.CityJSONSpace

Class to store BuildingPart objects.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
class BuildingPart(CityJSONSpace):
    """
    Class to store BuildingPart objects.
    """

    type_name = "BuildingPart"
    icon_z_offset = 1

    def __init__(
        self,
        cj_key: str,
        space_id: str,
        attributes: dict[str, Any] | None = None,
        geometries: Sequence[Geometry] | None = None,
        icon_position: IconPosition | None = None,
    ) -> None:
        super().__init__(
            cj_key=cj_key,
            space_id=space_id,
            attributes=attributes,
            geometries=geometries,
            icon_position=icon_position,
        )

    def apply_attr(self, attr: BdgPartAttr, overwrite: bool) -> None:
        self.add_attributes(new_attributes=attr.attributes)
        if attr.icon_position is not None:
            self.set_icon(attr.icon_position, overwrite=overwrite)

BuildingRoom

Bases: cj_helpers.cj_objects.CityJSONSpace

Class to store BuildingRoom objects.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
class BuildingRoom(CityJSONSpace):
    """
    Class to store BuildingRoom objects.
    """

    type_name = "BuildingRoom"
    icon_z_offset = 0.5

    def __init__(
        self,
        cj_key: str,
        space_id: str,
        attributes: dict[str, Any] | None = None,
        geometries: Sequence[Geometry] | None = None,
        icon_position: IconPosition | None = None,
    ) -> None:
        super().__init__(
            cj_key=cj_key,
            space_id=space_id,
            attributes=attributes,
            geometries=geometries,
            icon_position=icon_position,
        )

    def apply_attr(self, attr: BdgRoomAttr, overwrite: bool) -> None:
        self.add_attributes(new_attributes=attr.attributes)
        if attr.icon_position is not None:
            self.set_icon(attr.icon_position, overwrite=overwrite)

BuildingStorey

Bases: cj_helpers.cj_objects.CityJSONSpace

Class to store BuildingStorey objects.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
class BuildingStorey(CityJSONSpace):
    """
    Class to store BuildingStorey objects.
    """

    type_name = "BuildingStorey"
    icon_z_offset = 0.5

    def __init__(
        self,
        cj_key: str,
        space_id: str,
        attributes: dict[str, Any] | None = None,
        geometries: Sequence[Geometry] | None = None,
        icon_position: IconPosition | None = None,
    ) -> None:
        super().__init__(
            cj_key=cj_key,
            space_id=space_id,
            attributes=attributes,
            geometries=geometries,
            icon_position=icon_position,
        )

    def apply_attr(self, attr: BdgStoreyAttr, overwrite: bool) -> None:
        self.add_attributes(new_attributes=attr.attributes)
        self.add_attributes(
            new_attributes={
                ARGUMENT_TO_NAME["storey_level"]: attr.storey_level,
                ARGUMENT_TO_NAME["storey_space_id"]: attr.storey_space_id,
            }
        )
        if attr.icon_position is not None:
            self.set_icon(attr.icon_position, overwrite=overwrite)

BuildingUnit

Bases: cj_helpers.cj_objects.CityJSONObject

Class used to store a single BuildingUnit object. BuildingUnit objects can: - be linked to a group of CityJSONSpace objects, - have their own geometry, - or none of them (only the icon).

Methods:

Name Description
unit_code_to_cj_key

Formats the CityJSON key (i.e. the id) based on the code of the units that it stores, and a number.

unit_code_to_code_instance

Generate an identifier based on the given index.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
class BuildingUnit(CityJSONObject):
    """
    Class used to store a single BuildingUnit object.
    BuildingUnit objects can:
    - be linked to a group of CityJSONSpace objects,
    - have their own geometry,
    - or none of them (only the icon).
    """

    type_name = "BuildingUnit"
    icon_z_offset = 0.5
    id_prefix = "BuildingUnit"

    def __init__(
        self,
        cj_key: str,
        unit_code: str,
        unit_storeys: list[str],
        geometry: Geometry | None = None,
        attributes: dict[str, Any] | None = None,
        icon_position: IconPosition | None = None,
    ) -> None:
        geometries = [geometry] if geometry is not None else None
        super().__init__(
            cj_key=cj_key,
            geometries=geometries,
            attributes=attributes,
            icon_position=icon_position,
        )
        self.unit_code = unit_code
        code_name = ARGUMENT_TO_NAME["code"]
        self.add_attributes({code_name: unit_code})

        self.unit_storeys = unit_storeys
        storeys_name = ARGUMENT_TO_NAME["unit_storeys"]
        self.add_attributes({storeys_name: unit_storeys})

        self.unit_spaces: set[str] = set()

    def _add_space(self, new_space_id: str) -> None:
        """
        Add a space that this unit contains.

        Parameters
        ----------
        new_space_id : str
            Id of the space.
        """
        self.unit_spaces.add(new_space_id)

    def get_cityobject(self) -> dict[str, Any]:
        unit_spaces_key = ARGUMENT_TO_NAME["unit_spaces"]
        self.add_attributes({unit_spaces_key: list(self.unit_spaces)})
        content_dict = super().get_cityobject()
        # Remove it from the attributes to ensure the object is unchanged
        self.attributes.pop(unit_spaces_key)
        return content_dict

    @classmethod
    def unit_code_to_code_instance(cls, code: str, index: int) -> str:
        """
        Generate an identifier based on the given index.
        The uniqueness of the identifier is not checked, but has to be ensured by providing a combination of inputs not used by another BuildingUnit.

        Parameters
        ----------
        code : str
            The usage code.
        index : int
            The index of the unit.

        Returns
        -------
        str
            A formatted identifier
        """
        index_str = str(index).replace(".", "_").replace("-", "_")
        code = code.replace(".", "_").replace("-", "_")
        return f"{code}@{index_str}"

    @classmethod
    def unit_code_to_cj_key(cls, code: str, prefix: str, index: int) -> str:
        """
        Formats the CityJSON key (i.e. the id) based on the code of the units that it stores, and a number.

        Parameters
        ----------
        code : str
            The usage code.
        prefix : str
            The building prefix.
        index : int
            The index of the unit.

        Returns
        -------
        str
            The id.
        """
        code_instance = cls.unit_code_to_code_instance(code=code, index=index)
        prefix = prefix.replace("-", "_")
        return f"{prefix}-{cls.type_name}-{cls.id_prefix}_{code_instance}"

    def apply_attr(self, attr: BdgUnitAttr, overwrite: bool) -> None:
        self.add_attributes(new_attributes=attr.attributes)
        if attr.icon_position is not None:
            self.set_icon(attr.icon_position, overwrite=overwrite)

unit_code_to_cj_key(code, prefix, index) classmethod

Formats the CityJSON key (i.e. the id) based on the code of the units that it stores, and a number.

Parameters:

Name Type Description Default
code str

The usage code.

required
prefix str

The building prefix.

required
index int

The index of the unit.

required

Returns:

Type Description
str

The id.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
@classmethod
def unit_code_to_cj_key(cls, code: str, prefix: str, index: int) -> str:
    """
    Formats the CityJSON key (i.e. the id) based on the code of the units that it stores, and a number.

    Parameters
    ----------
    code : str
        The usage code.
    prefix : str
        The building prefix.
    index : int
        The index of the unit.

    Returns
    -------
    str
        The id.
    """
    code_instance = cls.unit_code_to_code_instance(code=code, index=index)
    prefix = prefix.replace("-", "_")
    return f"{prefix}-{cls.type_name}-{cls.id_prefix}_{code_instance}"

unit_code_to_code_instance(code, index) classmethod

Generate an identifier based on the given index. The uniqueness of the identifier is not checked, but has to be ensured by providing a combination of inputs not used by another BuildingUnit.

Parameters:

Name Type Description Default
code str

The usage code.

required
index int

The index of the unit.

required

Returns:

Type Description
str

A formatted identifier

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
@classmethod
def unit_code_to_code_instance(cls, code: str, index: int) -> str:
    """
    Generate an identifier based on the given index.
    The uniqueness of the identifier is not checked, but has to be ensured by providing a combination of inputs not used by another BuildingUnit.

    Parameters
    ----------
    code : str
        The usage code.
    index : int
        The index of the unit.

    Returns
    -------
    str
        A formatted identifier
    """
    index_str = str(index).replace(".", "_").replace("-", "_")
    code = code.replace(".", "_").replace("-", "_")
    return f"{code}@{index_str}"

BuildingUnitContainer

Bases: cj_helpers.cj_objects.CityJSONObject

Class used to group together all the BuildingUnit objects per code.

Methods:

Name Description
unit_code_to_cj_key

Formats the CityJSON key (i.e. the id) based on the code of the units that it stores.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
class BuildingUnitContainer(CityJSONObject):
    """
    Class used to group together all the BuildingUnit objects per code.
    """

    type_name = "CityObjectGroup"
    main_parent_code = ""
    id_prefix = "BuildingUnitContainer"

    def __init__(
        self,
        cj_key: str,
        unit_code: str,
        attributes: dict[str, Any] | None = None,
        icon_position: IconPosition | None = None,
    ) -> None:
        super().__init__(
            cj_key=cj_key,
            attributes=attributes,
            geometries=None,
            icon_position=icon_position,
        )
        self.unit_code = unit_code
        code_name = ARGUMENT_TO_NAME["code"]
        self.add_attributes({code_name: unit_code})

    @classmethod
    def unit_code_to_cj_key(cls, code: str, prefix: str) -> str:
        """
        Formats the CityJSON key (i.e. the id) based on the code of the units that it stores.

        Parameters
        ----------
        code : str
            The usage code.
        prefix : str
            The building prefix.

        Returns
        -------
        str
            The id.
        """
        code = code.replace(".", "_").replace("-", "_")
        prefix = prefix.replace("-", "_")
        return f"{prefix}-{cls.type_name}-{cls.id_prefix}_{code}"

    def apply_attr(self, attr: BdgAttr, overwrite: bool) -> None:
        raise NotImplementedError()

unit_code_to_cj_key(code, prefix) classmethod

Formats the CityJSON key (i.e. the id) based on the code of the units that it stores.

Parameters:

Name Type Description Default
code str

The usage code.

required
prefix str

The building prefix.

required

Returns:

Type Description
str

The id.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
@classmethod
def unit_code_to_cj_key(cls, code: str, prefix: str) -> str:
    """
    Formats the CityJSON key (i.e. the id) based on the code of the units that it stores.

    Parameters
    ----------
    code : str
        The usage code.
    prefix : str
        The building prefix.

    Returns
    -------
    str
        The id.
    """
    code = code.replace(".", "_").replace("-", "_")
    prefix = prefix.replace("-", "_")
    return f"{prefix}-{cls.type_name}-{cls.id_prefix}_{code}"

BuildingUnitObject

Bases: cj_helpers.cj_objects.CityJSONObject

Class used to represent the object, child of the Building, that will be the parent of all the BuildingUnitContainer objects.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
class BuildingUnitObject(CityJSONObject):
    """
    Class used to represent the object, child of the Building, that will be the parent of all the BuildingUnitContainer objects.
    """

    type_name = "CityObjectGroup"
    icon_z_offset = 2
    id_prefix = "BuildingUnitObject"

    def __init__(self, prefix: str) -> None:
        super().__init__(
            cj_key=f"{prefix}-{self.type_name}-{self.id_prefix}",
            attributes={},
            geometries=None,
            icon_position=None,
        )

    def apply_attr(self, attr: Attr, overwrite: bool) -> None:
        raise NotImplementedError()

CityJSONFile

Main CityJSON file handler, allowing to store CityJSON objects and write them to a file by checking the correctness of the hierarchy.

Methods:

Name Description
__init__

Initialise the CityJSON file handler, with the properties to write the geometry.

add_cityjson_objects

Add a list of CityJSON objects to the file.

check_objects_hierarchy

Check the hierarchy of the objects to ensure that all parent/child relationships are stored in both directions.

get_root_position

Return the index in self.city_objects of the root of the file.

to_json

Formats all the objects into a correct CityJSON file, and dumps it into a string that can be directly written to a file.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
 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
class CityJSONFile:
    """
    Main CityJSON file handler, allowing to store CityJSON objects and write them to a file by checking the correctness of the hierarchy.
    """

    def __init__(
        self, scale: NDArray[np.float64], translate: NDArray[np.float64] | None
    ) -> None:
        """
        Initialise the CityJSON file handler, with the properties to write the geometry.

        Parameters
        ----------
        scale : NDArray[np.float64]
            Array of shape (3,) containing the scale used to store the vertices.
        translate : NDArray[np.float64] | None
            Array of shape (3,) containing the translation used to store the vertices.
            If None, it will be computed automatically.

        Raises
        ------
        RuntimeError
            If `scale` has an incorrect shape.
        RuntimeError
            If `translate` has an incorrect shape.
        """
        self.city_objects: list[CityJSONObjectSubclass] = []

        # Transform
        scale_shape = (3,)
        if scale.shape != scale_shape:
            raise RuntimeError(
                f"The given scale has shape {scale.shape} instead of {scale_shape}."
            )
        translate_shape = (3,)
        if translate is not None and translate.shape != translate_shape:
            raise RuntimeError(
                f"The given translate has shape {translate.shape} instead of {translate_shape}."
            )

        self.scale = scale
        self.translate = translate

    def check_objects_hierarchy(self, n_components: int | None = None) -> None:
        """
        Check the hierarchy of the objects to ensure that all parent/child relationships are stored in both directions.
        Also allows to check that the number of roots (objects without parents) corresponds to what is expected.

        Parameters
        ----------
        n_components : int | None, optional
            The expected number of roots.
            If None, uses the number of Building objects.
            By default None.

        Raises
        ------
        RuntimeError
            If a node is missing in the hierarchy.
        RuntimeError
            If a cycle is detected in the hierarchy.
        RuntimeError
            If the number of connected components / number of roots is different from the expected value.
        RuntimeError
            If an edge does not go both ways.
        """
        G = nx.DiGraph()

        # First add every node
        for obj in self.city_objects:
            G.add_node(obj.id)

        # Then add directed edges parent -> child
        for obj in self.city_objects:
            for child_id in obj.children_ids:
                G.add_edge(obj.id, child_id)

        # Look for missing nodes
        missing = set(G.edges()) - {
            (u, v) for u, v in G.edges() if G.has_node(u) and G.has_node(v)
        }
        if missing:
            raise RuntimeError(f"Edges reference unknown nodes: {missing}")

        # Cycle detection
        if not nx.is_directed_acyclic_graph(G):
            cycles = list(nx.simple_cycles(G))
            raise RuntimeError(f"Cycle(s) detected - e.g. {cycles[:1][0]}")

        # Connectivity (ignore direction)
        if not nx.is_weakly_connected(G):
            # Find the separate components for a nicer error message
            comps = list(nx.weakly_connected_components(G))
            # The number of expected components should be the number of buildings
            if n_components is None:
                expected_components = sum(
                    map(
                        lambda obj: isinstance(obj, Building),
                        self.city_objects,
                    )
                )
            else:
                expected_components = n_components
            if len(comps) != expected_components:
                raise RuntimeError(
                    f"The number of connected components is {len(comps)} (expected {expected_components})"
                )

        # Ensure every edge is mirrored in the opposite list
        for obj in self.city_objects:
            if obj.parent_id is not None:
                G.add_edge(obj.id, obj.parent_id)

        for u, v, d in G.edges(data=True):
            if not G.has_edge(v, u):
                raise RuntimeError(
                    f"The edge between {u} and {v} doesn't go both ways."
                )

    def to_json(self) -> str:
        """
        Formats all the objects into a correct CityJSON file, and dumps it into a string that can be directly written to a file.

        Returns
        -------
        str
            The formatted CityJSON file.
        """
        full_object = {}
        full_object["type"] = "CityJSON"
        full_object["version"] = "2.0"
        full_object["metadata"] = {
            "referenceSystem": "https://www.opengis.net/def/crs/EPSG/0/7415"
        }

        # Check objectw with and without the geometry
        geometries_indices: list[list[int] | None] = []
        next_index = 0
        unprocessed_geoms = []
        for obj in self.city_objects:
            if len(obj.geometries) == 0:
                geometries_indices.append(None)
            else:
                indices = []
                for geometry in obj.geometries:
                    unprocessed_geoms.append(geometry)
                    indices.append(next_index)
                    next_index += 1
                geometries_indices.append(indices)

        # Process the geometry
        geoms_formatter = CityJSONGeometries(unprocessed_geoms)
        self.translate = geoms_formatter.get_optimal_translate(scale=self.scale)
        list_dict_geoms = geoms_formatter.get_geometry_cj()
        vertices = geoms_formatter.get_vertices_cj(
            scale=self.scale, translate=self.translate
        )

        # Write the CityObjects
        cityobjects = {}
        for obj, geom_indices in zip(self.city_objects, geometries_indices):
            cityobject = obj.get_cityobject()
            if geom_indices is not None:
                cityobject["geometry"] = [list_dict_geoms[idx] for idx in geom_indices]
            cityobjects[obj.id] = cityobject
        full_object["CityObjects"] = cityobjects

        # Write the transform
        full_object["transform"] = {
            "scale": self.scale.tolist(),
            "translate": self.translate.tolist(),
        }

        full_object["vertices"] = vertices

        return json.dumps(full_object)

    def add_cityjson_objects(
        self, cj_objects: Sequence[CityJSONObjectSubclass]
    ) -> None:
        """
        Add a list of CityJSON objects to the file.

        Parameters
        ----------
        cj_objects : Sequence[CityJSONObjectSubclass]
            List of CityJSON objects to add.
        """
        self.city_objects.extend(cj_objects)

    def get_root_position(self) -> int:
        """
        Return the index in `self.city_objects` of the root of the file.
        Only works if there is exactly one root.

        Returns
        -------
        int
            The index of the root in `self.city_objects`.

        Raises
        ------
        RuntimeError
            If there is not exactly one root.
        """
        roots_ids: list[int] = []
        for i, cj_obj in enumerate(self.city_objects):
            if cj_obj.parent_id == None:
                roots_ids.append(i)
        if len(roots_ids) != 1:
            raise RuntimeError(
                f"The current CityJSONFile instance has {len(roots_ids)} roots, but 1 was expected."
            )
        return roots_ids[0]

__init__(scale, translate)

Initialise the CityJSON file handler, with the properties to write the geometry.

Parameters:

Name Type Description Default
scale numpy.typing.NDArray[numpy.float64]

Array of shape (3,) containing the scale used to store the vertices.

required
translate numpy.typing.NDArray[numpy.float64] | None

Array of shape (3,) containing the translation used to store the vertices. If None, it will be computed automatically.

required

Raises:

Type Description
RuntimeError

If scale has an incorrect shape.

RuntimeError

If translate has an incorrect shape.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
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
def __init__(
    self, scale: NDArray[np.float64], translate: NDArray[np.float64] | None
) -> None:
    """
    Initialise the CityJSON file handler, with the properties to write the geometry.

    Parameters
    ----------
    scale : NDArray[np.float64]
        Array of shape (3,) containing the scale used to store the vertices.
    translate : NDArray[np.float64] | None
        Array of shape (3,) containing the translation used to store the vertices.
        If None, it will be computed automatically.

    Raises
    ------
    RuntimeError
        If `scale` has an incorrect shape.
    RuntimeError
        If `translate` has an incorrect shape.
    """
    self.city_objects: list[CityJSONObjectSubclass] = []

    # Transform
    scale_shape = (3,)
    if scale.shape != scale_shape:
        raise RuntimeError(
            f"The given scale has shape {scale.shape} instead of {scale_shape}."
        )
    translate_shape = (3,)
    if translate is not None and translate.shape != translate_shape:
        raise RuntimeError(
            f"The given translate has shape {translate.shape} instead of {translate_shape}."
        )

    self.scale = scale
    self.translate = translate

add_cityjson_objects(cj_objects)

Add a list of CityJSON objects to the file.

Parameters:

Name Type Description Default
cj_objects collections.abc.Sequence[cj_helpers.cj_objects.CityJSONObjectSubclass]

List of CityJSON objects to add.

required
Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
205
206
207
208
209
210
211
212
213
214
215
216
def add_cityjson_objects(
    self, cj_objects: Sequence[CityJSONObjectSubclass]
) -> None:
    """
    Add a list of CityJSON objects to the file.

    Parameters
    ----------
    cj_objects : Sequence[CityJSONObjectSubclass]
        List of CityJSON objects to add.
    """
    self.city_objects.extend(cj_objects)

check_objects_hierarchy(n_components=None)

Check the hierarchy of the objects to ensure that all parent/child relationships are stored in both directions. Also allows to check that the number of roots (objects without parents) corresponds to what is expected.

Parameters:

Name Type Description Default
n_components int | None

The expected number of roots. If None, uses the number of Building objects. By default None.

None

Raises:

Type Description
RuntimeError

If a node is missing in the hierarchy.

RuntimeError

If a cycle is detected in the hierarchy.

RuntimeError

If the number of connected components / number of roots is different from the expected value.

RuntimeError

If an edge does not go both ways.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
 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
def check_objects_hierarchy(self, n_components: int | None = None) -> None:
    """
    Check the hierarchy of the objects to ensure that all parent/child relationships are stored in both directions.
    Also allows to check that the number of roots (objects without parents) corresponds to what is expected.

    Parameters
    ----------
    n_components : int | None, optional
        The expected number of roots.
        If None, uses the number of Building objects.
        By default None.

    Raises
    ------
    RuntimeError
        If a node is missing in the hierarchy.
    RuntimeError
        If a cycle is detected in the hierarchy.
    RuntimeError
        If the number of connected components / number of roots is different from the expected value.
    RuntimeError
        If an edge does not go both ways.
    """
    G = nx.DiGraph()

    # First add every node
    for obj in self.city_objects:
        G.add_node(obj.id)

    # Then add directed edges parent -> child
    for obj in self.city_objects:
        for child_id in obj.children_ids:
            G.add_edge(obj.id, child_id)

    # Look for missing nodes
    missing = set(G.edges()) - {
        (u, v) for u, v in G.edges() if G.has_node(u) and G.has_node(v)
    }
    if missing:
        raise RuntimeError(f"Edges reference unknown nodes: {missing}")

    # Cycle detection
    if not nx.is_directed_acyclic_graph(G):
        cycles = list(nx.simple_cycles(G))
        raise RuntimeError(f"Cycle(s) detected - e.g. {cycles[:1][0]}")

    # Connectivity (ignore direction)
    if not nx.is_weakly_connected(G):
        # Find the separate components for a nicer error message
        comps = list(nx.weakly_connected_components(G))
        # The number of expected components should be the number of buildings
        if n_components is None:
            expected_components = sum(
                map(
                    lambda obj: isinstance(obj, Building),
                    self.city_objects,
                )
            )
        else:
            expected_components = n_components
        if len(comps) != expected_components:
            raise RuntimeError(
                f"The number of connected components is {len(comps)} (expected {expected_components})"
            )

    # Ensure every edge is mirrored in the opposite list
    for obj in self.city_objects:
        if obj.parent_id is not None:
            G.add_edge(obj.id, obj.parent_id)

    for u, v, d in G.edges(data=True):
        if not G.has_edge(v, u):
            raise RuntimeError(
                f"The edge between {u} and {v} doesn't go both ways."
            )

get_root_position()

Return the index in self.city_objects of the root of the file. Only works if there is exactly one root.

Returns:

Type Description
int

The index of the root in self.city_objects.

Raises:

Type Description
RuntimeError

If there is not exactly one root.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def get_root_position(self) -> int:
    """
    Return the index in `self.city_objects` of the root of the file.
    Only works if there is exactly one root.

    Returns
    -------
    int
        The index of the root in `self.city_objects`.

    Raises
    ------
    RuntimeError
        If there is not exactly one root.
    """
    roots_ids: list[int] = []
    for i, cj_obj in enumerate(self.city_objects):
        if cj_obj.parent_id == None:
            roots_ids.append(i)
    if len(roots_ids) != 1:
        raise RuntimeError(
            f"The current CityJSONFile instance has {len(roots_ids)} roots, but 1 was expected."
        )
    return roots_ids[0]

to_json()

Formats all the objects into a correct CityJSON file, and dumps it into a string that can be directly written to a file.

Returns:

Type Description
str

The formatted CityJSON file.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
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
def to_json(self) -> str:
    """
    Formats all the objects into a correct CityJSON file, and dumps it into a string that can be directly written to a file.

    Returns
    -------
    str
        The formatted CityJSON file.
    """
    full_object = {}
    full_object["type"] = "CityJSON"
    full_object["version"] = "2.0"
    full_object["metadata"] = {
        "referenceSystem": "https://www.opengis.net/def/crs/EPSG/0/7415"
    }

    # Check objectw with and without the geometry
    geometries_indices: list[list[int] | None] = []
    next_index = 0
    unprocessed_geoms = []
    for obj in self.city_objects:
        if len(obj.geometries) == 0:
            geometries_indices.append(None)
        else:
            indices = []
            for geometry in obj.geometries:
                unprocessed_geoms.append(geometry)
                indices.append(next_index)
                next_index += 1
            geometries_indices.append(indices)

    # Process the geometry
    geoms_formatter = CityJSONGeometries(unprocessed_geoms)
    self.translate = geoms_formatter.get_optimal_translate(scale=self.scale)
    list_dict_geoms = geoms_formatter.get_geometry_cj()
    vertices = geoms_formatter.get_vertices_cj(
        scale=self.scale, translate=self.translate
    )

    # Write the CityObjects
    cityobjects = {}
    for obj, geom_indices in zip(self.city_objects, geometries_indices):
        cityobject = obj.get_cityobject()
        if geom_indices is not None:
            cityobject["geometry"] = [list_dict_geoms[idx] for idx in geom_indices]
        cityobjects[obj.id] = cityobject
    full_object["CityObjects"] = cityobjects

    # Write the transform
    full_object["transform"] = {
        "scale": self.scale.tolist(),
        "translate": self.translate.tolist(),
    }

    full_object["vertices"] = vertices

    return json.dumps(full_object)

CityJSONObject

Bases: abc.ABC

Abstract base class to handle CityJSON objects.

Methods:

Name Description
__init__

Initialise a CityJSON object.

add_attributes

Add the given attributes to the dictionary of attributes.

add_parent_child

Add a parent-child relationship.

add_unit_space

Add a unit-space relationship.

get_cityobject

Returns the object formatted like a CityJSON object, but without the geometry.

set_icon

Set the icon position, potentially overwriting it.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
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
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
class CityJSONObject(ABC):
    """
    Abstract base class to handle CityJSON objects.
    """

    type_name = "CityJSONObject"
    icon_z_offset = 2

    def __init__(
        self,
        cj_key: str,
        attributes: dict[str, Any] | None = None,
        geometries: Sequence[Geometry] | None = None,
        icon_position: IconPosition | None = None,
    ) -> None:
        """
        Initialise a CityJSON object.

        Parameters
        ----------
        cj_key : str
            Unique key of the object.
        attributes : dict[str, Any] | None, optional
            Dictionary of attributes.
            If None, initialised empty.
            By default None.
        geometries : Sequence[Geometry] | None, optional
            List of geometries with their associated LoDs.
            If None, initialised empty.
            By default None.
        icon_position : IconPosition | None, optional
            Position of the icon.
            If None, initialised based on the geometry of highest LoD, or as None if there is no geometry.
            By default None.

        Raises
        ------
        RuntimeError
            If `attributes` is not None or a dictionary.
        RuntimeError
            If any of the keys of the dictionary is not a string.
        """
        if attributes is not None:
            if not isinstance(attributes, dict):
                raise RuntimeError(
                    f"The attributes of a CityJSONObject should be a dictionary or None."
                )
            for key in attributes.keys():
                if not isinstance(key, str):
                    raise RuntimeError(
                        f"The attributes of a CityJSONObject should have strings as keys."
                    )

        self.id = cj_key
        self.attributes = attributes if attributes is not None else {}
        self.parent_id = None
        self.children_ids: set[str] = set()
        self.geometries = list(geometries if geometries is not None else [])

        # Add the key to the attributes
        self.add_attributes({"key": self.id})

        # Compute the icon
        self._process_icon(icon_position)

    def _process_icon(self, icon_position: IconPosition | None = None) -> None:
        """
        Add the icon position it to the attributes.
        If no icon position is given, it is computed based on the geometry of highest LoD.

        Parameters
        ----------
        icon_position : IconPosition | None, optional
            The position of the icon on the map.
            By default None.
        """
        if (
            icon_position is None
            and self.geometries is not None
            and len(self.geometries) > 0
        ):
            # Use the highest lod to create a point
            best_idx = 0
            for idx in range(1, len(self.geometries)):
                if self.geometries[idx].lod > self.geometries[best_idx].lod:
                    best_idx = idx

            icon_position = IconPosition.from_mesh(
                self.geometries[best_idx].to_trimesh(), z_offset=self.icon_z_offset
            )
        self.icon_position = icon_position
        if self.icon_position is not None:
            icon_position_name = ARGUMENT_TO_NAME["icon_position"]
            self.add_attributes(
                {
                    icon_position_name: [
                        self.icon_position.x,
                        self.icon_position.y,
                        self.icon_position.z,
                    ]
                }
            )

    def set_icon(self, icon_position: IconPosition, overwrite: bool = False) -> None:
        """
        Set the icon position, potentially overwriting it.

        Parameters
        ----------
        icon_position : IconPosition
            Position of the icon.
        overwrite : bool, optional
            Whether to overwrite the value if a correct value (not None) is already stored.
            By default False.
        """
        self.icon_position = icon_position
        icon_position_name = ARGUMENT_TO_NAME["icon_position"]
        self.add_attributes(
            {
                icon_position_name: [
                    self.icon_position.x,
                    self.icon_position.y,
                    self.icon_position.z,
                ]
            },
            overwrite=overwrite,
        )

    def __repr__(self) -> str:
        return f"{type(self)}(id={self.id}, parent_id={self.parent_id}, children_ids={self.children_ids})"

    def _set_parent(self, parent_id: str, replace: bool = False) -> None:
        """
        Set the parent of the current object.

        Warning
        -------
        Should not be called directly, but instead indirectly with `CityJSONObject.add_parent_child` to avoid one-way relations.

        Parameters
        ----------
        parent_id : str
            The id of the parent CityJSONObject.
        replace : bool, optional
            Whether to overwrite the value if a correct value (not None) is already stored.
            By default False.

        Raises
        ------
        RuntimeError
            If `parent_id` already contains a value and `replace` was not set to True.
        """
        if self.parent_id is not None and not replace:
            raise RuntimeError(
                "Parent id is already set. To replace it, set `replace` to True."
            )
        self.parent_id = parent_id

    def _add_child(self, child_id: str) -> None:
        """
        Add the given object id to the set of children of the current object.
        Does nothing if the id was already in the list.

        Warning
        -------
        Should not be called directly, but instead indirectly with CityJSONObject.add_parent_child to avoid one-way relations.

        Parameters
        ----------
        child_id : str
            The id of the child CityJSONObject.
        """
        self.children_ids.add(child_id)

    def get_cityobject(self) -> dict[str, Any]:
        """
        Returns the object formatted like a CityJSON object, but without the geometry.

        Returns
        -------
        dict[str, Any]
            Dictionary following CityJSON format for a CityObject, but **without** the geometry.
        """
        content_dict = {}
        content_dict["type"] = type(self).type_name
        if self.parent_id is not None:
            content_dict["parents"] = [self.parent_id]
        if len(self.children_ids) > 0:
            content_dict["children"] = list(self.children_ids)
        content_dict["attributes"] = self.attributes.copy()
        return content_dict

    def add_attributes(
        self, new_attributes: dict[str, Any], overwrite: bool = False
    ) -> None:
        """
        Add the given attributes to the dictionary of attributes.

        Parameters
        ----------
        new_attributes : dict[str, Any]
            The new attributes to add.
        overwrite : bool, optional
            Whether an attribute with the same key as an already existing attribute should overwrite the old one.
            By default False.

        Raises
        ------
        RuntimeError
            If an attribute key already existed and `overwrite` was not set to True.
        """
        for key, value in new_attributes.items():
            if key in self.attributes and not overwrite:
                raise RuntimeError(
                    f"The key '{key}' is already in the attributes. Set `overwrite` to True to overwrite."
                )
            self.attributes[key] = value

    @abstractmethod
    def apply_attr(self, attr: Attr, overwrite: bool) -> None:
        raise NotImplementedError()

    @classmethod
    def add_parent_child(
        cls, parent: CityJSONObjectSubclass, child: CityJSONObjectSubclass
    ) -> None:
        """
        Add a parent-child relationship.
        Adds the relationship to both objects.

        Parameters
        ----------
        parent : CityJSONObjectSubclass
            CityJSON object that is the parent of `child`.
        child : CityJSONObjectSubclass
            CityJSON object that is the child of `parent`.
        """
        parent._add_child(child.id)
        child._set_parent(parent.id)

    @classmethod
    def add_unit_space(cls, unit: BuildingUnit, space: CityJSONSpace) -> None:
        """
        Add a unit-space relationship.
        Adds the relationship to both objects.

        Parameters
        ----------
        unit : BuildingUnit
            BuildingUnit that contains the `space`.
        space : CityJSONSpace
            CityJSONSpace that is part of the `unit`.
        """
        unit._add_space(space.id)
        space._add_unit(unit.id)

__init__(cj_key, attributes=None, geometries=None, icon_position=None)

Initialise a CityJSON object.

Parameters:

Name Type Description Default
cj_key str

Unique key of the object.

required
attributes dict[str, typing.Any] | None

Dictionary of attributes. If None, initialised empty. By default None.

None
geometries collections.abc.Sequence[data_pipeline.cj_helpers.cj_geometry.Geometry] | None

List of geometries with their associated LoDs. If None, initialised empty. By default None.

None
icon_position data_pipeline.utils.icon_positions.IconPosition | None

Position of the icon. If None, initialised based on the geometry of highest LoD, or as None if there is no geometry. By default None.

None

Raises:

Type Description
RuntimeError

If attributes is not None or a dictionary.

RuntimeError

If any of the keys of the dictionary is not a string.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
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
def __init__(
    self,
    cj_key: str,
    attributes: dict[str, Any] | None = None,
    geometries: Sequence[Geometry] | None = None,
    icon_position: IconPosition | None = None,
) -> None:
    """
    Initialise a CityJSON object.

    Parameters
    ----------
    cj_key : str
        Unique key of the object.
    attributes : dict[str, Any] | None, optional
        Dictionary of attributes.
        If None, initialised empty.
        By default None.
    geometries : Sequence[Geometry] | None, optional
        List of geometries with their associated LoDs.
        If None, initialised empty.
        By default None.
    icon_position : IconPosition | None, optional
        Position of the icon.
        If None, initialised based on the geometry of highest LoD, or as None if there is no geometry.
        By default None.

    Raises
    ------
    RuntimeError
        If `attributes` is not None or a dictionary.
    RuntimeError
        If any of the keys of the dictionary is not a string.
    """
    if attributes is not None:
        if not isinstance(attributes, dict):
            raise RuntimeError(
                f"The attributes of a CityJSONObject should be a dictionary or None."
            )
        for key in attributes.keys():
            if not isinstance(key, str):
                raise RuntimeError(
                    f"The attributes of a CityJSONObject should have strings as keys."
                )

    self.id = cj_key
    self.attributes = attributes if attributes is not None else {}
    self.parent_id = None
    self.children_ids: set[str] = set()
    self.geometries = list(geometries if geometries is not None else [])

    # Add the key to the attributes
    self.add_attributes({"key": self.id})

    # Compute the icon
    self._process_icon(icon_position)

add_attributes(new_attributes, overwrite=False)

Add the given attributes to the dictionary of attributes.

Parameters:

Name Type Description Default
new_attributes dict[str, typing.Any]

The new attributes to add.

required
overwrite bool

Whether an attribute with the same key as an already existing attribute should overwrite the old one. By default False.

False

Raises:

Type Description
RuntimeError

If an attribute key already existed and overwrite was not set to True.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
def add_attributes(
    self, new_attributes: dict[str, Any], overwrite: bool = False
) -> None:
    """
    Add the given attributes to the dictionary of attributes.

    Parameters
    ----------
    new_attributes : dict[str, Any]
        The new attributes to add.
    overwrite : bool, optional
        Whether an attribute with the same key as an already existing attribute should overwrite the old one.
        By default False.

    Raises
    ------
    RuntimeError
        If an attribute key already existed and `overwrite` was not set to True.
    """
    for key, value in new_attributes.items():
        if key in self.attributes and not overwrite:
            raise RuntimeError(
                f"The key '{key}' is already in the attributes. Set `overwrite` to True to overwrite."
            )
        self.attributes[key] = value

add_parent_child(parent, child) classmethod

Add a parent-child relationship. Adds the relationship to both objects.

Parameters:

Name Type Description Default
parent cj_helpers.cj_objects.CityJSONObjectSubclass

CityJSON object that is the parent of child.

required
child cj_helpers.cj_objects.CityJSONObjectSubclass

CityJSON object that is the child of parent.

required
Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
@classmethod
def add_parent_child(
    cls, parent: CityJSONObjectSubclass, child: CityJSONObjectSubclass
) -> None:
    """
    Add a parent-child relationship.
    Adds the relationship to both objects.

    Parameters
    ----------
    parent : CityJSONObjectSubclass
        CityJSON object that is the parent of `child`.
    child : CityJSONObjectSubclass
        CityJSON object that is the child of `parent`.
    """
    parent._add_child(child.id)
    child._set_parent(parent.id)

add_unit_space(unit, space) classmethod

Add a unit-space relationship. Adds the relationship to both objects.

Parameters:

Name Type Description Default
unit cj_helpers.cj_objects.BuildingUnit

BuildingUnit that contains the space.

required
space cj_helpers.cj_objects.CityJSONSpace

CityJSONSpace that is part of the unit.

required
Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
@classmethod
def add_unit_space(cls, unit: BuildingUnit, space: CityJSONSpace) -> None:
    """
    Add a unit-space relationship.
    Adds the relationship to both objects.

    Parameters
    ----------
    unit : BuildingUnit
        BuildingUnit that contains the `space`.
    space : CityJSONSpace
        CityJSONSpace that is part of the `unit`.
    """
    unit._add_space(space.id)
    space._add_unit(unit.id)

get_cityobject()

Returns the object formatted like a CityJSON object, but without the geometry.

Returns:

Type Description
dict[str, typing.Any]

Dictionary following CityJSON format for a CityObject, but without the geometry.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
def get_cityobject(self) -> dict[str, Any]:
    """
    Returns the object formatted like a CityJSON object, but without the geometry.

    Returns
    -------
    dict[str, Any]
        Dictionary following CityJSON format for a CityObject, but **without** the geometry.
    """
    content_dict = {}
    content_dict["type"] = type(self).type_name
    if self.parent_id is not None:
        content_dict["parents"] = [self.parent_id]
    if len(self.children_ids) > 0:
        content_dict["children"] = list(self.children_ids)
    content_dict["attributes"] = self.attributes.copy()
    return content_dict

set_icon(icon_position, overwrite=False)

Set the icon position, potentially overwriting it.

Parameters:

Name Type Description Default
icon_position data_pipeline.utils.icon_positions.IconPosition

Position of the icon.

required
overwrite bool

Whether to overwrite the value if a correct value (not None) is already stored. By default False.

False
Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
def set_icon(self, icon_position: IconPosition, overwrite: bool = False) -> None:
    """
    Set the icon position, potentially overwriting it.

    Parameters
    ----------
    icon_position : IconPosition
        Position of the icon.
    overwrite : bool, optional
        Whether to overwrite the value if a correct value (not None) is already stored.
        By default False.
    """
    self.icon_position = icon_position
    icon_position_name = ARGUMENT_TO_NAME["icon_position"]
    self.add_attributes(
        {
            icon_position_name: [
                self.icon_position.x,
                self.icon_position.y,
                self.icon_position.z,
            ]
        },
        overwrite=overwrite,
    )

CityJSONSpace

Bases: cj_helpers.cj_objects.CityJSONObject

Abstract base class to handle CityJSON objects in the room hierarchy (i.e. Building, BuildingPart, BuildingStorey and BuildingRoom).

Methods:

Name Description
key_to_cj_key

Formats the CityJSON key (i.e. the id) based on the key (the space id).

key_to_prefix

Formats the prefix of the object based on its key.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
class CityJSONSpace(CityJSONObject):
    """
    Abstract base class to handle CityJSON objects in the room hierarchy (i.e. Building,
    BuildingPart, BuildingStorey and BuildingRoom).
    """

    type_name = "CityJSONSpace"

    def __init__(
        self,
        cj_key: str,
        space_id: str,
        attributes: dict[str, Any] | None = None,
        geometries: Sequence[Geometry] | None = None,
        icon_position: IconPosition | None = None,
    ) -> None:

        super().__init__(
            cj_key=cj_key,
            attributes=attributes,
            geometries=geometries,
            icon_position=icon_position,
        )
        self.cj_key = cj_key
        self.space_id = space_id
        space_id_key = ARGUMENT_TO_NAME["space_id"]
        self.add_attributes({space_id_key: space_id})
        self.parent_units: set[str] = set()

    def _add_unit(self, new_unit_id: str) -> None:
        """
        Add a unit that this space is part of.

        Parameters
        ----------
        new_unit_id : str
            Id of the unit.
        """
        self.parent_units.add(new_unit_id)

    def get_cityobject(self) -> dict[str, Any]:
        parent_units_key = ARGUMENT_TO_NAME["parent_units"]
        self.add_attributes({parent_units_key: list(self.parent_units)})
        content_dict = super().get_cityobject()
        # Remove it from the attributes to ensure the object is unchanged
        self.attributes.pop(parent_units_key)
        return content_dict

    @classmethod
    def key_to_prefix(cls, key: str) -> str:
        """
        Formats the prefix of the object based on its key.

        Parameters
        ----------
        key : str
            The key of the object.

        Returns
        -------
        str
            The prefix of the object.
        """
        return f"Building_{key.split(".")[0]}"

    @classmethod
    def key_to_cj_key(cls, key: str) -> str:
        """
        Formats the CityJSON key (i.e. the id) based on the key (the space id).

        Parameters
        ----------
        key : str
            The key of the object.

        Returns
        -------
        str
            The id.
        """
        prefix = cls.key_to_prefix(key=key)
        key = key.replace(".", "_").replace("-", "_")
        return f"{prefix}-{cls.type_name}-{key}"

key_to_cj_key(key) classmethod

Formats the CityJSON key (i.e. the id) based on the key (the space id).

Parameters:

Name Type Description Default
key str

The key of the object.

required

Returns:

Type Description
str

The id.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
@classmethod
def key_to_cj_key(cls, key: str) -> str:
    """
    Formats the CityJSON key (i.e. the id) based on the key (the space id).

    Parameters
    ----------
    key : str
        The key of the object.

    Returns
    -------
    str
        The id.
    """
    prefix = cls.key_to_prefix(key=key)
    key = key.replace(".", "_").replace("-", "_")
    return f"{prefix}-{cls.type_name}-{key}"

key_to_prefix(key) classmethod

Formats the prefix of the object based on its key.

Parameters:

Name Type Description Default
key str

The key of the object.

required

Returns:

Type Description
str

The prefix of the object.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
@classmethod
def key_to_prefix(cls, key: str) -> str:
    """
    Formats the prefix of the object based on its key.

    Parameters
    ----------
    key : str
        The key of the object.

    Returns
    -------
    str
        The prefix of the object.
    """
    return f"Building_{key.split(".")[0]}"

OutdoorObject

Bases: cj_helpers.cj_objects.CityJSONObject

Class used to represent the object, root of the file, that will be the parent of all the OutdoorUnitContainer objects.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
class OutdoorObject(CityJSONObject):
    """
    Class used to represent the object, root of the file, that will be the parent of all the OutdoorUnitContainer objects.
    """

    type_name = "CityObjectGroup"
    icon_z_offset = 2
    id_prefix = "OutdoorObject"

    def __init__(self, prefix: str) -> None:
        super().__init__(
            cj_key=f"{prefix}-{self.type_name}-{self.id_prefix}",
            attributes={},
            geometries=None,
            icon_position=None,
        )

    def apply_attr(self, attr: Attr, overwrite: bool) -> None:
        raise NotImplementedError()

OutdoorUnit

Bases: cj_helpers.cj_objects.BuildingUnitContainer

Class used to store a single OutdoorUnit object. OutdoorUnit objects can only have an icon and no geometry, but this could be extended similarly to BuildingUnit.

Methods:

Name Description
unit_code_to_code_instance

Generate an identifier based on the given number.

unit_code_to_id

Formats the CityJSON key (i.e. the id) based on the code of the units that it stores, and a number.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
class OutdoorUnit(BuildingUnitContainer):
    """
    Class used to store a single OutdoorUnit object.
    OutdoorUnit objects can only have an icon and no geometry, but this could be extended similarly to BuildingUnit.
    """

    type_name = "GenericCityObject"
    icon_z_offset = 2
    id_prefix = "OutdoorUnit"

    def __init__(
        self,
        cj_key: str,
        unit_code: str,
        attributes: dict[str, Any] | None = None,
        icon_position: IconPosition | None = None,
    ) -> None:
        super().__init__(
            cj_key=cj_key,
            unit_code=unit_code,
            attributes=attributes,
            icon_position=icon_position,
        )

    @classmethod
    def unit_code_to_code_instance(cls, code: str, number: int) -> str:
        """
        Generate an identifier based on the given number.
        The uniqueness of the identifier is not checked, but has to be ensured by providing a combination of inputs not used by another OutdoorUnit.

        Parameters
        ----------
        code : str
            The usage code.
        number : int
            The number of the unit.

        Returns
        -------
        str
            A formatted identifier
        """
        number_str = str(number).replace(".", "_").replace("-", "_")
        code = code.replace(".", "_").replace("-", "_")
        return f"{code}@{number_str}"

    @classmethod
    def unit_code_to_id(cls, code: str, prefix: str, number: int) -> str:
        """
        Formats the CityJSON key (i.e. the id) based on the code of the units that it stores, and a number.

        Parameters
        ----------
        code : str
            The usage code.
        prefix : str
            The outdoor prefix.
        number : int
            The number of the unit.

        Returns
        -------
        str
            The id.
        """
        code_instance = cls.unit_code_to_code_instance(code=code, number=number)
        prefix = prefix.replace("-", "_")
        return f"{prefix}-{cls.type_name}-{cls.id_prefix}_{code_instance}"

unit_code_to_code_instance(code, number) classmethod

Generate an identifier based on the given number. The uniqueness of the identifier is not checked, but has to be ensured by providing a combination of inputs not used by another OutdoorUnit.

Parameters:

Name Type Description Default
code str

The usage code.

required
number int

The number of the unit.

required

Returns:

Type Description
str

A formatted identifier

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
@classmethod
def unit_code_to_code_instance(cls, code: str, number: int) -> str:
    """
    Generate an identifier based on the given number.
    The uniqueness of the identifier is not checked, but has to be ensured by providing a combination of inputs not used by another OutdoorUnit.

    Parameters
    ----------
    code : str
        The usage code.
    number : int
        The number of the unit.

    Returns
    -------
    str
        A formatted identifier
    """
    number_str = str(number).replace(".", "_").replace("-", "_")
    code = code.replace(".", "_").replace("-", "_")
    return f"{code}@{number_str}"

unit_code_to_id(code, prefix, number) classmethod

Formats the CityJSON key (i.e. the id) based on the code of the units that it stores, and a number.

Parameters:

Name Type Description Default
code str

The usage code.

required
prefix str

The outdoor prefix.

required
number int

The number of the unit.

required

Returns:

Type Description
str

The id.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
@classmethod
def unit_code_to_id(cls, code: str, prefix: str, number: int) -> str:
    """
    Formats the CityJSON key (i.e. the id) based on the code of the units that it stores, and a number.

    Parameters
    ----------
    code : str
        The usage code.
    prefix : str
        The outdoor prefix.
    number : int
        The number of the unit.

    Returns
    -------
    str
        The id.
    """
    code_instance = cls.unit_code_to_code_instance(code=code, number=number)
    prefix = prefix.replace("-", "_")
    return f"{prefix}-{cls.type_name}-{cls.id_prefix}_{code_instance}"

OutdoorUnitContainer

Bases: cj_helpers.cj_objects.BuildingUnitContainer

Class used to group together all the OutdoorUnit objects per code.

Source code in python/src/data_pipeline/cj_helpers/cj_objects.py
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
class OutdoorUnitContainer(BuildingUnitContainer):
    """
    Class used to group together all the OutdoorUnit objects per code.
    """

    type_name = "CityObjectGroup"
    main_parent_code = ""
    id_prefix = "OutdoorUnitContainer"

    def __init__(
        self,
        cj_key: str,
        unit_code: str,
        attributes: dict[str, Any] | None = None,
    ) -> None:
        super().__init__(
            cj_key=cj_key,
            unit_code=unit_code,
            attributes=attributes,
            icon_position=None,
        )

    def apply_attr(self, attr: BdgAttr, overwrite: bool) -> None:
        raise NotImplementedError()