Newer
Older
import base64
import io
import json
import tarfile
import warnings
from dsiUnits import dsiUnit
from bokeh.models import FileInput, Div, CustomJS, Button, TabPanel, Tabs, Dropdown, TextInput, Button, MathText, Label, Arrow, NormalHead,CheckboxGroup
issueTemplate="""There was an issue with the following unit conversion:
| Which unit | User entered input | Parsed input |
| ---------- | ------------------ | ------------ |
| Left unit | {} | {} |
| Right unit | {} | {} |
### Result/Expectation:
| Calculation | Result | Expectation | Did match |
|-------------|--------|-------------|-----------|
| Base unit | {} | | [ ] |
| factor * left = right | {} | | [ ] |
| factor * right = left | {} | | [ ] |
| factor * left = base | {} | | [ ] |
| factor * base = left | {} | | [ ] |
| factor * right = base | {} | | [ ] |
| factor * base = right | {} | | [ ] |
If there was an error with the calculation, please fill out the table above. Feel free to also add additional info here:
*Free text comment*"""
labelOffsetX=[0.0,0.0,-0.02,0.07,0.02,-0.07]
labelOffsetY=[-0.02,0.09,0,0.07,0,0.07]
def __init__(self,defaultInput="",additionalComparisonCallbacks=[]):
self.additionalComparisonCallbacks=additionalComparisonCallbacks
self.dsiInput = TextInput(value=defaultInput, title="DSI unit string:", width=500)
self.dsiInput.on_event(ValueSubmit, self.parseInput)
self.dsiSubmitButton = Button(label="Convert", button_type="primary")
self.dsiSubmitButton.on_click(self.parseInput)
self.inputRow = row(children = [self.dsiInput, self.dsiSubmitButton], css_classes = ["textInputRow"])
self.widget = column(children=[self.inputRow, self.results], css_classes=["doubleColumn"])
def parseInpuitWithCallbacls(self):
self.parseInput()
for callback in self.additionalComparisonCallbacks:
callback()
self.results.children = []
input = self.dsiInput.value
resultTree = dsiUnit(input)
parsingMessages = []
if resultTree.valid:
parsingMessages.append(
Div(
text = "DSI string parsed without warnings",
css_classes = ["msg-positive"],
)
)
else:
for message in resultTree.warnings:
parsingMessages.append(
Div(
text = message,
css_classes = ["msg-negative"]
)
)
self.resultErrors = column(children = parsingMessages)
# latexOutput = TextInput(value=resultTree.toLatex(), title="DSI unit string:")
# latexOutput.js_on_change()
latexOutput = row(children=[
Div(text = "$$\mathrm{\LaTeX{}}$$ code:"),
Div(text = "<pre><code>"+resultTree.toLatex()+"</code></pre>", disable_math=True)
])
p = figure(width=500, height=100, x_range=(0, 1), y_range=(0, 1), tools="save")
p.xaxis.visible = False
p.yaxis.visible = False
p.grid.visible = False
p.add_layout(Label(
x=0.5, y=0.5, text=resultTree.toLatex(), text_font_size="30px", text_baseline="middle", text_align="center",
))
imageOutput = row(children = [
Div(text = "$$\mathrm{\LaTeX{}}$$ output:"),
p
self.results.children = [self.resultErrors, latexOutput, imageOutput]
def removeArrowsAndLabelsFromPlot(plot):
for location in ['left', 'right', 'above', 'below', 'center']:
for plotObj in [plot,plot.plot]:
for layout in plotObj.__getattr__(location):
if isinstance(layout, Arrow) or isinstance(layout, Label):
plotObj.__getattr__(location).remove(layout)
def flush(self):
# Assuming 'plot' is your Bokeh figure
renderers = self.plot.renderers
for r in renderers:
self.plot.renderers.remove(r)
removeArrowsAndLabelsFromPlot(self.plot)
removeArrowsAndLabelsFromPlot(self.plot)# only second call removes the three arrows generated last
self.widget = row(self.plot)
self.unitLabels = {}
self.arrows = {}
self.scalFactorLables = {}
def __init__(self,treeA=None,treeB=None):
self.treeA=treeA
self.treeB=treeB
self.plot = figure(width=1000, height=500, x_range=(-2, 2), y_range=(0, 1), tools="save")
self.plot.xaxis.visible = False
self.plot.yaxis.visible = False
self.plot.grid.visible = False
def reDraw(self,treeA=None,treeB=None,complete=False):
self.scalfactorAB, self.baseUnit = self.treeA.isScalablyEqualTo(self.treeB,complete=complete)
self.scalfactorABase, baseUnitABase = self.treeA.isScalablyEqualTo(self.baseUnit,complete=complete)
self.scalfactorBaseA, baseUnitBaseA = self.baseUnit.isScalablyEqualTo(self.treeA,complete=complete)
self.scalfactorBaseB, baseUnitbaseB = self.baseUnit.isScalablyEqualTo(self.treeB,complete=complete)
self.scalfactorBA, baseUnitBA = self.treeB.isScalablyEqualTo(self.treeA,complete=complete)
self.scalfactorBBase, baseUnitBBase = self.treeB.isScalablyEqualTo(self.baseUnit,complete=complete)
self.coordinatList = [{'coords':(-0.8, 0.8),'baseUnit':self.treeA,'name':'A','text_baseLine':'middle','text_align':"right"},
{'coords': (0.8, 0.8), 'baseUnit': self.treeB,'name':'B','text_baseLine':'middle','text_align':"left"},
{'coords': (0.0, 0.2), 'baseUnit': self.baseUnit,'name':'Base','text_baseLine':'top','text_align':"center"},]
for unitToDraw in self.coordinatList:
x=unitToDraw['coords'][0]
y = unitToDraw['coords'][1]
tree=unitToDraw['baseUnit']
name=unitToDraw['name']
if not name in self.unitLabels:
self.unitLabels[name]=Label(x=x, y=y, text=tree.toLatex(), text_font_size="24px", text_baseline=unitToDraw['text_baseLine'], text_align=unitToDraw['text_align'],)
self.plot.add_layout(self.unitLabels[name])
else:
self.unitLabels[name].text=tree.toLatex()
colIDX=0
for quant1, quant2 in itertools.combinations(self.coordinatList, 2):
x1 = quant1['coords'][0]
y1 = quant1['coords'][1]
name1 = quant1['name']
x2 = quant2['coords'][0]
y2 = quant2['coords'][1]
name2 = quant2['name']
if not name1+'_'+name2 in self.arrows:
nh = NormalHead(fill_color=colors[colIDX], fill_alpha=0.5, line_color=colors[colIDX])
self.arrows[name1+'_'+name2]=Arrow(end=nh, line_color=colors[colIDX], line_dash=[15, 5],x_start=x1, y_start=y1, x_end=x2, y_end=y2)
self.plot.add_layout(self.arrows[name1+'_'+name2])
scale12, baseUnit = quant1['baseUnit'].isScalablyEqualTo(quant2['baseUnit'],complete=complete)# TODO remove this un neescary reclaculation
#isSpecial, Latex = format_special_scale_factor(scale12)
isSpecial = False
Latex=""
if isSpecial:
text="{:.4g}".format(scale12)+" = "+Latex
else:
text="{:.4g}".format(scale12)
if not name1 + '_' + name2 in self.scalFactorLables:
angle_deg = -1*np.arctan2(y2 - y1, x2 - x1)
if abs(angle_deg)>np.pi/8:
angle_deg+=-np.pi/2
self.scalFactorLables[name1 + '_' + name2]=Label(x=np.abs(x1-x2)/2+np.min([x1,x2])+labelOffsetX[colIDX], y=np.abs(y1-y2)/2+np.min([y1,y2])+labelOffsetY[colIDX], text=text, text_font_size="24px", text_baseline=unitToDraw['text_baseLine'], text_align=unitToDraw['text_align'],text_color=colors[colIDX],angle=angle_deg)
self.plot.add_layout(self.scalFactorLables[name1 + '_' + name2])
else:
self.scalFactorLables[name1 + '_' + name2].text=text
if not name2 + '_' + name1 in self.arrows:
nh = NormalHead(fill_color=colors[colIDX+1], fill_alpha=0.5, line_color=colors[colIDX+1])
self.arrows[name2+'_'+name1]=Arrow(end=nh, line_color=colors[colIDX+1], line_dash=[15, 5], x_start=x2, y_start=y2+0.05, x_end=x1, y_end=y1+0.05)
self.plot.add_layout(self.arrows[name2+'_'+name1])
scale21, baseUnit = quant2['baseUnit'].isScalablyEqualTo(quant1['baseUnit'],complete=complete)
#isSpecial, Latex = format_special_scale_factor(scale21)
isSpecial=False
Latex=""
if isSpecial:
text="{:.4g}".format(scale21)+" = "+Latex
else:
text="{:.4g}".format(scale21)
if not name2 + '_' + name1 in self.scalFactorLables:
angle_deg = -1*np.arctan2(y2 - y1, x2 - x1)
if abs(angle_deg)>np.pi/8:
angle_deg+=-np.pi/2
self.scalFactorLables[name2 + '_' + name1]=Label(x=np.abs(x1-x2)/2+np.min([x1,x2])+labelOffsetX[colIDX+1], y=np.abs(y1-y2)/2+np.min([y1,y2])+labelOffsetY[colIDX+1], text=text, text_font_size="24px", text_baseline=unitToDraw['text_baseLine'], text_align=unitToDraw['text_align'],text_color=colors[colIDX+1],angle=angle_deg)
self.plot.add_layout(self.scalFactorLables[name2 + '_' + name1])
else:
self.scalFactorLables[name2+ '_' + name1].text=text
class page():
def __init__(self):
curdoc().template_variables["VERSION"] = VERSION
curdoc().title = "DSI to Latex"
curdoc().add_root(bokehCssPTB.getStyleDiv())
curdoc().theme = bokehCssPTB.getTheme()
self.dsiInput1 = dsiparserInput(defaultInput="\\milli\\newton\\metre",additionalComparisonCallbacks=[self.clearComparison,self.tryComparison])
self.dsiInput2 = dsiparserInput(defaultInput="\\kilo\\joule",additionalComparisonCallbacks=[self.clearComparison,self.tryComparison])
self.inputs=row([self.dsiInput1.widget, self.dsiInput2.widget])
curdoc().add_root(self.inputs)
self.comapreButton = Button(label="Compare", button_type="primary")
self.comapreButton.on_click(self.compare)
LABELS = ['Complete Comparison']
self.completeComCBGPR = CheckboxGroup(labels=LABELS, active=[])
self.compaReresult = Div(text = "", css_classes = ["msg-positive"],visible=False)
self.compareRow = row(children = [self.comapreButton,self.completeComCBGPR,self.compaReresult], css_classes = ["textInputRow"])
curdoc().add_root(self.compareRow)
self.dsiCompGraphGen=dsiCompGraphGen(self.dsiInput1,self.dsiInput2)
curdoc().add_root(self.dsiCompGraphGen.widget)
self.createIssueButton = Button(label="Report conversion error", disabled=True)
# self.createIssueButton.on_click(self.createIssueUrl)
curdoc().add_root(self.dsiCompGraphGen.widget)
curdoc().add_root(self.createIssueButton)
def compare(self):
self.dsiInput1.parseInput()
self.dsiInput2.parseInput()
completeConversion=self.completeComCBGPR.active==[0]
scalfactor,baseUnit=self.dsiInput1.dsiTree.isScalablyEqualTo(self.dsiInput2.dsiTree,complete=completeConversion)
if not math.isnan(scalfactor):
self.compaReresult.text = "The two units are equal up to a scaling factor of "+str(scalfactor)+" and a base unit of "+str(baseUnit)
self.compaReresult.css_classes=["msg-positive"]
else:
self.compaReresult.text = "The two units are not equal"
self.compaReresult.css_classes = ["msg-negative"]
if self.dsiInput1.valideUnit and self.dsiInput2.valideUnit:
self.dsiCompGraphGen.reDraw(self.dsiInput1.dsiTree,self.dsiInput2.dsiTree,complete=completeConversion)
else:
self.dsiCompGraphGen.flush()
except AttributeError as Ae:
warnings.warn("AttributeError: "+str(Ae))
self.compaReresult.text = "The two units are not equal"
self.compaReresult.css_classes = ["msg-negative"]
self.compaReresult.visible = True
self.createIssueButton.disabled=False
self.createIssueButton.button_type="danger"
self.createIssueButton.js_on_event("button_click",CustomJS(code=f"window.open('{self.createIssueUrl()}', '_blank');"))
def createIssueUrl(self):
issueArgs=[self.dsiInput1.dsiInput.value,str(self.dsiInput1.dsiTree),self.dsiInput2.dsiInput.value,str(self.dsiInput2.dsiTree)]
comGenAtrssFroIssue=['baseUnit', 'scalfactorAB', 'scalfactorBA', 'scalfactorABase', 'scalfactorBaseA', 'scalfactorBBase', 'scalfactorBaseB']
for comGenAtrss in comGenAtrssFroIssue:
try:
issueArgs.append(str(getattr(self.dsiCompGraphGen,comGenAtrss)))
except AttributeError as Ae:
issueArgs.append("AttributeError: "+str(Ae))
#quantitiesToAdd=[self.dsiInput1.dsiInput.value,str(self.dsiInput1.dsiTree),self.dsiInput2.dsiInput.value,str(self.dsiInput2.dsiTree),str(self.dsiCompGraphGen.baseUnit),self.dsiCompGraphGen.scalfactorAB,self.dsiCompGraphGen.scalfactorBA,self.dsiCompGraphGen.scalfactorABase,self.dsiCompGraphGen.scalfactorBaseA,self.dsiCompGraphGen.scalfactorBBase,self.dsiCompGraphGen.scalfactorBaseB]
#issueTemplate=open('./issue.md').read() #TODO add file inculde instead of the str....
filledTitle = f'Unexpected comparison result: {self.dsiInput1.dsiInput.value} to {self.dsiInput2.dsiInput.value}'
issueUrl = r'https://gitlab1.ptb.de/digitaldynamicmeasurement/dsi-parser-frontend/-/issues/new?'
title = quote(filledTitle)
body = quote(filledResult)
url = issueUrl + 'issue[title]=' + title + '&issue[description]=' + body
return url
def clearComparison(self):
self.dsiCompGraphGen.flush()
self.compaReresult.text = ""
self.createIssueButton.disabled=True
self.createIssueButton.button_type="primary"
self.compaReresult.visible = False
def tryComparison(self):
if self.dsiInput1.valideUnit and self.dsiInput2.valideUnit:
self.compare()