First Commit

This commit is contained in:
2026-05-31 10:17:09 +07:00
commit 17a9c69379
4547 changed files with 1170384 additions and 0 deletions
@@ -0,0 +1,41 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""Manage georeferencing metadata
IFC model geometry may have a coordinate reference system (CRS) assigned to it.
It may also optionally have a map conversion defined to transform to and from
map coordinates and project local engineering coordinates.
"""
from .. import wrap_usecases
from .add_georeferencing import add_georeferencing
from .edit_georeferencing import edit_georeferencing
from .edit_true_north import edit_true_north
from .edit_wcs import edit_wcs
from .remove_georeferencing import remove_georeferencing
wrap_usecases(__path__, __name__)
__all__ = [
"add_georeferencing",
"edit_georeferencing",
"edit_true_north",
"edit_wcs",
"remove_georeferencing",
]
@@ -0,0 +1,105 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.georeference
import ifcopenshell.api.pset
import ifcopenshell.util.element
def add_georeferencing(file: ifcopenshell.file, ifc_class: str = "IfcMapConversion", name: str = "EPSG:3857") -> None:
"""Add empty georeferencing entities to a model
By default, models are not georeferenced. Georeferencing requires two
entities: a definition of the projected coordinated reference system
(CRS) used, and the transformation parameters between any local coordinate
system and that projected CRS if any.
This function will create the entities to store the projected CRS and
map conversion transformation, but will leave all the parameters blank.
It is this the users responsibility to specify the correct
georeferencing parameters. See
ifcopenshell.api.georeference.edit_georeferencing.
:param ifc_class: A type of IfcCoordinateOperation. For IFC2X3, this has no
impact and only uses ePSet_MapConversion.
Example:
.. code:: python
ifcopenshell.api.georeference.add_georeferencing(model)
"""
if file.schema == "IFC2X3":
if not (project := file.by_type("IfcProject")):
return
project = project[0]
if ifcopenshell.util.element.get_pset(project, "ePSet_ProjectedCRS"):
return
conversion = ifcopenshell.api.pset.add_pset(file, project, "ePSet_MapConversion")
crs = ifcopenshell.api.pset.add_pset(file, project, "ePSet_ProjectedCRS")
ifcopenshell.api.pset.edit_pset(file, crs, properties={"Name": name})
ifcopenshell.api.pset.edit_pset(
file,
conversion,
properties={
"Eastings": file.createIfcLengthMeasure(0),
"Northings": file.createIfcLengthMeasure(0),
"OrthogonalHeight": file.createIfcLengthMeasure(0),
},
)
return
has_crs = bool(file.by_type("IfcProjectedCRS"))
has_conversion = bool(file.by_type("IfcCoordinateOperation"))
if has_crs and has_conversion:
return
if has_crs or has_conversion:
# This is technically invalid, but we shall forgive the industry here if they are wrong ...
ifcopenshell.api.georeference.remove_georeferencing(file)
source_crs = None
for context in file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.ContextType == "Model":
source_crs = context
break
if not source_crs:
return
projected_crs = file.create_entity("IfcProjectedCRS", Name=name)
if ifc_class == "IfcMapConversion":
file.create_entity(
ifc_class, SourceCRS=source_crs, TargetCRS=projected_crs, Eastings=0, Northings=0, OrthogonalHeight=0
)
elif ifc_class == "IfcMapConversionScaled":
file.create_entity(
ifc_class,
SourceCRS=source_crs,
TargetCRS=projected_crs,
Eastings=0,
Northings=0,
OrthogonalHeight=0,
FactorX=1,
FactorY=1,
FactorZ=1,
)
elif ifc_class == "IfcRigidOperation":
file.create_entity(
ifc_class,
SourceCRS=source_crs,
TargetCRS=projected_crs,
FirstCoordinate=file.createIfcLengthMeasure(0),
SecondCoordinate=file.createIfcLengthMeasure(0),
)
@@ -0,0 +1,117 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Any, Optional
import ifcopenshell
import ifcopenshell.api.pset
import ifcopenshell.util.element
def edit_georeferencing(
file: ifcopenshell.file,
coordinate_operation: Optional[dict[str, Any]] = None,
projected_crs: Optional[dict[str, Any]] = None,
) -> None:
"""Edits the attributes of a map conversion, projected CRS, and true north
Setting the correct georeferencing parameters is a complex topic and
should ideally be done with three parties present: the lead architect,
surveyor, and a third-party digital engineer with expertise in IFC to
moderate. For more information, read the Bonsai documentation
for Georeferencing:
https://docs.bonsaibim.org/guides/authoring/georeferencing.html
For more information about the attributes and data types of an
IfcCoordinateOperation, consult the IFC documentation.
For more information about the attributes and data types of an
IfcProjectedCRS, consult the IFC documentation.
See ifcopenshell.util.geolocation for more utilities to convert to and
from local and map coordinates to check your results.
:param coordinate_operation: The dictionary of attribute names and values
you want to edit.
'MapUnit' attribute in IFC2X3 should be presented as a full unit name (string),
in other IFC versions it's presented an IfcNamedUnit.
:param projected_crs: The IfcProjectedCRS dictionary of attribute
names and values you want to edit.
Example:
.. code:: python
ifcopenshell.api.georeference.add_georeferencing(model)
# This is the simplest scenario, a defined CRS (GDA2020 / MGA Zone
# 56, typically used in Sydney, Australia) but with no local
# coordinates. This is only recommended for horizontal construction
# projects, not for vertical construction (such as buildings).
ifcopenshell.api.georeference.edit_georeferencing(model,
projected_crs={"Name": "EPSG:7856"})
# For buildings, it is almost always recommended to specify map
# conversion parameters to a false origin and orientation to project
# north. See the diagram in the Bonsai Georeferencing
# documentation for correct calculation of the X Axis Abcissa and
# Ordinate.
ifcopenshell.api.georeference.edit_georeferencing(model,
projected_crs={"Name": "EPSG:7856"},
coordinate_operation={
"Eastings": 335087.17, # The architect nominates a false origin
"Northings": 6251635.41, # The architect nominates a false origin
# Note: this is the angle difference between Project North
# and Grid North. Remember: True North should never be used!
"XAxisAbscissa": cos(radians(-30)), # The architect nominates a project north
"XAxisOrdinate": sin(radians(-30)), # The architect nominates a project north
"Scale": 0.99956, # Ask your surveyor for your site's average combined scale factor!
})
"""
if file.schema == "IFC2X3":
if not (project := file.by_type("IfcProject")):
return
project = project[0]
if projected_crs:
if crs := ifcopenshell.util.element.get_pset(project, "ePSet_ProjectedCRS"):
crs = file.by_id(crs["id"])
for k, v in projected_crs.items():
if k == "Description":
v = file.createIfcText(v)
elif k == "Name":
v = file.createIfcLabel(v)
elif v is not None:
v = file.createIfcIdentifier(v)
ifcopenshell.api.pset.edit_pset(file, crs, properties=projected_crs)
if coordinate_operation:
if conversion := ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion"):
conversion = file.by_id(conversion["id"])
for k, v in coordinate_operation.items():
if k in ("XAxisAbscissa", "XAxisOrdinate", "Scale"):
v = file.createIfcReal(v)
else:
v = file.createIfcLengthMeasure(v)
ifcopenshell.api.pset.edit_pset(file, conversion, properties=coordinate_operation)
return
if projected_crs:
crs = file.by_type("IfcProjectedCRS")[0]
for name, value in projected_crs.items():
setattr(crs, name, value)
if coordinate_operation:
conversion = file.by_type("IfcCoordinateOperation")[0]
for name, value in coordinate_operation.items():
setattr(conversion, name, value)
@@ -0,0 +1,73 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Optional, Union
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.geolocation
def edit_true_north(file: ifcopenshell.file, true_north: Optional[Union[tuple[float, float], float]] = 0.0) -> None:
"""Edits the true north
Given project north being up (i.e. a vector of 0, 1), true north is defined
as a unitised 2D vector pointing to true north. Alternatively, true north
may be defined as a rotation from project north to true north.
Anticlockwise is positive.
Note that true north is not part of georeferencing, and is only optionally
provided as a reference value, typically for solar analysis. Remember: grid
north (what your surveyor will typically use) is not the same as true
north!
:param true_north: A unitised 2D vector, where each ordinate is a float, or
an angle in decimal degrees where anticlockwise is positive.
Example:
.. code:: python
# Both of these are identical, and indicate that:
# - If project north is up the page, true north is in the top left
# - The building is therefore facing north east
ifcopenshell.api.georeference.edit_true_north(model, true_north=30)
ifcopenshell.api.georeference.edit_true_north(model, true_north=(-0.5, 0.8660254))
# This unsets true north
ifcopenshell.api.georeference.edit_true_north(model, true_north=None)
"""
if isinstance(true_north, (float, int)):
x, y = ifcopenshell.util.geolocation.angle2yaxis(true_north)
elif true_north is not None:
x, y = true_north
for context in file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.TrueNorth and true_north is None:
old_true_north = context.TrueNorth
context.TrueNorth = None
if not file.get_total_inverses(old_true_north):
ifcopenshell.util.element.remove_deep2(file, old_true_north)
continue
if context.TrueNorth:
if file.get_total_inverses(context.TrueNorth) != 1:
context.TrueNorth = file.create_entity("IfcDirection")
else:
context.TrueNorth = file.create_entity("IfcDirection")
context.TrueNorth.DirectionRatios = (x, y)
@@ -0,0 +1,95 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from math import cos, radians, sin
import numpy as np
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.unit
from ifcopenshell.util.shape_builder import ShapeBuilder
def edit_wcs(
file: ifcopenshell.file,
x: float = 0.0,
y: float = 0.0,
z: float = 0.0,
rotation: float = 0.0,
is_si: bool = True,
) -> None:
"""Edits the WCS for all geometric contexts to a translation and rotation
Typically, a project's local engineering origin (0, 0, 0) has a coordinate
operation (e.g. map conversion) to a projected CRS. If a WCS is provided,
the coordinate operation is relative to the WCS, not the local engineering
origin.
For example, if I have an IfcSite with a placement at (10, 0, 0) and a map
conversion of (50, 0, 0), my IfcSite's local XYZ is at (10, 0, 0) with an
ENH (Easting, Northing, Height) of (60, 0, 0).
If I then define by WCS at (15, 0, 0), my IfcSite's local XYZ is still at
(10, 0, 0) but its ENH is now at (45, 0, 0).
It's recommended to leave the WCS at 0,0,0. Please :)
:param x: The X translation of the WCS
:param y: The Y translation of the WCS
:param z: The Z translation of the WCS
:param rotation: The rotation around the Z axis (i.e. top down plan view)
in decimal degrees of the WCS. Anticlockwise is positive.
Example:
.. code:: python
# This is the simplest scenario, resetting the WCS to 0,0,0 with no rotation (recommended)
ifcopenshell.api.georeference.edit_wcs(model)
"""
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
builder = ShapeBuilder(file)
if np.isclose(rotation, 0):
xaxis_x = 1.0
xaxis_y = 0.0
else:
xaxis_x = cos(radians(rotation))
xaxis_y = sin(radians(rotation))
if np.allclose((x, y, z), (0, 0, 0)):
x = y = z = 0.0
for context in file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
old_wcs = context.WorldCoordinateSystem
if context.CoordinateSpaceDimension == 3:
if is_si:
xyz = (x / unit_scale, y / unit_scale, z / unit_scale)
else:
xyz = (x, y, z)
placement = builder.create_axis2_placement_3d(xyz, (0.0, 0.0, 1.0), (xaxis_x, xaxis_y, 0.0))
elif context.CoordinateSpaceDimension == 2:
if is_si:
point = file.createIfcCartesianPoint((x / unit_scale, y / unit_scale))
else:
point = file.createIfcCartesianPoint((x, y))
placement = file.createIfcAxis2Placement2D(
point,
file.createIfcDirection((xaxis_x, xaxis_y)),
)
context.WorldCoordinateSystem = placement
if file.get_total_inverses(old_wcs) == 0:
ifcopenshell.util.element.remove_deep2(file, old_wcs)
@@ -0,0 +1,51 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.pset
import ifcopenshell.util.element
def remove_georeferencing(file: ifcopenshell.file) -> None:
"""Remove georeferencing data
All georeferencing parameters such as projected CRS and map conversion
data will be lost.
In IFC2X3, the psets will be removed from the IfcProject.
Example:
ifcopenshell.api.georeference.add_georeferencing(model)
# Let's change our mind
ifcopenshell.api.georeference.remove_georeferencing(model)
"""
if file.schema == "IFC2X3":
project = file.by_type("IfcProject")[0]
if pset := ifcopenshell.util.element.get_pset(project, "ePSet_ProjectedCRS"):
ifcopenshell.api.pset.remove_pset(file, project, file.by_id(pset["id"]))
if pset := ifcopenshell.util.element.get_pset(project, "ePSet_MapConversion"):
ifcopenshell.api.pset.remove_pset(file, project, file.by_id(pset["id"]))
return
for projected_crs in file.by_type("IfcProjectedCRS"):
if (unit := projected_crs.MapUnit) and file.get_total_inverses(unit) == 1:
projected_crs.MapUnit = None
ifcopenshell.util.element.remove_deep2(file, unit)
file.remove(projected_crs)
for coordinate_operation in file.by_type("IfcCoordinateOperation"):
file.remove(coordinate_operation)