2022-01-19 12:18:33 +11:00
|
|
|
# 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/>.
|
|
|
|
|
|
2020-12-31 11:39:33 +01:00
|
|
|
import re
|
2021-08-25 17:22:02 +10:00
|
|
|
import pathlib
|
2020-11-03 09:53:03 +11:00
|
|
|
import ifcopenshell
|
2025-02-07 17:52:28 +05:00
|
|
|
import ifcopenshell.ifcopenshell_wrapper as W
|
2021-08-25 17:22:02 +10:00
|
|
|
import ifcopenshell.util.schema
|
2022-02-18 17:11:24 +11:00
|
|
|
import ifcopenshell.util.type
|
2020-12-31 11:39:33 +01:00
|
|
|
from ifcopenshell.entity_instance import entity_instance
|
2021-08-25 17:22:02 +10:00
|
|
|
from functools import lru_cache
|
2025-02-07 17:52:28 +05:00
|
|
|
from typing import Optional, Literal
|
2020-12-31 11:39:33 +01:00
|
|
|
|
2024-05-07 14:58:12 +10:00
|
|
|
templates: dict[str, "PsetQto"] = {}
|
2021-04-29 19:35:21 +10:00
|
|
|
|
|
|
|
|
|
2024-05-07 14:58:12 +10:00
|
|
|
def get_template(schema: str) -> "PsetQto":
|
2021-04-29 19:35:21 +10:00
|
|
|
global templates
|
|
|
|
|
if schema not in templates:
|
|
|
|
|
templates[schema] = PsetQto(schema)
|
|
|
|
|
return templates[schema]
|
|
|
|
|
|
2020-12-31 11:39:33 +01:00
|
|
|
|
|
|
|
|
class PsetQto:
|
2024-03-26 12:36:13 +05:00
|
|
|
# fmt: off
|
2020-12-31 11:39:33 +01:00
|
|
|
templates_path = {
|
2023-02-21 11:50:09 +05:00
|
|
|
"IFC2X3": "Pset_IFC2X3.ifc",
|
2020-12-31 11:39:33 +01:00
|
|
|
"IFC4": "Pset_IFC4_ADD2.ifc",
|
2024-06-15 14:05:37 +10:00
|
|
|
"IFC4X3_ADD2": "Pset_IFC4X3.ifc"
|
2020-12-31 11:39:33 +01:00
|
|
|
}
|
2024-03-26 12:36:13 +05:00
|
|
|
# fmt: on
|
2020-12-31 11:39:33 +01:00
|
|
|
|
2024-06-15 14:05:37 +10:00
|
|
|
def __init__(self, schema_identifier: str, templates=None) -> None:
|
2024-11-12 14:52:37 +05:00
|
|
|
if schema_identifier == "IFC4X3":
|
|
|
|
|
schema_identifier = "IFC4X3_ADD2"
|
2024-06-15 14:05:37 +10:00
|
|
|
self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_identifier)
|
2020-12-31 11:39:33 +01:00
|
|
|
if not templates:
|
|
|
|
|
folder_path = pathlib.Path(__file__).parent.absolute()
|
2024-06-15 14:05:37 +10:00
|
|
|
path = str(folder_path.joinpath("schema", self.templates_path[schema_identifier]))
|
2020-12-31 11:39:33 +01:00
|
|
|
templates = [ifcopenshell.open(path)]
|
2023-08-20 20:50:32 +10:00
|
|
|
# See bug 3583. We backport this change from IFC4X3 because it just makes sense.
|
|
|
|
|
# Users aren't forced to use it.
|
2024-06-15 14:05:37 +10:00
|
|
|
if schema_identifier == "IFC4":
|
2023-08-20 20:50:32 +10:00
|
|
|
for element in templates[0].by_type("IfcPropertySetTemplate"):
|
|
|
|
|
if element.TemplateType == "QTO_OCCURRENCEDRIVEN":
|
|
|
|
|
element.TemplateType = "QTO_TYPEDRIVENOVERRIDE"
|
2020-12-31 11:39:33 +01:00
|
|
|
self.templates = templates
|
|
|
|
|
|
2021-01-04 15:42:19 +11:00
|
|
|
@lru_cache()
|
2020-12-31 11:39:33 +01:00
|
|
|
def get_applicable(
|
2025-02-07 17:52:28 +05:00
|
|
|
self,
|
|
|
|
|
ifc_class="",
|
|
|
|
|
predefined_type="",
|
|
|
|
|
pset_only=False,
|
|
|
|
|
qto_only=False,
|
|
|
|
|
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4",
|
|
|
|
|
) -> list[entity_instance]:
|
2020-12-31 11:39:33 +01:00
|
|
|
any_class = not ifc_class
|
|
|
|
|
if not any_class:
|
2025-02-07 17:52:28 +05:00
|
|
|
entity: W.entity
|
2020-12-31 11:39:33 +01:00
|
|
|
entity = self.schema.declaration_by_name(ifc_class)
|
2021-03-16 23:23:06 +01:00
|
|
|
result = []
|
2020-12-31 11:39:33 +01:00
|
|
|
for template in self.templates:
|
|
|
|
|
for prop_set in template.by_type("IfcPropertySetTemplate"):
|
|
|
|
|
if pset_only:
|
2023-07-28 17:41:36 +10:00
|
|
|
if prop_set.TemplateType and prop_set.TemplateType.startswith("QTO_"):
|
2020-12-31 11:39:33 +01:00
|
|
|
continue
|
|
|
|
|
if qto_only:
|
2023-07-28 17:41:36 +10:00
|
|
|
if prop_set.TemplateType and prop_set.TemplateType.startswith("PSET_"):
|
2020-12-31 11:39:33 +01:00
|
|
|
continue
|
2022-02-18 17:10:38 +11:00
|
|
|
if any_class or self.is_applicable(
|
2025-02-07 17:52:28 +05:00
|
|
|
entity, prop_set.ApplicableEntity or "IfcRoot", predefined_type, prop_set.TemplateType, schema
|
2022-02-18 17:10:38 +11:00
|
|
|
):
|
2021-03-16 23:23:06 +01:00
|
|
|
result.append(prop_set)
|
|
|
|
|
return result
|
2020-12-31 11:39:33 +01:00
|
|
|
|
2021-03-16 23:23:06 +01:00
|
|
|
@lru_cache()
|
2025-02-07 17:52:28 +05:00
|
|
|
def get_applicable_names(
|
|
|
|
|
self,
|
|
|
|
|
ifc_class: str,
|
|
|
|
|
predefined_type: str = "",
|
|
|
|
|
pset_only: bool = False,
|
|
|
|
|
qto_only: bool = False,
|
|
|
|
|
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4",
|
|
|
|
|
) -> list[str]:
|
2020-12-31 11:39:33 +01:00
|
|
|
"""Return names instead of objects for other use eg. enum"""
|
2025-02-07 17:52:28 +05:00
|
|
|
return [
|
|
|
|
|
prop_set.Name for prop_set in self.get_applicable(ifc_class, predefined_type, pset_only, qto_only, schema)
|
|
|
|
|
]
|
2020-12-31 11:39:33 +01:00
|
|
|
|
2022-02-18 17:10:38 +11:00
|
|
|
def is_applicable(
|
2025-02-07 17:52:28 +05:00
|
|
|
self,
|
|
|
|
|
entity: W.entity,
|
|
|
|
|
applicables: str,
|
|
|
|
|
predefined_type: str = "",
|
|
|
|
|
template_type: str = "NOTDEFINED",
|
|
|
|
|
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4",
|
2022-02-18 17:10:38 +11:00
|
|
|
) -> bool:
|
2020-12-31 11:39:33 +01:00
|
|
|
"""applicables can have multiple possible patterns :
|
|
|
|
|
IfcBoilerType (IfcClass)
|
|
|
|
|
IfcBoilerType/STEAM (IfcClass/PREDEFINEDTYPE)
|
|
|
|
|
IfcBoilerType[PerformanceHistory] (IfcClass[PerformanceHistory])
|
|
|
|
|
IfcBoilerType/STEAM[PerformanceHistory] (IfcClass/PREDEFINEDTYPE[PerformanceHistory])
|
|
|
|
|
"""
|
|
|
|
|
for applicable in applicables.split(","):
|
|
|
|
|
match = re.match(r"(\w+)(\[\w+\])*/*(\w+)*(\[\w+\])*", applicable)
|
|
|
|
|
if not match:
|
|
|
|
|
continue
|
|
|
|
|
# Uncomment if usage found
|
|
|
|
|
# applicable_perf_history = match.group(2) or match.group(4)
|
2022-02-18 17:10:38 +11:00
|
|
|
matched_type = match.group(3)
|
|
|
|
|
if matched_type and not predefined_type:
|
|
|
|
|
continue
|
2024-05-26 11:23:08 +10:00
|
|
|
# Case insensitive to handle things like material categories
|
|
|
|
|
elif matched_type and predefined_type.lower() != match.group(3).lower():
|
2020-12-31 11:39:33 +01:00
|
|
|
continue
|
2020-11-03 09:53:03 +11:00
|
|
|
|
2020-12-31 11:39:33 +01:00
|
|
|
applicable_class = match.group(1)
|
2021-08-25 17:22:02 +10:00
|
|
|
if ifcopenshell.util.schema.is_a(entity, applicable_class):
|
2020-12-31 11:39:33 +01:00
|
|
|
return True
|
2022-02-18 17:11:24 +11:00
|
|
|
# There is an implementer agreement that if the template type is
|
|
|
|
|
# type based, the type need not be explicitly mentioned
|
|
|
|
|
# https://github.com/buildingSMART/IFC4.3.x-development/issues/22
|
|
|
|
|
# This will be fixed in IFC4.3
|
|
|
|
|
template_type = template_type or ""
|
|
|
|
|
if "TYPE" in template_type and ifcopenshell.util.schema.is_a(entity, "IfcTypeObject"):
|
2025-02-07 17:52:28 +05:00
|
|
|
types = ifcopenshell.util.type.get_applicable_types(applicable_class, schema)
|
2023-11-24 14:02:16 +11:00
|
|
|
if not types:
|
|
|
|
|
# Abstract classes will not have an "applicable type" but
|
|
|
|
|
# the implementer agreement still applies to them.
|
|
|
|
|
occurrence_class = None
|
|
|
|
|
try:
|
|
|
|
|
occurrence_class = self.schema.declaration_by_name(applicable_class + "Type")
|
|
|
|
|
except:
|
|
|
|
|
try:
|
|
|
|
|
occurrence_class = self.schema.declaration_by_name("IfcType" + applicable_class[3:])
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
if occurrence_class:
|
|
|
|
|
types = [occurrence_class.name()]
|
2022-02-18 17:11:24 +11:00
|
|
|
for ifc_type in types:
|
|
|
|
|
if ifcopenshell.util.schema.is_a(entity, ifc_type):
|
|
|
|
|
return True
|
2020-12-31 11:39:33 +01:00
|
|
|
return False
|
2021-01-04 02:13:04 +01:00
|
|
|
|
2021-01-04 15:42:19 +11:00
|
|
|
@lru_cache()
|
2021-01-04 02:13:04 +01:00
|
|
|
def get_by_name(self, name: str) -> Optional[entity_instance]:
|
|
|
|
|
for template in self.templates:
|
|
|
|
|
for prop_set in template.by_type("IfcPropertySetTemplate"):
|
|
|
|
|
if prop_set.Name == name:
|
|
|
|
|
return prop_set
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def is_templated(self, name: str) -> bool:
|
|
|
|
|
return bool(self.get_by_name(name))
|
2024-11-15 18:09:19 +05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_pset_template_type(pset_template: entity_instance) -> Literal["PSET", "QTO", None]:
|
|
|
|
|
"""Get the type of the pset template.
|
|
|
|
|
If type is mixed or not defined, return None."""
|
|
|
|
|
|
|
|
|
|
# Try to identify whether it's pset or qto from the template type.
|
|
|
|
|
template_type = pset_template.TemplateType
|
|
|
|
|
if template_type:
|
|
|
|
|
if template_type.startswith("PSET_"):
|
|
|
|
|
return "PSET"
|
|
|
|
|
elif template_type.startswith("QTO_"):
|
|
|
|
|
return "QTO"
|
|
|
|
|
# Can also be 'NOTDEFINED'.
|
|
|
|
|
|
|
|
|
|
pset_types = set()
|
|
|
|
|
for prop in pset_template.HasPropertyTemplates:
|
|
|
|
|
prop_template_type = prop.TemplateType
|
|
|
|
|
if prop_template_type:
|
|
|
|
|
if prop_template_type.startswith("P_"):
|
|
|
|
|
pset_types.add("PSET")
|
|
|
|
|
else: # All other values are Q_.
|
|
|
|
|
pset_types.add("QTO")
|
|
|
|
|
|
|
|
|
|
pset_type = next(iter(pset_types)) if len(pset_types) == 1 else None
|
|
|
|
|
return pset_type
|