mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 06:39:13 +00:00
Fix #3587. IfcCSV now supports custom formatting functions.
This commit is contained in:
@@ -101,6 +101,7 @@ class ImportCsvAttributes(bpy.types.Operator):
|
|||||||
new.sort = attribute["sort"]
|
new.sort = attribute["sort"]
|
||||||
new.group = attribute["group"]
|
new.group = attribute["group"]
|
||||||
new.summary = attribute["summary"]
|
new.summary = attribute["summary"]
|
||||||
|
new.formatting = attribute["formatting"]
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
@@ -123,7 +124,14 @@ class ExportCsvAttributes(bpy.types.Operator):
|
|||||||
data = {
|
data = {
|
||||||
"query": tool.Search.export_filter_query(props.filter_groups),
|
"query": tool.Search.export_filter_query(props.filter_groups),
|
||||||
"attributes": [
|
"attributes": [
|
||||||
{"name": a.name, "header": a.header, "sort": a.sort, "group": a.group, "summary": a.summary}
|
{
|
||||||
|
"name": a.name,
|
||||||
|
"header": a.header,
|
||||||
|
"sort": a.sort,
|
||||||
|
"group": a.group,
|
||||||
|
"summary": a.summary,
|
||||||
|
"formatting": a.formatting,
|
||||||
|
}
|
||||||
for a in props.csv_attributes
|
for a in props.csv_attributes
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -173,6 +181,7 @@ class ExportIfcCsv(bpy.types.Operator):
|
|||||||
sort = []
|
sort = []
|
||||||
groups = []
|
groups = []
|
||||||
summaries = []
|
summaries = []
|
||||||
|
formatting = []
|
||||||
for attribute in props.csv_attributes:
|
for attribute in props.csv_attributes:
|
||||||
if attribute.sort != "NONE":
|
if attribute.sort != "NONE":
|
||||||
sort.append({"name": attribute.name, "order": attribute.sort})
|
sort.append({"name": attribute.name, "order": attribute.sort})
|
||||||
@@ -181,6 +190,9 @@ class ExportIfcCsv(bpy.types.Operator):
|
|||||||
if attribute.summary != "NONE":
|
if attribute.summary != "NONE":
|
||||||
summaries.append({"name": attribute.name, "type": attribute.summary})
|
summaries.append({"name": attribute.name, "type": attribute.summary})
|
||||||
|
|
||||||
|
if attribute.formatting != "{{value}}" and "{{value}}" in attribute.formatting:
|
||||||
|
formatting.append({"name": attribute.name, "format": attribute.formatting})
|
||||||
|
|
||||||
sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter
|
sep = props.csv_custom_delimiter if props.csv_delimiter == "CUSTOM" else props.csv_delimiter
|
||||||
ifc_csv.export(
|
ifc_csv.export(
|
||||||
ifc_file,
|
ifc_file,
|
||||||
@@ -198,6 +210,7 @@ class ExportIfcCsv(bpy.types.Operator):
|
|||||||
sort=sort,
|
sort=sort,
|
||||||
groups=groups,
|
groups=groups,
|
||||||
summaries=summaries,
|
summaries=summaries,
|
||||||
|
formatting=formatting,
|
||||||
)
|
)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class CsvAttribute(PropertyGroup):
|
|||||||
("MAX", "Max", "Gets the maximum value of all rows"),
|
("MAX", "Max", "Gets the maximum value of all rows"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
formatting: StringProperty(default="{{value}}", name="Formatting")
|
||||||
|
|
||||||
|
|
||||||
class CsvProperties(PropertyGroup):
|
class CsvProperties(PropertyGroup):
|
||||||
@@ -94,4 +95,5 @@ class CsvProperties(PropertyGroup):
|
|||||||
should_show_sort: BoolProperty(default=False, name="Show Sorting")
|
should_show_sort: BoolProperty(default=False, name="Show Sorting")
|
||||||
should_show_group: BoolProperty(default=False, name="Show Grouping")
|
should_show_group: BoolProperty(default=False, name="Show Grouping")
|
||||||
should_show_summary: BoolProperty(default=False, name="Show Summary")
|
should_show_summary: BoolProperty(default=False, name="Show Summary")
|
||||||
|
should_show_formatting: BoolProperty(default=False, name="Show Formatting")
|
||||||
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
|
should_load_from_memory: BoolProperty(default=False, name="Load from Memory")
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from blenderbim.bim.module.search.data import SearchData
|
|||||||
|
|
||||||
|
|
||||||
class BIM_PT_ifccsv(Panel):
|
class BIM_PT_ifccsv(Panel):
|
||||||
bl_label = "CSV Import/Export"
|
bl_label = "Spreadsheet Import/Export"
|
||||||
bl_idname = "BIM_PT_ifccsv"
|
bl_idname = "BIM_PT_ifccsv"
|
||||||
bl_options = {"DEFAULT_CLOSED"}
|
bl_options = {"DEFAULT_CLOSED"}
|
||||||
bl_space_type = "PROPERTIES"
|
bl_space_type = "PROPERTIES"
|
||||||
@@ -86,6 +86,7 @@ class BIM_PT_ifccsv(Panel):
|
|||||||
row.prop(props, "should_show_sort", icon="SORTSIZE", text="")
|
row.prop(props, "should_show_sort", icon="SORTSIZE", text="")
|
||||||
row.prop(props, "should_show_group", icon="OUTLINER_COLLECTION", text="")
|
row.prop(props, "should_show_group", icon="OUTLINER_COLLECTION", text="")
|
||||||
row.prop(props, "should_show_summary", icon="SYNTAX_ON", text="")
|
row.prop(props, "should_show_summary", icon="SYNTAX_ON", text="")
|
||||||
|
row.prop(props, "should_show_formatting", icon="CON_TRANSLIKE", text="")
|
||||||
|
|
||||||
total = len(props.csv_attributes)
|
total = len(props.csv_attributes)
|
||||||
for index, attribute in enumerate(props.csv_attributes):
|
for index, attribute in enumerate(props.csv_attributes):
|
||||||
@@ -100,6 +101,8 @@ class BIM_PT_ifccsv(Panel):
|
|||||||
row.prop(attribute, "varies_value", text="")
|
row.prop(attribute, "varies_value", text="")
|
||||||
if props.should_show_summary:
|
if props.should_show_summary:
|
||||||
row.prop(attribute, "summary", text="")
|
row.prop(attribute, "summary", text="")
|
||||||
|
if props.should_show_formatting:
|
||||||
|
row.prop(attribute, "formatting", text="")
|
||||||
if total > 1:
|
if total > 1:
|
||||||
if index != 0:
|
if index != 0:
|
||||||
op = row.operator(f"bim.reorder_csv_attribute", icon="TRIA_UP", text="")
|
op = row.operator(f"bim.reorder_csv_attribute", icon="TRIA_UP", text="")
|
||||||
|
|||||||
+26
-1
@@ -75,6 +75,7 @@ class IfcCsv:
|
|||||||
sort=None,
|
sort=None,
|
||||||
groups=None,
|
groups=None,
|
||||||
summaries=None,
|
summaries=None,
|
||||||
|
formatting=None,
|
||||||
):
|
):
|
||||||
self.ifc_file = ifc_file
|
self.ifc_file = ifc_file
|
||||||
self.results = []
|
self.results = []
|
||||||
@@ -116,6 +117,7 @@ class IfcCsv:
|
|||||||
self.group_results(groups, attributes)
|
self.group_results(groups, attributes)
|
||||||
self.summarise_results(summaries, attributes)
|
self.summarise_results(summaries, attributes)
|
||||||
self.sort_results(sort, attributes, include_global_id)
|
self.sort_results(sort, attributes, include_global_id)
|
||||||
|
self.format_results(formatting, attributes, null)
|
||||||
|
|
||||||
if format == "csv":
|
if format == "csv":
|
||||||
self.export_csv(output, delimiter=delimiter)
|
self.export_csv(output, delimiter=delimiter)
|
||||||
@@ -225,7 +227,27 @@ class IfcCsv:
|
|||||||
self.summaries[si] = max(summary_values[si])
|
self.summaries[si] = max(summary_values[si])
|
||||||
self.summaries[si] = summary_type.title() + ": " + str(self.summaries[si])
|
self.summaries[si] = summary_type.title() + ": " + str(self.summaries[si])
|
||||||
|
|
||||||
|
def format_results(self, formatting, attributes, null):
|
||||||
|
if not formatting:
|
||||||
|
return
|
||||||
|
|
||||||
|
formatting_indices = {}
|
||||||
|
|
||||||
|
for data in formatting:
|
||||||
|
index = attributes.index(data["name"])
|
||||||
|
formatting_indices[index] = data["format"]
|
||||||
|
|
||||||
|
for row in self.results:
|
||||||
|
for index, format_query in formatting_indices.items():
|
||||||
|
if row[index] == null:
|
||||||
|
continue
|
||||||
|
if not isinstance(row[index], str):
|
||||||
|
row[index] = '"' + str(row[index]) + '"'
|
||||||
|
row[index] = ifcopenshell.util.selector.format(format_query.replace("{{value}}", row[index]))
|
||||||
|
|
||||||
def sort_results(self, sort, attributes, include_global_id):
|
def sort_results(self, sort, attributes, include_global_id):
|
||||||
|
if not self.results:
|
||||||
|
return
|
||||||
if sort:
|
if sort:
|
||||||
def natural_sort(value):
|
def natural_sort(value):
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
@@ -240,7 +262,10 @@ class IfcCsv:
|
|||||||
reverse = sort_data["order"] == "DESC"
|
reverse = sort_data["order"] == "DESC"
|
||||||
self.results = sorted(self.results, key=lambda x: natural_sort(x[i]), reverse=reverse)
|
self.results = sorted(self.results, key=lambda x: natural_sort(x[i]), reverse=reverse)
|
||||||
else:
|
else:
|
||||||
self.results = sorted(self.results, key=lambda x: x[1 if include_global_id else 0])
|
if include_global_id and len(self.results[0]) > 1:
|
||||||
|
self.results = sorted(self.results, key=lambda x: x[1])
|
||||||
|
elif not include_global_id:
|
||||||
|
self.results = sorted(self.results, key=lambda x: x[0])
|
||||||
|
|
||||||
def export_csv(self, output, delimiter=None):
|
def export_csv(self, output, delimiter=None):
|
||||||
with open(output, "w", newline="", encoding="utf-8") as f:
|
with open(output, "w", newline="", encoding="utf-8") as f:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import re
|
|||||||
import lark
|
import lark
|
||||||
import ifcopenshell.util
|
import ifcopenshell.util
|
||||||
import ifcopenshell.util.fm
|
import ifcopenshell.util.fm
|
||||||
|
import ifcopenshell.util.unit
|
||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
import ifcopenshell.util.classification
|
import ifcopenshell.util.classification
|
||||||
|
|
||||||
@@ -112,6 +113,106 @@ get_element_grammar = lark.Lark(
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
format_grammar = lark.Lark(
|
||||||
|
"""start: function
|
||||||
|
|
||||||
|
function: round | format_length | lower | upper | title | concat | ESCAPED_STRING | NUMBER
|
||||||
|
|
||||||
|
round: "round(" function "," NUMBER ")"
|
||||||
|
format_length: metric_length | imperial_length
|
||||||
|
metric_length: "metric_length(" function "," NUMBER "," NUMBER ")"
|
||||||
|
imperial_length: "imperial_length(" function "," NUMBER ["," ESCAPED_STRING] ")"
|
||||||
|
lower: "lower(" function ")"
|
||||||
|
upper: "upper(" function ")"
|
||||||
|
title: "title(" function ")"
|
||||||
|
concat: "concat(" function ("," function)* ")"
|
||||||
|
|
||||||
|
// Embed common.lark for packaging
|
||||||
|
DIGIT: "0".."9"
|
||||||
|
HEXDIGIT: "a".."f"|"A".."F"|DIGIT
|
||||||
|
INT: DIGIT+
|
||||||
|
SIGNED_INT: ["+"|"-"] INT
|
||||||
|
DECIMAL: INT "." INT? | "." INT
|
||||||
|
_EXP: ("e"|"E") SIGNED_INT
|
||||||
|
FLOAT: INT _EXP | DECIMAL _EXP?
|
||||||
|
SIGNED_FLOAT: ["+"|"-"] FLOAT
|
||||||
|
NUMBER: FLOAT | INT
|
||||||
|
SIGNED_NUMBER: ["+"|"-"] NUMBER
|
||||||
|
_STRING_INNER: /.*?/
|
||||||
|
_STRING_ESC_INNER: _STRING_INNER /(?<!\\\\)(\\\\\\\\)*?/
|
||||||
|
ESCAPED_STRING : "\\"" _STRING_ESC_INNER "\\""
|
||||||
|
LCASE_LETTER: "a".."z"
|
||||||
|
UCASE_LETTER: "A".."Z"
|
||||||
|
LETTER: UCASE_LETTER | LCASE_LETTER
|
||||||
|
WORD: LETTER+
|
||||||
|
CNAME: ("_"|LETTER) ("_"|LETTER|DIGIT)*
|
||||||
|
WS_INLINE: (" "|/\\t/)+
|
||||||
|
WS: /[ \\t\\f\\r\\n]/+
|
||||||
|
CR : /\\r/
|
||||||
|
LF : /\\n/
|
||||||
|
NEWLINE: (CR? LF)+
|
||||||
|
|
||||||
|
%ignore WS // Disregard spaces in text
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FormatTransformer(lark.Transformer):
|
||||||
|
def start(self, args):
|
||||||
|
return args[0]
|
||||||
|
|
||||||
|
def function(self, args):
|
||||||
|
return args[0]
|
||||||
|
|
||||||
|
def ESCAPED_STRING(self, args):
|
||||||
|
return args[1:-1].replace("\\", "")
|
||||||
|
|
||||||
|
def NUMBER(self, args):
|
||||||
|
return str(args)
|
||||||
|
|
||||||
|
def lower(self, args):
|
||||||
|
return str(args[0]).lower()
|
||||||
|
|
||||||
|
def upper(self, args):
|
||||||
|
return str(args[0]).upper()
|
||||||
|
|
||||||
|
def title(self, args):
|
||||||
|
return str(args[0]).title()
|
||||||
|
|
||||||
|
def concat(self, args):
|
||||||
|
return "".join(args)
|
||||||
|
|
||||||
|
def round(self, args):
|
||||||
|
return str(round(float(args[0]) / float(args[1])) * float(args[1]))
|
||||||
|
|
||||||
|
def format_length(self, args):
|
||||||
|
return args[0]
|
||||||
|
|
||||||
|
def metric_length(self, args):
|
||||||
|
value, precision, decimal_places = args
|
||||||
|
return ifcopenshell.util.unit.format_length(
|
||||||
|
float(value), float(precision), int(decimal_places), unit_system="metric"
|
||||||
|
)
|
||||||
|
|
||||||
|
def imperial_length(self, args):
|
||||||
|
if len(args) == 2:
|
||||||
|
imperial_unit = "foot"
|
||||||
|
value, precision = args
|
||||||
|
else:
|
||||||
|
value, precision, imperial_unit = args
|
||||||
|
if imperial_unit == "inch":
|
||||||
|
imperial_unit = "inch"
|
||||||
|
else:
|
||||||
|
imperial_unit = "foot"
|
||||||
|
|
||||||
|
return ifcopenshell.util.unit.format_length(
|
||||||
|
float(value), int(precision), unit_system="imperial", imperial_unit=imperial_unit
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format(query):
|
||||||
|
return FormatTransformer().transform(format_grammar.parse(query))
|
||||||
|
|
||||||
|
|
||||||
def get_element_value(element, query):
|
def get_element_value(element, query):
|
||||||
start = get_element_grammar.parse(query)
|
start = get_element_grammar.parse(query)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from math import pi
|
from math import pi
|
||||||
|
from fractions import Fraction
|
||||||
|
|
||||||
prefixes = {
|
prefixes = {
|
||||||
"EXA": 1e18,
|
"EXA": 1e18,
|
||||||
|
|||||||
@@ -22,6 +22,32 @@ import ifcopenshell.api
|
|||||||
import ifcopenshell.util.selector as subject
|
import ifcopenshell.util.selector as subject
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormat():
|
||||||
|
def test_no_formatting(self):
|
||||||
|
assert subject.format("123") == "123"
|
||||||
|
assert subject.format('\"123\"') == "123"
|
||||||
|
assert subject.format('\"foo\"') == "foo"
|
||||||
|
|
||||||
|
def test_string_formatting(self):
|
||||||
|
assert subject.format('upper(\"fOo\")') == "FOO"
|
||||||
|
assert subject.format('lower(\"fOo\")') == "foo"
|
||||||
|
assert subject.format('title(\"fOo\")') == "Foo"
|
||||||
|
assert subject.format('concat(\"fOo\", \"bar\")') == "fOobar"
|
||||||
|
assert subject.format('upper(concat(\"fOo\", \"bar\"))') == "FOOBAR"
|
||||||
|
|
||||||
|
def test_number_formatting(self):
|
||||||
|
assert subject.format("round(123, 5)") == "125.0"
|
||||||
|
assert subject.format('round(\"123\", 5)') == "125.0"
|
||||||
|
assert subject.format('metric_length(123, 5, 2)') == "125.00"
|
||||||
|
assert subject.format('metric_length(123.123, 0.1, 2)') == "123.10"
|
||||||
|
assert subject.format('metric_length(\"123\", 5, 2)') == "125.00"
|
||||||
|
assert subject.format('imperial_length(1, 1)') == "1'"
|
||||||
|
assert subject.format('imperial_length(3.123, 1)') == "3' - 1\""
|
||||||
|
assert subject.format('imperial_length(3.123, 2)') == "3' - 1 1/2\""
|
||||||
|
assert subject.format('imperial_length(\"3.123\", 2)') == "3' - 1 1/2\""
|
||||||
|
assert subject.format('imperial_length(\"123.123\", 2, \"inch\")') == "10' - 3\""
|
||||||
|
|
||||||
|
|
||||||
class TestGetElementValue(test.bootstrap.IFC4):
|
class TestGetElementValue(test.bootstrap.IFC4):
|
||||||
def test_selecting_an_elements_class_or_id_using_a_query(self):
|
def test_selecting_an_elements_class_or_id_using_a_query(self):
|
||||||
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
|
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
|
||||||
|
|||||||
Reference in New Issue
Block a user