diff --git a/src/blenderbim/blenderbim/bim/module/georeference/__init__.py b/src/blenderbim/blenderbim/bim/module/georeference/__init__.py index 71196793d5..7d644a30df 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/__init__.py @@ -24,15 +24,15 @@ classes = ( operator.DisableEditingGeoreferencing, operator.EditGeoreferencing, operator.SetIfcGridNorth, -# operator.SetBlenderGridNorth, -# operator.SetIfcTrueNorth, -# operator.SetBlenderTrueNorth, + operator.SetBlenderGridNorth, + operator.SetIfcTrueNorth, + operator.SetBlenderTrueNorth, operator.RemoveGeoreferencing, operator.AddGeoreferencing, -# operator.ConvertLocalToGlobal, -# operator.ConvertGlobalToLocal, -# operator.GetCursorLocation, -# operator.SetCursorLocation, + operator.ConvertLocalToGlobal, + operator.ConvertGlobalToLocal, + operator.GetCursorLocation, + operator.SetCursorLocation, prop.BIMGeoreferenceProperties, ui.BIM_PT_gis, ui.BIM_PT_gis_utilities, diff --git a/src/blenderbim/blenderbim/bim/module/georeference/operator.py b/src/blenderbim/blenderbim/bim/module/georeference/operator.py index 62e8e02b7d..5b3e09037d 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/operator.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/operator.py @@ -17,72 +17,345 @@ # along with BlenderBIM Add-on. If not, see . import bpy -import blenderbim.tool as tool -import blenderbim.core.georeference as core -import blenderbim.bim.handler +import json +import ifcopenshell +import ifcopenshell.util.unit +import ifcopenshell.util.attribute +import ifcopenshell.api +import blenderbim.bim.helper +from blenderbim.bim.ifc import IfcStore +from ifcopenshell.api.georeference.data import Data +from ifcopenshell.api.unit.data import Data as UnitData +from math import radians, degrees, atan, tan, cos, sin -class AddGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.add_georeferencing" - bl_label = "Add Georeferencing" - bl_options = {"REGISTER", "UNDO"} - bl_description = "Add a new georeference" - def _execute(self, context): - core.add_georeferencing(tool.Ifc) - -class EnableEditingGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): +class EnableEditingGeoreferencing(bpy.types.Operator): bl_idname = "bim.enable_editing_georeferencing" bl_label = "Enable Editing Georeferencing" bl_options = {"REGISTER", "UNDO"} - bl_description = "Enable editing georeferencing" - - def _execute(self, context): - core.enable_editing_georeferencing(tool.Georeference) -class RemoveGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.remove_georeferencing" - bl_label = "Remove Georeferencing" - bl_options = {"REGISTER", "UNDO"} - bl_description = "Remove the georeferencing" - - def _execute(self, context): - core.remove_georeferencing(tool.Ifc) - -class EditGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.edit_georeferencing" - bl_label = "Edit Georeferencing" - bl_options = {"REGISTER", "UNDO"} - bl_description = "Edit the georeferencing" - - def _execute(self, context): - core.edit_georeferencing(tool.Ifc, tool.Georeference) - -class DisableEditingGeoreferencing(bpy.types.Operator, tool.Ifc.Operator): + def execute(self, context): + self.file = IfcStore.get_file() + props = context.scene.BIMGeoreferenceProperties + self.props = props + + props.projected_crs.clear() + + blenderbim.bim.helper.import_attributes( + "IfcProjectedCRS", props.projected_crs, Data.projected_crs, self.import_projected_crs_attributes + ) + + props.map_conversion.clear() + blenderbim.bim.helper.import_attributes( + "IfcMapConversion", props.map_conversion, Data.map_conversion, self.import_map_conversion_attributes + ) + + props.has_true_north = bool(Data.true_north) + if Data.true_north: + props.true_north_abscissa = str(Data.true_north[0]) + props.true_north_ordinate = str(Data.true_north[1]) + + props.is_editing = True + return {"FINISHED"} + + def import_projected_crs_attributes(self, name, prop, data): + if name == "MapUnit": + new = self.props.projected_crs.add() + new.name = name + new.data_type = "enum" + new.is_null = data[name] is None + new.is_optional = True + new.enum_items = json.dumps( + {u["id"]: u["Name"] for u in UnitData.units.values() if u["UnitType"] == "LENGTHUNIT"} + ) + if data["MapUnit"]: + new.enum_value = str(data["MapUnit"]["id"]) + return True + + def import_map_conversion_attributes(self, name, prop, data): + if name not in ["SourceCRS", "TargetCRS"]: + # Enforce a string data type to prevent data loss in single-precision Blender props + prop.data_type = "string" + prop.string_value = "" if prop.is_null else str(data[name]) + return True + + +class DisableEditingGeoreferencing(bpy.types.Operator): bl_idname = "bim.disable_editing_georeferencing" bl_label = "Disable Editing Georeferencing" bl_options = {"REGISTER", "UNDO"} - bl_description = "Close editing panel" - def _execute(self, context): - core.disable_editing_georeferencing(tool.Georeference) + def execute(self, context): + props = context.scene.BIMGeoreferenceProperties + props.is_editing = False + return {"FINISHED"} -class SetIfcGridNorth(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.set_ifc_grid_north" - bl_label = "Set IFC Grid North" + +class EditGeoreferencing(bpy.types.Operator): + bl_idname = "bim.edit_georeferencing" + bl_label = "Edit Georeferencing" bl_options = {"REGISTER", "UNDO"} - bl_description = "Set IFC grid north" - - def _execute(self, context): - core.set_ifc_grid_north() -class SetBlenderGridNorth(bpy.types.Operator, tool.Ifc.Operator): + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + props = context.scene.BIMGeoreferenceProperties + + projected_crs = blenderbim.bim.helper.export_attributes(props.projected_crs, self.export_crs_attributes) + map_conversion = blenderbim.bim.helper.export_attributes(props.map_conversion, self.export_map_attributes) + + true_north = None + if props.has_true_north: + try: + true_north = [float(props.true_north_abscissa), float(props.true_north_ordinate)] + except ValueError: + self.report({"ERROR"}, "True North Abscissa and Ordinate expect a number") + + ifcopenshell.api.run( + "georeference.edit_georeferencing", + self.file, + **{ + "map_conversion": map_conversion, + "projected_crs": projected_crs, + "true_north": true_north, + } + ) + Data.load(self.file) + bpy.ops.bim.disable_editing_georeferencing() + return {"FINISHED"} + + def export_map_attributes(self, attributes, prop): + if not prop.is_null and prop.data_type == "string": + # We store our floats as string to prevent single precision data loss + attributes[prop.name] = float(prop.string_value) + return True + + def export_crs_attributes(self, attributes, prop): + if not prop.is_null and prop.name == "MapUnit": + attributes[prop.name] = self.file.by_id(int(prop.enum_value)) + return True + + +class SetBlenderGridNorth(bpy.types.Operator): bl_idname = "bim.set_blender_grid_north" bl_label = "Set Blender Grid North" bl_options = {"REGISTER", "UNDO"} - bl_description = "Set Blender grif north" - + + def execute(self, context): + context.scene.sun_pos_properties.north_offset = -radians( + ifcopenshell.util.geolocation.xaxis2angle( + float(context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").string_value), + float(context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").string_value), + ) + ) + return {"FINISHED"} + + +class SetIfcGridNorth(bpy.types.Operator): + bl_idname = "bim.set_ifc_grid_north" + bl_label = "Set IFC Grid North" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + x_angle = -context.scene.sun_pos_properties.north_offset + context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").string_value = str(cos(x_angle)) + context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").string_value = str(sin(x_angle)) + return {"FINISHED"} + + +class SetBlenderTrueNorth(bpy.types.Operator): + bl_idname = "bim.set_blender_true_north" + bl_label = "Set Blender True North" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + context.scene.sun_pos_properties.north_offset = -radians( + ifcopenshell.util.geolocation.yaxis2angle( + float(context.scene.BIMGeoreferenceProperties.true_north_abscissa), + float(context.scene.BIMGeoreferenceProperties.true_north_ordinate), + ) + ) + return {"FINISHED"} + + +class SetIfcTrueNorth(bpy.types.Operator): + bl_idname = "bim.set_ifc_true_north" + bl_label = "Set IFC True North" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + y_angle = -context.scene.sun_pos_properties.north_offset + radians(90) + context.scene.BIMGeoreferenceProperties.true_north_abscissa = str(cos(y_angle)) + context.scene.BIMGeoreferenceProperties.true_north_ordinate = str(sin(y_angle)) + return {"FINISHED"} + + +class RemoveGeoreferencing(bpy.types.Operator): + bl_idname = "bim.remove_georeferencing" + bl_label = "Remove Georeferencing" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + def _execute(self, context): - core.set_blender_grid_north() + ifcopenshell.api.run("georeference.remove_georeferencing", IfcStore.get_file()) + Data.load(IfcStore.get_file()) + return {"FINISHED"} +class AddGeoreferencing(bpy.types.Operator): + bl_idname = "bim.add_georeferencing" + bl_label = "Add Georeferencing" + bl_options = {"REGISTER", "UNDO"} + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + ifcopenshell.api.run("georeference.add_georeferencing", IfcStore.get_file()) + Data.load(IfcStore.get_file()) + return {"FINISHED"} + + +class ConvertLocalToGlobal(bpy.types.Operator): + bl_idname = "bim.convert_local_to_global" + bl_label = "Convert Local To Global" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + file = IfcStore.get_file() + props = context.scene.BIMGeoreferenceProperties + return file and props.coordinate_input.count(",") == 2 + + def execute(self, context): + if not Data.is_loaded: + Data.load(IfcStore.get_file()) + props = context.scene.BIMGeoreferenceProperties + x, y, z = [float(co) for co in props.coordinate_input.split(",")] + + if props.has_blender_offset: + results = ifcopenshell.util.geolocation.xyz2enh( + x, + y, + z, + float(props.blender_eastings), + float(props.blender_northings), + float(props.blender_orthogonal_height), + float(props.blender_x_axis_abscissa), + float(props.blender_x_axis_ordinate), + 1.0, + ) + x, y, z = results + + # TODO: what if the project CRS units and the project units are different? + + if Data.map_conversion: + results = ifcopenshell.util.geolocation.xyz2enh( + x, + y, + z, + Data.map_conversion["Eastings"], + Data.map_conversion["Northings"], + Data.map_conversion["OrthogonalHeight"], + Data.map_conversion.get("XAxisAbscissa", 1.0), + Data.map_conversion.get("XAxisOrdinate", 0.0), + Data.map_conversion.get("Scale", 1.0), + ) + else: + results = (x, y, z) + + props.coordinate_output = ",".join([str(r) for r in results]) + context.scene.cursor.location = results + return {"FINISHED"} + + +class ConvertGlobalToLocal(bpy.types.Operator): + bl_idname = "bim.convert_global_to_local" + bl_label = "Convert Global To Local" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + file = IfcStore.get_file() + props = context.scene.BIMGeoreferenceProperties + return file and file.by_type("IfcUnitAssignment") and props.coordinate_input.count(",") == 2 + + def execute(self, context): + if not Data.is_loaded: + Data.load(IfcStore.get_file()) + props = context.scene.BIMGeoreferenceProperties + x, y, z = [float(co) for co in props.coordinate_input.split(",")] + + if Data.map_conversion: + results = ifcopenshell.util.geolocation.enh2xyz( + x, + y, + z, + Data.map_conversion["Eastings"], + Data.map_conversion["Northings"], + Data.map_conversion["OrthogonalHeight"], + Data.map_conversion.get("XAxisAbscissa", 1.0), + Data.map_conversion.get("XAxisOrdinate", 0.0), + Data.map_conversion.get("Scale", 1.0), + ) + else: + results = (x, y, z) + + if props.has_blender_offset: + results = ifcopenshell.util.geolocation.enh2xyz( + results[0], + results[1], + results[2], + float(props.blender_eastings), + float(props.blender_northings), + float(props.blender_orthogonal_height), + float(props.blender_x_axis_abscissa), + float(props.blender_x_axis_ordinate), + 1.0, + ) + + props.coordinate_output = ",".join([str(r) for r in results]) + + scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file()) + context.scene.cursor.location = [o * scale for o in results] + return {"FINISHED"} + + +class GetCursorLocation(bpy.types.Operator): + bl_idname = "bim.get_cursor_location" + bl_label = "Get Cursor Location" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + file = IfcStore.get_file() + return file and file.by_type("IfcUnitAssignment") + + def execute(self, context): + props = context.scene.BIMGeoreferenceProperties + scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file()) + project_coordinates = [o / scale for o in context.scene.cursor.location] + props.coordinate_input = ",".join([str(o) for o in project_coordinates]) + return {"FINISHED"} + + +class SetCursorLocation(bpy.types.Operator): + bl_idname = "bim.set_cursor_location" + bl_label = "Set Cursor Location" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + file = IfcStore.get_file() + props = context.scene.BIMGeoreferenceProperties + return file and file.by_type("IfcUnitAssignment") and props.coordinate_output.count(",") == 2 + + def execute(self, context): + props = context.scene.BIMGeoreferenceProperties + scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file()) + context.scene.cursor.location = [float(co) * scale for co in props.coordinate_output.split(",")] + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/georeference/ui.py b/src/blenderbim/blenderbim/bim/module/georeference/ui.py index 984590e51c..456d378f03 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/ui.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/ui.py @@ -18,10 +18,10 @@ import ifcopenshell.util.geolocation from bpy.types import Panel -#from ifcopenshell.api.georeference.data import Data +from ifcopenshell.api.georeference.data import Data from blenderbim.bim.ifc import IfcStore from blenderbim.bim.helper import draw_attributes, draw_attribute -from blenderbim.bim.module.georeference.data import GeoreferenceData + class BIM_PT_gis(Panel): bl_label = "IFC Georeferencing" @@ -31,15 +31,18 @@ class BIM_PT_gis(Panel): bl_region_type = "WINDOW" bl_context = "scene" bl_parent_id = "BIM_PT_geometry" - + + @classmethod + def poll(cls, context): + return IfcStore.get_file() + def draw(self, context): self.layout.use_property_split = True self.layout.use_property_decorate = False props = context.scene.BIMGeoreferenceProperties - - if not GeoreferenceData.is_loaded: - GeoreferenceData.load() - + if not Data.is_loaded: + Data.load(IfcStore.get_file()) + if props.is_editing: return self.draw_editable_ui(context) self.draw_ui(context) @@ -73,19 +76,18 @@ class BIM_PT_gis(Panel): row.prop(props, "true_north_ordinate") if hasattr(context.scene, "sun_pos_properties"): row = self.layout.row(align=True) - # row.operator("bim.set_ifc_true_north", text="Set IFC North") - # row.operator("bim.set_blender_true_north", text="Set Blender North") - + row.operator("bim.set_ifc_true_north", text="Set IFC North") + row.operator("bim.set_blender_true_north", text="Set Blender North") def draw_ui(self, context): props = context.scene.BIMGeoreferenceProperties - if not GeoreferenceData.data["projected_crs"]: + if not Data.projected_crs: row = self.layout.row(align=True) row.label(text="Not Georeferenced") if IfcStore.get_file().schema != "IFC2X3": row.operator("bim.add_georeferencing", icon="ADD", text="") - + if props.has_blender_offset: row = self.layout.row() row.label(text="Blender Offset", icon="TRACKING_REFINE_FORWARDS") @@ -118,13 +120,13 @@ class BIM_PT_gis(Panel): ) ) - if GeoreferenceData.data["projected_crs"]: + if Data.projected_crs: row = self.layout.row(align=True) row.label(text="Projected CRS", icon="WORLD") row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="") row.operator("bim.remove_georeferencing", icon="X", text="") - for key, value in GeoreferenceData.data["projected_crs"].items(): + for key, value in Data.projected_crs.items(): if key == "id" or key == "type" or not value: continue if key == "MapUnit": @@ -135,11 +137,11 @@ class BIM_PT_gis(Panel): row.label(text=key) row.label(text=str(value)) - if GeoreferenceData.data["map_conversion"]: + if Data.map_conversion: row = self.layout.row(align=True) row.label(text="Map Conversion", icon="GRID") - for key, value in GeoreferenceData.data["map_conversion"].items(): + for key, value in Data.map_conversion.items(): if key == "id" or key == "type" or key == "SourceCRS" or key == "TargetCRS" or value is None: continue row = self.layout.row(align=True) @@ -152,22 +154,22 @@ class BIM_PT_gis(Panel): text=str( round( ifcopenshell.util.geolocation.xaxis2angle( - GeoreferenceData.data["map_conversion"]["XAxisAbscissa"], GeoreferenceData.data["map_conversion"]["XAxisOrdinate"] + Data.map_conversion["XAxisAbscissa"], Data.map_conversion["XAxisOrdinate"] ), 3, ) ) ) - if GeoreferenceData.data["true_north"]: + if Data.true_north: row = self.layout.row() row.label(text="True North", icon="LIGHT_SUN") row = self.layout.row(align=True) row.label(text="Vector") - row.label(text=str(GeoreferenceData.data["true_north"][0:2])[1:-1]) + row.label(text=str(Data.true_north[0:2])[1:-1]) row = self.layout.row(align=True) row.label(text="Derived Angle") - row.label(text=str(round(ifcopenshell.util.geolocation.yaxis2angle(*GeoreferenceData.data["true_north"][0:2]), 3))) + row.label(text=str(round(ifcopenshell.util.geolocation.yaxis2angle(*Data.true_north[0:2]), 3))) class BIM_PT_gis_utilities(Panel): @@ -183,12 +185,11 @@ class BIM_PT_gis_utilities(Panel): row = self.layout.row(align=True) row.prop(props, "coordinate_input", text="Input") - #row.operator("bim.get_cursor_location", text="", icon="TRACKER") + row.operator("bim.get_cursor_location", text="", icon="TRACKER") row = self.layout.row(align=True) row.prop(props, "coordinate_output", text="Output") - #row.operator("bim.set_cursor_location", text="", icon="TRACKER") + row.operator("bim.set_cursor_location", text="", icon="TRACKER") row = self.layout.row(align=True) - #row.operator("bim.convert_local_to_global", text="Local to Global") - #row.operator("bim.convert_global_to_local", text="Global to Local") - + row.operator("bim.convert_local_to_global", text="Local to Global") + row.operator("bim.convert_global_to_local", text="Global to Local") diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 8d5997e8c4..ef8440f7c5 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -246,19 +246,6 @@ class Geometry: def should_generate_uvs(cls, obj): pass def should_use_presentation_style_assignment(cls): pass -@interface -class Georeference: - def clear_projected_crs(cls): pass - def clear_map_conversion(cls): pass - def get_file(cls): pass - def set_false_is_editing(cls): pass - def import_projected_crs_attributes(cls, name, prop, data): pass - def import_map_conversion_attributes(cls, name, prop, data): pass - def export_crs_attributes(cls, attributes, prop): pass - def export_map_attributes(cls, attributes, prop): pass - def set_ifc_grid_north(cls): pass - def set_blender_grid_north(cls): pass - @interface class Ifc: diff --git a/src/blenderbim/blenderbim/tool/__init__.py b/src/blenderbim/blenderbim/tool/__init__.py index 929c26f030..7ae4c6fbea 100644 --- a/src/blenderbim/blenderbim/tool/__init__.py +++ b/src/blenderbim/blenderbim/tool/__init__.py @@ -27,7 +27,6 @@ from blenderbim.tool.demo import Demo from blenderbim.tool.document import Document from blenderbim.tool.drawing import Drawing from blenderbim.tool.geometry import Geometry -from blenderbim.tool.georeference import Georeference from blenderbim.tool.ifc import Ifc from blenderbim.tool.library import Library from blenderbim.tool.material import Material