WIP implement georeference module for correctly geolocated files. See #1222.

This commit is contained in:
Dion Moult
2021-01-19 11:18:26 +11:00
parent 45bba127ff
commit ea524767a0
14 changed files with 548 additions and 217 deletions
@@ -0,0 +1,25 @@
import bpy
from . import ui, prop, operator
classes = (
operator.EnableEditingGeoreferencing,
operator.DisableEditingGeoreferencing,
operator.EditGeoreferencing,
operator.SetNorthOffset,
operator.GetNorthOffset,
operator.RemoveGeoreferencing,
operator.AddGeoreferencing,
operator.ConvertLocalToGlobal,
operator.ConvertGlobalToLocal,
prop.BIMGeoreferenceProperties,
ui.BIM_PT_gis,
ui.BIM_PT_gis_utilities,
)
def register():
bpy.types.Scene.BIMGeoreferenceProperties = bpy.props.PointerProperty(type=prop.BIMGeoreferenceProperties)
def unregister():
del bpy.types.Scene.BIMGeoreferenceProperties
@@ -0,0 +1,20 @@
class Usecase:
def __init__(self, file, settings=None):
self.file = file
def execute(self):
source_crs = None
for context in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.ContextType == "Model":
source_crs = context
break
if not source_crs:
return
projected_crs = self.file.create_entity("IfcProjectedCRS", **{"Name": ""})
self.file.create_entity("IfcMapConversion", **{
"SourceCRS": source_crs,
"TargetCRS": projected_crs,
"Eastings": 0,
"Northings": 0,
"OrthogonalHeight": 0,
})
@@ -0,0 +1,29 @@
from blenderbim.bim.ifc import IfcStore
class Data:
is_loaded = False
map_conversion = {}
projected_crs = {}
@classmethod
def load(cls):
file = IfcStore.get_file()
if not file:
return
cls.map_conversion = {}
cls.projected_crs = {}
if file.schema == "IFC2X3":
return
for context in file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if not context.HasCoordinateOperation:
continue
map_conversion = context.HasCoordinateOperation[0]
cls.map_conversion = map_conversion.get_info()
cls.map_conversion["SourceCRS"] = cls.map_conversion["SourceCRS"].id()
cls.map_conversion["TargetCRS"] = cls.map_conversion["TargetCRS"].id()
cls.projected_crs = map_conversion.TargetCRS.get_info()
if cls.projected_crs["MapUnit"]:
cls.projected_crs["MapUnit"] = map_conversion.TargetCRS.MapUnit.get_info()
break
cls.is_loaded = True
@@ -0,0 +1,52 @@
import ifcopenshell.util.unit
class Usecase:
def __init__(self, file, settings=None):
self.file = file
self.settings = {
"map_conversion": {},
"projected_crs": {},
"map_unit": "",
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
map_conversion = self.file.by_type("IfcMapConversion")[0]
projected_crs = self.file.by_type("IfcProjectedCRS")[0]
for name, value in self.settings["map_conversion"].items():
setattr(map_conversion, name, value)
for name, value in self.settings["projected_crs"].items():
setattr(projected_crs, name, value)
self.remove_existing_map_unit(projected_crs)
self.set_map_unit(projected_crs)
def remove_existing_map_unit(self, projected_crs):
if projected_crs.MapUnit and len(self.file.get_inverse(projected_crs.MapUnit)) == 1:
# TODO: go deeper for conversion units
self.file.remove(projected_crs.MapUnit)
def set_map_unit(self, projected_crs):
if not self.settings["map_unit"]:
return
if "METRE" in self.settings["map_unit"]:
projected_crs.MapUnit = self.file.createIfcSIUnit(
None,
"LENGTHUNIT",
ifcopenshell.util.unit.get_prefix(self.settings["map_unit"]),
ifcopenshell.util.unit.get_unit_name(self.settings["map_unit"]),
)
return
value_component = self.file.create_entity(
"IfcReal", **{"wrappedValue": ifcopenshell.util.unit.si_conversions[self.settings["map_unit"]]}
)
si_unit = self.file.createIfcSIUnit(None, "LENGTHUNIT", None, "METRE")
projected_crs.MapUnit = self.file.createIfcConversionBasedUnit(
self.file.createIfcDimensionalExponents(1, 0, 0, 0, 0, 0, 0),
"LENGTHUNIT",
self.settings["map_unit"],
self.file.createIfcMeasureWithUnit(value_component, si_unit),
)
@@ -0,0 +1,242 @@
import bpy
import json
import ifcopenshell
import ifcopenshell.util.unit
import blenderbim.bim.module.georeference.add_georeferencing as add_georeferencing
import blenderbim.bim.module.georeference.edit_georeferencing as edit_georeferencing
import blenderbim.bim.module.georeference.remove_georeferencing as remove_georeferencing
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.georeference.data import Data
from math import radians, degrees, atan, tan, cos, sin
class EnableEditingGeoreferencing(bpy.types.Operator):
bl_idname = "bim.enable_editing_georeferencing"
bl_label = "Enable Editing Georeferencing"
def execute(self, context):
self.file = IfcStore.get_file()
props = context.scene.BIMGeoreferenceProperties
while len(props.map_conversion) > 0:
props.map_conversion.remove(0)
for attribute in IfcStore.get_schema().declaration_by_name("IfcMapConversion").all_attributes():
data_type = str(attribute.type_of_attribute)
if "<entity" in data_type:
continue
new = props.map_conversion.add()
new.name = attribute.name()
new.is_null = Data.map_conversion[attribute.name()] is None
new.is_optional = attribute.optional()
if "<string>" in data_type:
new.string_value = "" if new.is_null else Data.map_conversion[attribute.name()]
new.data_type = "string"
elif "<real>" in data_type:
new.float_value = 0.0 if new.is_null else Data.map_conversion[attribute.name()]
new.data_type = "float"
elif "<integer>" in data_type:
new.int_value = 0 if new.is_null else Data.map_conversion[attribute.name()]
new.data_type = "integer"
elif "<boolean>" in data_type or "<logical>" in data_type:
new.bool_value = False if new.is_null else Data.map_conversion[attribute.name()]
new.data_type = "boolean"
while len(props.projected_crs) > 0:
props.projected_crs.remove(0)
for attribute in IfcStore.get_schema().declaration_by_name("IfcProjectedCRS").all_attributes():
data_type = str(attribute.type_of_attribute)
if "<entity" in data_type:
continue
new = props.projected_crs.add()
new.name = attribute.name()
new.is_null = Data.projected_crs[attribute.name()] is None
new.is_optional = attribute.optional()
if "<string>" in data_type:
new.string_value = "" if new.is_null else Data.projected_crs[attribute.name()]
new.data_type = "string"
elif "<real>" in data_type:
new.float_value = 0.0 if new.is_null else Data.projected_crs[attribute.name()]
new.data_type = "float"
elif "<integer>" in data_type:
new.int_value = 0 if new.is_null else Data.projected_crs[attribute.name()]
new.data_type = "integer"
elif "<boolean>" in data_type or "<logical>" in data_type:
new.bool_value = False if new.is_null else Data.projected_crs[attribute.name()]
new.data_type = "boolean"
props.is_map_unit_null = Data.projected_crs["MapUnit"] is None
if not props.is_map_unit_null:
props.map_unit_type = Data.projected_crs["MapUnit"]["type"]
if props.map_unit_type == "IfcSIUnit":
prefix = ifcopenshell.util.unit.get_prefix(Data.projected_crs["MapUnit"]["Prefix"]) or ""
name = ifcopenshell.util.unit.get_unit_name(Data.projected_crs["MapUnit"]["Name"])
props.map_unit_si = prefix + name
elif props.map_unit_type == "IfcConversionBasedUnit":
props.map_unit_imperial = Data.projected_crs["MapUnit"]["Name"]
props.is_editing = True
return {"FINISHED"}
class DisableEditingGeoreferencing(bpy.types.Operator):
bl_idname = "bim.disable_editing_georeferencing"
bl_label = "Disable Editing Georeferencing"
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"
def execute(self, context):
self.file = IfcStore.get_file()
props = context.scene.BIMGeoreferenceProperties
map_conversion = {}
for attribute in IfcStore.get_schema().declaration_by_name("IfcMapConversion").all_attributes():
data_type = str(attribute.type_of_attribute)
if "<entity" in data_type:
continue
blender_attribute = props.map_conversion.get(attribute.name())
if blender_attribute.is_null:
map_conversion[attribute.name()] = None
elif blender_attribute.data_type == "string":
map_conversion[attribute.name()] = blender_attribute.string_value
elif blender_attribute.data_type == "float":
map_conversion[attribute.name()] = blender_attribute.float_value
elif blender_attribute.data_type == "integer":
map_conversion[attribute.name()] = blender_attribute.int_value
elif blender_attribute.data_type == "boolean":
map_conversion[attribute.name()] = blender_attribute.bool_value
projected_crs = {}
for attribute in IfcStore.get_schema().declaration_by_name("IfcProjectedCRS").all_attributes():
data_type = str(attribute.type_of_attribute)
if "<entity" in data_type:
continue
blender_attribute = props.projected_crs.get(attribute.name())
if blender_attribute.is_null:
projected_crs[attribute.name()] = None
elif blender_attribute.data_type == "string":
projected_crs[attribute.name()] = blender_attribute.string_value
elif blender_attribute.data_type == "float":
projected_crs[attribute.name()] = blender_attribute.float_value
elif blender_attribute.data_type == "integer":
projected_crs[attribute.name()] = blender_attribute.int_value
elif blender_attribute.data_type == "boolean":
projected_crs[attribute.name()] = blender_attribute.bool_value
map_unit = ""
if not props.is_map_unit_null:
map_unit = props.map_unit_si if props.map_unit_type == "IfcSIUnit" else props.map_unit_imperial
edit_georeferencing.Usecase(self.file, {
"map_conversion": map_conversion,
"projected_crs": projected_crs,
"map_unit": map_unit
}).execute()
Data.load()
bpy.ops.bim.disable_editing_georeferencing()
return {"FINISHED"}
class SetNorthOffset(bpy.types.Operator):
bl_idname = "bim.set_north_offset"
bl_label = "Set North Offset"
def execute(self, context):
context.scene.sun_pos_properties.north_offset = -radians(
ifcopenshell.util.geolocation.xy2angle(
context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").float_value,
context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").float_value
)
)
return {"FINISHED"}
class GetNorthOffset(bpy.types.Operator):
bl_idname = "bim.get_north_offset"
bl_label = "Get North Offset"
def execute(self, context):
x_angle = -context.scene.sun_pos_properties.north_offset
context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").float_value = cos(x_angle)
context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").float_value = sin(x_angle)
return {"FINISHED"}
class RemoveGeoreferencing(bpy.types.Operator):
bl_idname = "bim.remove_georeferencing"
bl_label = "Remove Georeferencing"
def execute(self, context):
remove_georeferencing.Usecase(IfcStore.get_file()).execute()
Data.load()
return {"FINISHED"}
class AddGeoreferencing(bpy.types.Operator):
bl_idname = "bim.add_georeferencing"
bl_label = "Add Georeferencing"
def execute(self, context):
add_georeferencing.Usecase(IfcStore.get_file()).execute()
Data.load()
return {"FINISHED"}
class ConvertLocalToGlobal(bpy.types.Operator):
bl_idname = "bim.convert_local_to_global"
bl_label = "Convert Local To Global"
def execute(self, context):
if not Data.is_loaded:
Data.load()
props = context.scene.BIMGeoreferenceProperties
x, y, z = [float(co) for co in props.coordinate_input.split(",")]
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("XAxisAbscissa", 0.0),
Data.map_conversion.get("Scale", 1.0),
)
props.coordinate_output = ",".join([str(r) for r in results])
bpy.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"
def execute(self, context):
if not Data.is_loaded:
Data.load()
props = context.scene.BIMGeoreferenceProperties
x, y, z = [float(co) for co in props.coordinate_input.split(",")]
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("XAxisAbscissa", 0.0),
Data.map_conversion.get("Scale", 1.0),
)
props.coordinate_output = ",".join([str(r) for r in results])
bpy.context.scene.cursor.location = results
return {"FINISHED"}
@@ -0,0 +1,37 @@
import bpy
from blenderbim.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
StringProperty,
EnumProperty,
BoolProperty,
IntProperty,
FloatProperty,
FloatVectorProperty,
CollectionProperty,
)
class BIMGeoreferenceProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
map_conversion: CollectionProperty(name="Map Conversion", type=Attribute)
projected_crs: CollectionProperty(name="Projected CRS", type=Attribute)
map_unit_type: EnumProperty(
items=[(n, n, "") for n in ["IfcSIUnit", "IfcConversionBasedUnit"]],
name="Map Unit Type",
default="IfcSIUnit",
)
map_unit_si: EnumProperty(
items=[(n, n.lower().capitalize(), "") for n in ["MILLIMETRE", "CENTIMETRE", "METRE", "KILOMETRE"]],
name="Map Unit SI",
default="METRE",
)
map_unit_imperial: EnumProperty(
items=[(n, n.lower().capitalize(), "") for n in ["inch", "foot", "yard", "mile"]],
name="Map Unit SI",
default="foot",
)
is_map_unit_null: BoolProperty(name="Is Map Unit Null")
coordinate_input: StringProperty(name="Coordinate Input")
coordinate_output: StringProperty(name="Coordinate Output")
@@ -0,0 +1,12 @@
class Usecase:
def __init__(self, file, settings=None):
self.file = file
def execute(self):
map_conversion = self.file.by_type("IfcMapConversion")[0]
projected_crs = self.file.by_type("IfcProjectedCRS")[0]
if projected_crs.MapUnit and len(self.file.get_inverse(projected_crs.MapUnit)) == 1:
# TODO: go deeper for conversion units
self.file.remove(projected_crs.MapUnit)
self.file.remove(projected_crs)
self.file.remove(map_conversion)
@@ -0,0 +1,125 @@
from bpy.types import Panel
from blenderbim.bim.module.georeference.data import Data
from blenderbim.bim.ifc import IfcStore
class BIM_PT_gis(Panel):
bl_label = "IFC Georeferencing"
bl_idname = "BIM_PT_gis"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return IfcStore.get_file()
def draw(self, context):
props = context.scene.BIMGeoreferenceProperties
if not Data.is_loaded:
Data.load()
if props.is_editing:
return self.draw_editable_ui(context)
self.draw_ui()
def draw_editable_ui(self, context):
props = context.scene.BIMGeoreferenceProperties
row = self.layout.row(align=True)
row.label(text="Map Conversion", icon="GRID")
row.operator("bim.edit_georeferencing", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_georeferencing", icon="X", text="")
for attribute in props.map_conversion:
if attribute.name == "XAxisAbscissa" and hasattr(context.scene, "sun_pos_properties"):
row = self.layout.row(align=True)
row.operator("bim.get_north_offset", text="Set IFC North")
row.operator("bim.set_north_offset", text="Set Blender North")
row = self.layout.row(align=True)
if attribute.data_type == "string":
row.prop(attribute, "string_value", text=attribute.name)
elif attribute.data_type == "integer":
row.prop(attribute, "int_value", text=attribute.name)
elif attribute.data_type == "float":
row.prop(attribute, "float_value", text=attribute.name)
elif attribute.data_type == "boolean":
row.prop(attribute, "bool_value", text=attribute.name)
if attribute.is_optional:
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
row = self.layout.row(align=True)
row.label(text="Projected CRS", icon="WORLD")
for attribute in props.projected_crs:
row = self.layout.row(align=True)
if attribute.data_type == "string":
row.prop(attribute, "string_value", text=attribute.name)
elif attribute.data_type == "integer":
row.prop(attribute, "int_value", text=attribute.name)
elif attribute.data_type == "float":
row.prop(attribute, "float_value", text=attribute.name)
elif attribute.data_type == "boolean":
row.prop(attribute, "bool_value", text=attribute.name)
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
row = self.layout.row(align=True)
row.prop(props, "map_unit_type", text="MapUnit")
if props.map_unit_type == "IfcSIUnit":
row.prop(props, "map_unit_si", text="")
elif props.map_unit_type == "IfcConversionBasedUnit":
row.prop(props, "map_unit_imperial", text="")
row.prop(props, "is_map_unit_null", icon="RADIOBUT_OFF" if props.is_map_unit_null else "RADIOBUT_ON", text="")
def draw_ui(self):
if not Data.map_conversion:
row = self.layout.row(align=True)
row.label(text="Not Georeferenced")
row.operator("bim.add_georeferencing", icon="ADD", text="")
if Data.map_conversion:
row = self.layout.row(align=True)
row.label(text="Map Conversion", icon="GRID")
row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="")
row.operator("bim.remove_georeferencing", icon="X", text="")
for key, value in Data.map_conversion.items():
if key == "id" or key == "type" or key == "SourceCRS" or key == "TargetCRS" or not value:
continue
row = self.layout.row(align=True)
row.label(text=key)
row.label(text=str(value))
if Data.projected_crs:
row = self.layout.row(align=True)
row.label(text="Projected CRS", icon="WORLD")
for key, value in Data.projected_crs.items():
if key == "id" or key == "type" or not value:
continue
if key == "MapUnit":
unit_value = value.get("Prefix", "") or ""
unit_value += value["Name"]
value = unit_value
row = self.layout.row(align=True)
row.label(text=key)
row.label(text=str(value))
class BIM_PT_gis_utilities(Panel):
bl_idname = "BIM_PT_gis_utilities"
bl_label = "Georeferencing Utilities"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BlenderBIM"
def draw(self, context):
props = context.scene.BIMGeoreferenceProperties
row = self.layout.row()
row.prop(props, "coordinate_input", text="Input")
row = self.layout.row()
row.prop(props, "coordinate_output", text="Output")
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")