Script 04 — Building Features¶
File: sql/scripts/main/04_calc_bld_feat.sql
Reads from: {city2tabula_schema}.{lod_schema}_surface_raw
Writes to: {city2tabula_schema}.{lod_schema}_building
Purpose¶
Scripts 01–03 produce one row per polygon face. This script collapses all those face-level rows into one summary row per building. It aggregates surface areas, counts faces, computes building height, and classifies shape complexity into the _building table.
Step-by-step walkthrough¶
Step 1 — new_buildings CTE¶
WITH new_buildings AS (
SELECT DISTINCT building_feature_id
FROM {city2tabula_schema}.{lod_schema}_surface_raw
WHERE building_feature_id IN {building_ids}
)
Unlike previous scripts, there is no "already processed" exclusion here — this CTE simply scopes the query to the current batch. The INSERT will naturally skip buildings already in the output table if building_feature_id is a unique key.
Step 2 — aggregated_surfaces CTE¶
This CTE does most of the work, grouping all surface rows by building and computing summary values. The key computed columns are:
Surface areas by type¶
SUM(surface_area) FILTER (WHERE classname = 'GroundSurface') AS footprint_area,
SUM(surface_area) FILTER (WHERE classname = 'RoofSurface') AS area_total_roof,
SUM(surface_area) FILTER (WHERE classname = 'WallSurface') AS area_total_wall,
SUM(surface_area) FILTER (WHERE classname = 'GroundSurface') AS area_total_floor,
FILTER (WHERE ...) applies the SUM only to rows matching the condition. This avoids needing separate subqueries for each surface type.
Note that area_total_floor is initialised here to the same value as footprint_area (the raw GroundSurface area sum). Script 06 will overwrite it with footprint_area × number_of_storeys to represent total heated floor area across all floors.
Height¶
MAX(height) FILTER (WHERE classname = 'WallSurface') AS min_height,
MAX(height) FILTER (WHERE classname = 'WallSurface') +
COALESCE(MAX(height) FILTER (WHERE classname = 'RoofSurface'), 0) AS max_height,
Height is derived indirectly from the vertical spans of individual surface faces.
min_height(eave height) — the maximum vertical span of any single wall face. This approximates the height to the eave (where the walls meet the roof), because a wall face typically spans the full height of the building's vertical portion.max_height(ridge height) — eave height plus the maximum vertical span of any single roof face. This approximates the height to the ridge (highest point of the roof).
The column names min_height / max_height refer to minimum and maximum height estimates of the building, not the smallest and largest face heights.
Footprint complexity¶
CASE
WHEN ST_NPoints(ST_Boundary(ST_Union(geom) FILTER (WHERE classname = 'GroundSurface'))) <= 4 THEN 0
WHEN ST_NPoints(...) BETWEEN 5 AND 10 THEN 1
ELSE 2
END AS footprint_complexity,
The building footprint is reconstructed by unioning all GroundSurface polygons into one geometry and counting the vertices on its outer boundary:
| Vertex count | Code | Meaning |
|---|---|---|
| ≤ 4 | 0 | Simple (rectangle or triangle) |
| 5–10 | 1 | Regular (L-shape, U-shape) |
| > 10 | 2 | Complex (many-sided, irregular) |
Roof complexity¶
CASE
WHEN COUNT(*) FILTER (WHERE classname = 'RoofSurface') = 1 THEN 0
WHEN COUNT(*) FILTER (WHERE classname = 'RoofSurface') BETWEEN 2 AND 4 THEN 1
ELSE 2
END AS roof_complexity,
Measured by the number of distinct RoofSurface polygon faces:
| Face count | Code | Meaning |
|---|---|---|
| 1 | 0 | Simple (flat or single-pitch) |
| 2–4 | 1 | Regular (gable, hip) |
| > 4 | 2 | Complex (multi-faceted, mansard) |
Storey count (initial estimate)¶
CASE
WHEN MAX(height) FILTER (WHERE classname = 'WallSurface') > 0 AND 2.5 > 0
THEN MAX(height) FILTER (WHERE classname = 'WallSurface') / 2.5
ELSE 1
END AS number_of_storeys,
A first rough estimate: wall height divided by a default room height of 2.5 m. This is refined in script 06 using the same formula but with the properly stored values, which allows the room height to be overridden per dataset.
Building footprint geometry¶
ST_Transform(ST_Force2D(ST_Centroid(
ST_Union(geom) FILTER (WHERE classname = 'GroundSurface')
)), {srid}) AS building_centroid_geom,
ST_Transform(
ST_Union(geom) FILTER (WHERE classname = 'GroundSurface'),
{srid}) AS building_footprint_geom
The merged GroundSurface geometry is re-projected to the target CRS ({srid}). The centroid is the geometric centre of the merged footprint, used for mapping.
Placeholder columns¶
Several columns are set to placeholder values that are not yet available at this stage:
| Column | Initial value | Updated by |
|---|---|---|
construction_year |
0 | External data (not automated) |
heating_demand |
0.0 | External energy model |
has_attached_neighbour |
FALSE |
Not yet implemented |
surface_count_floor |
0 | Not computed (ground is counted differently) |
area_total_floor |
= footprint_area |
Script 06 (overwritten) |
number_of_storeys |
wall_height / 2.5 |
Script 06 (refined) |
Output columns (key)¶
| Column | Description |
|---|---|
footprint_area |
Sum of all GroundSurface face areas (sqm) |
footprint_complexity |
0 = simple, 1 = regular, 2 = complex |
roof_complexity |
0 = simple, 1 = regular, 2 = complex |
area_total_roof |
Sum of all RoofSurface face areas (sqm) |
area_total_wall |
Sum of all WallSurface face areas (sqm) |
area_total_floor |
Initially = footprint_area; overwritten in script 06 |
min_height |
Eave height — max wall face span (m) |
max_height |
Ridge height — eave + max roof face span (m) |
number_of_storeys |
Wall height / 2.5; refined in script 06 |
building_centroid_geom |
2D centroid of merged footprint |
building_footprint_geom |
Merged 2D footprint geometry |
What comes next¶
Script 05 adds volume estimates (height × footprint area). Script 06 then refines storey count and overwrites the floor area.