Script 01 — Get Child Features¶
File: sql/scripts/main/01_get_child_feat.sql
Reads from: {lod_schema}.feature, {lod_schema}.geometry_data, {lod_schema}.property, {lod_schema}.objectclass
Writes to: {city2tabula_schema}.{lod_schema}_child_feature
Purpose¶
A CityDB building solid is stored as a single feature, but it is composed of many smaller surface features — the roof faces, wall faces, and ground faces. This script finds those child surface features for each building by asking: which features in the database are geometrically inside or touching this building's solid?
The result is one row per surface feature per building, stored in _child_feature.
Background: how features are stored in CityDB¶
CityDB does not store a building and its surfaces in the same table row. Instead:
- A building is a row in
featurewith anobjectclass_idin the range 900–999. - Its surfaces (RoofSurface, WallSurface, etc.) are separate rows in the same
featuretable, each with their ownobjectclass_idoutside that range. - Geometries live in
geometry_data, linked viafeature_id. - The building solid is identified by a
propertyrow withname = 'lod2Solid'(orlod3Solid).
There is no direct foreign key from surface rows to their parent building. Instead, this script uses 3D spatial intersection to discover the relationship.
Step-by-step walkthrough¶
Step 1 — buildings CTE¶
WITH buildings AS (
SELECT f.id AS building_feature_id, g.geometry AS building_geom
FROM {lod_schema}.feature f
JOIN {lod_schema}.geometry_data g ON f.id = g.feature_id
JOIN {lod_schema}.property p ON f.id = p.feature_id
AND p.name = 'lod' || {lod_level} || 'Solid'
WHERE objectclass_id BETWEEN 900 AND 999
AND f.id NOT IN (
SELECT building_feature_id FROM {city2tabula_schema}.{lod_schema}_child_feature
)
AND f.id IN {building_ids}
)
This CTE selects the building features for the current batch. Three filters apply:
objectclass_id BETWEEN 900 AND 999— keeps only rows that represent buildings (not surfaces, furniture, trees, etc.).p.name = 'lod2Solid'— joins to the property that holds the full building solid geometry, not just a wall or footprint face.f.id NOT IN (...)— idempotency guard: skips any building that already has rows in_child_featureso re-runs are safe.
The output is a small temporary result set: one row per unprocessed building containing the building's ID and its 3D solid geometry.
Step 2 — Main SELECT with ST_3DIntersects¶
SELECT ...
FROM {lod_schema}.feature f
JOIN {lod_schema}.objectclass oc ON f.objectclass_id = oc.id
JOIN {lod_schema}.geometry_data g ON f.id = g.feature_id
JOIN buildings b ON ST_3DIntersects(g.geometry, b.building_geom)
WHERE f.objectclass_id NOT BETWEEN 900 AND 999
AND f.id != b.building_feature_id
AND GeometryType(g.geometry) IN ('MULTIPOLYGON')
This is the heart of the script. For every feature in the database (all rows), it asks: does this feature's geometry intersect the building's 3D solid? If yes, and if the feature is not itself a building (objectclass outside 900–999) and is not the building we're querying for, it's a child surface.
ST_3DIntersects performs a 3D spatial intersection test. A wall surface that shares a face with the building solid will return true. Features on the other side of the city return false and are discarded.
GeometryType IN ('MULTIPOLYGON') keeps only polygon-based surface features. Points, lines, and other geometry types are excluded because they cannot represent surface areas.
The result is inserted into _child_feature with a generated UUID, the LoD level, the building ID, the surface feature ID, its object class, class name, and geometry.
Output columns¶
| Column | Description |
|---|---|
id |
Auto-generated UUID for this row |
lod |
LoD level (2 or 3) |
building_feature_id |
ID of the parent building |
surface_feature_id |
ID of the surface feature |
objectclass_id |
Numeric type code (e.g. 709 = RoofSurface) |
classname |
Human-readable type name (e.g. RoofSurface) |
geom |
3D MULTIPOLYGON geometry of the surface |
What comes next¶
Script 02 takes these MULTIPOLYGON geometries and explodes each one into individual POLYGON faces, because normal and attribute calculations in script 03 operate on single faces.