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

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

1404 lines
52 KiB
Python
Raw Normal View History

2021-08-24 12:57:44 +02:00
# IDS - Information Delivery Specification.
# Copyright (C) 2021 Artur Tomczak <artomczak@gmail.com>, Thomas Krijnen <mail@thomaskrijnen.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 os
2021-08-24 12:57:44 +02:00
import re
import logging
2021-11-23 17:53:32 +01:00
import operator
2021-08-24 12:57:44 +02:00
import numpy as np
2022-02-23 00:24:40 +01:00
import datetime
2021-08-24 12:57:44 +02:00
2021-02-27 11:04:45 +01:00
import ifcopenshell.util.element
2021-08-24 12:57:44 +02:00
import ifcopenshell.util.placement
from bcf.v2.bcfxml import BcfXml
from bcf.v2 import data as bcf
2021-06-08 10:34:02 +02:00
from xmlschema import XMLSchema
2021-07-08 18:00:33 +02:00
from xmlschema import etree_tostring
from xmlschema.validators import identities
from xml.etree import ElementTree as ET
2021-07-08 18:00:33 +02:00
# http://standards.buildingsmart.org/IDS/ids_05.xsd
2021-08-24 12:57:44 +02:00
cwd = os.path.dirname(os.path.realpath(__file__))
ids_schema = XMLSchema(os.path.join(cwd, "ids.xsd"))
2021-02-28 09:34:35 +01:00
2021-02-27 11:04:45 +01:00
def error(msg):
2021-08-24 12:57:44 +02:00
raise Exception(msg)
class ids:
"""Represents the XML root <ids> node and its <specification> childNodes."""
def __init__(
self,
title="Untitled",
2021-08-24 12:57:44 +02:00
copyright=None,
version=None,
2022-02-23 00:24:40 +01:00
description=None,
author=None,
date=None,
2021-08-24 12:57:44 +02:00
purpose=None,
milestone=None,
):
"""Create an IDS object.
2022-02-23 00:24:40 +01:00
:param title: Name of the IDS file, defaults to None
:type title: str, required
2021-08-24 12:57:44 +02:00
:param copyright:, defaults to None
:type copyright: str, optional
:param version: IDS file version, defaults to None
:type version: float, optional
2022-02-23 00:24:40 +01:00
:param description:, defaults to None
:type description: str, optional
:param author: Email of the IDS author, defaults to None
:type author: str, optional
:param date: Date in 'yyyy-mm-dd' format, defaults to current date
:type date: str, optional
2021-08-24 12:57:44 +02:00
:param purpose:, defaults to None
:type purpose: str, optional
:param milestone:, defaults to None
:type milestone: str, optional
"""
self.specifications = []
self.info = {}
self.info["title"] = title or "Untitled"
2021-08-24 12:57:44 +02:00
if copyright:
self.info["copyright"] = copyright
if version:
self.info["version"] = version
2022-02-23 00:24:40 +01:00
if description:
self.info["description"] = description
if author and "@" in author:
self.info["author"] = author
2022-02-23 00:24:40 +01:00
if date:
try:
self.info["date"] = datetime.date.fromisoformat(date).isoformat()
except ValueError:
pass
2021-08-24 12:57:44 +02:00
if purpose:
self.info["purpose"] = purpose
if milestone:
self.info["milestone"] = milestone
def asdict(self):
"""Converts object to a dictionary, adding required attributes.
:return: Xmlschema compliant dictionary.
:rtype: dict
"""
ids_dict = {
"@xmlns": "http://standards.buildingsmart.org/IDS",
"@xmlns:xs": "http://www.w3.org/2001/XMLSchema",
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
2022-02-23 00:24:40 +01:00
"@xsi:schemaLocation": "http://standards.buildingsmart.org/IDS/ids_05.xsd",
2021-08-24 12:57:44 +02:00
"info": self.info,
2022-02-23 00:24:40 +01:00
"specifications": [],
2021-08-24 12:57:44 +02:00
}
for spec in self.specifications:
ids_dict["specifications"].append({"specification": spec.asdict()}) # TEST!
2021-08-24 12:57:44 +02:00
return ids_dict
def to_string(self, ids_schema=ids_schema):
"""Convert IDS object to XML string
2021-08-24 12:57:44 +02:00
:param ids_schema: XML Schema for an IDS file, defaults to ids_schema object from buildingSMART
:type ids_schema: XMLschema, optional
:return: The contents of the XML data in string form
:rtype: string
"""
ns = {"": "http://standards.buildingsmart.org/IDS"}
return etree_tostring(ids_schema.encode(self.asdict()), namespaces=ns)
def to_xml(self, filepath="output.xml", ids_schema=ids_schema):
"""Writes IDS object to an XML file.
:param filepath: Path to the file, defaults to "output.xml"
2021-08-24 12:57:44 +02:00
:type filepath: str, optional
:param ids_schema: XML Schema for an IDS file, defaults to ids_schema object from buildingSMART
:type ids_schema: XMLschema, optional
:return: Result of the newly created file validation against the schema.
:rtype: bool
"""
ET.ElementTree(ids_schema.encode(self.asdict())).write(filepath, encoding="utf-8", xml_declaration=True)
2021-08-24 12:57:44 +02:00
return ids_schema.is_valid(filepath)
@staticmethod
def open(filepath, ids_schema=ids_schema):
"""Use to open ids.xml files
:param filepath: ids file path
:type filepath: str
:param ids_schema: XML Schema for an IDS file, defaults to ids_schema object from buildingSMART
:type ids_schema: XMLschema, optional
:return: IDS file as a python object
:rtype: ids object
"""
ids_schema.validate(filepath)
ids_content = ids_schema.decode(
filepath, strip_namespaces=True, namespaces={"": "http://standards.buildingsmart.org/IDS"}
)
ids_file = ids()
2022-02-23 00:24:40 +01:00
ids_file.specifications = [specification.parse(s) for s in ids_content["specifications"]["specification"]]
2021-08-24 12:57:44 +02:00
return ids_file
def validate(self, ifc_file, logger=None):
"""Use to validate IFC model against IDS specifications.
:param ifc_file: path to ifc file
:type ifc_file: str
:param logger: Logging object with handlers, defaults to None
:type logger: logging, optional
"""
if not isinstance(logger, logging.Logger):
logger = logging.getLogger("IDS_Logger")
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger.setLevel(logging.INFO)
# Consider other way around: for elem, for spec so we can see if an element pass all IDSes?
for spec in self.specifications:
self.ifc_applicable = 0
self.ifc_passed = 0
for elem in ifc_file:
2021-08-24 12:57:44 +02:00
apply, comply = spec(elem, logger)
if apply:
self.ifc_applicable += 1
if comply:
self.ifc_passed += 1
if self.ifc_applicable == 0:
2022-02-23 00:24:40 +01:00
if spec.use == "required":
2021-08-24 12:57:44 +02:00
logger.error("No applicable elements found. Minimum 1 applicable element required.")
else:
logger.debug("No applicable elements found. None required.")
try:
percentage = self.ifc_passed / self.ifc_applicable * 100
except ZeroDivisionError:
percentage = 0
2021-08-24 12:57:44 +02:00
logger.debug(
"Out of %s IFC elements, %s were applicable and %s of them passed (%s)."
% (
len(ifc_file.by_type("IfcProduct")),
self.ifc_applicable,
self.ifc_passed,
str(percentage) + "%",
2021-08-24 12:57:44 +02:00
)
)
for h in logger.handlers:
h.flush()
class specification:
"""Represents the XML <specification> node and its two children <applicability> and <requirements>"""
def __init__(
self, name="Unnamed", use="required", ifcVersion=["IFC2X3", "IFC4"], identifier=None, description=None, instructions=None
):
2021-08-24 12:57:44 +02:00
"""Create a specification to be added in ids.
:param name: Name describing the specification to a contract reader
:type name: str
2022-02-23 00:24:40 +01:00
:param use: 'required'|'optional', defaults to "required"
:type use: str, optional
2021-08-24 12:57:44 +02:00
"""
self.name = name or "Unnamed"
2021-08-24 12:57:44 +02:00
self.applicability = None
self.requirements = None
2022-02-23 00:24:40 +01:00
self.use = use
self.ifcVersion = ifcVersion
self.identifier = identifier
self.description = description
self.instructions = instructions
2021-08-24 12:57:44 +02:00
def asdict(self):
"""Converts object to a dictionary, adding required attributes.
:return: Xmlschema compliant dictionary.
:rtype: dict
"""
# if older python collections.OrderedDict()
results = {
2021-08-24 12:57:44 +02:00
"@name": self.name,
2022-02-23 00:24:40 +01:00
"@use": self.use,
"@ifcVersion": self.ifcVersion,
2021-08-24 12:57:44 +02:00
"applicability": {},
"requirements": {},
}
for attribute in ["identifier", "description", "instructions"]:
value = getattr(self, attribute)
if value:
results[f"@{attribute}"] = value
for clause_type in ["applicability", "requirements"]:
clause = getattr(self, clause_type)
if not clause:
continue
for fac in clause.terms:
2021-08-24 12:57:44 +02:00
fclass = type(fac).__name__
if fclass in results[clause_type]:
results[clause_type][fclass].append(fac.asdict())
2021-08-24 12:57:44 +02:00
else:
results[clause_type][fclass] = [fac.asdict()]
return results
2021-08-24 12:57:44 +02:00
@staticmethod
def parse(ids_dict):
"""Parse xml specification to python object.
:param ids_dict:
:type ids_dict: dict
"""
def parse_rules(dict):
facet_names = list(dict.keys())
facet_properties = [v[0] if isinstance(v, list) else v for v in list(dict.values())]
classes = [meta_facet.facets.__getitem__(f) for f in facet_names]
facets = [cls(n) for cls, n in zip(classes, facet_properties)]
return facets
spec = specification()
2022-02-23 00:24:40 +01:00
try:
spec.name = ids_dict["@name"]
except KeyError:
2022-02-23 00:24:40 +01:00
spec.name = ""
spec.use = ids_dict["@use"]
spec.ifcVersion = ids_dict["@ifcVersion"]
2021-08-24 12:57:44 +02:00
spec.applicability = boolean_and(parse_rules(ids_dict["applicability"]))
spec.requirements = boolean_and(parse_rules(ids_dict["requirements"]))
return spec
def add_applicability(self, facet):
"""Applicability specifies a filter for IFC entities are to be validated.
At least one filter must be added.
2021-08-24 12:57:44 +02:00
:param facet: any of entity|attribute|classification|property|material
2021-08-24 12:57:44 +02:00
:type facet: facet
Example::
specs = ids.ids()
spec = ids.specification(name="Test_Specification")
spec.add_applicability(ids.entity.create(name="IfcWall"))
specs.specifications.append(spec)
2021-08-24 12:57:44 +02:00
"""
if self.applicability:
self.applicability = boolean_and(self.applicability.terms + [facet])
else:
self.applicability = boolean_and([facet])
def add_requirement(self, facet):
"""A requirement specifies data to be checked for all applicable entities.
2021-08-24 12:57:44 +02:00
At least one requirement must be added.
:param facet: any of entity|attribute|classification|property|material|partOf
2021-08-24 12:57:44 +02:00
:type facet: facet
"""
if self.requirements:
self.requirements = boolean_and(self.requirements.terms + [facet])
else:
self.requirements = boolean_and([facet])
def __call__(self, inst, logger):
"""When specification is called on an ifc instance, it validates against applicability and requirements.
:param inst: IFC entity element
:type inst: IFC entity
:param logger: Logging object
:type logger: logging
:return: results of validation on applicability and requirements
:rtype: [bool,bool]
"""
if self.applicability(inst, logger):
valid = self.requirements(inst, logger)
if valid:
logger.info(
{
"guid": inst.GlobalId,
"result": valid.success,
"sentence": str(self)
+ ".\n"
+ inst.is_a()
+ " '"
+ str(inst.Name)
+ "' (#"
+ str(inst.id())
+ ") has "
+ str(valid)
+ " so is compliant",
"ifc_element": inst,
}
)
return True, True
else:
# BUG "has does not have"
logger.error(
{
"guid": inst.GlobalId,
"result": valid.success,
"sentence": str(self)
+ ".\n"
+ inst.is_a()
+ " '"
+ str(inst.Name)
+ "' (#"
+ str(inst.id())
+ ") has "
+ str(valid)
+ " so is not compliant",
"ifc_element": inst,
}
)
return True, False
else:
return False, False
def __str__(self):
2021-10-28 17:56:02 -04:00
"""Represent the specification in human readable sentence.
2021-08-24 12:57:44 +02:00
:return: sentence
:rtype: str
"""
return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
class facet_evaluation:
2021-08-24 12:57:44 +02:00
"""The evaluation of a facet with data from IFC. Converts to bool and has a human readable string format."""
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
def __init__(self, success, str):
self.success = success
self.str = str
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
def __bool__(self):
return self.success
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
def __str__(self):
return self.str
2021-02-27 11:04:45 +01:00
class meta_facet(type):
2021-08-24 12:57:44 +02:00
"""A metaclass for automatically registering facets in a map to be instantiated based on XML tagnames."""
2021-02-27 11:04:45 +01:00
facets = {}
def __new__(cls, clsname, bases, attrs):
newclass = super(meta_facet, cls).__new__(cls, clsname, bases, attrs)
meta_facet.facets[clsname] = newclass
return newclass
class facet(metaclass=meta_facet):
"""
The base class for IDS facets. IDS facets are functors constructed from
XML nodes that return True or False. A getattr method is provided for
conveniently extracting XML child node text content.
2021-08-24 12:57:44 +02:00
Use child classes instead: entity, classification, property and material.
"""
2021-02-27 11:04:45 +01:00
2021-07-08 18:04:50 +02:00
def __init__(self, node=None, location=None):
2021-12-22 12:44:45 +01:00
2021-07-08 18:04:50 +02:00
if node:
self.node = node
2021-08-24 12:57:44 +02:00
if "@location" in self:
self.location = self.node["@location"]
2021-07-08 18:04:50 +02:00
else:
2021-08-24 12:57:44 +02:00
self.location = "any"
2021-12-22 12:44:45 +01:00
2021-07-08 18:04:50 +02:00
if location:
self.location = location
else:
2021-08-24 12:57:44 +02:00
self.location = "any"
2021-02-27 11:04:45 +01:00
2021-12-22 12:44:45 +01:00
def __getattr__(self, attr):
if attr in getattr(self, "node", None):
2021-12-22 12:44:45 +01:00
v = self.node[attr]
2021-08-24 12:57:44 +02:00
# BUG list of dictionaries should not happen
if isinstance(v, list):
v = v[0]
2021-12-22 12:44:45 +01:00
2021-08-24 12:57:44 +02:00
if "simpleValue" in list(v):
return v["simpleValue"]
elif "restriction" in list(v):
return restriction.parse(v["restriction"][0])
# TODO handle more than one restriction: return [restriction(r) for r in v["restriction"]]
2021-04-29 20:46:55 +02:00
else:
2021-08-24 12:57:44 +02:00
raise Exception("Unknown value declaration.")
2021-12-22 12:44:45 +01:00
# except KeyError:
2021-04-30 11:06:36 +02:00
else:
2021-04-29 20:46:55 +02:00
return None
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
def __iter__(self):
for k in self.parameters:
yield k, getattr(self, k)
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
def __str__(self):
di = dict(list(self))
for k, v in di.items():
if isinstance(v, str) and not len(v):
di[k] = "not specified"
return self.message % di
2021-02-27 11:04:45 +01:00
class entity(facet):
2021-08-24 12:57:44 +02:00
"""The IDS entity facet currently *with* inheritance"""
2021-02-28 09:34:35 +01:00
2022-02-23 00:24:40 +01:00
parameters = ["name", "predefinedType"]
2021-08-24 12:57:44 +02:00
@staticmethod
2022-02-23 00:24:40 +01:00
def create(name=None, predefinedType=None):
2021-08-24 12:57:44 +02:00
"""Create an entity facet that can be added to applicability or requirements of IDS specification.
:param name: IFC entity name that is required. e.g. IfcWall, defaults to None
:type name: str, optional
2022-02-23 00:24:40 +01:00
:param predefinedType: name of the predefined type, defaults to None
:type predefinedType: str, optional
2021-08-24 12:57:44 +02:00
:return: entity object
:rtype: entity
"""
2021-07-09 08:35:04 +02:00
inst = entity()
2021-07-08 18:12:55 +02:00
inst.name = name
2022-02-23 00:24:40 +01:00
inst.predefinedType = predefinedType
2021-07-08 18:12:55 +02:00
return inst
2021-07-08 18:06:07 +02:00
def asdict(self):
2021-08-24 12:57:44 +02:00
"""Converts object to a dictionary, adding required attributes.
:return: Xmlschema compliant dictionary.
:rtype: dict
"""
results = {"name": parameter_asdict(self.name)}
if self.predefinedType:
results["predefinedType"] = parameter_asdict(self.predefinedType)
return results
2021-07-08 18:06:07 +02:00
def __call__(self, inst, logger=None):
"""Validate an entity.
Subclasses are not considered to pass the requirements. PredefinedType
checks support userdefined types for both element and type elements.
2021-08-24 12:57:44 +02:00
:param inst: IFC entity element
:type inst: IFC entity
:param logger: Logging object
:type logger: logging
:return: result of the validation as bool and message
:rtype: facet_evaluation(bool, str)
"""
if isinstance(self.name, str):
is_class = inst.is_a().lower() == self.name.lower()
else:
is_class = inst.is_a() == self.name
if self.predefinedType:
predefined_type = ifcopenshell.util.element.get_predefined_type(inst)
2022-02-23 00:24:40 +01:00
self.message = "an entity name '%(name)s' of predefined type '%(predefinedType)s'"
2021-06-10 15:19:23 +02:00
return facet_evaluation(
is_class and predefined_type == self.predefinedType,
self.message % {"name": inst.is_a(), "predefinedType": predefined_type},
2021-08-24 12:57:44 +02:00
)
2021-04-29 20:46:55 +02:00
else:
self.message = "an entity name '%(name)s'"
return facet_evaluation(is_class, self.message % {"name": inst.is_a()})
2021-02-27 11:04:45 +01:00
2022-05-10 16:23:07 +10:00
class attribute(facet):
"""The IDS attribute facet"""
parameters = ["name", "value", "location"]
@staticmethod
def create(name=None, value=None, location="any"):
"""Create an attribute facet that can be added to applicability or requirements of IDS specification.
:param name: Attribute name, such as "Description"
:type name: str
:param value: Attribute value
:type value: str, optional
:param location: Where to check for the parameter. One of "any"|"instance"|"type", defaults to "any"
:type location: str, optional
:return: entity object
:rtype: entity
"""
inst = attribute()
inst.name = name
inst.value = value
inst.location = location
return inst
def asdict(self):
"""Converts object to a dictionary, adding required attributes.
:return: Xmlschema compliant dictionary.
:rtype: dict
"""
fac_dict = {"name": parameter_asdict(self.name)}
if self.value:
fac_dict["value"] = parameter_asdict(self.value)
if self.location:
fac_dict["@location"] = self.location
return fac_dict
def __call__(self, inst, logger=None):
2022-05-10 16:23:07 +10:00
"""Validate an ifc instance against that entity facet.
:param inst: IFC entity element
:type inst: IFC entity
:param logger: Logging object
:type logger: logging
:return: result of the validation as bool and message
:rtype: facet_evaluation(bool, str)
"""
element_type = ifcopenshell.util.element.get_type(inst)
if self.location == "instance":
value = getattr(inst, self.name, None)
elif self.location == "type":
value = getattr(element_type, self.name, None) if element_type else None
elif self.location == "any":
value = getattr(element_type, self.name, None) if element_type else None
value = getattr(inst, self.name, value)
if self.value:
self.message = "foo"
return facet_evaluation(value == self.value, f"an entity with {self.name} set to '{value}'")
else:
return facet_evaluation(value is not None and value != "", f"an entity with {self.name}")
2022-05-10 16:23:07 +10:00
2021-02-27 11:04:45 +01:00
class classification(facet):
"""
The IDS classification facet by traversing the HasAssociations inverse attribute
"""
2021-02-28 09:34:35 +01:00
2021-06-21 17:48:27 +02:00
parameters = ["system", "value", "location"]
message = "%(location)sclassification reference %(value)s from '%(system)s'"
2021-02-27 11:04:45 +01:00
2021-08-24 12:57:44 +02:00
@staticmethod
def create(location="any", value=None, system=None):
"""Create a classification facet that can be added to applicability or requirements of IDS specification.
2022-05-10 16:17:38 +10:00
:param location: Where to check for the parameter. One of "any"|"instance"|"type", defaults to "any"
2021-08-24 12:57:44 +02:00
:type location: str, optional
:param value: Value that is required. Could be alphanumeric or restriction object, defaults to None
:type value: restriction|alphanumeric, optional
:param system: System that is required. Could be alphanumeric or restriction object, defaults to None
:type system: restriction|alphanumeric, optional
:return: classification object
:rtype: classification
"""
2021-07-09 08:35:04 +02:00
inst = classification()
2021-07-08 18:12:55 +02:00
inst.location = location
inst.value = value
inst.system = system
return inst
2021-07-08 18:06:07 +02:00
def asdict(self):
2021-08-24 12:57:44 +02:00
"""Converts object to a dictionary, adding required attributes.
:return: Xmlschema compliant dictionary.
:rtype: dict
"""
2021-07-08 18:06:07 +02:00
fac_dict = {
2021-08-24 12:57:44 +02:00
"value": parameter_asdict(self.value),
"system": parameter_asdict(self.system),
"@location": self.location,
# "instructions": "SAMPLE_INSTRUCTIONS",
}
2021-07-08 18:06:07 +02:00
return fac_dict
2021-06-21 17:48:27 +02:00
2021-07-08 18:04:50 +02:00
def __call__(self, inst, logger):
2021-08-24 12:57:44 +02:00
"""Validate an ifc instance against that classification facet.
:param inst: IFC entity element
:type inst: IFC entity
:param logger: Logging object
:type logger: logging
:return: result of the validation as bool and message
:rtype: facet_evaluation(bool, str)
"""
2021-06-21 17:48:27 +02:00
instance_classiciations = inst.HasAssociations
if ifcopenshell.util.element.get_type(inst):
type_classifications = ifcopenshell.util.element.get_type(inst).HasAssociations
else:
type_classifications = ()
2021-08-24 12:57:44 +02:00
if self.location == "instance" and instance_classiciations:
2021-06-21 17:48:27 +02:00
associations = instance_classiciations
2021-08-24 12:57:44 +02:00
elif self.location == "type" and type_classifications:
2021-06-21 17:48:27 +02:00
associations = type_classifications
2021-08-24 12:57:44 +02:00
elif self.location == "any" and (instance_classiciations or type_classifications):
2021-06-21 17:48:27 +02:00
associations = instance_classiciations + type_classifications
else:
associations = ()
2021-02-27 11:04:45 +01:00
refs = []
2021-06-21 17:48:27 +02:00
for association in associations:
2021-02-27 11:04:45 +01:00
if association.is_a("IfcRelAssociatesClassification"):
cref = association.RelatingClassification
2021-08-24 12:57:44 +02:00
if hasattr(cref, "ItemReference"): # IFC2x3
2021-06-19 11:44:33 +02:00
refs.append((cref.ReferencedSource.Name, cref.ItemReference))
2021-08-24 12:57:44 +02:00
elif hasattr(cref, "Identification"): # IFC4
refs.append((cref.ReferencedSource.Name, cref.Identification))
2021-06-21 17:48:27 +02:00
2021-07-08 18:04:50 +02:00
self.location_msg = location[self.location]
2021-06-21 17:48:27 +02:00
2021-06-10 15:21:51 +02:00
if refs:
return facet_evaluation(
(self.system, self.value) in refs,
2021-08-24 12:57:44 +02:00
self.message
% {
"system": refs[0][0],
"value": "'" + refs[0][1] + "'",
"location": self.location_msg,
}, # what if not first item of refs?
2021-06-10 15:21:51 +02:00
)
2021-08-24 12:57:44 +02:00
else:
return facet_evaluation(False, "does not have %sclassification reference" % self.location_msg)
2021-02-27 11:04:45 +01:00
2021-06-11 17:10:54 +02:00
2022-02-23 00:24:40 +01:00
class partOf(facet):
"""
The IDS partOf facet by traversing the _______ inverse attribute
"""
parameters = ["entity"]
message = "relation as part of %(entity)s"
# TODO temp default
2022-02-23 00:24:40 +01:00
entity = "IfcElementAssembly"
@staticmethod
# TODO should not assume IfcElementAssembly
2022-02-23 00:24:40 +01:00
# def create(entity=None):
def create(entity="IfcElementAssembly"):
"""Create a partOf facet that can be added to applicability or requirements of IDS specification.
:param entity: Entity that should contain this object. Could be alphanumeric or restriction object, defaults to None
:type entity: restriction|alphanumeric, optional
:return: partOf object
:rtype: partOf
"""
inst = partOf()
inst.entity = entity
return inst
def asdict(self):
"""Converts object to a dictionary, adding required attributes.
:return: Xmlschema compliant dictionary.
:rtype: dict
"""
fac_dict = {
"@entity": parameter_asdict(self.entity),
# "instructions": "SAMPLE_INSTRUCTIONS",
}
return fac_dict
def __call__(self, inst, logger):
"""Validate an ifc instance against that partOf facet.
:param inst: IFC entity element
:type inst: IFC entity
:param logger: Logging object
:type logger: logging
:return: result of the validation as bool and message
:rtype: facet_evaluation(bool, str)
"""
# TODO handle partOf facet
2022-02-23 00:24:40 +01:00
# instance_classiciations = inst.HasAssociations
# if ifcopenshell.util.element.get_type(inst):
# type_classifications = ifcopenshell.util.element.get_type(inst).HasAssociations
# else:
# type_classifications = ()
2022-02-23 00:24:40 +01:00
# if self.location == "instance" and instance_classiciations:
# associations = instance_classiciations
# elif self.location == "type" and type_classifications:
# associations = type_classifications
# elif self.location == "any" and (instance_classiciations or type_classifications):
# associations = instance_classiciations + type_classifications
# else:
# associations = ()
2022-02-23 00:24:40 +01:00
refs = []
# for association in associations:
# if association.is_a("IfcRelAssociatesClassification"):
# cref = association.RelatingClassification
# if hasattr(cref, "ItemReference"): # IFC2x3
# refs.append((cref.ReferencedSource.Name, cref.ItemReference))
# elif hasattr(cref, "Identification"): # IFC4
# refs.append((cref.ReferencedSource.Name, cref.Identification))
2022-02-23 00:24:40 +01:00
# self.location_msg = location[self.location]
2022-02-23 00:24:40 +01:00
if refs:
pass
# return facet_evaluation(
# (self.system, self.value) in refs,
# self.message
# % {
# "system": refs[0][0],
# "value": "'" + refs[0][1] + "'",
# "location": self.location_msg,
# }, # what if not first item of refs?
# )
else:
return facet_evaluation(False, "is not a part of %s" % self.node["@entity"])
2022-02-23 00:24:40 +01:00
2021-02-27 11:04:45 +01:00
class property(facet):
"""
2021-06-21 15:23:03 +02:00
The IDS property facet implemented using `ifcopenshell.util.element`
2021-02-27 11:04:45 +01:00
"""
2022-02-23 00:24:40 +01:00
parameters = ["name", "propertySet", "value", "location"]
message = "%(location)sproperty '%(name)s' in '%(propertySet)s' with a value %(value)s"
2021-08-24 12:57:44 +02:00
@staticmethod
2022-02-23 00:24:40 +01:00
def create(location="any", propertySet=None, name=None, value=None):
2021-08-24 12:57:44 +02:00
"""Create a property facet that can be added to applicability or requirements of IDS specification.
2022-05-10 16:17:38 +10:00
:param location: Where to check for the parameter. One of "any"|"instance"|"type", defaults to "any"
2021-08-24 12:57:44 +02:00
:type location: str, optional
2022-02-23 00:24:40 +01:00
:param propertySet: Propertyset that is required. Could be alphanumeric or restriction object, defaults to None
:type propertySet: restriction|alphanumeric, optional
2021-08-24 12:57:44 +02:00
:param name: Name that is required. Could be alphanumeric or restriction object, defaults to None
:type name: restriction|alphanumeric, optional
:param value: Value that is required. Could be alphanumeric or restriction object, defaults to None
:type value: restriction|alphanumeric, optional
:return: property object
:rtype: property
"""
2021-07-08 18:12:55 +02:00
inst = property()
inst.location = location
2022-02-23 00:24:40 +01:00
inst.propertySet = propertySet
2021-07-08 18:12:55 +02:00
inst.name = name
inst.value = value
# cls.attributes = {'@location': location} # 'type', 'instance', 'any'
# BUG '@href': 'http://identifier.buildingsmart.org/uri/buildingsmart/ifc-4.3/prop/FireRating', #https://identifier.buildingsmart.org/uri/something
# BUG 'instructions': 'Please add the desired rating.',
return inst
2021-07-08 18:06:07 +02:00
def asdict(self):
2021-08-24 12:57:44 +02:00
"""Converts object to a dictionary, adding required attributes.
:return: Xmlschema compliant dictionary.
:rtype: dict
"""
2021-07-08 18:06:07 +02:00
fac_dict = {
2021-08-24 12:57:44 +02:00
"@location": self.location,
2022-02-23 00:24:40 +01:00
"propertySet": parameter_asdict(self.propertySet),
2021-08-24 12:57:44 +02:00
"name": parameter_asdict(self.name),
"value": parameter_asdict(self.value),
# "instructions": "SAMPLE_INSTRUCTIONS",
2021-07-08 18:06:07 +02:00
# TODO '@href': 'http://identifier.buildingsmart.org/uri/buildingsmart/ifc-4.3/prop/FireRating', #https://identifier.buildingsmart.org/uri/something
2021-08-24 12:57:44 +02:00
}
2021-07-08 18:06:07 +02:00
return fac_dict
2021-02-27 11:04:45 +01:00
def __call__(self, inst, logger):
2021-08-24 12:57:44 +02:00
"""Validate an ifc instance against that property facet.
:param inst: IFC entity element
:type inst: IFC entity
:param logger: Logging object
:type logger: logging
:return: result of the validation as bool and message
:rtype: facet_evaluation(bool, str)
"""
2021-06-21 17:48:27 +02:00
2021-12-22 12:44:45 +01:00
# self.location = self.node["@location"]
2021-06-21 17:48:27 +02:00
# TODO sometimes AttributeError: 'str' object has no attribute 'wrappedValue'
try:
instance_props = ifcopenshell.util.element.get_psets(inst)
except AttributeError:
instance_props = {}
if ifcopenshell.util.element.get_type(inst):
# TODO sometimes AttributeError: 'str' object has no attribute 'wrappedValue'
2021-12-22 12:44:45 +01:00
try:
type_props = ifcopenshell.util.element.get_psets(ifcopenshell.util.element.get_type(inst))
2021-12-22 12:44:45 +01:00
except AttributeError:
type_props = {}
else:
type_props = {}
if self.location == "instance":
props = instance_props
elif self.location == "type" and type_props:
props = type_props
elif self.location == "any" and (instance_props or type_props):
props = {**instance_props, **type_props}
else:
props = {}
2021-08-24 12:57:44 +02:00
pset = props.get(self.propertySet)
val = pset.get(self.name) if pset else None
2021-08-24 12:57:44 +02:00
2021-07-08 18:04:50 +02:00
self.location_msg = location[self.location]
2022-02-23 00:24:40 +01:00
di = {"name": self.name, "propertySet": self.propertySet, "value": "'%s'" % val, "location": self.location_msg}
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
if val is not None:
msg = self.message % di
else:
if pset:
2022-02-23 00:24:40 +01:00
msg = "does not have %(location)sproperty '%(name)s' in a set '%(propertySet)s'" % di
2021-02-28 09:31:21 +01:00
else:
2022-02-23 00:24:40 +01:00
msg = "does not have %(location)sset '%(propertySet)s'" % di
2021-02-28 09:34:35 +01:00
2021-08-24 12:57:44 +02:00
# TODO implement data type comparison
# xs:string
# xs:decimal
# xs:integer
# xs:boolean
# xs:anyURI
# xs:date YYYY-MM-DD
# xs:time hh:mm:ss
# xs:dateTime YYYY-MM-DDThh:mm:ss
# xs:duration PnYnMnDTnHnMnS
return facet_evaluation(val == self.value, msg)
2021-02-27 11:04:45 +01:00
class material(facet):
2021-08-24 12:57:44 +02:00
"""The IDS material facet used to traverse the HasAssociations inverse attribute."""
2021-06-21 17:48:27 +02:00
parameters = ["value", "location"]
message = "%(location)smaterial '%(value)s'"
2021-08-24 12:57:44 +02:00
@staticmethod
def create(location="any", value=None):
"""Create a material facet that can be added to applicability or requirements of IDS specification.
2022-05-10 16:17:38 +10:00
:param location: Where to check for the parameter. One of "any"|"instance"|"type", defaults to "any"
2021-08-24 12:57:44 +02:00
:type location: str, optional
:param value: Value that is required. Could be alphanumeric or restriction object, defaults to None
:type value: restriction|alphanumeric, optional
:return: material object
:rtype: material
"""
2021-07-09 08:35:04 +02:00
inst = material()
2021-07-08 18:12:55 +02:00
inst.location = location
inst.value = value
2021-08-24 12:57:44 +02:00
# TODO '@use': 'optional'
# TODO '@href': 'https://identifier.buildingsmart.org/uri/something',
# TODO 'instructions': 'Please add the desired...',
2021-07-08 18:12:55 +02:00
return inst
2021-07-08 18:06:07 +02:00
def asdict(self):
2021-08-24 12:57:44 +02:00
"""Converts object to a dictionary, adding required attributes.
:return: Xmlschema compliant dictionary.
:rtype: dict
"""
2021-07-08 18:06:07 +02:00
fac_dict = {
2021-08-24 12:57:44 +02:00
"value": parameter_asdict(self.value),
"@location": self.location,
# TODO "instructions": "SAMPLE_INSTRUCTIONS",
2021-07-08 18:06:07 +02:00
# TODO '@href': 'http://identifier.buildingsmart.org/uri/buildingsmart/ifc-4.3/prop/FireRating', #https://identifier.buildingsmart.org/uri/something
# TODO '@use': 'optional'
2021-08-24 12:57:44 +02:00
}
2021-07-08 18:06:07 +02:00
return fac_dict
def __call__(self, inst, logger):
2021-08-24 12:57:44 +02:00
"""Validate an ifc instance against that material facet.
:param inst: IFC entity element
:type inst: IFC entity
:param logger: Logging object
:type logger: logging
:return: result of the validation as bool and message
:rtype: facet_evaluation(bool, str)
"""
2021-06-21 17:48:27 +02:00
2021-12-22 12:44:45 +01:00
# self.location = self.node["@location"]
2021-06-21 17:48:27 +02:00
instance_material_rel = [rel for rel in inst.HasAssociations if rel.is_a("IfcRelAssociatesMaterial")]
if ifcopenshell.util.element.get_type(inst):
2021-08-24 12:57:44 +02:00
type_material_rel = [
rel
for rel in ifcopenshell.util.element.get_type(inst).HasAssociations
if rel.is_a("IfcRelAssociatesMaterial")
]
2021-06-21 17:48:27 +02:00
else:
type_material_rel = []
2021-08-24 12:57:44 +02:00
if self.location == "instance":
2021-06-21 17:48:27 +02:00
material_relations = list(instance_material_rel)
2021-08-24 12:57:44 +02:00
elif self.location == "type" and type_material_rel:
2021-06-21 17:48:27 +02:00
material_relations = list(type_material_rel)
2021-08-24 12:57:44 +02:00
elif self.location == "any" and (instance_material_rel or type_material_rel):
2021-06-21 17:48:27 +02:00
material_relations = instance_material_rel + type_material_rel
else:
material_relations = []
2021-06-11 17:10:54 +02:00
materials = []
for rel in material_relations:
2021-06-11 17:10:54 +02:00
if rel.RelatingMaterial.is_a() == "IfcMaterial":
materials.append(rel.RelatingMaterial.Name)
2022-02-23 00:24:40 +01:00
elif rel.RelatingMaterial.is_a() == "IfcMaterialList": # DEPRECATED in IFC4
[materials.append(mat.Name) for mat in rel.RelatingMaterial.Materials]
2021-06-11 17:10:54 +02:00
elif rel.RelatingMaterial.is_a() == "IfcMaterialConstituentSet":
[materials.append(mat.Material.Name) for mat in rel.RelatingMaterial.MaterialConstituents]
elif rel.RelatingMaterial.is_a() == "IfcMaterialLayerSet":
[materials.append(mat.Name) for mat in rel.RelatingMaterial.MaterialLayers]
elif rel.RelatingMaterial.is_a() == "IfcMaterialLayerSetUsage":
layers = rel.RelatingMaterial.ForLayerSet.MaterialLayers
2021-06-11 17:10:54 +02:00
[materials.append(layer.Material.Name) for layer in layers]
elif rel.RelatingMaterial.is_a() == "IfcMaterialProfileSet":
[materials.append(mat.Material.Name) for mat in rel.RelatingMaterial.MaterialProfiles]
elif rel.RelatingMaterial.is_a() == "IfcMaterialProfileSetUsage":
profileSets = rel.RelatingMaterial.ForProfileSet.MaterialProfiles
[materials.append(pset.Material.Name) for pset in profileSets]
else:
2021-08-24 12:57:44 +02:00
raise Exception("IfcRelAssociatesMaterial not implemented")
2021-06-11 17:10:54 +02:00
2021-06-18 16:50:26 +02:00
if not materials:
2021-08-24 12:57:44 +02:00
materials.append("UNDEFINED")
2021-06-18 16:50:26 +02:00
2021-07-08 18:04:50 +02:00
self.location_msg = location[self.location]
2021-06-21 17:48:27 +02:00
return facet_evaluation(
2021-06-11 17:10:54 +02:00
self.value in materials,
2021-07-08 18:04:50 +02:00
self.message % {"value": "'/'".join(materials), "location": self.location_msg},
)
2021-08-24 12:57:44 +02:00
def parameter_asdict(parameter):
"""Converts parameter to an IDS compliant dictionary, handling both value and restrictions.
:return: Xmlschema compliant dictionary.
:rtype: dict
2021-02-27 11:04:45 +01:00
"""
2021-08-24 12:57:44 +02:00
if isinstance(parameter, str):
parameter_dict = {"simpleValue": parameter}
elif isinstance(parameter, restriction):
parameter_dict = {"xs:restriction": [parameter.asdict()]}
elif isinstance(parameter, list):
restrictions = {"@base": "xs:" + parameter[0].base}
for p in parameter:
x = p.asdict()
restrictions[list(x)[1]] = x[list(x)[1]]
parameter_dict = {"xs:restriction": [restrictions]}
2021-11-23 17:53:32 +01:00
else:
raise Exception(str(parameter) + " was not able to be converted into 'Parameter_dict'")
2021-08-24 12:57:44 +02:00
return parameter_dict
class boolean_logic:
"""Boolean conjunction over a collection of functions"""
2021-02-27 11:04:45 +01:00
def __init__(self, terms):
self.terms = terms
def __call__(self, *args):
2021-02-28 09:31:21 +01:00
eval = [t(*args) for t in self.terms]
join = [" and ", " or "][self.fold == any]
2021-08-24 12:57:44 +02:00
return facet_evaluation(self.fold(eval), join.join(map(str, eval)))
2021-02-28 09:34:35 +01:00
2021-02-28 09:31:21 +01:00
def __str__(self):
return [" and ", " or "][self.fold == any].join(map(str, self.terms))
2021-02-27 11:04:45 +01:00
class boolean_and(boolean_logic):
fold = all
class boolean_or(boolean_logic):
fold = any
class restriction:
"""
The value restriction from XSD implemented as a list of values and a containment test
"""
2021-08-24 12:57:44 +02:00
def __init__(self):
"""Create a restriction that can be used instead of value of a parameter."""
2021-06-12 18:08:13 +02:00
self.type = ""
self.options = []
2021-08-24 12:57:44 +02:00
@staticmethod
def parse(ids_dict):
"""Parse xml restriction to python object.
2021-08-24 12:57:44 +02:00
:param ids_dict:
:type ids_dict: dict
"""
r = restriction()
if ids_dict:
# TODO 'base' missing in some IDS?!
2022-02-23 00:24:40 +01:00
try:
r.base = ids_dict["@base"][3:]
except KeyError:
2022-02-23 00:24:40 +01:00
r.base = "String"
2021-08-24 12:57:44 +02:00
for n in ids_dict:
if n == "enumeration":
r.type = "enumeration"
for x in ids_dict[n]:
r.options.append(x["@value"])
elif n[-7:] == "clusive":
r.type = "bounds"
r.options.append({n: ids_dict[n]["@value"]})
2021-06-12 22:35:37 +02:00
elif n[-5:] == "ength":
2021-08-24 12:57:44 +02:00
r.type = "length"
2021-06-12 22:35:37 +02:00
if n[3:6] == "min":
2021-08-24 12:57:44 +02:00
r.options.append(">=")
2021-06-12 22:35:37 +02:00
elif n[3:6] == "max":
2021-08-24 12:57:44 +02:00
r.options.append("<=")
2021-06-12 22:35:37 +02:00
else:
r.options.append("==")
2021-08-24 12:57:44 +02:00
r.options[-1] += str(ids_dict[n]["@value"])
elif n == "pattern":
r.type = "pattern"
r.options.append(ids_dict[n]["@value"])
# TODO add fractionDigits
# TODO add totalDigits
# TODO add whiteSpace
elif n == "@base":
pass
2021-06-12 18:08:13 +02:00
else:
2021-08-24 12:57:44 +02:00
print("Error! Restriction not implemented")
return r
def asdict(self):
"""Converts object to a dictionary, adding required attributes.
:return: Xmlschema compliant dictionary.
:rtype: dict
"""
rest_dict = {"@base": "xs:" + self.base}
if self.type == "enumeration":
for option in self.options:
if "xs:enumeration" not in rest_dict:
rest_dict["xs:enumeration"] = [{"@value": option}]
else:
rest_dict["xs:enumeration"].append({"@value": option})
elif self.type == "bounds":
for option in self.options:
2021-11-23 17:53:32 +01:00
rest_dict["xs:" + option] = [{"@value": self.options[option], "@fixed": False}]
2021-08-24 12:57:44 +02:00
elif self.type == "pattern":
if "xs:pattern" not in rest_dict:
rest_dict["xs:pattern"] = [{"@value": self.options}]
else:
rest_dict["xs:pattern"].append({"@value": self.options})
return rest_dict
@staticmethod
def create(options, type="pattern", base="string"):
"""Create restriction instead of simpleValue
:param type: One of "enumeration"|"pattern"|"bounds", defaults to "pattern"
:type type: str, optional
:param options: if enumeration: list of possible values
if pattern: xml regular expression string
if bounds: dictionary with possible keys: 'minInclusive', 'maxInclusive', 'minExclusive', 'maxExclusive'
:type options: list|str|dict
:param base: One of "string"|"boolean"|"decimal"|"integer", defaults to "string"
:type base: str, optional
:raises Exception: If not properly defined restriction.
:return: restriction object
:rtype: restriction
"""
rest = restriction()
if type in ["enumeration", "pattern", "bounds"]:
rest.type = type
rest.base = base
rest.options = options
if (
(type == "enumeration" and isinstance(options, list))
or (type == "bounds" and isinstance(options, dict))
or (type == "pattern" and isinstance(options, str))
):
rest.options = options
else:
2021-11-23 17:53:32 +01:00
raise Exception("Options were not properly defined.")
2021-08-24 12:57:44 +02:00
return rest
else:
raise Exception(
"Such restriction not implemented. Try: 'enumeration', 'pattern' or 'min/maxInclusive' or 'min/maxExclusive'."
)
2021-04-28 00:10:13 +02:00
2021-02-27 11:04:45 +01:00
def __eq__(self, other):
2021-08-24 12:57:44 +02:00
"""Evaluate the restriction using equality sign.
:param other: value to compare with the restriction.
:type other: str|float|int
:return: True if 'other' match the restriction, False if not.
:rtype: bool
"""
result = False
# TODO implement data type comparison
if self and (other or other == 0):
if self.type == "enumeration" and self.base == "bool":
2021-06-18 16:50:26 +02:00
self.options = [x.lower() for x in self.options]
result = str(other).lower() in self.options
elif self.type == "enumeration":
result = other in self.options
elif self.type == "bounds":
2021-08-24 12:57:44 +02:00
result = True
for sign in self.options.keys():
if sign == "minInclusive" and other < self.options[sign]:
result = False
elif sign == "maxInclusive" and other > self.options[sign]:
result = False
elif sign == "minExclusive" and other <= self.options[sign]:
result = False
elif sign == "maxExclusive" and other >= self.options[sign]:
result = False
2021-06-18 16:50:26 +02:00
elif self.type == "length":
for op in self.options:
2021-08-24 12:57:44 +02:00
if eval(str(len(other)) + op): # TODO eval not safe?
2021-06-18 16:50:26 +02:00
result = True
elif self.type == "pattern":
2022-02-23 00:24:40 +01:00
if isinstance(self.options, list):
# TODO handle case with multiple pattern options
2022-02-23 00:24:40 +01:00
translated_pattern = identities.translate_pattern(self.options[0])
else:
translated_pattern = identities.translate_pattern(self.options)
2021-06-21 15:23:03 +02:00
regex_pattern = re.compile(translated_pattern)
if regex_pattern.fullmatch(other) is not None:
result = True
2021-08-24 12:57:44 +02:00
# TODO add fractionDigits
# TODO add totalDigits
# TODO add whiteSpace
2021-06-18 16:50:26 +02:00
return result
2021-02-27 11:04:45 +01:00
def __repr__(self):
2021-10-28 17:56:02 -04:00
"""Represent the restriction in human readable sentence.
2021-08-24 12:57:44 +02:00
:return: sentence
:rtype: str
"""
msg = "of type '%s', " % (self.base)
if self.type == "enumeration":
2021-08-24 12:57:44 +02:00
msg = msg + "of value: '%s'" % "' or '".join(self.options)
elif self.type == "bounds":
2021-08-24 12:57:44 +02:00
msg = msg + "of value %s" % ", and ".join([bounds[x] + str(self.options[x]) for x in self.options])
elif self.type == "length":
2021-08-24 12:57:44 +02:00
msg = msg + "with %s letters" % " and ".join(self.options)
elif self.type == "pattern":
2021-08-24 12:57:44 +02:00
msg = msg + "respecting the pattern '%s'" % self.options
# TODO add fractionDigits
# TODO add totalDigits
# TODO add whiteSpace
return msg
2021-02-27 11:04:45 +01:00
2021-07-08 18:09:02 +02:00
2021-08-24 12:57:44 +02:00
class SimpleHandler(logging.StreamHandler):
"""Logging handler listing all cases in python list."""
2021-07-08 18:09:02 +02:00
2021-08-24 12:57:44 +02:00
def __init__(self, report_valid=False):
"""Logging handler listing all cases in python list.
2021-07-08 18:09:02 +02:00
2021-08-24 12:57:44 +02:00
:param report_valid: True if you want to list all the compliant cases as well, defaults to False
:type report_valid: bool, optional
2021-07-08 18:09:02 +02:00
"""
2021-08-24 12:57:44 +02:00
logging.StreamHandler.__init__(self)
self.statements = []
if report_valid:
2021-12-22 12:44:45 +01:00
self.setLevel(logging.DEBUG)
2021-11-23 17:53:32 +01:00
else:
self.setLevel(logging.ERROR)
def emit(self, mymsg):
"""Triggered on each use of logging with the Simple handler enabled.
:param log_content: default logger message
:type log_content: string|dict
"""
self.statements.append(mymsg.msg)
class CsvHandler(logging.StreamHandler):
"""Logging handler listing all cases in csv file."""
def __init__(self, filepath="./Report.csv", report_valid=False):
"""Logging handler listing all cases in csv file.
:param report_valid: True if you want to list all the compliant cases as well, defaults to False
:type report_valid: bool, optional
"""
import csv
logging.StreamHandler.__init__(self)
if report_valid:
self.setLevel(logging.INFO)
else:
self.setLevel(logging.ERROR)
self.file = open(filepath, "w", encoding="UTF8", newline="")
self.csvwriter = csv.writer(self.file)
self.csvwriter.writerow(["guid", "result", "sentence"]) # header
def emit(self, mymsg):
"""Triggered on each use of logging with the Simple handler enabled.
:param log_content: default logger message
:type log_content: string|dict
"""
# BUG bytes-like object is required, not 'str'
self.csvwriter.writerow(mymsg.msg)
def flush(self):
self.file.close()
2021-08-24 12:57:44 +02:00
class BcfHandler(logging.StreamHandler):
"""Logging handler for creation of BCF report files.
2021-06-10 15:19:23 +02:00
2021-08-24 12:57:44 +02:00
:param project_name: defaults to "IDS Project"
:type project_name: str, optional
:param author: Email of the person creating the BCF report, defaults to "your@email.com"
:type author: str, optional
:param filepath: Path to save the BCF report, defaults to None
:type filepath: str, optional
:param report_valid: True if you want to list all the compliant cases as well, defaults to False
:type report_valid: bool, optional
2021-02-27 11:04:45 +01:00
2021-08-24 12:57:44 +02:00
Example::
2021-04-30 00:09:04 +02:00
2021-08-24 12:57:44 +02:00
bcf_handler = BcfHandler(
project_name="Default IDS Project",
author="your@email.com",
2022-05-10 16:17:38 +10:00
filepath="example.bcf",
2021-08-24 12:57:44 +02:00
)
logger = logging.getLogger("IDS_Logger")
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger.addHandler(bcf_handler)
"""
2021-06-10 15:21:07 +02:00
2021-08-24 12:57:44 +02:00
def __init__(self, project_name="IDS Project", author="your@email.com", filepath=None, report_valid=False):
2021-06-21 17:48:27 +02:00
2021-08-24 12:57:44 +02:00
logging.StreamHandler.__init__(self)
if report_valid:
self.setLevel(logging.INFO)
else:
self.setLevel(logging.ERROR)
self.bcf = BcfXml()
self.bcf.author = author
self.bcf.new_project()
self.bcf.project.name = project_name
self.filepath = filepath
self.bcf.edit_project()
def emit(self, log_content):
"""Triggered on each use of logging with the BCF handler enabled.
:param log_content: default logger message
:type log_content: string|dict
"""
topic = bcf.Topic()
topic.title = log_content.msg["sentence"].split(".\n")[1]
topic.description = log_content.msg["sentence"].split(".\n")[0]
self.bcf.add_topic(topic)
2021-11-23 17:53:32 +01:00
# try: # Add viewpoint and link to ifc object
viewpoint = bcf.Viewpoint()
viewpoint.perspective_camera = bcf.PerspectiveCamera()
ifc_elem = log_content.msg["ifc_element"]
# ifc_elem = ifc_file.by_guid(log_content.msg["guid"])
target_position = np.array(ifcopenshell.util.placement.get_local_placement(ifc_elem.ObjectPlacement))
target_position = target_position[:, 3][0:3]
camera_position = target_position + np.array((5, 5, 5))
viewpoint.perspective_camera.camera_view_point.x = camera_position[0]
viewpoint.perspective_camera.camera_view_point.y = camera_position[1]
viewpoint.perspective_camera.camera_view_point.z = camera_position[2]
camera_direction = camera_position - target_position
camera_direction = camera_direction / np.linalg.norm(camera_direction)
camera_right = np.cross(np.array([0.0, 0.0, 1.0]), camera_direction)
camera_right = camera_right / np.linalg.norm(camera_right)
camera_up = np.cross(camera_direction, camera_right)
camera_up = camera_up / np.linalg.norm(camera_up)
rotation_transform = np.zeros((4, 4))
rotation_transform[0, :3] = camera_right
rotation_transform[1, :3] = camera_up
rotation_transform[2, :3] = camera_direction
rotation_transform[-1, -1] = 1
translation_transform = np.eye(4)
translation_transform[:3, -1] = -camera_position
look_at_transform = np.matmul(rotation_transform, translation_transform)
mat = np.linalg.inv(look_at_transform)
viewpoint.perspective_camera.camera_direction.x = mat[0][2] * -1
viewpoint.perspective_camera.camera_direction.y = mat[1][2] * -1
viewpoint.perspective_camera.camera_direction.z = mat[2][2] * -1
viewpoint.perspective_camera.camera_up_vector.x = mat[0][1]
viewpoint.perspective_camera.camera_up_vector.y = mat[1][1]
viewpoint.perspective_camera.camera_up_vector.z = mat[2][1]
viewpoint.components = bcf.Components()
c = bcf.Component()
c.ifc_guid = log_content.msg["guid"]
viewpoint.components.selection.append(c)
viewpoint.components.visibility = bcf.ComponentVisibility()
viewpoint.components.visibility.default_visibility = True
viewpoint.snapshot = None
self.bcf.add_viewpoint(topic, viewpoint)
2021-08-24 12:57:44 +02:00
def flush(self):
"""Saves the BCF report to file. Triggered at the end of the validation process."""
if not self.filepath:
2021-11-23 17:53:32 +01:00
self.filepath = os.getcwd() + r"\IDS_report.bcf"
2021-08-24 12:57:44 +02:00
if not (self.filepath.endswith(".bcf") or self.filepath.endswith(".bcfzip")):
2021-11-23 17:53:32 +01:00
self.filepath = self.filepath + r"\IDS_report.bcf"
2021-08-24 12:57:44 +02:00
self.bcf.save_project(self.filepath)
location = {"instance": "an instance ", "type": "a type ", "any": "a "}
bounds = {
"minInclusive": "larger or equal ",
"maxInclusive": "smaller or equal ",
"minExclusive": "larger than ",
"maxExclusive": "smaller than ",
2021-06-21 17:48:27 +02:00
}
2021-02-27 11:04:45 +01:00
if __name__ == "__main__":
import sys, os
2021-02-27 11:04:45 +01:00
import ifcopenshell
2021-08-24 12:57:44 +02:00
ids_file = ids.open(sys.argv[1])
ifc_file = ifcopenshell.open(sys.argv[2])
filepath = sys.argv[3]
2021-08-24 12:57:44 +02:00
logger = logging.getLogger("IDS_Logger")
logging.basicConfig(filename=filepath, level=logging.INFO, format="%(message)s")
logging.FileHandler(filepath + r"\report.txt", mode="w")
bcf_handler = BcfHandler(
project_name="Default IDS Project",
author="your@email.com",
filepath=filepath + r"\report.bcfzip",
)
logger.addHandler(bcf_handler)
report = SimpleHandler()
logger.addHandler(report)
2021-02-27 11:04:45 +01:00
ids_file.validate(ifc_file, logger)