diff --git a/dockerfile b/dockerfile index 55fc24ec7664d88ac3a021bc4dc115d209798bef..d81554fa2e0b4a9834fc22404f2b6171aa36b1ca 100644 --- a/dockerfile +++ b/dockerfile @@ -13,11 +13,10 @@ ENV no_proxy="ptb.de" # Install any needed packages specified in requirements.txt RUN git clone https://dockerPull:Fu7bxBnYeyhq8Xz8seHS@gitlab1.ptb.de/digitaldynamicmeasurement/dsi-parser-frontend -RUN pip install --proxy=webproxy.bs.ptb.de:8080 --no-cache-dir -r dsi-parser-frontend/requirements.txt +#RUN pip install --proxy=webproxy.bs.ptb.de:8080 --no-cache-dir -r dsi-parser-frontend/requirements.txt -# RUN pip install --no-cache-dir -r dsi-parser-frontend/requirements.txt -#COPY . . +RUN pip install --no-cache-dir -r dsi-parser-frontend/requirements.txt EXPOSE 5020 -#WORKDIR ./pydccdisplayer/src + CMD ["bokeh", "serve", "dsi-parser-frontend/", "--port", "5020", "--allow-websocket-origin", "*","--use-xheaders","--prefix","dsi-parser-frontend"] diff --git a/issue.md b/issue.md new file mode 100644 index 0000000000000000000000000000000000000000..480b9415fff91052dccd93fbe979d1fe04b3cb86 --- /dev/null +++ b/issue.md @@ -0,0 +1,22 @@ +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* \ No newline at end of file diff --git a/main.py b/main.py index 65f783234505672450eca5c4e67fe26d54cf9a2b..4e8d65869044cd74cb74cfd01ad5667e5e3ae353 100644 --- a/main.py +++ b/main.py @@ -3,38 +3,37 @@ import io import json import tarfile import warnings +import itertools +import math import bokehCssPTB +from urllib.parse import quote from dsiParser import dsiParser from bokeh.plotting import curdoc,figure from bokeh.layouts import column, row -from bokeh.models import FileInput, Div, CustomJS, Button, TabPanel, Tabs, Dropdown, TextInput, Button, MathText, Label +from bokeh.models import FileInput, Div, CustomJS, Button, TabPanel, Tabs, Dropdown, TextInput, Button, MathText, Label, Arrow, NormalHead +from bokeh.palettes import Category10 from bokeh.events import ValueSubmit - +import numpy as np +colors=Category10[10] VERSION = "0.1.0" +class dsiparserInput(): -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.dsiInput = TextInput(value="", title="DSI unit string:", width=500) + def __init__(self,defaultInput=""): + 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.results = column(children = []) - - self.layoutColumn = column(children = [self.inputRow, self.results], css_classes = ["textlikeColumn"]) - curdoc().add_root(self.layoutColumn) + self.widget = column(children=[self.inputRow, self.results], css_classes=["textlikeColumn"]) + self.valideUnit = False def parseInput(self): self.results.children = [] input = self.dsiInput.value p = dsiParser() resultTree = p.parse(input) + parsingMessages = [] if resultTree.valid: parsingMessages.append( @@ -74,5 +73,164 @@ class page(): p ], css_classes = ["latexImageRow"]) self.results.children = [self.resultErrors, latexOutput, imageOutput] + self.dsiTree = resultTree + if resultTree.valid: + self.valideUnit = True + +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) + +class dsiCompGraphGen: + + 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=500, height=500, x_range=(-1, 1), y_range=(0, 1), tools="save") + self.plot.xaxis.visible = False + self.plot.yaxis.visible = False + self.plot.grid.visible = False + self.flush() + self.widget=row(self.plot) + + def reDraw(self,treeA=None,treeB=None): + self.treeA=treeA + self.treeB=treeB + self.scalfactorAB, self.baseUnit = self.treeA.isScalablyEqualTo(self.treeB) + self.scalfactorABase, baseUnitABase = self.treeA.isScalablyEqualTo(self.baseUnit) + + self.scalfactorBaseA, baseUnitBaseA = self.baseUnit.isScalablyEqualTo(self.treeA) + self.scalfactorBaseB, baseUnitbaseB = self.baseUnit.isScalablyEqualTo(self.treeB) + + self.scalfactorBA, baseUnitBA = self.treeB.isScalablyEqualTo(self.treeA) + self.scalfactorBBase, baseUnitBBase = self.treeB.isScalablyEqualTo(self.baseUnit) + 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="12px", 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']) + 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]), y=np.abs(y1-y2)/2+np.min([y1,y2]), text="{:.4g}".format(scale12), text_font_size="12px", 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="{:.4g}".format(scale12) + + 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']) + 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])+0.05, y=np.abs(y1-y2)/2+np.min([y1,y2])+0.05, text="{:.4g}".format(scale21), text_font_size="12px", 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="{:.4g}".format(scale21) + colIDX+=2 + +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") + self.dsiInput2 = dsiparserInput(defaultInput="\\kilo\\joule") + 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) + self.compaReresult = Div(text = "", css_classes = ["msg-positive"]) + self.compareRow = row(children = [self.comapreButton,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() + try: + scalfactor,baseUnit=self.dsiInput1.dsiTree.isScalablyEqualTo(self.dsiInput2.dsiTree) + 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) + 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.dsiCompGraphGen.flush() + 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): + 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] + templateStr=open('./issue.md').read() + filledResult=templateStr.format(*quantitiesToAdd) + 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 thisPage = page()