Newer
Older
# This file is part of dsiUnitsFrontend (https://gitlab1.ptb.de/digitaldynamicmeasurement/dsi-parser-frontend)
# Copyright 2024 [Benedikt Seeger(PTB), Vanessa Stehr(PTB)]
#This library is free software; you can redistribute it and/or
#modify it under the terms of the GNU Lesser General Public
#License as published by the Free Software Foundation; either
#version 2.1 of the License, or (at your option) any later version.
#This library 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
#Lesser General Public License for more details.
#You should have received a copy of the GNU Lesser General Public
#License along with this library; if not, write to the Free Software
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from dsiUnits import dsiUnit
from bokeh.plotting import curdoc, figure
from bokeh.layouts import column, row, GridBox
from bokeh.models import FileInput, Div, CustomJS, Button, TabPanel, Tabs, TextInput, Label, Arrow, NormalHead, CheckboxGroup
from base64 import b64decode
import XMLUnitExtractor
colors = Category10[10]
import socket
def get_local_ip():
try:
# Creates a socket connection to Google's public DNS server to determine the internal network IP
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("8.8.8.8", 80)) # Using Google DNS, and connecting via dummy request
local_ip = s.getsockname()[0] # Returns the IP where the socket is connected
return local_ip
except Exception as e:
return f"Unable to get IP: {e}"
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]
class dsiparserInput():
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.results = column(children=[])
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)
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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)
isSpecial = False
if isSpecial:
text = "{:.4g}".format(scale12) + " = " + Latex
else:
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
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 = False
Latex = ""
if isSpecial:
text = "{:.4g}".format(scale21) + " = " + Latex
else:
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
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()
# Tab 1: DSI Parser and Comparator
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])
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"])
self.dsiCompGraphGen = dsiCompGraphGen(self.dsiInput1, self.dsiInput2)
self.createIssueButton = Button(label="Report conversion error", disabled=True)
tab1_layout = column(self.inputs, self.compareRow, self.dsiCompGraphGen.widget, self.createIssueButton)
tab1 = TabPanel(child=tab1_layout, title="DSI Parser and Comparator")
# Tab 2: XML Unit Validator
self.title = Div(text="<h4>Please upload DCC or other XML containing D-SI Unit Strings here</h4>")
self.upload_widget = FileInput(accept=".xml")
self.upload_widget.on_change('value', self.process_xml)
self.valid_units_message = Div(text="Upload an XML file to validate units", css_classes=["msg-neutral"])
self.invalid_units_message = Div(text="")
self.invalid_units_list = GridBox()
tab2_layout = column(self.title, self.upload_widget, self.valid_units_message, self.invalid_units_message, self.invalid_units_list)
tab2 = TabPanel(child=tab2_layout, title="XML Unit Validator")
# Handle URL parameter active tab
try:
active_tab = int(curdoc().session_context.request.arguments["active_tab"][0])
except:
active_tab = 0
if active_tab >= len(tabs.tabs):
active_tab = 0
tabs.active=active_tab
curdoc().add_root(tabs)
def process_xml(self, attr, old, new):
decoded = b64decode(new).decode('utf-8')
result = XMLUnitExtractor.parse_and_process(decoded)
valid_units = result['valid_units']
invalid_units = result['invalid_units']
# Update valid units message
if valid_units:
self.valid_units_message.text = f"{len(valid_units)} valid units found"
self.valid_units_message.css_classes = ["msg-positive"]
else:
self.valid_units_message.text = "No valid units found"
self.valid_units_message.css_classes = ["msg-negative"]
# Update invalid units message
if invalid_units:
self.invalid_units_message.text = f"{len(invalid_units)} invalid units found"
self.invalid_units_message.css_classes = ["msg-negative"]
else:
self.invalid_units_message.text = ""
self.invalid_units_message.css_classes = [""]
# Update invalid units content
self.invalid_units_list.children = []
for row_num, (line_num, details) in enumerate(invalid_units.items()):
warnings_div = column([Div(text=warning, css_classes=["msg-negative"]) for warning in details.get("warnings", [])])
unit_row = [
(Div(text="", css_classes=["divider-hline"]), 2*row_num, 0, 1, 4),
(Div(text=str(line_num), height_policy="min", css_classes=["msg-neutral"]), 2*row_num+1, 0),
(Div(text=details["unit"], css_classes=["msg-raw"]), 2*row_num+1, 1),
(Div(text=f"{dsiUnit(details['unit']).toLatex()}", width=150), 2*row_num+1, 2),
(warnings_div, 2*row_num+1, 3),
]
self.invalid_units_list.children += unit_row
self.invalid_units_list.children += [(Div(text="", css_classes=["divider-hline"]), 2*len(invalid_units.items()), 0, 1, 4),]
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)
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"
try:
#try to update existing callback if callback cant be updatet crate the callback since it wasent initialized
self.createIssueButton.js_event_callbacks['button_click'][0].update(code=f"window.open('{self.createIssueUrl()}', '_blank');")
except:
self.createIssueButton.js_on_event("button_click", CustomJS(code=f"window.open('{self.createIssueUrl()}', '_blank');"))
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']
issueArgs.append(str(getattr(self.dsiCompGraphGen, comGenAtrss)))
issueArgs.append("AttributeError: " + str(Ae))
filledResult = issueTemplate.format(*issueArgs)
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()