Extensions
CityGML ADEs and CityJSON Extensions: typed where possible, every added name carrying its extension's namespace.
The CityGML CM is extensible: a CityGML Application Domain Extension (ADE), or the equivalent CityJSON Extension, may add new feature types, new semantic surface types, and new attributes. CityParquet encodes each mechanism into its columnar model without losing the source data, preferring typed columns where the extension is regular and an escape hatch where it is not (see the extensions decision).
Each extension is identified by a short namespace, and every name the extension
adds to a package carries this namespace as a prefix. The examples on this page use the
Energy ADE 3.0 (beta 7; Agugiaro and Padsala,
tudelft3d/Energy_ADE), whose namespace is
energy. It is split into Core, Building Physics, Occupant Behaviour, Material and
Construction, and Energy Systems modules; the examples draw on its Building Physics
module.
The namespace
Every extension a package uses has a namespace that:
- MUST match
^[a-z][a-z0-9]*$— a lower-case letter followed by lower-case letters and digits, with no_; and - MUST be unique within the package: a namespace denotes the same extension in every file of the package.
The prefix form of a namespace is the namespace followed by _ (energy_). Because
a namespace contains no _, the first _ of a prefixed name always ends its namespace.
Derivation. A writer derives the namespace from the source:
- CityJSON Extension — the extension’s name, which is its key in the document’s
extensionsmember, lower-cased, with every character outside[a-z0-9]removed:"Energy"→energy,"Noise"→noise,"Energy ADE"→energyade. - CityGML ADE — the prefix the source document first binds to the ADE’s XML
namespace, normalised the same way;
adewhen that leaves nothing or a digit first, and numbered from 2 (uro2) when an ADE the document uses earlier has it, so that every ADE of a document has its own.
A writer MAY instead accept a namespace supplied by the operator for an extension; the result MUST still satisfy the two rules above. A writer MUST reject the input (a hard error) when a derivation yields an empty namespace, a namespace that does not start with a letter, or a namespace that collides with another extension’s.
What carries the prefix. Every name the extension adds:
| Name | Source | Stored as |
|---|---|---|
| Extended attribute (a column name) | +heatCapacity |
energy_heatCapacity |
Extended semantic surface type (geometry_properties_lod*.surfaces[].type) |
+PartyWallSurface |
energy_PartyWallSurface |
Extended city-object class (object_type) |
+ThermalZone |
energy_ThermalZone |
| Module an extension declares (a file name) | Building Physics module | energy_building_physics.parquet |
| The extension’s own file (a file name) | — | energy.parquet |
Core module files and core names are never prefixed.
Extended feature types
An extension — a CityGML ADE, or the equivalent CityJSON Extension — groups its feature
types into modules, so they follow the same
by-module rule as core types. Within a file the
object_type column distinguishes each row’s class, and an extension class’s
object_type is its class name with the namespace prefix (energy_ThermalZone).
The ModuleKey. Every class routes to a file via a single key: a core CityGML 3.0
module (Building, Transportation, …), an owned extension module — the module
the extension itself declares for that class — or the extension’s own file. This is one uniform mechanism, not a
special case bolted onto the by-module rule:
- A class belonging to a core module gets the module’s file, exactly as any core type does.
- An extension class that declares its own module gets a file named after that
module: the namespace prefix followed by the module name in snake_case (the
file-name rule of core modules). The Energy ADE’s
ThermalZoneandThermalBoundaryboth belong to its Building Physics module, so both are stored inenergy_building_physics.parquet, distinguished byobject_type(energy_ThermalZone,energy_ThermalBoundary). - An extension class that specialises an existing class (declares no module of its
own, but subclasses a core or extension class) routes to that class’s module
file — an extension class specialising
Buildinglands inbuilding.parquet, exactly asBuildingPartdoes. This holds recursively. A CityJSON Extension states the class it specialises in itsextraCityObjectsschema, by anallOfmember whose$refnames a definition of CityJSON’scityobjects.schema.json("$ref": "cityobjects.schema.json#/_AbstractBuilding"): a City Object type routes to its module,_AbstractBuildingto Building’s,_AbstractTransportationComplexto Transportation’s, and_AbstractCityObjector another definition that names no class in particular (ExtensionObject) to none. A CityGML ADE states it in its schema, by the element’ssubstitutionGroup(see the CityJSON form of ADE content). - An extension class that declares neither a module of its own nor a class it
specialises — a CityJSON Extension class (CityJSON has no modules) whose schema names
no definition or only
_AbstractCityObject, and a CityGML ADE class whose schema is not read or substitutes it for no CityGML class CityJSON has a definition for — gets its extension’s own file, named by the namespace alone: theurfADE’sUseDistrictandFirePreventionDistrictare both stored inurf.parquet. A namespace contains no_, so this file never collides with an extension module’s file. - Routing is indifferent to CityJSON’s
+marker. The+is a detail of CityJSON’s own encoding of extension types, not a precondition for being one: an ADE class with no+and a resolvable declared module routes by that module like any other. A+-marked class whose name, without the+, is a core class name (+Building) routes to that core class’s module file, but it remains an extension class, so itsobject_typecarries the prefix (energy_Building) and stays distinct from the core class’s. - A class with no resolvable
ModuleKeyat all — not a recognised core CityGML class, and not a class of any declared extension — is a hard error. Routing is total: every class the writer encounters must resolve to some file, or the input is rejected.
The file of a row is therefore its class’s module: the declaration records the class an extension class specialises, never its file.
🟡 Carrying full ADE object-class metadata (beyond the class name, its module file and the extension declaration below) remains an open question.
Extended semantic surfaces
An extended semantic surface type is stored with the namespace prefix in the type of
its surface object inside the
geometry_properties_lod* surfaces value: the
Energy ADE’s +PartyWallSurface is stored as { "type": "energy_PartyWallSurface" }.
Semantic surfaces are never promoted to column names, but they carry the prefix all the
same, so one naming rule identifies every extension name in a package and keeps the
names of different extensions apart.
Extended attributes
An extended attribute becomes a typed attribute column like any other, so it gets the
same predicate and projection pushdown as a core attribute. CityJSON writes these with
a leading + (+heatCapacity). Parquet itself imposes no restriction on a leading +
in a column name, but such a name is hostile to SQL and to many query engines (it
cannot be referenced unquoted, and reads as an operator). The column name is therefore
the attribute name with the namespace prefix: the heatCapacity attribute of an Energy
ADE ThermalZone is the column energy_heatCapacity.
The extended attribute columns are listed in the attributes
metadata key like any other attribute; the prefix, resolved
through city.extensions, identifies the extension each one belongs to.
The extension declaration: city.extensions
city.extensions (metadata key) declares the extensions a
file uses. It is a JSON object keyed by namespace; each value references the
extension’s schema document rather than embedding it:
"extensions": {
"energy": {
"name": "Energy",
"url": "https://…/energy.ext.json",
"version": "3.0"
}
}
| Field | Req. | Description |
|---|---|---|
name |
✔ | The extension’s name in the source: the key of the CityJSON extensions member, or the ADE’s name. |
url |
✔ | The extension’s schema document — a JSON Schema for a CityJSON Extension, an XSD for a CityGML ADE. |
version |
The extension’s version, when the source states one. | |
xmlns |
CityGML ADE only: the ADE’s XML namespace URI. | |
xmlAttributes |
CityGML ADE only: the XML attributes that every occurrence of an ADE element carries with one value, by element local name, then attribute name ({"width": {"uom": "m"}}); see CityGML ADE content. |
|
extraCityObjects, extraSemanticSurfaces, extraRootProperties, extraAttributes |
The names the extension adds, as a CityJSON Extension document lists them (extraAttributes by City Object type), when the source states them with the declaration; kept verbatim. |
|
substitutionGroups, containers, propertyOrder |
CityGML ADE only, when the ADE’s XML Schema was read: the CityGML element each ADE attribute, class and container substitutes for, as {namespace URI}local; the element a CityGML 3.0 adeOf… hook holds each attribute in; and the order of the attributes and geometry properties of each extended class and container. Where a CityGML writer puts the ADE’s elements; kept verbatim. |
A file that carries any extension name — a prefixed attribute column, object_type
value or semantic surface type — MUST declare that name’s namespace here. Every file
of a package that declares a namespace MUST declare it with the same value.
Attributing names to an extension
A writer attributes each extension name in the source to exactly one declared extension:
- A CityGML ADE name belongs to the ADE whose XML namespace it is in.
- A CityJSON source that declares exactly one extension: every
+name belongs to that extension. - A CityJSON source that declares several extensions: a
+name belongs to the one extension that lists it among the names of its kind — an attribute inextraAttributes, a City Object type inextraCityObjects, a semantic surface type inextraSemanticSurfaces— in its declaration, or in its schema document if the writer reads it. A name that no declared extension lists as its kind, or that several do, cannot be attributed and is a hard error. - A
+name in a CityJSON source that declares no extension is a hard error: CityJSON requires every extension used in a document to be declared in itsextensionsmember.
Collisions
A core (non-extension) attribute, semantic surface type or class whose name starts with
a declared namespace followed by _ is a hard error: on export it would be
indistinguishable from an extension name. With the Energy ADE declared, a core
attribute named energy_label is rejected.
Likewise, an extension module whose file name equals a core module’s file name (a
namespace water with a module Body → water_body.parquet) is a hard error, and
so is an extension’s own file that does (a namespace building with a class of its
own → building.parquet).
Export
On export to CityJSON, a name that starts with a declared namespace prefix becomes
CityJSON’s + form: energy_heatCapacity → +heatCapacity,
energy_ThermalZone → +ThermalZone, energy_PartyWallSurface → +PartyWallSurface.
The document’s extensions member is rebuilt from city.extensions: each entry’s
name becomes the member key, with its url, version and every other field it
carries. A name with no declared
prefix is written unchanged. On export to CityGML, xmlns gives the XML namespace the
ADE’s names are written in, and substitutionGroups, containers and propertyOrder
where its elements go.
CityJSON form of CityGML ADE content
A CityGML ADE adds XML elements a CityJSON document has no member for. A writer reading CityGML gives them the CityJSON form below, and from there they follow the rules above.
- The extension. Each ADE the document uses is declared under the prefix the
document binds to its namespace (
uro, see the namespace), withurlthe schema documentxsi:schemaLocationnames for it when that is an absolute URL, else the ADE’s published schema when the writer knows it, else the location as written, else the namespace URI;versionthe last segment of the namespace URI when that is a version number, else empty (CityJSON requires the member);xmlnsthe namespace URI; its classes, semantic surfaces and attributes inextraCityObjects,extraSemanticSurfacesandextraAttributes; and, when its schema was read, what the schema says of where its elements go insubstitutionGroups,containersandpropertyOrder. - The schema. A writer reads an ADE’s XML Schema when the document’s
xsi:schemaLocationnames it by a path relative to the document and that file exists (the PLATEAU archive’s../../schemas/iur/uro/3.2/urbanObject.xsd), with the documents it includes or imports by a relative path. A schema named by URL need not be fetched. Without its schema an ADE is read by position alone, as below. - Classes and surfaces. An ADE element where a city object belongs (a
cityObjectMember, a nesting property, agroupMember) is a City Object of type+<local name>; one directly inside aboundedByis a semantic surface of type+<local name>. Their ownlod*geometry properties are geometry, their CityGML properties attributes as for a core class, and their own ADE’s properties attributes named by local name without+. When the schema is read, a class’sextraCityObjectsentry names the CityGML class its element substitutes for, through the ADE’s own abstract classes, as the CityJSON definition it corresponds to:uro:UndergroundBuildingsubstitutes forbldg:_AbstractBuilding, so it is{"allOf": [{"$ref": "cityobjects.schema.json#/_AbstractBuilding"}]}and routes tobuilding.parquet. CityJSON has no abstract bridge, tunnel or water object, so a class specialising one namesBridge,TunnelorWaterBody; one that reaches only the CityGML city object, or a CityGML class CityJSON has no definition for (veg:_VegetationObject), names_AbstractCityObject. Without the schema the entry is an empty object. - Extension properties. An ADE element directly inside an object of a CityGML
class, or of another ADE’s class, is the attribute
+<local name>. - Values. An element’s content becomes JSON: its child elements an object keyed by
local name, prefixed
<prefix>:when the child is in another namespace than its parent element, a repeated child an array; an empty element an empty object. Text takes the type the schema declares for the element in its context: an integer forxs:integerand its restrictions, a number forxs:double,xs:float,xs:decimaland the GML measures (gml:MeasureType,gml:LengthType, …), a boolean forxs:boolean, else a string. Without a declared type it is a string, or a number when the element has auomattribute. Text that is not a value of its type stays a string. An XML attribute without a prefix that every occurrence of its element carries with one value is stated once, in the declaration’sxmlAttributes; an element with any other attribute becomes{"$": <value>, "@<attribute>": "…"}. Text between child elements, and GML geometry inside an ADE property, have no CityJSON form and are left out.
The Chiyoda-ku urf tile of PLATEAU thus becomes +FirePreventionDistrict and
+UseDistrict City Objects in urf.parquet, with object_type
urf_FirePreventionDistrict and an xs:integer floorAreaRate column of type
BIGINT; a uro:roadStructureAttribute of a tran:Road the column
uro_roadStructureAttribute of transportation.parquet; and the ubld tile’s
uro:UndergroundBuilding a row of building.parquet with object_type
uro_UndergroundBuilding.
Example
Constructed. A CityJSONSeq source declares one extension, Energy, and a
Building carries a core attribute and an Energy ADE attribute. (The Energy ADE
attaches heatCapacity to a ThermalZone; it sits on the building here so that one
object shows every rule.)
{ "type": "CityJSON", "version": "2.0", "transform": { /* … */ },
"extensions": { "Energy": { "url": "https://…/energy.ext.json", "version": "3.0" } },
"CityObjects": {}, "vertices": [] }
{ "type": "CityJSONFeature", "id": "b-42",
"CityObjects": {
"b-42": { "type": "Building",
"attributes": { "yearOfConstruction": 1998, "+heatCapacity": 120000.0 } } },
"vertices": [] }
The source declares exactly one extension, so +heatCapacity belongs to it, and the
namespace is the member key lower-cased, energy. The attribute becomes the column
energy_heatCapacity, inferred DOUBLE, beside the core yearOfConstruction:
| id | object_type | yearOfConstruction BIGINT |
energy_heatCapacity DOUBLE |
|---|---|---|---|
b-42 |
Building |
1998 |
120000.0 |
The footer of building.parquet declares the namespace, with the member key as its
name, and lists both columns in attributes:
"attributes": ["yearOfConstruction", "energy_heatCapacity"],
"extensions": {
"energy": { "name": "Energy", "url": "https://…/energy.ext.json", "version": "3.0" }
}
On export the energy_ prefix becomes + again, and the extensions member is
rebuilt from city.extensions under the key Energy.