2021-08-24 12:57:44 +02:00
|
|
|
# IDS - Information Delivery Specification.
|
2022-05-11 13:06:15 +10:00
|
|
|
# Copyright (C) 2021 Artur Tomczak <artomczak@gmail.com>, Thomas Krijnen <mail@thomaskrijnen.com>, Dion Moult <dion@thinkmoult.com>
|
2021-08-24 12:57:44 +02:00
|
|
|
#
|
|
|
|
|
# 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/>.
|
|
|
|
|
|
2021-09-08 20:16:39 +10:00
|
|
|
import os
|
2021-08-24 12:57:44 +02:00
|
|
|
import re
|
|
|
|
|
import logging
|
|
|
|
|
import numpy as np
|
2022-02-23 00:24:40 +01:00
|
|
|
import datetime
|
2022-05-11 19:59:10 +10:00
|
|
|
import ifcopenshell.util.unit
|
2021-02-27 11:04:45 +01:00
|
|
|
import ifcopenshell.util.element
|
2021-08-24 12:57:44 +02:00
|
|
|
import ifcopenshell.util.placement
|
2022-05-11 14:49:47 +10:00
|
|
|
import ifcopenshell.util.classification
|
2021-08-24 12:57:44 +02:00
|
|
|
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
|
2022-05-10 16:22:12 +10:00
|
|
|
from xml.etree import ElementTree as ET
|
2021-07-08 18:00:33 +02:00
|
|
|
|
|
|
|
|
|
2022-05-10 17:36:24 +10: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__))
|
2022-05-10 17:36:24 +10:00
|
|
|
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,
|
2022-05-10 19:26:12 +10:00
|
|
|
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 = {}
|
2022-05-10 19:26:12 +10:00
|
|
|
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
|
2022-05-10 17:36:24 +10:00
|
|
|
if author and "@" in author:
|
|
|
|
|
self.info["author"] = author
|
2022-02-23 00:24:40 +01:00
|
|
|
if date:
|
2022-05-10 17:36:24 +10:00
|
|
|
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-05-25 17:49:12 +10:00
|
|
|
"specifications": {"specification": []},
|
2021-08-24 12:57:44 +02:00
|
|
|
}
|
|
|
|
|
for spec in self.specifications:
|
2022-05-25 17:49:12 +10:00
|
|
|
ids_dict["specifications"]["specification"].append(spec.asdict())
|
2021-08-24 12:57:44 +02:00
|
|
|
return ids_dict
|
|
|
|
|
|
2022-05-10 16:22:12 +10:00
|
|
|
def to_string(self, ids_schema=ids_schema):
|
|
|
|
|
"""Convert IDS object to XML string
|
2021-08-24 12:57:44 +02:00
|
|
|
|
2022-05-10 16:22:12 +10: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
|
|
|
|
|
"""
|
2022-05-25 17:49:12 +10:00
|
|
|
ET.register_namespace("", "http://standards.buildingsmart.org/IDS")
|
2022-05-10 16:22:12 +10:00
|
|
|
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
|
|
|
|
|
|
2022-05-25 17:49:12 +10:00
|
|
|
def validate2(self, ifc_file):
|
|
|
|
|
"""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
|
|
|
|
|
"""
|
|
|
|
|
for specification in self.specifications:
|
|
|
|
|
specification.applicable_entities.clear()
|
|
|
|
|
specification.failed_entities.clear()
|
|
|
|
|
specification.status = None
|
|
|
|
|
for element in ifc_file:
|
|
|
|
|
for specification in self.specifications:
|
|
|
|
|
if not specification.applicability(element, None):
|
|
|
|
|
continue
|
|
|
|
|
specification.applicable_entities.append(element)
|
|
|
|
|
if specification.requirements(element, None):
|
|
|
|
|
specification.status = True
|
|
|
|
|
else:
|
|
|
|
|
specification.status = False
|
|
|
|
|
specification.failed_entities.append(element)
|
|
|
|
|
|
2021-08-24 12:57:44 +02:00
|
|
|
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
|
2022-05-10 22:13:28 +10:00
|
|
|
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-05-31 15:35:47 +10:00
|
|
|
if spec.minOccurs != "0":
|
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.")
|
|
|
|
|
|
2021-09-08 20:16:39 +10:00
|
|
|
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,
|
2021-09-08 20:16:39 +10:00
|
|
|
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>"""
|
|
|
|
|
|
2022-05-10 19:26:12 +10:00
|
|
|
def __init__(
|
2022-05-11 13:06:15 +10:00
|
|
|
self,
|
|
|
|
|
name="Unnamed",
|
2022-05-31 15:35:47 +10:00
|
|
|
minOccurs=None,
|
|
|
|
|
maxOccurs=None,
|
2022-05-11 13:06:15 +10:00
|
|
|
ifcVersion=["IFC2X3", "IFC4"],
|
|
|
|
|
identifier=None,
|
|
|
|
|
description=None,
|
|
|
|
|
instructions=None,
|
2022-05-10 19:26:12 +10:00
|
|
|
):
|
2021-08-24 12:57:44 +02:00
|
|
|
"""Create a specification to be added in ids.
|
|
|
|
|
|
2022-05-10 21:20:10 +10:00
|
|
|
:param name: Name describing the specification to a contract reader
|
|
|
|
|
:type name: str
|
2022-05-31 15:35:47 +10:00
|
|
|
:param minOccurs: The minimum total entities that should pass as an integer >= 0
|
|
|
|
|
:type minOccurs: str, optional
|
|
|
|
|
:param maxOccurs: The maximum total entities that should pass as an integer >= 0 or "unbounded"
|
|
|
|
|
:type maxOccurs: str, optional
|
2021-08-24 12:57:44 +02:00
|
|
|
"""
|
2022-05-10 19:26:12 +10:00
|
|
|
self.name = name or "Unnamed"
|
2021-08-24 12:57:44 +02:00
|
|
|
self.applicability = None
|
|
|
|
|
self.requirements = None
|
2022-05-31 15:35:47 +10:00
|
|
|
self.minOccurs = minOccurs
|
|
|
|
|
self.maxOccurs = maxOccurs
|
2022-05-10 19:26:12 +10:00
|
|
|
self.ifcVersion = ifcVersion
|
|
|
|
|
self.identifier = identifier
|
|
|
|
|
self.description = description
|
|
|
|
|
self.instructions = instructions
|
2021-08-24 12:57:44 +02:00
|
|
|
|
2022-05-25 17:49:12 +10:00
|
|
|
self.applicable_entities = []
|
|
|
|
|
self.failed_entities = []
|
|
|
|
|
self.status = None
|
|
|
|
|
|
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()
|
2022-05-10 19:26:12 +10:00
|
|
|
results = {
|
2021-08-24 12:57:44 +02:00
|
|
|
"@name": self.name,
|
2022-02-23 00:24:40 +01:00
|
|
|
"@ifcVersion": self.ifcVersion,
|
2021-08-24 12:57:44 +02:00
|
|
|
"applicability": {},
|
|
|
|
|
"requirements": {},
|
|
|
|
|
}
|
2022-05-31 15:35:47 +10:00
|
|
|
for attribute in ["identifier", "description", "instructions", "minOccurs", "maxOccurs"]:
|
2022-05-10 19:26:12 +10:00
|
|
|
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__
|
2022-05-10 19:26:12 +10:00
|
|
|
if fclass in results[clause_type]:
|
|
|
|
|
results[clause_type][fclass].append(fac.asdict())
|
2021-08-24 12:57:44 +02:00
|
|
|
else:
|
2022-05-10 19:26:12 +10:00
|
|
|
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"]
|
2022-05-10 19:26:12 +10:00
|
|
|
except KeyError:
|
2022-02-23 00:24:40 +01:00
|
|
|
spec.name = ""
|
2022-05-31 15:35:47 +10:00
|
|
|
spec.minOccurs = ids_dict["@minOccurs"]
|
|
|
|
|
spec.maxOccurs = ids_dict["@maxOccurs"]
|
2022-02-23 00:24:40 +01:00
|
|
|
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):
|
2022-05-10 21:20:10 +10:00
|
|
|
"""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
|
|
|
|
2022-05-10 21:20:10 +10:00
|
|
|
:param facet: any of entity|attribute|classification|property|material
|
2021-08-24 12:57:44 +02:00
|
|
|
:type facet: facet
|
|
|
|
|
|
|
|
|
|
Example::
|
|
|
|
|
|
2022-05-10 21:20:10 +10:00
|
|
|
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):
|
2022-05-10 21:20:10 +10:00
|
|
|
"""A requirement specifies data to be checked for all applicable entities.
|
2021-08-24 12:57:44 +02:00
|
|
|
|
2022-05-10 21:20:10 +10: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
|
|
|
|
2022-05-31 15:35:47 +10:00
|
|
|
def __init__(self, node=None):
|
2021-07-08 18:04:50 +02:00
|
|
|
if node:
|
|
|
|
|
self.node = node
|
2021-02-27 11:04:45 +01:00
|
|
|
|
2021-12-22 12:44:45 +01:00
|
|
|
def __getattr__(self, attr):
|
2022-05-10 19:26:12 +10:00
|
|
|
|
|
|
|
|
if attr in getattr(self, "node", None):
|
2021-12-22 12:44:45 +01:00
|
|
|
v = self.node[attr]
|
2022-05-10 19:26:12 +10:00
|
|
|
|
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):
|
2021-04-09 10:16:08 +02:00
|
|
|
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-05-25 17:49:12 +10:00
|
|
|
parameters = ["name", "predefinedType", "instructions"]
|
2021-08-24 12:57:44 +02:00
|
|
|
|
|
|
|
|
@staticmethod
|
2022-05-25 17:49:12 +10:00
|
|
|
def create(name=None, predefinedType=None, instructions=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
|
2022-05-25 17:49:12 +10:00
|
|
|
inst.instructions = instructions
|
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
|
|
|
|
|
"""
|
2022-05-10 21:20:10 +10:00
|
|
|
results = {"name": parameter_asdict(self.name)}
|
|
|
|
|
if self.predefinedType:
|
|
|
|
|
results["predefinedType"] = parameter_asdict(self.predefinedType)
|
2022-05-25 17:49:12 +10:00
|
|
|
if self.instructions:
|
|
|
|
|
results["@instructions"] = self.instructions
|
2022-05-10 21:20:10 +10:00
|
|
|
return results
|
2021-07-08 18:06:07 +02:00
|
|
|
|
2022-05-10 21:20:10 +10:00
|
|
|
def __call__(self, inst, logger=None):
|
|
|
|
|
"""Validate an entity.
|
|
|
|
|
|
2022-05-10 21:40:35 +10:00
|
|
|
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)
|
|
|
|
|
"""
|
2022-05-10 21:40:35 +10:00
|
|
|
if isinstance(self.name, str):
|
2022-05-11 13:06:15 +10:00
|
|
|
is_pass = inst.is_a().lower() == self.name.lower()
|
2022-05-10 21:40:35 +10:00
|
|
|
else:
|
2022-05-11 13:06:15 +10:00
|
|
|
is_pass = inst.is_a() == self.name
|
|
|
|
|
if is_pass and self.predefinedType:
|
2022-05-10 21:20:10 +10:00
|
|
|
predefined_type = ifcopenshell.util.element.get_predefined_type(inst)
|
2022-05-11 13:06:15 +10:00
|
|
|
is_pass = predefined_type == self.predefinedType
|
|
|
|
|
|
|
|
|
|
if self.predefinedType:
|
2022-02-23 00:24:40 +01:00
|
|
|
self.message = "an entity name '%(name)s' of predefined type '%(predefinedType)s'"
|
2022-05-11 14:49:47 +10:00
|
|
|
return facet_evaluation(is_pass, self.message % {"name": inst.is_a(), "predefinedType": predefined_type})
|
2021-04-29 20:46:55 +02:00
|
|
|
else:
|
|
|
|
|
self.message = "an entity name '%(name)s'"
|
2022-05-11 13:06:15 +10:00
|
|
|
return facet_evaluation(is_pass, self.message % {"name": inst.is_a()})
|
2021-06-21 08:48:46 +02:00
|
|
|
|
2021-02-27 11:04:45 +01:00
|
|
|
|
2022-05-10 16:23:07 +10:00
|
|
|
class attribute(facet):
|
|
|
|
|
"""The IDS attribute facet"""
|
|
|
|
|
|
2022-05-31 15:35:47 +10:00
|
|
|
parameters = ["name", "value", "minOccurs", "maxOccurs", "instructions"]
|
2022-05-10 16:23:07 +10:00
|
|
|
|
|
|
|
|
@staticmethod
|
2022-05-31 15:35:47 +10:00
|
|
|
def create(name="Name", value=None, minOccurs=None, maxOccurs=None, instructions=None):
|
2022-05-10 16:23:07 +10:00
|
|
|
"""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
|
2022-05-11 10:12:36 +10:00
|
|
|
:param value: Attribute value, with type being strictly checked
|
2022-05-10 16:23:07 +10:00
|
|
|
:type value: str, optional
|
2022-05-31 15:35:47 +10:00
|
|
|
:param minOccurs: The minimum total entities that should pass as an integer >= 0
|
|
|
|
|
:type minOccurs: str, optional
|
|
|
|
|
:param maxOccurs: The maximum total entities that should pass as an integer >= 0 or "unbounded"
|
|
|
|
|
:type maxOccurs: str, optional
|
2022-05-11 10:12:36 +10:00
|
|
|
:param instructions: Instructions as a guide for model authors when reading the requirements
|
|
|
|
|
:type instructions: str, optional
|
2022-05-10 16:23:07 +10:00
|
|
|
:return: entity object
|
|
|
|
|
:rtype: entity
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
inst = attribute()
|
|
|
|
|
inst.name = name
|
|
|
|
|
inst.value = value
|
2022-05-31 15:35:47 +10:00
|
|
|
inst.minOccurs = minOccurs
|
|
|
|
|
inst.maxOccurs = maxOccurs
|
2022-05-11 10:12:36 +10:00
|
|
|
inst.instructions = instructions
|
2022-05-10 16:23:07 +10:00
|
|
|
return inst
|
|
|
|
|
|
|
|
|
|
def asdict(self):
|
|
|
|
|
"""Converts object to a dictionary, adding required attributes.
|
|
|
|
|
|
|
|
|
|
:return: Xmlschema compliant dictionary.
|
|
|
|
|
:rtype: dict
|
|
|
|
|
"""
|
2022-05-25 17:49:12 +10:00
|
|
|
results = {"name": parameter_asdict(self.name)}
|
2022-05-10 16:23:07 +10:00
|
|
|
if self.value:
|
2022-05-25 17:49:12 +10:00
|
|
|
results["value"] = parameter_asdict(self.value)
|
2022-05-31 15:35:47 +10:00
|
|
|
if self.minOccurs:
|
|
|
|
|
results["@minOccurs"] = self.minOccurs
|
|
|
|
|
if self.maxOccurs:
|
|
|
|
|
results["@maxOccurs"] = self.maxOccurs
|
2022-05-11 10:12:36 +10:00
|
|
|
if self.instructions:
|
2022-05-25 17:49:12 +10:00
|
|
|
results["@instructions"] = self.instructions
|
|
|
|
|
return results
|
2022-05-10 16:23:07 +10:00
|
|
|
|
2022-05-10 21:20:10 +10:00
|
|
|
def __call__(self, inst, logger=None):
|
2022-05-11 13:06:15 +10:00
|
|
|
"""Validate an ifc instance.
|
2022-05-10 16:23:07 +10: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)
|
|
|
|
|
"""
|
2022-05-11 14:49:47 +10:00
|
|
|
|
2022-05-11 13:06:15 +10:00
|
|
|
def get_values(element, name):
|
|
|
|
|
if isinstance(name, str):
|
|
|
|
|
return [getattr(element, name, None)]
|
|
|
|
|
return [v for k, v in element.get_info().items() if k == name]
|
|
|
|
|
|
2022-05-31 15:35:47 +10:00
|
|
|
element_type = ifcopenshell.util.element.get_type(inst)
|
|
|
|
|
|
|
|
|
|
if isinstance(self.name, str):
|
|
|
|
|
type_value = getattr(element_type, self.name, None) if element_type else None
|
|
|
|
|
occurrence_value = getattr(inst, self.name, None)
|
|
|
|
|
values = [occurrence_value if occurrence_value is not None else type_value]
|
|
|
|
|
else:
|
|
|
|
|
if element_type:
|
|
|
|
|
info = element_type.get_info()
|
|
|
|
|
info.update({k: v for k, v in inst.get_info().items() if v is not None})
|
2022-05-11 13:06:15 +10:00
|
|
|
else:
|
2022-05-31 15:35:47 +10:00
|
|
|
info = inst.get_info()
|
|
|
|
|
values = [v for k, v in info.items() if k == self.name]
|
2022-05-11 13:06:15 +10:00
|
|
|
|
|
|
|
|
is_pass = bool(values) and all([v is not None and v != "" for v in values])
|
|
|
|
|
if is_pass and self.value:
|
|
|
|
|
is_pass = all([v == self.value for v in values])
|
|
|
|
|
|
2022-05-10 16:23:07 +10:00
|
|
|
if self.value:
|
|
|
|
|
self.message = "foo"
|
2022-05-11 13:06:15 +10:00
|
|
|
return facet_evaluation(is_pass, f"an entity with {self.name} set to something wrong")
|
2022-05-10 16:23:07 +10:00
|
|
|
else:
|
2022-05-11 13:06:15 +10:00
|
|
|
return facet_evaluation(is_pass, 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
|
|
|
|
2022-05-31 15:35:47 +10:00
|
|
|
parameters = ["system", "value", "uri", "minOccurs", "maxOccurs" "instructions"]
|
|
|
|
|
message = "sclassification reference %(value)s from '%(system)s'"
|
2021-02-27 11:04:45 +01:00
|
|
|
|
2021-08-24 12:57:44 +02:00
|
|
|
@staticmethod
|
2022-05-31 15:35:47 +10:00
|
|
|
def create(value=None, system=None, uri=None, minOccurs=None, maxOccurs=None, instructions=None):
|
2021-08-24 12:57:44 +02:00
|
|
|
"""Create a classification facet that can be added to applicability or requirements of IDS specification.
|
|
|
|
|
|
|
|
|
|
: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
|
2022-05-31 15:35:47 +10:00
|
|
|
:param minOccurs: The minimum total entities that should pass as an integer >= 0
|
|
|
|
|
:type minOccurs: str, optional
|
|
|
|
|
:param maxOccurs: The maximum total entities that should pass as an integer >= 0 or "unbounded"
|
|
|
|
|
:type maxOccurs: str, optional
|
2021-08-24 12:57:44 +02:00
|
|
|
:return: classification object
|
|
|
|
|
:rtype: classification
|
|
|
|
|
"""
|
2021-07-09 08:35:04 +02:00
|
|
|
inst = classification()
|
2021-07-08 18:12:55 +02:00
|
|
|
inst.value = value
|
|
|
|
|
inst.system = system
|
2022-05-11 13:27:19 +10:00
|
|
|
inst.uri = uri
|
2022-05-31 15:35:47 +10:00
|
|
|
inst.minOccurs = minOccurs
|
|
|
|
|
inst.maxOccurs = maxOccurs
|
2022-05-11 13:27:19 +10:00
|
|
|
inst.instructions = instructions
|
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
|
|
|
|
|
"""
|
2022-05-31 15:35:47 +10:00
|
|
|
results = {}
|
2022-05-11 13:27:19 +10:00
|
|
|
if self.value:
|
|
|
|
|
results["value"] = parameter_asdict(self.value)
|
|
|
|
|
if self.system:
|
|
|
|
|
results["system"] = parameter_asdict(self.system)
|
|
|
|
|
if self.uri:
|
|
|
|
|
results["@uri"] = self.uri
|
2022-05-31 15:35:47 +10:00
|
|
|
if self.minOccurs:
|
|
|
|
|
results["@minOccurs"] = self.minOccurs
|
|
|
|
|
if self.maxOccurs:
|
|
|
|
|
results["@maxOccurs"] = self.maxOccurs
|
2022-05-11 13:27:19 +10:00
|
|
|
if self.instructions:
|
|
|
|
|
results["@instructions"] = self.instructions
|
|
|
|
|
return results
|
2021-06-21 17:48:27 +02:00
|
|
|
|
2022-05-11 14:49:47 +10:00
|
|
|
def __call__(self, inst, logger=None):
|
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)
|
|
|
|
|
"""
|
2022-05-31 15:35:47 +10:00
|
|
|
leaf_references = ifcopenshell.util.classification.get_references(inst)
|
2021-08-24 12:57:44 +02:00
|
|
|
|
2022-05-11 14:49:47 +10:00
|
|
|
references = leaf_references.copy()
|
|
|
|
|
for leaf_reference in leaf_references:
|
|
|
|
|
references.update(ifcopenshell.util.classification.get_inherited_references(leaf_reference))
|
2021-06-21 17:48:27 +02:00
|
|
|
|
2022-05-11 14:49:47 +10:00
|
|
|
is_pass = bool(references)
|
|
|
|
|
if is_pass and self.value:
|
|
|
|
|
is_pass = any(
|
|
|
|
|
[self.value == getattr(r, "Identification", getattr(r, "ItemReference", None)) for r in references]
|
|
|
|
|
)
|
|
|
|
|
if is_pass and self.system:
|
|
|
|
|
is_pass = any(
|
|
|
|
|
[self.system == ifcopenshell.util.classification.get_classification(r).Name for r in references]
|
|
|
|
|
)
|
2021-06-21 17:48:27 +02:00
|
|
|
|
2022-05-11 14:49:47 +10:00
|
|
|
if references:
|
2021-06-10 15:21:51 +02:00
|
|
|
return facet_evaluation(
|
2022-05-11 14:49:47 +10:00
|
|
|
is_pass,
|
2021-08-24 12:57:44 +02:00
|
|
|
self.message
|
|
|
|
|
% {
|
2022-05-11 14:49:47 +10:00
|
|
|
"system": list(references)[0][0],
|
|
|
|
|
"value": list(references)[0][1],
|
|
|
|
|
}, # TODO Fix this 0 index reference assumption when I refactor out the messages
|
2021-06-10 15:21:51 +02:00
|
|
|
)
|
2021-08-24 12:57:44 +02:00
|
|
|
else:
|
2022-05-31 15:35:47 +10:00
|
|
|
return facet_evaluation(False, "does not have classification reference")
|
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"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2022-05-12 14:33:44 +10:00
|
|
|
def create(entity="IfcSystem"):
|
2022-02-23 00:24:40 +01:00
|
|
|
"""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
|
|
|
|
|
"""
|
2022-05-25 17:49:12 +10:00
|
|
|
return {"@entity": self.entity}
|
2022-02-23 00:24:40 +01:00
|
|
|
|
2022-05-12 14:33:44 +10:00
|
|
|
def __call__(self, inst, logger=None):
|
2022-02-23 00:24:40 +01:00
|
|
|
"""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)
|
|
|
|
|
"""
|
2022-05-12 14:33:44 +10:00
|
|
|
if self.entity == "IfcElementAssembly":
|
|
|
|
|
is_pass = False
|
|
|
|
|
aggregate = ifcopenshell.util.element.get_aggregate(inst)
|
|
|
|
|
while aggregate is not None:
|
|
|
|
|
if aggregate.is_a() == "IfcElementAssembly":
|
|
|
|
|
is_pass = True
|
|
|
|
|
break
|
|
|
|
|
aggregate = ifcopenshell.util.element.get_aggregate(aggregate)
|
2022-02-23 00:24:40 +01:00
|
|
|
else:
|
2022-05-12 14:33:44 +10:00
|
|
|
is_pass = False
|
|
|
|
|
for rel in getattr(inst, "HasAssignments", []) or []:
|
|
|
|
|
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.is_a(self.entity):
|
|
|
|
|
is_pass = True
|
|
|
|
|
|
|
|
|
|
return facet_evaluation(is_pass, "is not a part of")
|
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-05-31 15:35:47 +10:00
|
|
|
parameters = ["name", "propertySet", "value"]
|
|
|
|
|
message = "property '%(name)s' in '%(propertySet)s' with a value %(value)s"
|
2021-08-24 12:57:44 +02:00
|
|
|
|
|
|
|
|
@staticmethod
|
2022-05-11 19:59:10 +10:00
|
|
|
def create(
|
|
|
|
|
propertySet="Property_Set",
|
|
|
|
|
name="PropertyName",
|
|
|
|
|
value=None,
|
|
|
|
|
measure=None,
|
|
|
|
|
uri=None,
|
2022-05-31 15:35:47 +10:00
|
|
|
minOccurs=None,
|
|
|
|
|
maxOccurs=None,
|
2022-05-11 19:59:10 +10:00
|
|
|
instructions=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-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
|
2022-05-31 15:35:47 +10:00
|
|
|
:param minOccurs: The minimum total entities that should pass as an integer >= 0
|
|
|
|
|
:type minOccurs: str, optional
|
|
|
|
|
:param maxOccurs: The maximum total entities that should pass as an integer >= 0 or "unbounded"
|
|
|
|
|
:type maxOccurs: str, optional
|
2021-08-24 12:57:44 +02:00
|
|
|
:return: property object
|
|
|
|
|
:rtype: property
|
|
|
|
|
"""
|
2021-07-08 18:12:55 +02:00
|
|
|
inst = property()
|
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
|
2022-05-11 15:30:39 +10:00
|
|
|
inst.measure = measure
|
|
|
|
|
inst.uri = uri
|
2022-05-31 15:35:47 +10:00
|
|
|
inst.minOccurs = minOccurs
|
|
|
|
|
inst.maxOccurs = maxOccurs
|
2022-05-11 15:30:39 +10:00
|
|
|
inst.instructions = instructions
|
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
|
|
|
|
|
"""
|
2022-05-11 15:30:39 +10:00
|
|
|
results = {
|
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),
|
|
|
|
|
}
|
2022-05-11 15:30:39 +10:00
|
|
|
if self.value:
|
|
|
|
|
results["value"] = parameter_asdict(self.value)
|
|
|
|
|
if self.measure:
|
|
|
|
|
results["@measure"] = self.measure
|
|
|
|
|
if self.uri:
|
|
|
|
|
results["@uri"] = self.uri
|
2022-05-31 15:35:47 +10:00
|
|
|
if self.minOccurs:
|
|
|
|
|
results["@minOccurs"] = self.minOccurs
|
|
|
|
|
if self.maxOccurs:
|
|
|
|
|
results["@maxOccurs"] = self.maxOccurs
|
2022-05-11 15:30:39 +10:00
|
|
|
if self.instructions:
|
|
|
|
|
results["@instructions"] = self.instructions
|
|
|
|
|
# TODO '@href': 'http://identifier.buildingsmart.org/uri/buildingsmart/ifc-4.3/prop/FireRating', #https://identifier.buildingsmart.org/uri/something
|
|
|
|
|
return results
|
2021-07-08 18:06:07 +02:00
|
|
|
|
2022-05-11 17:10:22 +10:00
|
|
|
def __call__(self, inst, logger=None):
|
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)
|
|
|
|
|
"""
|
2022-05-31 15:35:47 +10:00
|
|
|
all_psets = ifcopenshell.util.element.get_psets(inst)
|
2021-06-21 17:48:27 +02:00
|
|
|
|
2022-05-11 17:10:22 +10:00
|
|
|
if isinstance(self.propertySet, str):
|
|
|
|
|
pset = all_psets.get(self.propertySet, None)
|
|
|
|
|
psets = {self.propertySet: pset} if pset else {}
|
2022-05-10 22:42:21 +10:00
|
|
|
else:
|
2022-05-11 17:10:22 +10:00
|
|
|
psets = {k: v for k, v in all_psets.items() if k == self.propertySet}
|
2021-09-08 20:16:39 +10:00
|
|
|
|
2022-05-11 17:10:22 +10:00
|
|
|
is_pass = bool(psets)
|
2021-08-24 12:57:44 +02:00
|
|
|
|
2022-05-11 17:10:22 +10:00
|
|
|
if is_pass:
|
|
|
|
|
props = {}
|
|
|
|
|
for pset_name, pset_props in psets.items():
|
|
|
|
|
props[pset_name] = {}
|
|
|
|
|
if isinstance(self.name, str):
|
|
|
|
|
prop = pset_props.get(self.name)
|
|
|
|
|
if prop:
|
|
|
|
|
props[pset_name][self.name] = prop
|
|
|
|
|
else:
|
|
|
|
|
props[pset_name] = {k: v for k, v in pset_props.items() if k == self.name}
|
2021-08-24 12:57:44 +02:00
|
|
|
|
2022-05-11 17:10:22 +10:00
|
|
|
if not bool(props[pset_name]):
|
|
|
|
|
is_pass = False
|
|
|
|
|
break
|
2021-02-28 09:34:35 +01:00
|
|
|
|
2022-05-11 19:59:10 +10:00
|
|
|
if self.measure:
|
|
|
|
|
pset_entity = inst.wrapped_data.file.by_id(pset_props["id"])
|
|
|
|
|
for prop_entity in pset_entity.HasProperties:
|
|
|
|
|
if (
|
|
|
|
|
prop_entity.Name not in props[pset_name].keys()
|
|
|
|
|
or not prop_entity.is_a("IfcPropertySingleValue")
|
|
|
|
|
or prop_entity.NominalValue is None
|
|
|
|
|
):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
data_type = prop_entity.NominalValue.is_a().replace("Ifc", "").replace("Measure", "")
|
|
|
|
|
|
|
|
|
|
if data_type != self.measure:
|
|
|
|
|
is_pass = False
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
unit = ifcopenshell.util.unit.get_property_unit(prop_entity, inst.wrapped_data.file)
|
|
|
|
|
|
|
|
|
|
props[pset_name][prop_entity.Name] = ifcopenshell.util.unit.convert(
|
|
|
|
|
prop_entity.NominalValue.wrappedValue,
|
|
|
|
|
getattr(unit, "Prefix", None),
|
|
|
|
|
unit.Name,
|
|
|
|
|
None,
|
|
|
|
|
ifcopenshell.util.unit.si_type_names[unit.UnitType],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not is_pass:
|
|
|
|
|
break
|
|
|
|
|
|
2022-05-11 17:10:22 +10:00
|
|
|
if self.value:
|
|
|
|
|
if any([v != self.value for v in props[pset_name].values()]):
|
|
|
|
|
is_pass = False
|
|
|
|
|
break
|
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
|
|
|
|
|
|
2022-05-11 17:10:22 +10:00
|
|
|
return facet_evaluation(is_pass, "todo")
|
2021-02-27 11:04:45 +01:00
|
|
|
|
|
|
|
|
|
2021-04-09 10:16:08 +02:00
|
|
|
class material(facet):
|
2021-08-24 12:57:44 +02:00
|
|
|
"""The IDS material facet used to traverse the HasAssociations inverse attribute."""
|
|
|
|
|
|
2022-05-31 15:35:47 +10:00
|
|
|
parameters = ["value"]
|
|
|
|
|
message = "material '%(value)s'"
|
2021-08-24 12:57:44 +02:00
|
|
|
|
|
|
|
|
@staticmethod
|
2022-05-31 15:35:47 +10:00
|
|
|
def create(value=None, uri=None, minOccurs=None, maxOccurs=None, instructions=None):
|
2021-08-24 12:57:44 +02:00
|
|
|
"""Create a material facet that can be added to applicability or requirements of IDS specification.
|
|
|
|
|
|
|
|
|
|
: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.value = value
|
2022-05-11 23:30:10 +10:00
|
|
|
inst.uri = uri
|
2022-05-31 15:35:47 +10:00
|
|
|
inst.minOccurs = minOccurs
|
|
|
|
|
inst.maxOccurs = maxOccurs
|
2022-05-11 23:30:10 +10:00
|
|
|
inst.instructions = instructions
|
2021-07-08 18:12:55 +02:00
|
|
|
return inst
|
2021-04-09 10:16:08 +02:00
|
|
|
|
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
|
|
|
|
|
"""
|
2022-05-31 15:35:47 +10:00
|
|
|
results = {}
|
2022-05-11 23:30:10 +10:00
|
|
|
if self.value:
|
|
|
|
|
results["value"] = parameter_asdict(self.value)
|
|
|
|
|
if self.uri:
|
|
|
|
|
results["@uri"] = self.uri
|
2022-05-31 15:35:47 +10:00
|
|
|
if self.minOccurs:
|
|
|
|
|
results["@minOccurs"] = self.minOccurs
|
|
|
|
|
if self.maxOccurs:
|
|
|
|
|
results["@maxOccurs"] = self.maxOccurs
|
2022-05-11 23:30:10 +10:00
|
|
|
if self.instructions:
|
|
|
|
|
results["@instructions"] = self.instructions
|
|
|
|
|
return results
|
2021-07-08 18:06:07 +02:00
|
|
|
|
2022-05-12 12:06:23 +10:00
|
|
|
def __call__(self, inst, logger=None):
|
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)
|
|
|
|
|
"""
|
2022-05-31 15:35:47 +10:00
|
|
|
material = ifcopenshell.util.element.get_material(inst, should_skip_usage=True)
|
2021-06-11 17:10:54 +02:00
|
|
|
|
2022-05-12 12:06:23 +10:00
|
|
|
is_pass = material is not None
|
2021-06-18 16:50:26 +02:00
|
|
|
|
2022-05-12 12:06:23 +10:00
|
|
|
if is_pass and self.value:
|
|
|
|
|
if material.is_a("IfcMaterial"):
|
|
|
|
|
values = {material.Name, getattr(material, "Category")}
|
|
|
|
|
elif material.is_a("IfcMaterialList"):
|
|
|
|
|
values = set()
|
|
|
|
|
for mat in material.Materials or []:
|
|
|
|
|
values.update([mat.Name, getattr(mat, "Category")])
|
|
|
|
|
elif material.is_a("IfcMaterialLayerSet"):
|
|
|
|
|
values = {material.LayerSetName}
|
|
|
|
|
for item in material.MaterialLayers or []:
|
|
|
|
|
values.update([item.Name, item.Category, item.Material.Name, getattr(item.Material, "Category")])
|
|
|
|
|
elif material.is_a("IfcMaterialProfileSet"):
|
|
|
|
|
values = {material.Name}
|
|
|
|
|
for item in material.MaterialProfiles or []:
|
|
|
|
|
values.update([item.Name, item.Category, item.Material.Name, getattr(item.Material, "Category")])
|
|
|
|
|
elif material.is_a("IfcMaterialConstituentSet"):
|
|
|
|
|
values = {material.Name}
|
|
|
|
|
for item in material.MaterialConstituents or []:
|
|
|
|
|
values.update([item.Name, item.Category, item.Material.Name, getattr(item.Material, "Category")])
|
|
|
|
|
|
|
|
|
|
is_pass = False
|
|
|
|
|
for value in values:
|
|
|
|
|
if value == self.value:
|
|
|
|
|
is_pass = True
|
|
|
|
|
break
|
2021-06-21 17:48:27 +02:00
|
|
|
|
2021-04-09 10:16:08 +02:00
|
|
|
return facet_evaluation(
|
2022-05-12 12:06:23 +10:00
|
|
|
is_pass,
|
|
|
|
|
self.message % {"value": "todo", "location": "todo"},
|
2021-04-09 10:16:08 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
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 = ""
|
2021-04-09 10:16:08 +02:00
|
|
|
self.options = []
|
2021-06-21 08:48:46 +02:00
|
|
|
|
2021-08-24 12:57:44 +02:00
|
|
|
@staticmethod
|
|
|
|
|
def parse(ids_dict):
|
|
|
|
|
"""Parse xml restriction to python object.
|
2022-05-10 19:26:12 +10:00
|
|
|
|
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-05-10 19:26:12 +10:00
|
|
|
|
2022-02-23 00:24:40 +01:00
|
|
|
try:
|
|
|
|
|
r.base = ids_dict["@base"][3:]
|
2022-05-10 19:26:12 +10:00
|
|
|
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:
|
2021-09-08 20:16:39 +10:00
|
|
|
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:
|
2022-05-25 17:49:12 +10:00
|
|
|
rest_dict["xs:" + option] = [{"@value": str(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):
|
2022-05-10 19:26:12 +10:00
|
|
|
# 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)
|
2021-04-09 10:16:08 +02:00
|
|
|
if self.type == "enumeration":
|
2021-08-24 12:57:44 +02:00
|
|
|
msg = msg + "of value: '%s'" % "' or '".join(self.options)
|
2021-04-09 10:16:08 +02:00
|
|
|
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])
|
2021-04-09 10:16:08 +02:00
|
|
|
elif self.type == "length":
|
2021-08-24 12:57:44 +02:00
|
|
|
msg = msg + "with %s letters" % " and ".join(self.options)
|
2021-04-09 10:16:08 +02:00
|
|
|
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__":
|
2021-04-09 10:16:08 +02:00
|
|
|
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-04-09 10:16:08 +02:00
|
|
|
|
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)
|