mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-18 06:21:40 +00:00
Scheduler builder enhancements #3154
Now it supports 1) print ranges - in svg you'll get only the part that's included in print-range. 2) children styles - sometimes cells inherit center alignment from column styles and before that commit text wouldn't be centered in svg in that case 3) repeating rows support
This commit is contained in:
@@ -19,14 +19,36 @@
|
|||||||
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
|
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
|
||||||
import svgwrite
|
import svgwrite
|
||||||
|
|
||||||
from odf.opendocument import load
|
from odf.opendocument import load as load_ods
|
||||||
from odf.table import Table, TableRow, TableColumn, TableCell
|
from odf.table import Table, TableRow, TableColumn, TableCell
|
||||||
from odf.text import P
|
from odf.text import P
|
||||||
from odf.style import Style
|
from odf.style import Style
|
||||||
from textwrap import wrap
|
from textwrap import wrap
|
||||||
|
from pathlib import Path
|
||||||
|
import string
|
||||||
|
|
||||||
FONT_SIZE = 4.13
|
FONT_SIZE = 4.13
|
||||||
FONT_WIDTH = FONT_SIZE * 0.45
|
FONT_WIDTH = FONT_SIZE * 0.45
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
|
||||||
|
def col2num(col):
|
||||||
|
"""convert letter column index to number:
|
||||||
|
`"A" -> 1`, `"AA" -> 27``
|
||||||
|
"""
|
||||||
|
num = 0
|
||||||
|
for c in col:
|
||||||
|
if c in string.ascii_letters:
|
||||||
|
num = num * 26 + (ord(c.upper()) - ord("A")) + 1
|
||||||
|
return num
|
||||||
|
|
||||||
|
|
||||||
|
def a1_to_rc(cell):
|
||||||
|
"""convert cell index from A1 format to RC: `"A1" -> (0,0)`"""
|
||||||
|
column_letter = cell.strip(string.digits)
|
||||||
|
col_number = col2num(column_letter) - 1
|
||||||
|
row_number = int(cell[len(column_letter) :]) - 1
|
||||||
|
return row_number, col_number
|
||||||
|
|
||||||
|
|
||||||
class Scheduler:
|
class Scheduler:
|
||||||
@@ -38,21 +60,33 @@ class Scheduler:
|
|||||||
)
|
)
|
||||||
self.padding = 1
|
self.padding = 1
|
||||||
self.margin = 1
|
self.margin = 1
|
||||||
doc = load(infile)
|
doc = load_ods(infile)
|
||||||
|
|
||||||
|
# useful for debugging ods
|
||||||
|
if DEBUG:
|
||||||
|
import xml.dom.minidom
|
||||||
|
|
||||||
|
path = Path(infile)
|
||||||
|
dom = xml.dom.minidom.parseString(doc.xml())
|
||||||
|
pretty_xml = dom.toprettyxml()
|
||||||
|
|
||||||
|
with open(path.with_suffix(".xml"), "w") as fo:
|
||||||
|
fo.write(pretty_xml)
|
||||||
|
|
||||||
styles = {}
|
styles = {}
|
||||||
for style in doc.getElementsByType(Style):
|
for cell_style in doc.getElementsByType(Style):
|
||||||
name = style.getAttribute("name")
|
name = cell_style.getAttribute("name")
|
||||||
styles[name] = {}
|
styles[name] = {}
|
||||||
|
|
||||||
# NOTE: there are also styles that inherit from parent styles that we do not process atm
|
# NOTE: there are also styles that inherit from parent styles that we do not process atm
|
||||||
if not style.firstChild:
|
if not cell_style.firstChild:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if style.firstChild.tagName in ["style:table-column-properties", "style:table-row-properties"]:
|
if cell_style.firstChild.tagName in ["style:table-column-properties", "style:table-row-properties"]:
|
||||||
style_children = [style.firstChild]
|
style_children = [cell_style.firstChild]
|
||||||
else:
|
else:
|
||||||
# for style:table-cell-properties we need to collect also text and paragraph properties
|
# for style:table-cell-properties we need to collect also text and paragraph properties
|
||||||
style_children = style.childNodes
|
style_children = cell_style.childNodes
|
||||||
|
|
||||||
for child in style_children:
|
for child in style_children:
|
||||||
child_params = {key[1]: value for key, value in child.attributes.items()}
|
child_params = {key[1]: value for key, value in child.attributes.items()}
|
||||||
@@ -60,116 +94,210 @@ class Scheduler:
|
|||||||
|
|
||||||
table = doc.getElementsByType(Table)[0]
|
table = doc.getElementsByType(Table)[0]
|
||||||
|
|
||||||
|
# related styles stored as a list of tuples:
|
||||||
|
# [(child, parent), ...]
|
||||||
|
related_styles = []
|
||||||
|
|
||||||
# collect columns width
|
# collect columns width
|
||||||
column_widths = []
|
column_widths = []
|
||||||
|
column_styles = []
|
||||||
for col in table.getElementsByType(TableColumn):
|
for col in table.getElementsByType(TableColumn):
|
||||||
style_name = col.getAttribute("stylename")
|
style_name = col.getAttribute("stylename")
|
||||||
repeat = col.getAttribute("numbercolumnsrepeated")
|
col_repeat = col.getAttribute("numbercolumnsrepeated")
|
||||||
repeat = int(repeat) if repeat else 1
|
col_repeat = int(col_repeat) if col_repeat else 1
|
||||||
for i in range(repeat):
|
for i in range(col_repeat):
|
||||||
if not style_name or "column-width" not in styles[style_name]:
|
if not style_name or "column-width" not in styles[style_name]:
|
||||||
column_width = 50
|
column_width = 50
|
||||||
else:
|
else:
|
||||||
column_width = self.convert_to_mm(styles[style_name]["column-width"])
|
column_width = self.convert_to_mm(styles[style_name]["column-width"])
|
||||||
|
column_styles.append(style_name)
|
||||||
column_widths.append(column_width)
|
column_widths.append(column_width)
|
||||||
|
cell_style = col.getAttribute("defaultcellstylename")
|
||||||
|
if cell_style:
|
||||||
|
related_styles.append((style_name, cell_style))
|
||||||
|
|
||||||
# collect rows height
|
# collect rows height
|
||||||
row_heights = []
|
row_heights = []
|
||||||
|
# TODO: never used yet because unsure about priority for row styles
|
||||||
|
# over column styles or vice versa
|
||||||
|
row_styles = []
|
||||||
|
|
||||||
for col in table.getElementsByType(TableRow):
|
for col in table.getElementsByType(TableRow):
|
||||||
style_name = col.getAttribute("stylename")
|
style_name = col.getAttribute("stylename")
|
||||||
repeat = col.getAttribute("numberrowsrepeated")
|
row_repeat = col.getAttribute("numberrowsrepeated")
|
||||||
repeat = int(repeat) if repeat else 1
|
row_repeat = int(row_repeat) if row_repeat else 1
|
||||||
for i in range(repeat):
|
for i in range(row_repeat):
|
||||||
if not style_name or "row-height" not in styles[style_name]:
|
if not style_name or "row-height" not in styles[style_name]:
|
||||||
row_height = 6
|
row_height = 6
|
||||||
else:
|
else:
|
||||||
row_height = self.convert_to_mm(styles[style_name]["row-height"])
|
row_height = self.convert_to_mm(styles[style_name]["row-height"])
|
||||||
|
row_styles.append(style_name)
|
||||||
row_heights.append(row_height)
|
row_heights.append(row_height)
|
||||||
|
cell_style = col.getAttribute("defaultcellstylename")
|
||||||
|
if cell_style:
|
||||||
|
related_styles.append((style_name, cell_style))
|
||||||
|
|
||||||
|
while len(related_styles) > 0:
|
||||||
|
# unzip related styles to children and parents
|
||||||
|
children, parents = zip(*related_styles)
|
||||||
|
independent_styles = set(parents) - set(children)
|
||||||
|
for relation in related_styles[:]:
|
||||||
|
child, parent = relation
|
||||||
|
if parent in independent_styles:
|
||||||
|
child_style = styles[child]
|
||||||
|
styles[child] = styles[parent] | child_style
|
||||||
|
related_styles.remove(relation)
|
||||||
|
|
||||||
|
# TODO: multiple print ranges? 😔
|
||||||
|
print_range = table.getAttribute("printranges")
|
||||||
|
if print_range:
|
||||||
|
min_rc, max_rc = [a1_to_rc(cell.rsplit(".", 1)[1]) for cell in print_range.split(":")]
|
||||||
|
else:
|
||||||
|
# fallback if print range is not defined
|
||||||
|
min_rc, max_rc = (0,0), (1048576, 16384)
|
||||||
|
min_row, min_col = min_rc
|
||||||
|
max_row, max_col = max_rc
|
||||||
|
|
||||||
# draw table
|
# draw table
|
||||||
y = self.margin
|
y = self.margin
|
||||||
for tri, tr in enumerate(table.getElementsByType(TableRow)):
|
tri = 0
|
||||||
x = self.margin
|
stop_iterating_over_rows = False
|
||||||
height = row_heights[tri]
|
# TODO: row spans support?
|
||||||
tdi = 0
|
for tr in table.getElementsByType(TableRow):
|
||||||
|
if stop_iterating_over_rows:
|
||||||
|
break
|
||||||
|
|
||||||
for td in tr.getElementsByType(TableCell):
|
row_repeat = tr.getAttribute("numberrowsrepeated")
|
||||||
column_span = td.getAttribute("numbercolumnsspanned")
|
row_repeat = int(row_repeat) if row_repeat else 1
|
||||||
column_span = int(column_span) if column_span else 1
|
|
||||||
|
|
||||||
repeat = td.getAttribute("numbercolumnsrepeated")
|
for i_row_repeat in range(row_repeat):
|
||||||
repeat = int(repeat) if repeat else 1
|
if tri < min_row:
|
||||||
|
tri += 1
|
||||||
|
continue
|
||||||
|
elif tri > max_row:
|
||||||
|
stop_iterating_over_rows = True
|
||||||
|
break
|
||||||
|
|
||||||
# figuring text alignment
|
x = self.margin
|
||||||
style_name = td.getAttribute("stylename")
|
height = row_heights[tri]
|
||||||
style = styles[style_name] if style_name else {}
|
tdi = 0
|
||||||
if style_name and "vertical-align" in style and style["vertical-align"] != "automatic":
|
stop_iterating_over_columns = False
|
||||||
vertical_align = style["vertical-align"]
|
|
||||||
else:
|
|
||||||
vertical_align = "bottom"
|
|
||||||
|
|
||||||
alignment_translation = {
|
for td in tr.getElementsByType(TableCell):
|
||||||
"center": "middle",
|
if stop_iterating_over_columns:
|
||||||
"end": "right",
|
break
|
||||||
"start": "left",
|
|
||||||
}
|
|
||||||
|
|
||||||
if style_name and "text-align" in style and style["text-align"] != "automatic":
|
column_span = td.getAttribute("numbercolumnsspanned")
|
||||||
horizontal_align = style["text-align"]
|
column_span = int(column_span) if column_span else 1
|
||||||
horizontal_align = alignment_translation.get(horizontal_align, horizontal_align)
|
|
||||||
else:
|
|
||||||
horizontal_align = "left"
|
|
||||||
|
|
||||||
if vertical_align == "middle" and horizontal_align == "middle":
|
col_repeat = td.getAttribute("numbercolumnsrepeated")
|
||||||
box_alignment = "center"
|
col_repeat = int(col_repeat) if col_repeat else 1
|
||||||
else:
|
|
||||||
box_alignment = f"{vertical_align}-{horizontal_align}"
|
|
||||||
|
|
||||||
# for future use
|
# figuring text alignment
|
||||||
wrap_text = style.get("wrap-option", None) == "wrap"
|
cell_style = self.get_style(td.getAttribute("stylename"), styles)
|
||||||
|
|
||||||
# drawing cells and text
|
# drawing cells and text
|
||||||
for i in range(0, repeat):
|
for i_col_repeat in range(col_repeat):
|
||||||
width = sum(column_widths[tdi : tdi + int(column_span)])
|
start_tdi = tdi
|
||||||
self.svg.add(
|
end_tdi = tdi + int(column_span) - 1
|
||||||
self.svg.rect(
|
# if the entire span is beyond print range => continue
|
||||||
insert=(x, y),
|
# if only part then keeping that part
|
||||||
size=(width, height),
|
if start_tdi < min_col:
|
||||||
style="fill: #ffffff; stroke-width:.125; stroke: #000000;",
|
if end_tdi < min_col:
|
||||||
|
tdi += column_span
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
start_tdi = min_col
|
||||||
|
|
||||||
|
# stop if start column is beyond print range
|
||||||
|
if start_tdi > max_col:
|
||||||
|
stop_iterating_over_columns = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# making sure last column won't go beyond the print range
|
||||||
|
if end_tdi > max_col:
|
||||||
|
end_tdi = max_col
|
||||||
|
|
||||||
|
width = sum(column_widths[start_tdi : end_tdi + 1])
|
||||||
|
|
||||||
|
self.svg.add(
|
||||||
|
self.svg.rect(
|
||||||
|
insert=(x, y),
|
||||||
|
size=(width, height),
|
||||||
|
style="fill: #ffffff; stroke-width:.125; stroke: #000000;",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
p_tags = td.getElementsByType(P)
|
||||||
p_tags = td.getElementsByType(P)
|
|
||||||
|
|
||||||
if p_tags:
|
col_style = self.get_style(column_styles[tdi], styles)
|
||||||
# figuring text position based on alignment
|
final_cell_style = col_style | cell_style
|
||||||
text_position = [0.0, 0.0]
|
box_alignment = self.get_box_alignment(final_cell_style)
|
||||||
if box_alignment.endswith("left"):
|
wrap_text = final_cell_style.get("wrap-option", None) == "wrap"
|
||||||
text_position[0] = x + self.padding
|
if p_tags:
|
||||||
elif box_alignment.endswith("middle") or box_alignment == "center":
|
# figuring text position based on alignment
|
||||||
text_position[0] = x + width / 2
|
text_position = [0.0, 0.0]
|
||||||
elif box_alignment.endswith("right"):
|
if box_alignment.endswith("left"):
|
||||||
text_position[0] = x + width - self.padding
|
text_position[0] = x + self.padding
|
||||||
|
elif box_alignment.endswith("middle") or box_alignment == "center":
|
||||||
|
text_position[0] = x + width / 2
|
||||||
|
elif box_alignment.endswith("right"):
|
||||||
|
text_position[0] = x + width - self.padding
|
||||||
|
|
||||||
if box_alignment.startswith("top"):
|
if box_alignment.startswith("top"):
|
||||||
text_position[1] = y + self.padding
|
text_position[1] = y + self.padding
|
||||||
elif box_alignment.startswith("middle") or box_alignment == "center":
|
elif box_alignment.startswith("middle") or box_alignment == "center":
|
||||||
text_position[1] = y + height / 2
|
text_position[1] = y + height / 2
|
||||||
elif box_alignment.startswith("bottom"):
|
elif box_alignment.startswith("bottom"):
|
||||||
text_position[1] = y + height - self.padding
|
text_position[1] = y + height - self.padding
|
||||||
|
|
||||||
self.add_text(
|
self.add_text(
|
||||||
p_tags, *text_position, box_alignment=box_alignment, wrap_text=wrap_text, cell_width=width
|
p_tags,
|
||||||
)
|
*text_position,
|
||||||
x += width
|
box_alignment=box_alignment,
|
||||||
tdi += column_span
|
wrap_text=wrap_text,
|
||||||
|
cell_width=width,
|
||||||
|
)
|
||||||
|
x += width
|
||||||
|
tdi += column_span
|
||||||
|
|
||||||
|
tri += 1
|
||||||
|
y += height
|
||||||
|
|
||||||
y += height
|
|
||||||
total_width = sum(column_widths) + (self.margin * 2)
|
total_width = sum(column_widths) + (self.margin * 2)
|
||||||
self.svg["width"] = "{}mm".format(total_width)
|
self.svg["width"] = "{}mm".format(total_width)
|
||||||
self.svg["height"] = "{}mm".format(y)
|
self.svg["height"] = "{}mm".format(y)
|
||||||
self.svg["viewBox"] = "0 0 {} {}".format(total_width, y)
|
self.svg["viewBox"] = "0 0 {} {}".format(total_width, y)
|
||||||
self.svg.save(pretty=True)
|
self.svg.save(pretty=True)
|
||||||
|
|
||||||
|
def get_style(self, style_name, styles):
|
||||||
|
style = styles[style_name] if style_name else {}
|
||||||
|
return style
|
||||||
|
|
||||||
|
def get_box_alignment(self, style):
|
||||||
|
if style and "vertical-align" in style and style["vertical-align"] != "automatic":
|
||||||
|
vertical_align = style["vertical-align"]
|
||||||
|
else:
|
||||||
|
vertical_align = "bottom"
|
||||||
|
|
||||||
|
alignment_translation = {
|
||||||
|
"center": "middle",
|
||||||
|
"end": "right",
|
||||||
|
"start": "left",
|
||||||
|
}
|
||||||
|
|
||||||
|
if style and "text-align" in style and style["text-align"] != "automatic":
|
||||||
|
horizontal_align = style["text-align"]
|
||||||
|
horizontal_align = alignment_translation.get(horizontal_align, horizontal_align)
|
||||||
|
else:
|
||||||
|
horizontal_align = "left"
|
||||||
|
|
||||||
|
if vertical_align == "middle" and horizontal_align == "middle":
|
||||||
|
box_alignment = "center"
|
||||||
|
else:
|
||||||
|
box_alignment = f"{vertical_align}-{horizontal_align}"
|
||||||
|
|
||||||
|
return box_alignment
|
||||||
|
|
||||||
def add_text(self, p_tags, x, y, box_alignment="bottom-left", wrap_text=False, cell_width=100):
|
def add_text(self, p_tags, x, y, box_alignment="bottom-left", wrap_text=False, cell_width=100):
|
||||||
"""
|
"""
|
||||||
Adds text to svg.
|
Adds text to svg.
|
||||||
|
|||||||
Reference in New Issue
Block a user