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)