Creating city models and objects¶
In this tutorial we explore how to create new city models with using cjio
’s API.
[1]:
from pathlib import Path
from cjio import cityjson
from cjio.models import CityObject, Geometry
Set up paths for the tutorial.
[2]:
package_dir = Path(__name__).resolve().parent.parent.parent
schema_dir = package_dir / 'cjio' / 'schemas'/ '1.0.0'
data_dir = package_dir / 'tests' / 'data'
Creating a single CityObject¶
We are building a single CityObject of type Building. This building has an LoD2 geometry, thus it has Semantic Surfaces. The geometric shape of the building is a simple cube (size 10x10x10), which is sufficient for this demonstration.
The idea is that we create empty containers for the CityModel, CityObjects and Geometries, then fill those up and add to the CityModel.
We create an empty CityModel
[3]:
cm = cityjson.CityJSON()
print(cm)
{
"cityjson_version": "1.0",
"epsg": null,
"bbox": [
9000000000.0,
9000000000.0,
9000000000.0,
-9000000000.0,
-9000000000.0,
-9000000000.0
],
"transform/compressed": false,
"cityobjects_total": 0,
"cityobjects_present": [],
"materials": false,
"textures": false
}
An empty CityObject. Note that the ID is required.
[4]:
co = CityObject(
id='1'
)
We can also add attributes
[5]:
co_attrs = {
'some_attribute': 42,
'other_attribute': 'bla bla'
}
co.attributes = co_attrs
Let’s see what do we have
[6]:
print(co)
{
"id": "1",
"type": null,
"attributes": {
"some_attribute": 42,
"other_attribute": "bla bla"
},
"children": [],
"parents": [],
"geometry_type": [],
"geometry_lod": [],
"semantic_surfaces": []
}
Instantiate a Geometry without boundaries and semantics
[7]:
geom = Geometry(type='Solid', lod=2)
We build the boundary Solid of the cube The surfaces are in this order: WallSurface, WallSurface, WallSurface, WallSurface, GroundSurface, RoofSurface
[8]:
bdry = [
[[(0.0, 0.0, 0.0), (10.0, 0.0, 0.0), (10.0, 0.0, 10.0), (0.0, 0.0, 10.0)]],
[[(10.0, 0.0, 0.0), (10.0, 10.0, 0.0), (10.0, 10.0, 10.0), (10.0, 0.0, 10.0)]],
[[(10.0, 10.0, 0.0), (0.0, 10.0, 0.0), (0.0, 10.0, 10.0), (10.0, 10.0, 10.0)]],
[[(0.0, 10.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, 10.0), (0.0, 10.0, 10.0)]],
[[(0.0, 0.0, 0.0), (0.0, 10.0, 0.0), (10.0, 10.0, 0.0), (10.0, 0.0, 0.0)]],
[[(10.0, 0.0, 10.0), (10.0, 10.0, 10.0), (0.0, 10.0, 10.0), (0.0, 0.0, 10.0)]]
]
Add the boundary to the Geometry
[9]:
geom.boundaries.append(bdry)
We build the SemanticSurfaces for the boundary. The surfaces
attribute must contain at least the surface_idx
and type
keys, optionally attributes
. We have three semantic surface types, WallSurface, GroundSurface, RoofSurface.
[10]:
srf = {
0: {'surface_idx': [], 'type': 'WallSurface'},
1: {'surface_idx': [], 'type': 'GroundSurface'},
2: {'surface_idx': [], 'type': 'RoofSurface'}
}
We use the surface_idx
to point to the surfaces of the boundary. Thus the index to a single boundary surface is composed as [Solid index, Shell index, Surface index]. Consequently, in case of a CompositeSolid which first Solid, outer Shell, second Surface is a WallSurface, one element in the surface_idx
would be [0, 0, 1]
. Then assuming that there is only a single WallSurface in the mentioned CompositeSolid, the index to the WallSurfaces is composed as
{'surface_idx': [ [0, 0, 1] ], 'type': 'WallSurface'}
. In case of a Solid boundary type the Solid index is omitted from the elements of surface_idx
. In case of a MultiSurface boundary type both the Solid index and Shell index are omitted from the elements of surface_idx
.
We create the surface index accordingly and assign them to the geometry.
[11]:
geom.surfaces[0] = {'surface_idx': [[0,0], [0,1], [0,2], [0,3]], 'type': 'WallSurface'}
geom.surfaces[1] = {'surface_idx': [[0,4]], 'type': 'GroundSurface'}
geom.surfaces[2] = {'surface_idx': [[0,5]], 'type': 'RoofSurface'}
Then we test if it works.
[12]:
ground = geom.get_surfaces('groundsurface')
ground_boundaries = []
for g in ground.values():
ground_boundaries.append(geom.get_surface_boundaries(g))
We have a list of generators
[13]:
res = list(ground_boundaries[0])
The generator creates a list of surfaces –> a MultiSurface
[14]:
assert res[0] == bdry[4]
# %%
wall = geom.get_surfaces('wallsurface')
wall_boundaries = []
for w in wall.values():
wall_boundaries.append(geom.get_surface_boundaries(w))
We put everything together, first filling up the CityObject
[15]:
co.geometry.append(geom)
co.type = 'Building'
Then adding the CityObject to the CityModel.
[16]:
cm.cityobjects[co.id] = co
Let’s validate the citymodel before writing it to a file. However, first we need to index the geometry boundaries and create the vertex list, second we need to add the cityobject and vertices to the internal json-store of the citymodel so the validate()
method can validate them.
Note: CityJSON version 1.0.0 only accepts the Geometry lod
as a numeric value and not a string.
[17]:
cityobjects, vertex_lookup = cm.reference_geometry()
cm.add_to_j(cityobjects,vertex_lookup)
cm.update_bbox()
#cm.validate(folder_schemas=schema_dir)
[17]:
[0.0, 0.0, 0.0, 10.0, 10.0, 10.0]
[18]:
cm
[18]:
{
"cityjson_version": "1.0",
"epsg": null,
"bbox": [
0.0,
0.0,
0.0,
10.0,
10.0,
10.0
],
"transform/compressed": false,
"cityobjects_total": 1,
"cityobjects_present": [
"Building"
],
"materials": false,
"textures": false
}
Finally, we write the citymodel to a CityJSON file.
[19]:
outfile = data_dir / 'test_create.json'
cityjson.save(cm, outfile)