From 4b426e1fc0d4ee270f8d61ed3749e8d4cb884a98 Mon Sep 17 00:00:00 2001 From: maxfb87 Date: Tue, 12 Apr 2022 22:13:55 +0200 Subject: [PATCH] Refactor Georeference Module --- .../bim/module/georeference/data.py | 92 ++++ .../bim/module/georeference/operator.py | 427 +++++------------- .../blenderbim/bim/module/georeference/ui.py | 43 +- .../blenderbim/core/georeference.py | 78 ++++ src/blenderbim/blenderbim/core/tool.py | 35 ++ src/blenderbim/blenderbim/tool/__init__.py | 1 + .../blenderbim/tool/georeference.py | 361 +++++++++++++++ 7 files changed, 703 insertions(+), 334 deletions(-) create mode 100644 src/blenderbim/blenderbim/bim/module/georeference/data.py create mode 100644 src/blenderbim/blenderbim/core/georeference.py create mode 100644 src/blenderbim/blenderbim/tool/georeference.py diff --git a/src/blenderbim/blenderbim/bim/module/georeference/data.py b/src/blenderbim/blenderbim/bim/module/georeference/data.py new file mode 100644 index 0000000000..93366b6baa --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/georeference/data.py @@ -0,0 +1,92 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2022 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blenderbim.tool as tool + +def refresh(): + GeoreferenceData.is_loaded = False + +class GeoreferenceData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.data = { + "map_conversion" : cls.map_conversion(), + "projected_crs" : cls.projected_crs(), + "true_north" : cls.true_north(), + } + cls.is_loaded = True + + @classmethod + def map_conversion(cls): + ifc = tool.Ifc.get() + map_conversion_v = {} + + if ifc.schema == "IFC2X3": + return + + for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if not context.HasCoordinateOperation: + continue + + map_conversion_v = context.HasCoordinateOperation[0].get_info() + map_conversion_v["SourceCRS"] = map_conversion_v["SourceCRS"].id() + map_conversion_v["TargetCRS"] = map_conversion_v["TargetCRS"].id() + + break + + return map_conversion_v + + @classmethod + def projected_crs(cls): + ifc = tool.Ifc.get() + projected_crs = {} + + if ifc.schema == "IFC2X3": + return + + for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if not context.HasCoordinateOperation: + continue + + projected_crs = context.HasCoordinateOperation[0].TargetCRS.get_info() + if projected_crs["MapUnit"]: + projected_crs["MapUnit"] = map_conversion.TargetCRS.MapUnit.get_info() + + break + + return projected_crs + + @classmethod + def true_north(cls): + ifc = tool.Ifc.get() + true_north = {} + + if ifc.schema == "IFC2X3": + return + + for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if not context.TrueNorth: + continue + true_north = context.TrueNorth.DirectionRatios + break + + return true_north + diff --git a/src/blenderbim/blenderbim/bim/module/georeference/operator.py b/src/blenderbim/blenderbim/bim/module/georeference/operator.py index 5b3e09037d..af7d34dec5 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/operator.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/operator.py @@ -17,345 +17,146 @@ # along with BlenderBIM Add-on. If not, see . import bpy -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 +import blenderbim.tool as tool +import blenderbim.core.georeference as core -class EnableEditingGeoreferencing(bpy.types.Operator): - bl_idname = "bim.enable_editing_georeferencing" - bl_label = "Enable Editing Georeferencing" - bl_options = {"REGISTER", "UNDO"} - - 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"} - - def execute(self, context): - props = context.scene.BIMGeoreferenceProperties - props.is_editing = False - return {"FINISHED"} - - -class EditGeoreferencing(bpy.types.Operator): - bl_idname = "bim.edit_georeferencing" - bl_label = "Edit Georeferencing" - bl_options = {"REGISTER", "UNDO"} - - 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"} - - 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): - ifcopenshell.api.run("georeference.remove_georeferencing", IfcStore.get_file()) - Data.load(IfcStore.get_file()) - return {"FINISHED"} - - -class AddGeoreferencing(bpy.types.Operator): +class AddGeoreferencing(bpy.types.Operator, tool.Ifc.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) + bl_description = "Add a new georeference" 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" + core.add_georeferencing(tool.Ifc) + +class EnableEditingGeoreferencing(bpy.types.Operator, tool.Ifc.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) - @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" +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): + bl_idname = "bim.disable_editing_georeferencing" + bl_label = "Disable Editing Georeferencing" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Close editing panel" - @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): + core.disable_editing_georeferencing(tool.Georeference) - 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(",")] +class SetIfcGridNorth(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.set_ifc_grid_north" + bl_label = "Set IFC Grid North" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Set IFC grid north" + + def _execute(self, context): + core.set_ifc_grid_north(tool.Georeference) - 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) +class SetBlenderGridNorth(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.set_blender_grid_north" + bl_label = "Set Blender Grid North" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Set Blender grid north" + + def _execute(self, context): + core.set_blender_grid_north(tool.Georeference) - 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): +class GetCursorLocation(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.get_cursor_location" bl_label = "Get Cursor Location" bl_options = {"REGISTER", "UNDO"} - + bl_description = "Insert the current cursor coordinates" + @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): + return tool.Ifc.get() + + def _execute(self, context): + core.get_cursor_location(tool.Georeference) + +class SetCursorLocation(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.set_cursor_location" bl_label = "Set Cursor Location" bl_options = {"REGISTER", "UNDO"} + bl_description = "Move curson location to the specified coordinates" @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() props = context.scene.BIMGeoreferenceProperties return file and file.by_type("IfcUnitAssignment") and props.coordinate_output.count(",") == 2 + + def _execute(self, context): + core.set_cursor_location(tool.Georeference) + +class SetIfcTrueNorth(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.set_ifc_true_north" + bl_label = "Set IFC True North" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Set IFC True north" - def execute(self, context): + def _execute(self, context): + core.set_ifc_true_north(tool.Georeference) + +class SetBlenderTrueNorth(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.set_blender_true_north" + bl_label = "Set Blender True North" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Set Blender true north" + + def _execute(self, context): + core.set_blender_true_north(tool.Georeference) + +class ConvertLocalToGlobal(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.convert_local_to_global" + bl_label = "Convert Local To Global" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Convert local coordinate to global coordinate" + + @classmethod + def poll(cls, context): + file = tool.Ifc.get() 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"} + return file and props.coordinate_input.count(",") == 2 + + def _execute(self, context): + core.convert_local_to_global(tool.Georeference) + +class ConvertGlobalToLocal(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.convert_global_to_local" + bl_label = "Convert Global To Local" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Convert global coordinate to local coordinate" + + @classmethod + def poll(cls, context): + file = tool.Ifc.get() + props = context.scene.BIMGeoreferenceProperties + return file and file.by_type("IfcUnitAssignment") and props.coordinate_input.count(",") == 2 + + def _execute(self, context): + core.convert_global_to_local(tool.Georeference) + diff --git a/src/blenderbim/blenderbim/bim/module/georeference/ui.py b/src/blenderbim/blenderbim/bim/module/georeference/ui.py index 456d378f03..93dcc34c38 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/ui.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/ui.py @@ -18,10 +18,9 @@ import ifcopenshell.util.geolocation from bpy.types import Panel -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,18 +30,15 @@ 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 Data.is_loaded: - Data.load(IfcStore.get_file()) - + + if not GeoreferenceData.is_loaded: + GeoreferenceData.load() + if props.is_editing: return self.draw_editable_ui(context) self.draw_ui(context) @@ -79,15 +75,16 @@ class BIM_PT_gis(Panel): 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 Data.projected_crs: + if not GeoreferenceData.data["projected_crs"]: row = self.layout.row(align=True) row.label(text="Not Georeferenced") - if IfcStore.get_file().schema != "IFC2X3": + if IfcStore.get_file().schema != "IFC2X3": #TODO Is it correct to use IfcStore.get_file() or is it better tool.Ifc.get()? 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") @@ -120,13 +117,13 @@ class BIM_PT_gis(Panel): ) ) - if Data.projected_crs: + if GeoreferenceData.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 Data.projected_crs.items(): + for key, value in GeoreferenceData.data["projected_crs"].items(): if key == "id" or key == "type" or not value: continue if key == "MapUnit": @@ -137,11 +134,11 @@ class BIM_PT_gis(Panel): row.label(text=key) row.label(text=str(value)) - if Data.map_conversion: + if GeoreferenceData.data["map_conversion"]: row = self.layout.row(align=True) row.label(text="Map Conversion", icon="GRID") - for key, value in Data.map_conversion.items(): + for key, value in GeoreferenceData.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) @@ -154,22 +151,22 @@ class BIM_PT_gis(Panel): text=str( round( ifcopenshell.util.geolocation.xaxis2angle( - Data.map_conversion["XAxisAbscissa"], Data.map_conversion["XAxisOrdinate"] + GeoreferenceData.data["map_conversion"]["XAxisAbscissa"], GeoreferenceData.data["map_conversion"]["XAxisOrdinate"] ), 3, ) ) ) - if Data.true_north: + if GeoreferenceData.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(Data.true_north[0:2])[1:-1]) + row.label(text=str(GeoreferenceData.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(*Data.true_north[0:2]), 3))) + row.label(text=str(round(ifcopenshell.util.geolocation.yaxis2angle(*GeoreferenceData.data["true_north"][0:2]), 3))) class BIM_PT_gis_utilities(Panel): @@ -181,6 +178,9 @@ class BIM_PT_gis_utilities(Panel): bl_category = "BlenderBIM" def draw(self, context): + if not GeoreferenceData.is_loaded: + GeoreferenceData.load() + props = context.scene.BIMGeoreferenceProperties row = self.layout.row(align=True) @@ -193,3 +193,4 @@ class BIM_PT_gis_utilities(Panel): 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") + diff --git a/src/blenderbim/blenderbim/core/georeference.py b/src/blenderbim/blenderbim/core/georeference.py new file mode 100644 index 0000000000..5096594ba1 --- /dev/null +++ b/src/blenderbim/blenderbim/core/georeference.py @@ -0,0 +1,78 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2022 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +def add_georeferencing(ifc): + ifc.run("georeference.add_georeferencing") + +def enable_editing_georeferencing(georeference): + georeference.clear_projected_crs() + georeference.import_projected_crs() + georeference.clear_map_conversion() + georeference.import_map_conversion() + georeference.set_has_true_north_prop() + georeference.set_true_north_props() + georeference.enable_editing() + +def remove_georeferencing(ifc): + ifc.run("georeference.remove_georeferencing") + +def disable_editing_georeferencing(georeference): + georeference.disable_editing() + +def edit_georeferencing(ifc, georeference): + projected_crs = georeference.get_projected_crs_attributes() + map_conversion = georeference.get_map_conversion_attributes() + true_north = georeference.get_true_north_attributes() + georeference.edit_georeferencing(projected_crs, map_conversion, true_north) + georeference.disable_editing() + +def set_ifc_grid_north(georeference): + x_angle = georeference.get_x_angle_from_sun_position() + georeference.set_xaxis_vector_from_angle(x_angle) + +def set_blender_grid_north(georeference): + angle = georeference.get_angle_from_xaxis() + georeference.set_sun_pos_north_offset(angle) + +def get_cursor_location(georeference): + georeference.get_cursor_location() + +def set_cursor_location(georeference): + scale = georeference.get_scale() + coordinates = georeference.get_coordinates_from_coordinate_output_prop() + georeference.set_cursor_location(coordinates, scale) + +def set_ifc_true_north(georeference): + georeference.set_ifc_true_north() + +def set_blender_true_north(georeference): + georeference.set_blender_true_north() + +def convert_local_to_global(georeference): + map_conversion = georeference.get_map_conversion() + coordinates = georeference.get_easting_northing_height_from_xyz(map_conversion) + georeference.set_coordinate_output_prop(coordinates) + georeference.set_cursor_location(coordinates, scale = 1) + +def convert_global_to_local(georeference): + map_conversion = georeference.get_map_conversion() + coordinates = georeference.get_xyz_from_easting_northig_height(map_conversion) + georeference.set_coordinate_output_prop(coordinates) + scale = georeference.get_scale() + georeference.set_cursor_location(coordinates, scale) + diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 6e478fb359..2858a6a943 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -247,6 +247,41 @@ 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 import_projected_crs(cls): pass + def import_map_conversion(cls): pass + def set_has_true_north_prop(cls): pass + def set_true_north_props(cls): pass + def enable_editing(cls): pass + def disable_editing(cls): pass + def get_projected_crs_attributes(cls): pass + def get_map_conversion_attributes(cls): pass + def get_true_north_attributes(cls): pass + def edit_georeferencing(cls, projected_crs, map_conversion, true_north): pass + def get_file(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 + def get_cursor_location(cls): pass + def set_cursor_location(cls, coordinates, scale): pass + def get_scale(cls): pass + def get_coordinates_from_coordinate_output_prop(cls): pass + def set_ifc_true_north(cls): pass + def set_blender_true_north(cls): pass + def get_map_conversion(cls): pass + def get_easting_northing_height_from_xyz(cls, map_conversion): pass + def get_xyz_from_easting_northig_height(cls, map_conversion): pass + def set_coordinate_output_prop(cls, coordinates): pass + def get_x_angle_from_sun_position(cls): pass + def set_xaxis_vector_from_angle(cls, x_angle): pass + def get_angle_from_xaxis(cls): pass + def set_sun_pos_north_offset(cls, angle): pass @interface class Ifc: diff --git a/src/blenderbim/blenderbim/tool/__init__.py b/src/blenderbim/blenderbim/tool/__init__.py index 12bf26b42d..705120b4b6 100644 --- a/src/blenderbim/blenderbim/tool/__init__.py +++ b/src/blenderbim/blenderbim/tool/__init__.py @@ -28,6 +28,7 @@ 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 diff --git a/src/blenderbim/blenderbim/tool/georeference.py b/src/blenderbim/blenderbim/tool/georeference.py new file mode 100644 index 0000000000..14c2f0dc95 --- /dev/null +++ b/src/blenderbim/blenderbim/tool/georeference.py @@ -0,0 +1,361 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2022 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import json +import blenderbim.core.tool +import blenderbim.tool as tool +import ifcopenshell +import blenderbim.bim.helper + +from ifcopenshell.api.unit.data import Data as UnitData +from math import radians, degrees, atan, tan, cos, sin + +class Georeference(blenderbim.core.tool.Georeference): + + @classmethod + def clear_projected_crs(cls): + bpy.context.scene.BIMGeoreferenceProperties.projected_crs.clear() + + @classmethod + def clear_map_conversion(cls): + bpy.context.scene.BIMGeoreferenceProperties.map_conversion.clear() + + @classmethod + def import_projected_crs(cls): + ifc = tool.Ifc.get() + projected_crs = {} + + if ifc.schema == "IFC2X3": + return + + for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if not context.HasCoordinateOperation: + continue + + projected_crs = context.HasCoordinateOperation[0].TargetCRS.get_info() + if projected_crs["MapUnit"]: + projected_crs["MapUnit"] = map_conversion.TargetCRS.MapUnit.get_info() + + break + + props = bpy.context.scene.BIMGeoreferenceProperties + blenderbim.bim.helper.import_attributes( + "IfcProjectedCRS", + props.projected_crs, + projected_crs, + cls.import_projected_crs_attributes, + ) + + @classmethod + def import_map_conversion(cls): + ifc = tool.Ifc.get() + map_conversion_v = {} + if ifc.schema == "IFC2X3": + return + + for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if not context.HasCoordinateOperation: + continue + + map_conversion_v = context.HasCoordinateOperation[0].get_info() + map_conversion_v["SourceCRS"] = map_conversion_v["SourceCRS"].id() + map_conversion_v["TargetCRS"] = map_conversion_v["TargetCRS"].id() + + break + + props = bpy.context.scene.BIMGeoreferenceProperties + blenderbim.bim.helper.import_attributes( + "IfcMapConversion", + props.map_conversion, + map_conversion_v, + cls.import_map_conversion_attributes, + ) + + @classmethod + def set_has_true_north_prop(cls): + ifc = tool.Ifc.get() + true_north = {} + + if ifc.schema == "IFC2X3": + return + + for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if not context.TrueNorth: + continue + true_north = context.TrueNorth.DirectionRatios + break + + bpy.context.scene.BIMGeoreferenceProperties.has_true_north = bool(true_north) + + @classmethod + def set_true_north_props(cls): + ifc = tool.Ifc.get() + true_north = {} + + if ifc.schema == "IFC2X3": + return + + for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if not context.TrueNorth: + continue + true_north = context.TrueNorth.DirectionRatios + break + + if true_north: + bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa = str(true_north[0]) + bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate = str(true_north[1]) + + @classmethod + def get_projected_crs_attributes(cls): + props = bpy.context.scene.BIMGeoreferenceProperties + projected_crs = blenderbim.bim.helper.export_attributes(props.projected_crs, cls.export_crs_attributes) + return projected_crs + + @classmethod + def get_map_conversion_attributes(cls): + props = bpy.context.scene.BIMGeoreferenceProperties + map_conversion = blenderbim.bim.helper.export_attributes(props.map_conversion, cls.export_map_attributes) + return map_conversion + + @classmethod + def get_true_north_attributes(cls): + props = bpy.context.scene.BIMGeoreferenceProperties + true_north = None + if props.has_true_north: + try: + true_north = [float(props.true_north_abscissa), float(props.true_north_ordinate)] + except ValueError: + print("ERROR, True North Abscissa and Ordinate expect a number") + #self.report({"ERROR"}, "True North Abscissa and Ordinate expect a number") + return true_north + + @classmethod + def enable_editing(cls): + bpy.context.scene.BIMGeoreferenceProperties.is_editing = True + + @classmethod + def disable_editing(cls): + bpy.context.scene.BIMGeoreferenceProperties.is_editing = False + + @classmethod + def edit_georeferencing(cls, projected_crs, map_conversion, true_north): + tool.Ifc.run( + "georeference.edit_georeferencing", + **{ + "projected_crs": projected_crs, + "map_conversion": map_conversion, + "true_north": true_north, + } + ) + + @classmethod + def get_file(cls): + return tool.Ifc.get() + + @classmethod + def import_projected_crs_attributes(cls, name, prop, data): + if name == "MapUnit": + new = bpy.context.scene.BIMGeoreferenceProperties.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 + + @classmethod + def import_map_conversion_attributes(cls, 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 + + @classmethod + def export_crs_attributes(cls, attributes, prop): + ifc_file = tool.Ifc.get() + if not prop.is_null and prop.name == "MapUnit": + attributes[prop.name] = ifc_file.by_id(int(prop.enum_value)) + return True + + @classmethod + def export_map_attributes(cls, 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 + + @classmethod + def get_cursor_location(cls): + props = bpy.context.scene.BIMGeoreferenceProperties + scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + project_coordinates = [o / scale for o in bpy.context.scene.cursor.location] + props.coordinate_input = ",".join([str(o) for o in project_coordinates]) + + @classmethod + def set_cursor_location(cls, coordinates, scale): + bpy.context.scene.cursor.location = coordinates * scale + + @classmethod + def get_scale(cls): + return ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + + @classmethod + def get_coordinates_from_coordinate_output_prop(cls): + props = bpy.context.scene.BIMGeoreferenceProperties + coordinates = [float(co) for co in props.coordinate_output.split(",")] + return coordinates + + @classmethod + def set_ifc_true_north(cls): + y_angle = -bpy.context.scene.sun_pos_properties.north_offset + radians(90) + bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa = str(cos(y_angle)) + bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate = str(sin(y_angle)) + + @classmethod + def set_blender_true_north(cls): + bpy.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), + ) + ) + + @classmethod + def get_map_conversion(cls): + ifc = tool.Ifc.get() + map_conversion = {} + if ifc.schema == "IFC2X3": + return + + for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False): + if not context.HasCoordinateOperation: + continue + + map_conversion = context.HasCoordinateOperation[0].get_info() + map_conversion["SourceCRS"] = map_conversion["SourceCRS"].id() + map_conversion["TargetCRS"] = map_conversion["TargetCRS"].id() + + break + + return map_conversion + + @classmethod + def get_easting_northing_height_from_xyz(cls, map_conversion): + props = bpy.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 map_conversion: #TODO return error when there is the map conversiona but no Xaxis, scale, etc.. + results = ifcopenshell.util.geolocation.xyz2enh( + x, + y, + z, + map_conversion["Eastings"], + map_conversion["Northings"], + map_conversion["OrthogonalHeight"], + map_conversion.get("XAxisAbscissa", 1.0), + map_conversion.get("XAxisOrdinate", 0.0), + map_conversion.get("Scale", 1.0), + ) + else: + results = (x, y, z) + + return results + + @classmethod + def get_xyz_from_easting_northig_height(cls, map_conversion): + props = bpy.context.scene.BIMGeoreferenceProperties + x, y, z = [float(co) for co in props.coordinate_input.split(",")] + + if map_conversion: + results = ifcopenshell.util.geolocation.enh2xyz( + x, + y, + z, + map_conversion["Eastings"], + map_conversion["Northings"], + map_conversion["OrthogonalHeight"], + map_conversion.get("XAxisAbscissa", 1.0), + map_conversion.get("XAxisOrdinate", 0.0), + 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, + ) + + return results + + @classmethod + def set_coordinate_output_prop(cls, coordinates): + props = bpy.context.scene.BIMGeoreferenceProperties + props.coordinate_output = ",".join([str(r) for r in coordinates]) + + @classmethod + def get_x_angle_from_sun_position(cls): + return -bpy.context.scene.sun_pos_properties.north_offset + + @classmethod + def set_xaxis_vector_from_angle(cls, x_angle): + bpy.context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").string_value = str(cos(x_angle)) + bpy.context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").string_value = str(sin(x_angle)) + + @classmethod + def get_angle_from_xaxis(cls): + return ifcopenshell.util.geolocation.xaxis2angle( + float(bpy.context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").string_value), + float(bpy.context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").string_value), + ) + + @classmethod + def set_sun_pos_north_offset(cls, angle): + bpy.context.scene.sun_pos_properties.north_offset = -radians(angle) +