2021-02-27 11:04:45 +01:00
|
|
|
import operator
|
|
|
|
|
import ifcopenshell.util.element
|
|
|
|
|
|
|
|
|
|
from xml.dom.minidom import parse
|
|
|
|
|
|
|
|
|
|
|
2021-02-28 09:34:35 +01:00
|
|
|
class exception(Exception):
|
|
|
|
|
pass
|
|
|
|
|
|
2021-02-27 11:04:45 +01:00
|
|
|
|
|
|
|
|
def error(msg):
|
|
|
|
|
raise exception(msg)
|
2021-02-28 09:34:35 +01:00
|
|
|
|
|
|
|
|
|
2021-02-28 09:31:21 +01:00
|
|
|
class facet_evaluation:
|
|
|
|
|
"""
|
|
|
|
|
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):
|
|
|
|
|
"""
|
|
|
|
|
A metaclass for automatically registering facets in a map to be instantiated based on XML tagnames.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, node):
|
|
|
|
|
self.node = node
|
|
|
|
|
|
|
|
|
|
def __getattr__(self, k):
|
|
|
|
|
v = self.node.getElementsByTagName(k)[0]
|
|
|
|
|
elems = [n for n in v.childNodes if n.nodeType == n.ELEMENT_NODE]
|
|
|
|
|
if elems:
|
|
|
|
|
return restriction(elems[0])
|
|
|
|
|
else:
|
|
|
|
|
return v.firstChild.nodeValue.strip()
|
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):
|
|
|
|
|
return self.message % dict(list(self))
|
2021-02-27 11:04:45 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class entity(facet):
|
|
|
|
|
"""
|
|
|
|
|
The IDS entity facet currently *with* inheritance
|
|
|
|
|
"""
|
2021-02-28 09:34:35 +01:00
|
|
|
|
2021-02-28 09:31:21 +01:00
|
|
|
parameters = ["name"]
|
|
|
|
|
message = "an entity name '%(name)s'"
|
2021-02-27 11:04:45 +01:00
|
|
|
|
|
|
|
|
def __call__(self, inst, logger):
|
|
|
|
|
logger.debug("Testing %s == %s", inst.is_a(), self.name)
|
|
|
|
|
# @nb with inheritance
|
|
|
|
|
# return inst.is_a() == self.name
|
2021-02-28 09:34:35 +01:00
|
|
|
return facet_evaluation(inst.is_a(self.name), self.message % {"name": inst.is_a()})
|
|
|
|
|
|
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-02-28 09:31:21 +01:00
|
|
|
parameters = ["system", "value"]
|
|
|
|
|
message = "a classification reference to '%(value)s' from '%(system)s'"
|
2021-02-27 11:04:45 +01:00
|
|
|
|
|
|
|
|
def __call__(self, inst, logger):
|
|
|
|
|
refs = []
|
|
|
|
|
for association in inst.HasAssociations:
|
|
|
|
|
if association.is_a("IfcRelAssociatesClassification"):
|
|
|
|
|
cref = association.RelatingClassification
|
|
|
|
|
refs.append((cref.ReferencedSource, cref.Name))
|
|
|
|
|
|
2021-02-28 09:31:21 +01:00
|
|
|
return facet_evaluation(
|
|
|
|
|
(self.system, self.value) in refs,
|
|
|
|
|
# @todo
|
2021-02-28 09:34:35 +01:00
|
|
|
"",
|
2021-02-28 09:31:21 +01:00
|
|
|
)
|
2021-02-27 11:04:45 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class property(facet):
|
|
|
|
|
"""
|
|
|
|
|
The IDS property facet implenented using `ifcopenshell.util.element`
|
|
|
|
|
"""
|
|
|
|
|
|
2021-02-28 09:31:21 +01:00
|
|
|
parameters = ["property", "propertyset", "value"]
|
|
|
|
|
message = "a property '%(property)s' in '%(propertyset)s' with value '%(value)s'"
|
|
|
|
|
|
2021-02-27 11:04:45 +01:00
|
|
|
def __call__(self, inst, logger):
|
|
|
|
|
props = ifcopenshell.util.element.get_psets(inst)
|
2021-02-28 09:31:21 +01:00
|
|
|
pset = props.get(self.propertyset)
|
|
|
|
|
val = pset.get(self.property) if pset else None
|
2021-02-27 11:04:45 +01:00
|
|
|
logger.debug("Testing %s == %s", val, self.value)
|
2021-02-28 09:34:35 +01:00
|
|
|
|
2021-02-28 09:31:21 +01:00
|
|
|
di = {
|
2021-02-28 09:34:35 +01:00
|
|
|
"property": self.property,
|
|
|
|
|
"propertyset": self.propertyset,
|
|
|
|
|
"value": val,
|
2021-02-28 09:31:21 +01:00
|
|
|
}
|
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:
|
|
|
|
|
msg = "a set '%(propertyset)s', but no property '%(property)'" % di
|
|
|
|
|
else:
|
|
|
|
|
msg = "no set '%(propertyset)s'" % di
|
2021-02-28 09:34:35 +01:00
|
|
|
|
|
|
|
|
return facet_evaluation(val == self.value, msg)
|
2021-02-27 11:04:45 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class boolean_logic:
|
|
|
|
|
"""
|
|
|
|
|
Boolean conjunction over a collection of functions
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
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-02-28 09:34:35 +01:00
|
|
|
return facet_evaluation(self.fold(eval), join.join(map(str, eval)))
|
|
|
|
|
|
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
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, node):
|
|
|
|
|
self.options = [
|
|
|
|
|
n.getAttribute("value")
|
|
|
|
|
for n in node.childNodes
|
|
|
|
|
if n.nodeType == n.ELEMENT_NODE and n.tagName.endswith("enumeration")
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
|
return other in self.options
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
2021-02-28 09:31:21 +01:00
|
|
|
return " or ".join(self.options)
|
2021-02-27 11:04:45 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class specification:
|
|
|
|
|
"""
|
|
|
|
|
Represents the XML <specification> node and its two children <applicability> and <requirements>
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, node):
|
|
|
|
|
def parse_rules(node):
|
|
|
|
|
children = [n for n in node.childNodes if n.nodeType == n.ELEMENT_NODE]
|
|
|
|
|
names = map(operator.attrgetter("tagName"), children)
|
|
|
|
|
classes = map(meta_facet.facets.__getitem__, names)
|
|
|
|
|
return [cls(n) for cls, n in zip(classes, children)]
|
|
|
|
|
|
|
|
|
|
phrases = [n for n in node.childNodes if n.nodeType == n.ELEMENT_NODE]
|
2021-02-28 09:34:35 +01:00
|
|
|
|
2021-02-27 11:04:45 +01:00
|
|
|
len(phrases) == 2 or error("expected two child nodes for <specification>")
|
|
|
|
|
phrases[0].tagName == "applicability" or error("expected <applicability>")
|
|
|
|
|
phrases[1].tagName == "requirements" or error("expected <requirements>")
|
2021-02-28 09:34:35 +01:00
|
|
|
|
2021-04-07 14:18:06 +02:00
|
|
|
self.applicability, self.requirements = (boolean_and(parse_rules(phrase)) for phrase in phrases)
|
2021-02-27 11:04:45 +01:00
|
|
|
|
|
|
|
|
def __call__(self, inst, logger):
|
2021-04-07 14:18:06 +02:00
|
|
|
if self.applicability(inst, logger):
|
2021-02-28 09:31:21 +01:00
|
|
|
valid = self.requirements(inst, logger)
|
|
|
|
|
if valid:
|
|
|
|
|
logger.info(str(self) + "\n%s has" % inst + " " + str(valid) + " so is compliant")
|
2021-02-27 11:04:45 +01:00
|
|
|
else:
|
2021-02-28 09:31:21 +01:00
|
|
|
logger.error(str(self) + "\n%s has" % inst + " " + str(valid) + " so is not compliant")
|
2021-02-28 09:34:35 +01:00
|
|
|
|
2021-02-28 09:31:21 +01:00
|
|
|
def __str__(self):
|
2021-04-07 14:18:06 +02:00
|
|
|
return "Given an instance with %(applicability)s\nWe expect %(requirements)s" % self.__dict__
|
2021-02-27 11:04:45 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ids:
|
|
|
|
|
"""
|
|
|
|
|
Represents the XML root <ids> node and its <specification> childNodes.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, fn):
|
|
|
|
|
dom = parse(fn)
|
|
|
|
|
ids = dom.childNodes[0]
|
|
|
|
|
ids.tagName == "ids" or error("expected <ids>")
|
|
|
|
|
|
|
|
|
|
self.specifications = [
|
2021-02-28 09:34:35 +01:00
|
|
|
specification(n) for n in ids.childNodes if n.nodeType == n.ELEMENT_NODE and n.tagName == "specification"
|
2021-02-27 11:04:45 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def validate(self, ifc_file, logger):
|
2021-02-28 09:31:21 +01:00
|
|
|
for spec in self.specifications:
|
|
|
|
|
for elem in ifc_file.by_type("IfcObject"):
|
2021-02-27 11:04:45 +01:00
|
|
|
spec(elem, logger)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
import sys
|
|
|
|
|
import logging
|
|
|
|
|
import ifcopenshell
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger("IDS")
|
2021-02-28 09:34:35 +01:00
|
|
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
2021-02-27 11:04:45 +01:00
|
|
|
|
|
|
|
|
ids_file = ids(sys.argv[1])
|
|
|
|
|
ifc_file = ifcopenshell.open(sys.argv[2])
|
|
|
|
|
ids_file.validate(ifc_file, logger)
|