diff --git a/src/ifcbimtester/features/steps/geolocation.py b/src/ifcbimtester/features/steps/geolocation.py new file mode 100644 index 0000000000..417cc6eddb --- /dev/null +++ b/src/ifcbimtester/features/steps/geolocation.py @@ -0,0 +1,179 @@ +from behave import step +from utils import IfcFile +import math +import ifcopenshell.util +import ifcopenshell.util.element + +@step(u'There must be at least one {ifc_class} element') +def step_impl(context, ifc_class): + assert len(IfcFile.get().by_type(ifc_class)) >= 1, 'An element of {} could not be found'.format(ifc_class) + + +def check_ifc2x3_geolocation(pset_name, prop_name=None, value=None): + for site in IfcFile.get().by_type('IfcSite'): + psets = ifcopenshell.util.element.get_psets(site) + if pset_name not in psets: + assert False, 'The site {} does not have a property set named {}'.format(site, pset_name) + if not prop_name: + continue + if prop_name not in psets[pset_name]: + assert False, 'The site {} does not have a property named "{}" in {}'.format(site, prop_name, pset_name) + actual_value = psets[pset_name][prop_name] + assert actual_value == value, 'We expected a value of "{}" but instead got "{}"'.format(value, actual_value) + +def check_ifc4_geolocation(entity_name, prop_name=None, value=None, should_assert=True): + if entity_name not in IfcFile.bookmarks: + has_entity = False + project = IfcFile.get().by_type('IfcProject')[0] + for context in project.RepresentationContexts: + if entity_name == 'IfcMapConversion': + if context.is_a('IfcGeometricRepresentationContext') \ + and context.ContextType == 'Model' \ + and context.HasCoordinateOperation: + IfcFile.bookmarks[entity_name] = context.HasCoordinateOperation[0] + has_entity = True + elif entity_name == 'IfcProjectedCRS': + if context.is_a('IfcGeometricRepresentationContext') \ + and context.ContextType == 'Model' \ + and context.HasCoordinateOperation \ + and context.HasCoordinateOperation[0].TargetCRS: + IfcFile.bookmarks[entity_name] = context.HasCoordinateOperation[0].TargetCRS + has_entity = True + if not has_entity: + assert False, 'No model geometric representation contexts refer to an {}'.format(entity_name) + if not prop_name: + return + actual_value = getattr(IfcFile.bookmarks[entity_name], prop_name) + if should_assert: + assert actual_value == value, 'We expected a value of "{}" but instead got "{}"'.format(value, actual_value) + else: + return actual_value + + +@step(u'The project must have coordinate reference system data') +def step_impl(context): + if IfcFile.get().schema == 'IFC2X3': + return check_ifc2x3_geolocation('EPset_ProjectedCRS') + check_ifc4_geolocation('IfcProjectedCRS') + + +@step(u'The name of the CRS must be {coordinate_reference_name}') +def step_impl(context, coordinate_reference_name): + if IfcFile.get().schema == 'IFC2X3': + return check_ifc2x3_geolocation('EPset_ProjectedCRS', 'Name', coordinate_reference_name) + check_ifc4_geolocation('IfcProjectedCRS', 'Name', coordinate_reference_name) + + +@step(u'The description of the CRS must be {value}') +def step_impl(context, value): + if IfcFile.get().schema == 'IFC2X3': + return check_ifc2x3_geolocation('EPset_ProjectedCRS', 'Description', value) + check_ifc4_geolocation('IfcProjectedCRS', 'Description', value) + + +@step(u'The geodetic datum must be {coordinate_reference_name}') +def step_impl(context, coordinate_reference_name): + if IfcFile.get().schema == 'IFC2X3': + return check_ifc2x3_geolocation('EPset_ProjectedCRS', 'GeodeticDatum', coordinate_reference_name) + check_ifc4_geolocation('IfcProjectedCRS', 'GeodeticDatum', coordinate_reference_name) + + +@step(u'The vertical datum must be {coordinate_reference_name}') +def step_impl(context, coordinate_reference_name): + if IfcFile.get().schema == 'IFC2X3': + return check_ifc2x3_geolocation('EPset_ProjectedCRS', 'VerticalDatum', coordinate_reference_name) + check_ifc4_geolocation('IfcProjectedCRS', 'VerticalDatum', coordinate_reference_name) + + +@step(u'The map projection must be {coordinate_reference_name}') +def step_impl(context, coordinate_reference_name): + if IfcFile.get().schema == 'IFC2X3': + return check_ifc2x3_geolocation('EPset_ProjectedCRS', 'MapProjection', coordinate_reference_name) + check_ifc4_geolocation('IfcProjectedCRS', 'MapProjection', coordinate_reference_name) + + +@step(u'The map zone must be {coordinate_reference_name}') +def step_impl(context, coordinate_reference_name): + if IfcFile.get().schema == 'IFC2X3': + return check_ifc2x3_geolocation('EPset_ProjectedCRS', 'MapZone', coordinate_reference_name) + check_ifc4_geolocation('IfcProjectedCRS', 'MapZone', coordinate_reference_name) + + +@step(u'The map unit must be {unit}') +def step_impl(context, unit): + if IfcFile.get().schema == 'IFC2X3': + return check_ifc2x3_geolocation('EPset_ProjectedCRS', 'MapUnit', unit) + actual_value = check_ifc4_geolocation('IfcProjectedCRS', 'MapUnit', should_assert=False) + if actual_value.is_a('IfcSIUnit'): + prefix = actual_value.Prefix if actual_value.Prefix else '' + actual_value = prefix + actual_value.Name + elif actual_value.is_a('IfcConversionBasedUnit'): + actual_value = actual_value.Name + assert actual_value == unit, 'We expected a value of "{}" but instead got "{}"'.format(unit, actual_value) + + +@step(u'The project must have coordinate transformations to convert from local to global coordinates') +def step_impl(context): + if IfcFile.get().schema == 'IFC2X3': + return check_ifc2x3_geolocation('EPset_MapConversion') + check_ifc4_geolocation('IfcMapConversion') + + +@step(u'The eastings of the model must be offset by {number} to derive its global coordinates') +def step_impl(context, number): + try: + number = float(number) + except: + assert False, 'A number should be specified, not {}'.format(number) + if IfcFile.get().schema == 'IFC2X3': + return check_ifc2x3_geolocation('EPset_MapConversion', 'Eastings', number) + check_ifc4_geolocation('IfcMapConversion', 'Eastings', number) + + +@step(u'The northings of the model must be offset by {number} to derive its global coordinates') +def step_impl(context, number): + try: + number = float(number) + except: + assert False, 'A number should be specified, not {}'.format(number) + if IfcFile.get().schema == 'IFC2X3': + return check_ifc2x3_geolocation('EPset_MapConversion', 'Northings', number) + check_ifc4_geolocation('IfcMapConversion', 'Northings', number) + + +@step(u'The height of the model must be offset by {number} to derive its global coordinates') +def step_impl(context, number): + try: + number = float(number) + except: + assert False, 'A number should be specified, not {}'.format(number) + if IfcFile.get().schema == 'IFC2X3': + return check_ifc2x3_geolocation('EPset_MapConversion', 'OrthogonalHeight', number) + check_ifc4_geolocation('IfcMapConversion', 'OrthogonalHeight', number) + + +@step(u'The model must be rotated clockwise by {number} to derive its global coordinates') +def step_impl(context, number): + try: + number = float(number) + except: + assert False, 'A number should be specified, not {}'.format(number) + if IfcFile.get().schema == 'IFC2X3': + return check_ifc2x3_geolocation('EPset_MapConversion', 'Height', number) + abscissa = check_ifc4_geolocation('IfcMapConversion', 'XAxisAbscissa', should_assert=False) + ordinate = check_ifc4_geolocation('IfcMapConversion', 'XAxisOrdinate', should_assert=False) + # TODO: migrate to geolocation util + actual_value = round(math.degrees(math.atan2(ordinate, abscissa)) - 90, 3) + value = round(number, 3) + assert actual_value == value, 'We expected a value of "{}" but instead got "{}"'.format(value, actual_value) + + +@step(u'The model must be scaled along the horizontal axis by {number} to derive its global coordinates') +def step_impl(context, number): + try: + number = float(number) + except: + assert False, 'A number should be specified, not {}'.format(number) + if IfcFile.get().schema == 'IFC2X3': + return check_ifc2x3_geolocation('EPset_MapConversion', 'Scale', number) + check_ifc4_geolocation('IfcMapConversion', 'Scale', number) diff --git a/src/ifcbimtester/features/steps/project_setup.py b/src/ifcbimtester/features/steps/project_setup.py new file mode 100644 index 0000000000..252c033d9d --- /dev/null +++ b/src/ifcbimtester/features/steps/project_setup.py @@ -0,0 +1,93 @@ +from behave import step +from utils import IfcFile + +@step('The IFC file "{file}" must be provided') +def step_impl(context, file): + try: + IfcFile.load(file) + except: + assert False, f'The file {file} could not be loaded' + + +@step('IFC data must use the {schema} schema') +def step_impl(context, schema): + assert IfcFile.get().schema == schema, \ + 'We expected a schema of {} but instead got {}'.format( + schema, IfcFile.get().schema) + + +@step('The IFC file "{file}" is exempt from being provided') +def step_impl(context, file): + pass + + +@step('No further requirements are specified because {reason}') +def step_impl(context, reason): + pass + + +@step('The project name, code, or short identifier must be "{value}"') +def step_impl(context, value): + project_value = getattr(IfcFile.get().by_type('IfcProject')[0], 'Name') + assert project_value == value, 'We expected a value of "{}" but instead got "{}"'.format( + value, project_value) + + +@step('The project must have a longer form name of "{value}"') +def step_impl(context, value): + project_value = getattr(IfcFile.get().by_type('IfcProject')[0], 'LongName') + assert project_value == value, 'We expected a value of "{}" but instead got "{}"'.format( + value, project_value) + + +@step('The project must be described as "{value}"') +def step_impl(context, value): + project_value = getattr(IfcFile.get().by_type('IfcProject')[0], 'Description') + assert project_value == value, 'We expected a value of "{}" but instead got "{}"'.format( + value, project_value) + + +@step('The project must be categorised under "{value}"') +def step_impl(context, value): + project_value = getattr(IfcFile.get().by_type('IfcProject')[0], 'ObjectType') + assert project_value == value, 'We expected a value of "{}" but instead got "{}"'.format( + value, project_value) + + +@step('The project must contain information about the "{value}" phase') +def step_impl(context, value): + project_value = getattr(IfcFile.get().by_type('IfcProject')[0], 'Phase') + assert project_value == value, 'We expected a value of "{}" but instead got "{}"'.format( + value, project_value) + + +@step('The project must contain 3D geometry representing the shape of objects') +def step_impl(context): + assert get_subcontext('Body', 'Model', 'MODEL_VIEW') + + +@step('The project must contain 3D geometry representing clearance zones') +def step_impl(context): + assert get_subcontext('Clearance', 'Model', 'MODEL_VIEW') + + +@step('The project must contain 3D geometry representing the center of gravity of objects') +def step_impl(context): + assert get_subcontext('CoG', 'Model', 'MODEL_VIEW') + + +@step('The project must contain 3D geometry representing the object bounding boxes') +def step_impl(context): + assert get_subcontext('Box', 'Model', 'MODEL_VIEW') + + +def get_subcontext(identifier, type, target_view): + project = IfcFile.get().by_type('IfcProject')[0] + for rep_context in project.RepresentationContexts: + for subcontext in rep_context.HasSubContexts: + if subcontext.ContextIdentifier == identifier \ + and subcontext.ContextType == type \ + and subcontext.TargetView == target_view: + return True + assert False, 'The subcontext with identifier {}, type {}, and target view {} could not be found'.format( + identifier, type, target_view) diff --git a/src/ifcbimtester/features/steps/steps.py b/src/ifcbimtester/features/steps/steps.py index ab4b96f278..6a4d59edf2 100644 --- a/src/ifcbimtester/features/steps/steps.py +++ b/src/ifcbimtester/features/steps/steps.py @@ -1,132 +1,5 @@ -import ifcopenshell from behave import step - -class IfcFile(object): - file = None - bookmarks = {} - - @classmethod - def load(cls, path=None): - cls.file = ifcopenshell.open(path) - - @classmethod - def get(cls): - if not cls.file: - assert False, 'No file was loaded, so this requirement cannot be checked' - return cls.file - - @classmethod - def get_pset(cls, element, pset_name): - if not element.IsDefinedBy: - return - for relationship in element.IsDefinedBy: - if relationship.RelatingPropertyDefinition.Name == pset_name: - return relationship.RelatingPropertyDefinition - - @classmethod - def get_property(cls, element, pset_name, property_name): - pset = cls.get_pset(element, pset_name) - if not pset: - return - for prop in pset.HasProperties: - if prop.Name == property_name: - return prop - - -# Project setup MicroMVD - - -@step('The IFC file "{file}" must be provided') -def step_impl(context, file): - try: - IfcFile.load(file) - except: - assert False, f'The file {file} could not be loaded' - - -@step('IFC data must use the {schema} schema') -def step_impl(context, schema): - assert IfcFile.get().schema == schema, \ - 'We expected a schema of {} but instead got {}'.format( - schema, IfcFile.get().schema) - - -@step('The IFC file "{file}" is exempt from being provided') -def step_impl(context, file): - pass - - -@step('No further requirements are specified because {reason}') -def step_impl(context, reason): - pass - - -@step('The project name, code, or short identifier must be "{value}"') -def step_impl(context, value): - project_value = getattr(IfcFile.get().by_type('IfcProject')[0], 'Name') - assert project_value == value, 'We expected a value of "{}" but instead got "{}"'.format( - value, project_value) - - -@step('The project must have a longer form name of "{value}"') -def step_impl(context, value): - project_value = getattr(IfcFile.get().by_type('IfcProject')[0], 'LongName') - assert project_value == value, 'We expected a value of "{}" but instead got "{}"'.format( - value, project_value) - - -@step('The project must be described as "{value}"') -def step_impl(context, value): - project_value = getattr(IfcFile.get().by_type('IfcProject')[0], 'Description') - assert project_value == value, 'We expected a value of "{}" but instead got "{}"'.format( - value, project_value) - - -@step('The project must be categorised under "{value}"') -def step_impl(context, value): - project_value = getattr(IfcFile.get().by_type('IfcProject')[0], 'ObjectType') - assert project_value == value, 'We expected a value of "{}" but instead got "{}"'.format( - value, project_value) - - -@step('The project must contain information about the "{value}" phase') -def step_impl(context, value): - project_value = getattr(IfcFile.get().by_type('IfcProject')[0], 'Phase') - assert project_value == value, 'We expected a value of "{}" but instead got "{}"'.format( - value, project_value) - - -@step('The project must contain 3D geometry representing the shape of objects') -def step_impl(context): - assert get_subcontext('Body', 'Model', 'MODEL_VIEW') - - -@step('The project must contain 3D geometry representing clearance zones') -def step_impl(context): - assert get_subcontext('Clearance', 'Model', 'MODEL_VIEW') - - -@step('The project must contain 3D geometry representing the center of gravity of objects') -def step_impl(context): - assert get_subcontext('CoG', 'Model', 'MODEL_VIEW') - - -@step('The project must contain 3D geometry representing the object bounding boxes') -def step_impl(context): - assert get_subcontext('Box', 'Model', 'MODEL_VIEW') - - -def get_subcontext(identifier, type, target_view): - project = IfcFile.get().by_type('IfcProject')[0] - for rep_context in project.RepresentationContexts: - for subcontext in rep_context.HasSubContexts: - if subcontext.ContextIdentifier == identifier \ - and subcontext.ContextType == type \ - and subcontext.TargetView == target_view: - return True - assert False, 'The subcontext with identifier {}, type {}, and target view {} could not be found'.format( - identifier, type, target_view) - +from utils import IfcFile # Custom MicroMVD @@ -170,9 +43,6 @@ def step_impl(context, id, reason): assert False, 'This element {} should be reevaluated.'.format(element) -@step(u'there is at least one {ifc_class} element') -def step_impl(context, ifc_class): - assert len(IfcFile.get().by_type(ifc_class)) >= 1 @step(u'there are no {ifc_class} elements because {reason}') @@ -296,30 +166,7 @@ def step_impl(context, ifc_class, qto_name, quantity_name): if not is_successful: assert False -@step(u'the project should have geolocation data') -def step_impl(context): - if IfcFile.get().schema == 'IFC2X3': - for site in IfcFile.get().by_type('IfcSite'): - if not IfcFile.get_pset(site, 'EPset_MapConversion'): - assert False - else: - project = IfcFile.get().by_type('IfcProject')[0] - for context in project.RepresentationContexts: - if context.is_a('IfcGeometricRepresentationContext') \ - and context.ContextType == 'Model' \ - and context.HasCoordinateOperation: - IfcFile.bookmarks['geolocation'] = context.HasCoordinateOperation[0].id() - return - assert False -@step(u'the project geolocation uses the "{crs_name}" CRS') -def step_impl(context, crs_name): - if IfcFile.get().schema == 'IFC2X3': - for site in IfcFile.get().by_type('IfcSite'): - if IfcFile.get_property(site, 'EPset_ProjectedCRS', 'Name').NominalValue.wrappedValue != crs_name: - assert False - else: - assert IfcFile.get().by_id(IfcFile.bookmarks['geolocation']).TargetCRS.Name == crs_name use_step_matcher('re') diff --git a/src/ifcbimtester/features/steps/utils.py b/src/ifcbimtester/features/steps/utils.py new file mode 100644 index 0000000000..5483da60e7 --- /dev/null +++ b/src/ifcbimtester/features/steps/utils.py @@ -0,0 +1,15 @@ +import ifcopenshell + +class IfcFile(object): + file = None + bookmarks = {} + + @classmethod + def load(cls, path=None): + cls.file = ifcopenshell.open(path) + + @classmethod + def get(cls): + if not cls.file: + assert False, 'No file was loaded, so this requirement cannot be checked' + return cls.file