Files
IfcOpenShell/src/ifcopenshell-python/ifcopenshell/file.py
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

578 lines
22 KiB
Python
Raw Normal View History

# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.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/>.
2016-06-22 15:03:18 +02:00
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
2022-11-26 05:19:01 +01:00
import os
import re
2016-06-22 15:03:18 +02:00
import numbers
2022-11-26 05:19:01 +01:00
import zipfile
import functools
from pathlib import Path
2016-06-22 15:03:18 +02:00
2022-11-26 05:19:01 +01:00
import ifcopenshell.util.element
import ifcopenshell.util.file
2016-06-22 15:03:18 +02:00
from . import ifcopenshell_wrapper
from .entity_instance import entity_instance
2017-09-25 14:19:59 +02:00
try:
# Python 2
basestring
except NameError:
# Python 3 or newer
basestring = (str, bytes)
2017-11-06 09:10:28 +01:00
class Transaction:
def __init__(self, ifc_file):
self.file = ifc_file
self.operations = []
self.is_batched = False
self.batch_delete_index = 0
self.batch_delete_ids = set()
self.batch_inverses = []
def serialise_entity_instance(self, element):
info = element.get_info()
for key, value in info.items():
info[key] = self.serialise_value(element, value)
return info
def serialise_value(self, element, value):
return element.walk(
lambda v: isinstance(v, entity_instance),
lambda v: {"id": v.id()} if v.id() else {"type": v.is_a(), "value": v.wrappedValue},
value,
)
def unserialise_value(self, element, value):
return element.walk(
lambda v: isinstance(v, dict),
lambda v: self.file.by_id(v["id"]) if v.get("id") else self.file.create_entity(v["type"], v["value"]),
value,
)
def batch(self):
self.is_batched = True
self.batch_delete_index = len(self.operations)
self.batch_delete_ids = set()
self.batch_inverses = []
def unbatch(self):
for inverses in self.batch_inverses:
if inverses:
self.operations.insert(self.batch_delete_index, {"action": "batch_delete", "inverses": inverses})
self.is_batched = False
self.batch_delete_index = 0
self.batch_delete_ids = set()
self.batch_inverses = []
def store_create(self, element):
if element.id():
self.operations.append({"action": "create", "value": self.serialise_entity_instance(element)})
def store_edit(self, element, index, value):
if element.id():
self.operations.append(
{
"action": "edit",
"id": element.id(),
"index": index,
"old": self.serialise_value(element, element[index]),
"new": self.serialise_value(element, value),
}
)
def store_delete(self, element):
inverses = {}
if self.is_batched:
if element.id() not in self.batch_delete_ids:
self.batch_inverses.append(self.get_element_inverses(element))
self.batch_delete_ids.add(element.id())
else:
inverses = self.get_element_inverses(element)
self.operations.append(
{"action": "delete", "inverses": inverses, "value": self.serialise_entity_instance(element)}
)
def get_element_inverses(self, element):
inverses = {}
for inverse in self.file.get_inverse(element):
inverse_references = []
for i, attribute in enumerate(inverse):
2021-08-31 17:28:31 +10:00
if ifcopenshell.util.element.has_element_reference(attribute, element):
inverse_references.append((i, self.serialise_value(inverse, attribute)))
inverses[inverse.id()] = inverse_references
return inverses
def rollback(self):
for operation in self.operations[::-1]:
if operation["action"] == "create":
element = self.file.by_id(operation["value"]["id"])
if hasattr(element, "GlobalId") and element.GlobalId is None:
# hack, otherwise ifcopenshell gets upset
element.GlobalId = "x"
self.file.remove(element)
elif operation["action"] == "edit":
element = self.file.by_id(operation["id"])
try:
element[operation["index"]] = self.unserialise_value(element, operation["old"])
except:
# Catch discrepancy where IfcOpenShell creates but doesn't allow editing of invalid values
pass
elif operation["action"] == "delete":
e = self.file.create_entity(operation["value"]["type"], id=operation["value"]["id"])
for k, v in operation["value"].items():
try:
setattr(e, k, self.unserialise_value(e, v))
except:
# Catch discrepancy where IfcOpenShell creates but doesn't allow editing of invalid values
pass
for inverse_id, data in operation["inverses"].items():
inverse = self.file.by_id(inverse_id)
for index, value in data:
inverse[index] = self.unserialise_value(inverse, value)
elif operation["action"] == "batch_delete":
for inverse_id, data in operation["inverses"].items():
inverse = self.file.by_id(inverse_id)
for index, value in data:
inverse[index] = self.unserialise_value(inverse, value)
def commit(self):
for operation in self.operations:
if operation["action"] == "create":
e = self.file.create_entity(operation["value"]["type"], id=operation["value"]["id"])
for k, v in operation["value"].items():
try:
setattr(e, k, self.unserialise_value(e, v))
except:
# Catch discrepancy where IfcOpenShell creates but doesn't allow editing of invalid values
pass
elif operation["action"] == "edit":
element = self.file.by_id(operation["id"])
element[operation["index"]] = self.unserialise_value(element, operation["new"])
elif operation["action"] == "delete":
element = self.file.by_id(operation["value"]["id"])
self.file.remove(element)
elif operation["action"] == "batch_delete":
pass
2023-07-08 12:07:28 +02:00
file_dict = {}
2016-06-22 15:03:18 +02:00
class file(object):
"""Base class for containing IFC files.
2017-12-04 09:16:30 -08:00
Class has instance methods for filtering by element Id, Type, etc.
Instantiated objects can be subscripted by Id or Guid
2023-01-10 10:16:28 +11:00
Example:
.. code:: python
2017-12-04 09:16:30 -08:00
ifc_file = ifcopenshell.open(file_path)
products = ifc_file.by_type("IfcProduct")
print(products[0].id(), products[0].GlobalId) # 122 2XQ$n5SLP5MBLyL442paFx
print(products[0] == ifc_file[122] == ifc_file["2XQ$n5SLP5MBLyL442paFx"]) # True
2017-12-04 09:16:30 -08:00
"""
2020-11-01 20:08:27 +07:00
def __init__(self, f=None, schema=None, schema_version=None):
"""Create a new blank IFC model
This IFC model does not have any entities in it yet. See the
``create_entity`` function for how to create new entities. All data is
stored in memory. If you wish to write the IFC model to disk, see the
``write`` function.
:param f: The underlying IfcOpenShell file object to be wrapped. This
is an internal implementation detail and should generally be left
as None by users.
:param schema: Which IFC schema to use, chosen from "IFC2X3", "IFC4",
or "IFC4X3". These refer to the ISO approved versions of IFC.
Defaults to "IFC4" if not specified, which is currently recommended
for all new projects.
:type schema: string
:param schema_version: If you want to specify an exact version of IFC
that may not be an ISO approved version, use this argument instead
of ``schema``. IFC versions on technical.buildingsmart.org are
described using 4 integers representing the major, minor, addendum,
and corrigendum number. For example, (4, 0, 2, 1) refers to IFC4
ADD2 TC1, which is the official version approved by ISO when people
refer to "IFC4". Generally you should not use this argument unless
you are testing non-ISO IFC releases.
:type schema_version: tuple[int]
Example:
.. code:: python
# Create a new IFC4 model, create a wall, then save it to an IFC-SPF file.
model = ifcopenshell.file()
model.create_entity("IfcWall")
model.write("/path/to/model.ifc")
# Create a new IFC4X3 model
model = ifcopenshell.file(schema="IFC4X3")
# A poweruser testing out a particular version of IFC4X3
model = ifcopenshell.file(schema_version=(4, 3, 0, 1))
"""
if schema_version:
prefixes = ("IFC", "X", "_ADD", "_TC")
schema = "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, schema_version))
else:
schema = {"IFC4X3": "IFC4X3_ADD1"}.get(schema, schema)
2017-12-31 12:20:13 +01:00
if f is not None:
self.wrapped_data = f
else:
args = filter(None, [schema])
args = map(ifcopenshell_wrapper.schema_by_name, args)
self.wrapped_data = ifcopenshell_wrapper.file(*args)
self.history_size = 64
self.history = []
self.future = []
self.transaction = None
2024-01-16 09:31:35 +01:00
file_dict[self.file_pointer()] = self
2023-07-08 12:07:28 +02:00
def set_history_size(self, size):
self.history_size = size
while len(self.history) > self.history_size:
self.history.pop(0)
def begin_transaction(self):
2022-09-01 11:16:33 +10:00
if self.history_size:
self.transaction = Transaction(self)
def end_transaction(self):
if self.transaction:
self.history.append(self.transaction)
if len(self.history) > self.history_size:
self.history.pop(0)
self.future = []
self.transaction = None
def discard_transaction(self):
if self.transaction:
self.transaction.rollback()
self.transaction = None
def undo(self):
if not self.history:
return
transaction = self.history.pop()
transaction.rollback()
self.future.append(transaction)
def redo(self):
if not self.future:
return
transaction = self.future.pop()
transaction.commit()
self.history.append(transaction)
2017-11-06 09:10:28 +01:00
def create_entity(self, type, *args, **kwargs):
"""Create a new IFC entity in the file.
:param type: Case insensitive name of the IFC class
:type type: string
:param args: The positional arguments of the IFC class
:param kwargs: The keyword arguments of the IFC class
:returns: An entity instance
:rtype: ifcopenshell.entity_instance.entity_instance
2023-01-10 10:16:28 +11:00
Example:
.. code:: python
f = ifcopenshell.file()
f.create_entity("IfcPerson")
# >>> #1=IfcPerson($,$,$,$,$,$,$,$)
f.create_entity("IfcPerson", "Foobar")
# >>> #2=IfcPerson('Foobar',$,$,$,$,$,$,$)
f.create_entity("IfcPerson", Identification="Foobar")
# >>> #3=IfcPerson('Foobar',$,$,$,$,$,$,$)
"""
2021-08-31 17:28:31 +10:00
eid = kwargs.pop("id", -1)
2021-07-17 09:38:31 +02:00
e = entity_instance((self.schema_identifier, type), self)
2021-07-17 09:38:31 +02:00
# Create pairs of {attribute index, attribute value}.
# Keyword arguments are mapped to their corresponding
# numeric index with get_argument_index().
# @todo we should probably check that values for
# attributes are not passed as duplicates using
# both regular arguments and keyword arguments.
2020-11-01 20:08:27 +07:00
attrs = list(enumerate(args)) + [(e.wrapped_data.get_argument_index(name), arg) for name, arg in kwargs.items()]
2021-07-17 09:38:31 +02:00
# Don't store these attributes as transactions
# as the creation it self is already stored with
# it's arguments
if attrs:
transaction = self.transaction
self.transaction = None
2021-07-17 09:38:31 +02:00
2017-11-06 09:10:28 +01:00
for idx, arg in attrs:
e[idx] = arg
2021-07-17 09:38:31 +02:00
# Restore transaction status
if attrs:
self.transaction = transaction
2021-07-17 09:38:31 +02:00
# Once the values are populated add the instance
# to the file.
self.wrapped_data.add(e.wrapped_data, eid)
# The file container now handles the lifetime of
# this instance. Tell SWIG that it is no longer
# the owner.
e.wrapped_data.this.disown()
2021-07-17 18:38:04 +10:00
if self.transaction:
self.transaction.store_create(e)
return e
2017-11-06 09:10:28 +01:00
def __getattr__(self, attr):
2020-11-01 20:08:27 +07:00
if attr[0:6] == "create":
2017-11-06 09:10:28 +01:00
return functools.partial(self.create_entity, attr[6:])
elif attr == "schema":
2024-01-04 15:05:40 +01:00
prefixes = ("IFC", "X", "_ADD", "_TC")
reg = "".join(f"(?P<{s}>{s}\d+)?" for s in prefixes)
match = re.match(reg, self.wrapped_data.schema)
version_tuple = tuple(
map(
lambda pp: int(pp[1][len(pp[0]):]) if pp[1] else None,
((p, match.group(p)) for p in prefixes),
)
)
return "".join(
"".join(map(str, t)) if t[1] else ""
for t in zip(prefixes, version_tuple[0:2])
)
elif attr == "schema_identifier":
return self.wrapped_data.schema
elif attr == "schema_version":
schema = self.wrapped_data.schema
version = []
for prefix in ("IFC", "X", "_ADD", "_TC"):
number = re.search(prefix + r"(\d)", schema)
version.append(int(number.group(1)) if number else 0)
return tuple(version)
2017-11-06 09:10:28 +01:00
else:
return getattr(self.wrapped_data, attr)
def __getitem__(self, key):
if isinstance(key, numbers.Integral):
return entity_instance(self.wrapped_data.by_id(key), self)
2017-08-04 16:31:10 +02:00
elif isinstance(key, basestring):
return entity_instance(self.wrapped_data.by_guid(str(key)), self)
2017-11-06 09:10:28 +01:00
2017-11-06 11:06:40 +01:00
def by_id(self, id):
2020-05-21 08:25:47 +10:00
"""Return an IFC entity instance filtered by IFC ID.
2017-12-04 09:16:30 -08:00
:param id: STEP numerical identifier
:type id: int
2020-05-21 08:25:47 +10:00
:returns: An ifcopenshell.entity_instance.entity_instance
:rtype: ifcopenshell.entity_instance.entity_instance
2017-12-04 09:16:30 -08:00
"""
2017-11-06 11:06:40 +01:00
return self[id]
2017-11-06 09:10:28 +01:00
2017-11-06 11:06:40 +01:00
def by_guid(self, guid):
2020-05-21 08:25:47 +10:00
"""Return an IFC entity instance filtered by IFC GUID.
2017-12-04 09:16:30 -08:00
:param guid: GlobalId value in 22-character encoded form
:type guid: string
2020-05-21 08:25:47 +10:00
:returns: An ifcopenshell.entity_instance.entity_instance
:rtype: ifcopenshell.entity_instance.entity_instance
2017-12-04 09:16:30 -08:00
"""
2017-11-06 11:06:40 +01:00
return self[guid]
2017-11-06 09:10:28 +01:00
def add(self, inst, _id=None):
2020-05-21 08:25:47 +10:00
"""Adds an entity including any dependent entities to an IFC file.
2020-08-09 17:32:54 +10:00
2020-05-21 08:25:47 +10:00
If the entity already exists, it is not re-added."""
if self.transaction:
2021-09-21 10:33:03 +10:00
max_id = self.wrapped_data.getMaxId()
inst.wrapped_data.this.disown()
result = entity_instance(self.wrapped_data.add(inst.wrapped_data, -1 if _id is None else _id), self)
if self.transaction:
added_elements = [e for e in self.traverse(result) if e.id() > max_id]
[self.transaction.store_create(e) for e in reversed(added_elements)]
return result
2017-11-06 09:10:28 +01:00
2020-08-09 17:32:54 +10:00
def by_type(self, type, include_subtypes=True):
"""Return IFC objects filtered by IFC Type and wrapped with the entity_instance class.
2017-12-04 09:16:30 -08:00
2020-04-02 06:04:38 +02:00
If an IFC type class has subclasses, all entities of those subclasses are also returned.
:param type: The case insensitive type of IFC class to return.
:type type: string
2020-08-09 17:32:54 +10:00
:param include_subtypes: Whether or not to return subtypes of the IFC class
:type include_subtypes: bool
:returns: A list of ifcopenshell.entity_instance.entity_instance objects
:rtype: list
2017-12-04 09:16:30 -08:00
"""
2020-08-09 17:32:54 +10:00
if include_subtypes:
return [entity_instance(e, self) for e in self.wrapped_data.by_type(type)]
return [entity_instance(e, self) for e in self.wrapped_data.by_type_excl_subtypes(type)]
2017-11-06 09:10:28 +01:00
def traverse(self, inst, max_levels=None, breadth_first=False):
2020-05-21 08:25:47 +10:00
"""Get a list of all referenced instances for a particular instance including itself
:param inst: The entity instance to get all sub instances
:type inst: ifcopenshell.entity_instance.entity_instance
:param max_levels: How far deep to recursively fetch sub instances. None or -1 means infinite.
:type max_levels: None|int
:param breadth_first: Whether to use breadth-first search, the default is depth-first.
:type max_levels: bool
:returns: A list of ifcopenshell.entity_instance.entity_instance objects
:rtype: list
"""
if max_levels is None:
max_levels = -1
if breadth_first:
fn = self.wrapped_data.traverse_breadth_first
else:
fn = self.wrapped_data.traverse
return [entity_instance(e, self) for e in fn(inst.wrapped_data, max_levels)]
2017-11-06 09:10:28 +01:00
def get_inverse(self, inst, allow_duplicate=False, with_attribute_indices=False):
"""Return a list of entities that reference this entity
:param inst: The entity instance to get inverse relationships
:type inst: ifcopenshell.entity_instance.entity_instance
:param allow_duplicate: Returns a `list` when True, `set` when False
:param with_attribute_indices: Returns pairs of <i, idx>
where i[idx] is inst or contains inst. Requires allow_duplicate=True
:returns: A list of ifcopenshell.entity_instance.entity_instance objects
:rtype: list
"""
if with_attribute_indices and not allow_duplicate:
raise ValueError("with_attribute_indices requires allow_duplicate to be True")
inverses = [entity_instance(e, self) for e in self.wrapped_data.get_inverse(inst.wrapped_data)]
if allow_duplicate:
if with_attribute_indices:
idxs = self.wrapped_data.get_inverse_indices(inst.wrapped_data)
return list(zip(inverses, idxs))
else:
return inverses
return set(inverses)
2017-11-06 09:10:28 +01:00
def get_total_inverses(self, inst):
"""Returns the number of entities that reference this entity
:param inst: The entity instance to get inverse relationships
:type inst: ifcopenshell.entity_instance.entity_instance
:returns: The total number of references
:rtype: int
"""
return self.wrapped_data.get_total_inverses(inst.wrapped_data)
def remove(self, inst):
"""Deletes an IFC object in the file.
2020-05-21 08:25:47 +10:00
Attribute values in other entity instances that reference the deleted
object will be set to null. In the case of a list or set of references,
the reference to the deleted will be removed from the aggregate.
:param inst: The entity instance to delete
:type inst: ifcopenshell.entity_instance.entity_instance
:rtype: None
"""
if self.transaction:
self.transaction.store_delete(inst)
return self.wrapped_data.remove(inst.wrapped_data)
2021-03-13 12:16:40 +01:00
def batch(self):
"""Low-level mechanism to speed up deletion of large subgraphs"""
if self.transaction:
self.transaction.batch()
2021-03-13 12:16:40 +01:00
return self.wrapped_data.batch()
2021-03-13 12:16:40 +01:00
def unbatch(self):
"""Low-level mechanism to speed up deletion of large subgraphs"""
if self.transaction:
self.transaction.unbatch()
2021-03-13 12:16:40 +01:00
return self.wrapped_data.unbatch()
2017-11-06 09:10:28 +01:00
def __iter__(self):
return iter(self[id] for id in self.wrapped_data.entity_names())
2017-11-06 09:10:28 +01:00
2022-11-26 05:19:01 +01:00
def write(self, path: "os.PathLike | str", format=None, zipped=False) -> None:
"""Write ifc model to file.
:param format: Force use of a specific format. Guessed from file name if None.
Supported formats : .ifc, .ifcXML, .ifcZIP (equivalent to format=".ifc" with zipped=True)
For zipped .ifcXML use format=".ifcXML" with zipped=True
:param zipped: zip the file after it is written
Examples:
>>> model.write("path/to/model.ifc")
>>> model.write("path/to/model.ifcXML")
>>> model.write("path/to/model.ifcZIP")
>>> model.write("path/to/model.ifcZIP", format=".ifcXML", zipped=True)
>>> model.write("path/to/model.anyextension", format=".ifcXML")
"""
path = Path(path)
2022-12-19 01:44:40 +01:00
path.parent.mkdir(parents=True, exist_ok=True)
2022-11-26 05:19:01 +01:00
if format == None:
format = ifcopenshell.util.file.guess_format(path)
if format == ".ifcXML":
serializer = ifcopenshell_wrapper.XmlSerializer(self, str(path))
serializer.finalize()
if zipped:
unzipped_path = path.with_suffix(format)
path.rename(unzipped_path)
with zipfile.ZipFile(path, "w") as zip_file:
zip_file.write(unzipped_path, unzipped_path.name, compress_type=zipfile.ZIP_DEFLATED)
unzipped_path.unlink()
return
if format == ".ifcZIP":
return self.write(path, ".ifc", zipped=True)
self.wrapped_data.write(str(path))
if zipped:
unzipped_path = path.with_suffix(format)
path.rename(unzipped_path)
with zipfile.ZipFile(path, "w") as zip_file:
2023-07-08 12:07:28 +02:00
zip_file.write(
unzipped_path,
unzipped_path.name,
compress_type=zipfile.ZIP_DEFLATED,
)
2022-11-26 05:19:01 +01:00
unzipped_path.unlink()
return
2017-08-22 10:16:07 +02:00
@staticmethod
def from_string(s):
return file(ifcopenshell_wrapper.read(s))
2023-07-08 12:07:28 +02:00
@staticmethod
def from_pointer(v):
return file_dict.get(v)