Files
create/src/Mod/Arch/ArchSchedule.py
2021-01-22 20:17:47 +01:00

692 lines
27 KiB
Python

# -*- coding: utf8 -*-
#***************************************************************************
#* Copyright (c) 2015 Yorik van Havre <yorik@uncreated.net> *
#* *
#* This program is free software; you can redistribute it and/or modify *
#* it under the terms of the GNU Lesser General Public License (LGPL) *
#* as published by the Free Software Foundation; either version 2 of *
#* the License, or (at your option) any later version. *
#* for detail see the LICENCE text file. *
#* *
#* This program 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 Library General Public License for more details. *
#* *
#* You should have received a copy of the GNU Library General Public *
#* License along with this program; if not, write to the Free Software *
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
#* USA *
#* *
#***************************************************************************
from __future__ import print_function
import sys
import FreeCAD, time
if FreeCAD.GuiUp:
import FreeCADGui, Arch_rc, os
from PySide import QtCore, QtGui
from DraftTools import translate
from PySide.QtCore import QT_TRANSLATE_NOOP
else:
# \cond
def translate(ctxt,txt):
return txt
def QT_TRANSLATE_NOOP(ctxt,txt):
return txt
# \endcond
## @package ArchSchedule
# \ingroup ARCH
# \brief The Schedule object and tools
#
# This module provides tools to build Schedule objects.
# Schedules are objects that can count and gather information
# about objects in the document, and fill a spreadsheet with the result
__title__ = "Arch Schedule"
__author__ = "Yorik van Havre"
__url__ = "https://www.freecadweb.org"
verbose = True # change this for silent recomputes
class CommandArchSchedule:
"the Arch Schedule command definition"
def GetResources(self):
return {'Pixmap': 'Arch_Schedule',
'MenuText': QT_TRANSLATE_NOOP("Arch_Schedule","Schedule"),
'ToolTip': QT_TRANSLATE_NOOP("Arch_Schedule","Creates a schedule to collect data from the model")}
def Activated(self):
if hasattr(self,"taskd"):
if self.taskd:
self.taskd.form.hide()
self.taskd = ArchScheduleTaskPanel()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
class _ArchSchedule:
"the Arch Schedule object"
def __init__(self,obj):
self.setProperties(obj)
obj.Proxy = self
self.Type = "Schedule"
def onDocumentRestored(self,obj):
self.setProperties(obj)
def setProperties(self,obj):
if not "Description" in obj.PropertiesList:
obj.addProperty("App::PropertyStringList","Description", "Arch",QT_TRANSLATE_NOOP("App::Property","The description column"))
if not "Value" in obj.PropertiesList:
obj.addProperty("App::PropertyStringList","Value", "Arch",QT_TRANSLATE_NOOP("App::Property","The values column"))
if not "Unit" in obj.PropertiesList:
obj.addProperty("App::PropertyStringList","Unit", "Arch",QT_TRANSLATE_NOOP("App::Property","The units column"))
if not "Objects" in obj.PropertiesList:
obj.addProperty("App::PropertyStringList","Objects", "Arch",QT_TRANSLATE_NOOP("App::Property","The objects column"))
if not "Filter" in obj.PropertiesList:
obj.addProperty("App::PropertyStringList","Filter", "Arch",QT_TRANSLATE_NOOP("App::Property","The filter column"))
if not "CreateSpreadsheet" in obj.PropertiesList:
obj.addProperty("App::PropertyBool", "CreateSpreadsheet", "Arch",QT_TRANSLATE_NOOP("App::Property","If True, a spreadsheet containing the results is recreated when needed"))
if not "Result" in obj.PropertiesList:
obj.addProperty("App::PropertyLink", "Result", "Arch",QT_TRANSLATE_NOOP("App::Property","The spreadsheet to print the results to"))
if not "DetailedResults" in obj.PropertiesList:
obj.addProperty("App::PropertyBool", "DetailedResults", "Arch",QT_TRANSLATE_NOOP("App::Property","If True, additional lines with each individual object are added to the results"))
def onChanged(self,obj,prop):
if (prop == "CreateSpreadsheet"):
if hasattr(obj,"CreateSpreadsheet") and obj.CreateSpreadsheet:
if not obj.Result:
import Spreadsheet
sp = FreeCAD.ActiveDocument.addObject("Spreadsheet::Sheet","Result")
obj.Result = sp
def setSpreadsheetData(self,obj,force=False):
"""Fills a spreadsheet with the stored data"""
if not hasattr(self,"data"):
self.execute(obj)
if not hasattr(self,"data"):
return
if not self.data:
return
if not obj.Result:
if obj.CreateSpreadsheet or force:
import Spreadsheet
sp = FreeCAD.ActiveDocument.addObject("Spreadsheet::Sheet","Result")
obj.Result = sp
else:
return
# clear spreadsheet
obj.Result.clearAll()
# set headers
obj.Result.set("A1","Description")
obj.Result.set("B1","Value")
obj.Result.set("C1","Unit")
obj.Result.setStyle('A1:C1', 'bold', 'add')
# write contents
for k,v in self.data.items():
obj.Result.set(k,v)
# recompute
obj.Result.recompute()
def execute(self,obj):
# verify the data
if not obj.Description:
# empty description column
return
for p in [obj.Value,obj.Unit,obj.Objects,obj.Filter]:
# different number of items in each column
if len(obj.Description) != len(p):
return
if not hasattr(obj,"Result"):
# silently fail on old schedule objects
return
self.data = {} # store all results in self.data, so it lives even without spreadsheet
li = 1 # row index - starts at 2 to leave 2 blank rows for the title
for i in range(len(obj.Description)):
li += 1
if not obj.Description[i]:
# blank line
continue
# write description
if sys.version_info.major >= 3:
# use unicode for python3
self.data["A"+str(li)] = obj.Description[i]
else:
self.data["A"+str(li)] = obj.Description[i].encode("utf8")
if verbose:
l= "OPERATION: "+obj.Description[i]
print("")
print (l)
print (len(l)*"=")
# build set of valid objects
objs = obj.Objects[i]
val = obj.Value[i]
if val:
import Draft,Arch
if objs:
objs = objs.split(";")
objs = [FreeCAD.ActiveDocument.getObject(o) for o in objs]
objs = [o for o in objs if o != None]
else:
objs = FreeCAD.ActiveDocument.Objects
if len(objs) == 1:
# remove object itself if the object is a group
if objs[0].isDerivedFrom("App::DocumentObjectGroup"):
objs = objs[0].Group
objs = Draft.get_group_contents(objs)
objs = Arch.pruneIncluded(objs,strict=True)
# remove the schedule object and its result from the list
objs = [o for o in objs if not o == obj]
objs = [o for o in objs if not o == obj.Result]
if obj.Filter[i]:
# apply filters
nobjs = []
for o in objs:
props = [p.upper() for p in o.PropertiesList]
ok = True
for f in obj.Filter[i].split(";"):
args = [a.strip() for a in f.strip().split(":")]
if args[0][0] == "!":
inv = True
prop = args[0][1:].upper()
else:
inv = False
prop = args[0].upper()
fval = args[1].upper()
if prop == "TYPE":
prop = "IFCTYPE"
if inv:
if prop in props:
csprop = o.PropertiesList[props.index(prop)]
if fval in getattr(o,csprop).upper():
ok = False
else:
if not (prop in props):
ok = False
else:
csprop = o.PropertiesList[props.index(prop)]
if not (fval in getattr(o,csprop).upper()):
ok = False
if ok:
nobjs.append(o)
objs = nobjs
# perform operation: count or retrieve property
if val.upper() == "COUNT":
val = len(objs)
if verbose:
print (val, ",".join([o.Label for o in objs]))
self.data["B"+str(li)] = str(val)
if obj.DetailedResults:
# additional blank line...
li += 1
self.data["A"+str(li)] = " "
else:
vals = val.split(".")
if vals[0][0].islower():
# old-style: first member is not a property
vals = vals[1:]
sumval = 0
# get unit
tp = None
unit = None
q = None
if obj.Unit[i]:
unit = obj.Unit[i]
if sys.version_info.major < 3:
unit = unit.encode("utf8")
unit = unit.replace("2","^2")
unit = unit.replace("3","^3")
unit = unit.replace("²","^2")
unit = unit.replace("³","^3")
if "2" in unit:
tp = FreeCAD.Units.Area
elif "3" in unit:
tp = FreeCAD.Units.Volume
elif "deg" in unit:
tp = FreeCAD.Units.Angle
else:
tp = FreeCAD.Units.Length
# format value
dv = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Units").GetInt("Decimals",2)
fs = "{:."+str(dv)+"f}" # format string
for o in objs:
if verbose:
l = o.Name+" ("+o.Label+"):"
print (l+(40-len(l))*" ",end="")
try:
d = o
for v in vals:
d = getattr(d,v)
if hasattr(d,"Value"):
d = d.Value
except:
FreeCAD.Console.PrintWarning(translate("Arch","Unable to retrieve value from object")+": "+o.Name+"."+".".join(vals)+"\n")
else:
if verbose:
if tp and unit:
v = fs.format(FreeCAD.Units.Quantity(d,tp).getValueAs(unit).Value)
print(v,unit)
else:
print(fs.format(d))
if obj.DetailedResults:
li += 1
self.data["A"+str(li)] = o.Name+" ("+o.Label+")"
if tp and unit:
q = FreeCAD.Units.Quantity(d,tp)
self.data["B"+str(li)] = str(q.getValueAs(unit).Value)
self.data["C"+str(li)] = unit
else:
self.data["B"+str(li)] = str(d)
if not sumval:
sumval = d
else:
sumval += d
val = sumval
if tp:
q = FreeCAD.Units.Quantity(val,tp)
# write data
if obj.DetailedResults:
li += 1
self.data["A"+str(li)] = "TOTAL"
if q and unit:
self.data["B"+str(li)] = str(q.getValueAs(unit).Value)
self.data["C"+str(li)] = unit
else:
self.data["B"+str(li)] = str(val)
if obj.DetailedResults:
# additional blank line...
li += 1
self.data["A"+str(li)] = " "
if verbose:
if tp and unit:
v = fs.format(FreeCAD.Units.Quantity(val,tp).getValueAs(unit).Value)
print("TOTAL:"+34*" "+v+" "+unit)
else:
v = fs.format(val)
print("TOTAL:"+34*" "+v)
self.setSpreadsheetData(obj)
def __getstate__(self):
return self.Type
def __setstate__(self,state):
if state:
self.Type = state
class _ViewProviderArchSchedule:
"A View Provider for Schedules"
def __init__(self,vobj):
vobj.Proxy = self
def getIcon(self):
import Arch_rc
return ":/icons/Arch_Schedule.svg"
def isShow(self):
return True
def attach(self, vobj):
self.Object = vobj.Object
def setEdit(self,vobj,mode=0):
if hasattr(self,"taskd"):
if self.taskd:
self.taskd.form.hide()
self.taskd = ArchScheduleTaskPanel(vobj.Object)
return True
def doubleClicked(self,vobj):
self.setEdit(vobj)
def unsetEdit(self,vobj,mode):
return True
def claimChildren(self):
if hasattr(self,"Object"):
return [self.Object.Result]
def __getstate__(self):
return None
def __setstate__(self,state):
return None
def getDisplayModes(self,vobj):
return ["Default"]
def getDefaultDisplayMode(self):
return "Default"
def setDisplayMode(self,mode):
return mode
def setupContextMenu(self,vobj,menu):
action1 = QtGui.QAction(QtGui.QIcon(":/icons/Arch_Schedule.svg"),"Attach spreadsheet",menu)
QtCore.QObject.connect(action1,QtCore.SIGNAL("triggered()"),self.attachSpreadsheet)
menu.addAction(action1)
def attachSpreadsheet(self):
if hasattr(self,"Object"):
self.Object.Proxy.setSpreadsheetData(self.Object,force=True)
class ArchScheduleTaskPanel:
'''The editmode TaskPanel for Schedules'''
def __init__(self,obj=None):
"""Sets the panel up"""
self.obj = obj
self.form = FreeCADGui.PySideUic.loadUi(":/ui/ArchSchedule.ui")
self.form.setWindowIcon(QtGui.QIcon(":/icons/Arch_Schedule.svg"))
# set icons
self.form.buttonAdd.setIcon(QtGui.QIcon(":/icons/list-add.svg"))
self.form.buttonDel.setIcon(QtGui.QIcon(":/icons/list-remove.svg"))
self.form.buttonClear.setIcon(QtGui.QIcon(":/icons/delete.svg"))
self.form.buttonImport.setIcon(QtGui.QIcon(":/icons/document-open.svg"))
self.form.buttonExport.setIcon(QtGui.QIcon(":/icons/document-save.svg"))
self.form.buttonSelect.setIcon(QtGui.QIcon(":/icons/edit-select-all.svg"))
# restore widths
p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch")
self.form.list.setColumnWidth(0,p.GetInt("ScheduleColumnWidth0",100))
self.form.list.setColumnWidth(1,p.GetInt("ScheduleColumnWidth1",100))
self.form.list.setColumnWidth(2,p.GetInt("ScheduleColumnWidth2",50))
self.form.list.setColumnWidth(3,p.GetInt("ScheduleColumnWidth3",100))
w = p.GetInt("ScheduleDialogWidth",300)
h = p.GetInt("ScheduleDialogHeight",200)
self.form.resize(w,h)
# set delegate - Not using custom delegates for now...
#self.form.list.setItemDelegate(ScheduleDelegate())
#self.form.list.setEditTriggers(QtGui.QAbstractItemView.DoubleClicked)
# connect slots
QtCore.QObject.connect(self.form.buttonAdd, QtCore.SIGNAL("clicked()"), self.add)
QtCore.QObject.connect(self.form.buttonDel, QtCore.SIGNAL("clicked()"), self.remove)
QtCore.QObject.connect(self.form.buttonClear, QtCore.SIGNAL("clicked()"), self.clear)
QtCore.QObject.connect(self.form.buttonImport, QtCore.SIGNAL("clicked()"), self.importCSV)
QtCore.QObject.connect(self.form.buttonExport, QtCore.SIGNAL("clicked()"), self.export)
QtCore.QObject.connect(self.form.buttonSelect, QtCore.SIGNAL("clicked()"), self.select)
QtCore.QObject.connect(self.form.buttonBox, QtCore.SIGNAL("accepted()"), self.accept)
QtCore.QObject.connect(self.form.buttonBox, QtCore.SIGNAL("rejected()"), self.reject)
QtCore.QObject.connect(self.form, QtCore.SIGNAL("rejected()"), self.reject)
self.form.list.clearContents()
if self.obj:
if not obj.Description:
return
for p in [obj.Value,obj.Unit,obj.Objects,obj.Filter]:
if len(obj.Description) != len(p):
return
self.form.list.setRowCount(len(obj.Description))
for i in range(5):
for j in range(len(obj.Description)):
item = QtGui.QTableWidgetItem([obj.Description,obj.Value,obj.Unit,obj.Objects,obj.Filter][i][j])
self.form.list.setItem(j,i,item)
self.form.lineEditName.setText(self.obj.Label)
self.form.checkDetailed.setChecked(self.obj.DetailedResults)
self.form.checkSpreadsheet.setChecked(self.obj.CreateSpreadsheet)
# center over FreeCAD window
mw = FreeCADGui.getMainWindow()
self.form.move(mw.frameGeometry().topLeft() + mw.rect().center() - self.form.rect().center())
# maintain above FreeCAD window
self.form.setWindowFlags(self.form.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
self.form.show()
def add(self):
"""Adds a new row below the last one"""
self.form.list.insertRow(self.form.list.currentRow()+1)
def remove(self):
"""Removes the current row"""
if self.form.list.currentRow() >= 0:
self.form.list.removeRow(self.form.list.currentRow())
def clear(self):
"""Clears the list"""
self.form.list.clearContents()
self.form.list.setRowCount(0)
def importCSV(self):
"""Imports a CSV file"""
filename = QtGui.QFileDialog.getOpenFileName(QtGui.QApplication.activeWindow(), translate("Arch","Import CSV File"), None, "CSV file (*.csv)");
if filename:
filename = filename[0]
if sys.version_info.major < 3:
filename = filename.encode("utf8")
self.form.list.clearContents()
import csv
with open(filename,'r') as csvfile:
r = 0
for row in csv.reader(csvfile):
self.form.list.insertRow(r)
for i in range(5):
if len(row) > i:
t = row[i]
#t = t.replace("²","^2")
#t = t.replace("³","^3")
self.form.list.setItem(r,i,QtGui.QTableWidgetItem(t))
r += 1
def export(self):
"""Exports the results as MD or CSV"""
# commit latest changes
self.writeValues()
# tests
if not("Up-to-date" in self.obj.State):
self.obj.Proxy.execute(self.obj)
if not hasattr(self.obj.Proxy,"data"):
return
if not self.obj.Proxy.data:
return
filename = QtGui.QFileDialog.getSaveFileName(QtGui.QApplication.activeWindow(),
translate("Arch","Export CSV File"),
None,
"Comma-separated values (*.csv);;TAB-separated values (*.tsv);;Markdown (*.md)");
if filename:
filt = filename[1]
filename = filename[0]
if sys.version_info.major < 3:
filename = filename.encode("utf8")
# add missing extension
if (not filename.lower().endswith(".csv")) and (not filename.lower().endswith(".tsv")) and (not filename.lower().endswith(".md")):
if "csv" in filt:
filename += ".csv"
elif "tsv" in filt:
filename += ".tsv"
else:
filename += ".md"
if filename.lower().endswith(".csv"):
self.exportCSV(filename,delimiter=",")
elif filename.lower().endswith(".tsv"):
self.exportCSV(filename,delimiter="\t")
elif filename.lower().endswith(".md"):
self.exportMD(filename)
else:
FreeCAD.Console.PrintError(translate("Arch","Unable to recognize that file type")+":"+filename+"\n")
def getRows(self):
"""get the rows that contain data"""
rows = []
if hasattr(self.obj.Proxy,"data") and self.obj.Proxy.data:
for key in self.obj.Proxy.data.keys():
n = key[1:]
if not n in rows:
rows.append(n)
rows.sort(key=int)
return rows
def exportCSV(self,filename,delimiter="\t"):
"""Exports the results as a CSV/TSV file"""
import csv
with open(filename, 'w') as csvfile:
csvfile = csv.writer(csvfile,delimiter=delimiter)
csvfile.writerow([translate("Arch","Description"),translate("Arch","Value"),translate("Arch","Unit")])
if self.obj.DetailedResults:
csvfile.writerow(["","",""])
for i in self.getRows():
r = []
for j in ["A","B","C"]:
if j+i in self.obj.Proxy.data:
r.append(str(self.obj.Proxy.data[j+i]))
else:
r.append("")
csvfile.writerow(r)
print("successfully exported ",filename)
def exportMD(self,filename):
"""Exports the results as a Markdown file"""
with open(filename, 'w') as mdfile:
mdfile.write("| "+translate("Arch","Description")+" | "+translate("Arch","Value")+" | "+translate("Arch","Unit")+" |\n")
mdfile.write("| --- | --- | --- |\n")
if self.obj.DetailedResults:
mdfile.write("| | | |\n")
for i in self.getRows():
r = []
for j in ["A","B","C"]:
if j+i in self.obj.Proxy.data:
r.append(str(self.obj.Proxy.data[j+i]))
else:
r.append("")
mdfile.write("| "+" | ".join(r)+" |\n")
print("successfully exported ",filename)
def select(self):
"""Adds selected objects to current row"""
if self.form.list.currentRow() >= 0:
sel = ""
for o in FreeCADGui.Selection.getSelection():
if o != self.obj:
if sel:
sel += ";"
sel += o.Name
if sel:
self.form.list.setItem(self.form.list.currentRow(),3,QtGui.QTableWidgetItem(sel))
def accept(self):
"""Saves the changes and closes the dialog"""
# store widths
p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch")
p.SetInt("ScheduleColumnWidth0",self.form.list.columnWidth(0))
p.SetInt("ScheduleColumnWidth1",self.form.list.columnWidth(1))
p.SetInt("ScheduleColumnWidth2",self.form.list.columnWidth(2))
p.SetInt("ScheduleColumnWidth3",self.form.list.columnWidth(3))
p.SetInt("ScheduleDialogWidth",self.form.width())
p.SetInt("ScheduleDialogHeight",self.form.height())
# commit values
self.writeValues()
self.form.hide()
return True
def reject(self):
"""Close dialog without saving"""
self.form.hide()
return True
def writeValues(self):
"""commits values and recalculate"""
if not self.obj:
self.obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython","Schedule")
self.obj.Label = translate("Arch","Schedule")
_ArchSchedule(self.obj)
if FreeCAD.GuiUp:
_ViewProviderArchSchedule(self.obj.ViewObject)
if hasattr(self.obj,"CreateSpreadsheet") and self.obj.CreateSpreadsheet:
import Spreadsheet
sp = FreeCAD.ActiveDocument.addObject("Spreadsheet::Sheet","Result")
self.obj.Result = sp
lists = [ [], [], [], [], [] ]
for i in range(self.form.list.rowCount()):
for j in range(5):
cell = self.form.list.item(i,j)
if cell:
lists[j].append(cell.text())
else:
lists[j].append("")
FreeCAD.ActiveDocument.openTransaction("Edited Schedule")
self.obj.Description = lists[0]
self.obj.Value = lists[1]
self.obj.Unit = lists[2]
self.obj.Objects = lists[3]
self.obj.Filter = lists[4]
self.obj.Label = self.form.lineEditName.text()
self.obj.DetailedResults = self.form.checkDetailed.isChecked()
self.obj.CreateSpreadsheet = self.form.checkSpreadsheet.isChecked()
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
FreeCADGui.addCommand('Arch_Schedule',CommandArchSchedule())