From 79555f9473fc9b6d27865a00ca3854304b5ca1f8 Mon Sep 17 00:00:00 2001 From: MichalO Date: Tue, 22 Feb 2022 12:49:04 +0100 Subject: [PATCH 01/14] initial commit --- RFEM/Reports/html.py | 330 +++++++++++++++++++++++++++++++++++ RFEM/initModel.py | 19 +- UnitTests/test_htmlReport.py | 14 ++ 3 files changed, 352 insertions(+), 11 deletions(-) create mode 100644 RFEM/Reports/html.py create mode 100644 UnitTests/test_htmlReport.py diff --git a/RFEM/Reports/html.py b/RFEM/Reports/html.py new file mode 100644 index 00000000..db91675e --- /dev/null +++ b/RFEM/Reports/html.py @@ -0,0 +1,330 @@ +################################# +## DEFINITION: +## This feature allows the user to create an output HTML file with the results. +## The results are in the same form as result tables in RFEM. +## The output file is written in HTML and consists of embedded tabular data. +## It will also include name of the model, 3 drop down menus as in RFEM, +## and data tables will be stuctured into tabs as in RFEM. +################################# +from fileinput import filename +from os import listdir, walk, path +from RFEM.initModel import ExportResultTablesToCsv + +columns = 0 + +def __HTMLheadAndHeader(modelName, category, subCategory, currentCaseOrCombination): + output = ['', + '', + 'Results', + '', + '', + '
', + '', + '
', + 'Dlubal logo', + '

Results report

', + f'

Model: {modelName}

', + '
', + '
', + '', + '', + '', + '|', + '', + '', + '', + '|', + '', + '', + '', + '
', + '
', + '', + '
', + ''] + return output + +def __HTMLfooter(sTabs): + output = ['
'] + for i in range(len(sTabs)): + output += f'', + output += ['
', + '', + ''] + return output + +def __isEmpty(dividedLine): + # returns True if all strings are empty + return not any (dividedLine) + +def __numberOfPOsitiveItems(dividedLine): + # return number of list items which are not empty + counter = 0 + for i in dividedLine: + if i: + counter += 1 + + return counter + +def __tableHeader(dividedLine_1, dividedLine_2): + # define htnl of header lines, rowspan and colspan + # parameters are lists of strings + global columns + columns = max(len(dividedLine_1), len(dividedLine_2)) + output = ['
', + '', + '', + ''] + if __isEmpty(dividedLine_1): + __emptyLine() + else: + for i in range(columns): + if dividedLine_1[i] and not dividedLine_2[i]: + output.append(f'') + elif not dividedLine_1[i] and not dividedLine_2[i]: + output.append(f'') + elif dividedLine_1[i] and dividedLine_2[i]: + colspan = 1 + for ii in range(i+1, columns): + if not dividedLine_1[ii] and ('Comment' not in dividedLine_2[ii]) and dividedLine_2[ii]: + colspan += 1 + else: + break + output.append(f'') + i += colspan-1 + output += ['', ''] + + for y in range(columns): + if not dividedLine_1[y] and not dividedLine_2[y]: + output.append('') + elif dividedLine_1[y] and not dividedLine_2[y]: + pass + else: + output.append(f'') + + output += ['', ''] + return output + +def __tableSubHeader(dividedLine): + # sub header; one liner; in the body of table + global columns + return f'' + +def __emptyLine(): + # define html of empty lines + global columns + return f'' + +def __otherLines(dividedLine): + # define html of other lines + global columns + output = [] + for c in range(columns): + sCheckIfDigit = dividedLine[c].replace('.','',1) + sCheckIfDigit = sCheckIfDigit.replace(',','',1) + sCheckIfDigit = sCheckIfDigit.replace('-','',1) + sCheckIfDigit = sCheckIfDigit.replace('+','',1) + align = 'left' + tag = 'td' + if c == 0: + align = 'center' + elif sCheckIfDigit.isdigit(): + align = 'right' + else: + if '' in dividedLine[c] or '[' in dividedLine[c]: + tag = 'th' + align = 'center' + output.append(f'<{tag} align="{align}">{dividedLine[c]}') + return output + +def ExportResultTablesToHtml(TargetFolderPath: str): + # Run ExportResultTablesToCsv() to create collection of source files + #ExportResultTablesToCsv(TargetFolderPath) + + modelName = next(walk(TargetFolderPath))[1][0] + dirList = listdir(path.join(TargetFolderPath, modelName)) + # Parse CSV file names into LC and CO, analysis type, types of object (nodes, lines, members, surfaces), + # and tabs (such as summary, Global Deformations, or Support Forces) + + # Sets of all catebories (not duplicates) + # Source for 3 drop down menus + sCurrentCaseOrCombinations = set() + sCategories = set() + sSubcategories = set() + sTabs = [] + + dirlist = dirList.sort() + + output = ['
', + ''] + print('') + for fileName in dirList: + print(fileName) + cats = fileName[:-4].split('_') + currentCaseOrCombination = '' + subcategory = '' + category = '' + tab = '' + + if cats[1] == 'design': + category = str(cats[0]).capitalize()+' '+str(cats[1]).capitalize() + if cats[2]=='errors' or cats[2]+cats[3]=='notvalid': + subcategory = 'Overview' + for i in range(2, len(cats)): + tab += str(cats[i]+' ').capitalize() + tab = tab[:-1] + elif cats[2]=='design': + subcategory = str('Design Ratios on'+str(cats[5]).capitalize()) + tab = 'Design Ratios by' + for i in range(7, len(cats)): + tab += str(cats[i]+' ').capitalize() + tab = tab[:-1] + elif cats[3]=='reinforcement': + subcategory = 'Reinforcement on '+str(cats[5]).capitalize + tab = str(cats[2]).capitalize+' Reinforcement by' + for i in range(7, len(cats)): + tab += str(cats[i]+' ').capitalize() + tab = tab[:-1] + elif cats[4]=='reinforcement': + subcategory = 'Reinforcement on '+str(cats[6]).capitalize + tab = 'Not Covered Reinforcement by' + for i in range(8, len(cats)): + tab += str(cats[i]+' ').capitalize() + tab = tab[:-1] + elif cats[2]=='governing': + subcategory = 'Governing Results' + else: + assert True, filename + + if cats[0] == 'stress' and cats[1] == 'analysis': + category = 'Stress-Strain Analysis' + if cats[2]=='errors': + subcategory = 'Overview' + elif cats[2]=='': + subcategory = 'Design Rations on Members' + elif cats[2]=='': + subcategory = 'Reinforcement on Members' + elif cats[2]=='': + subcategory = 'Design Rations on Surfaces' + elif cats[2]=='': + subcategory = 'Reinforcement on Surfaces' + elif cats[2]=='': + subcategory = 'Governing Results' + else: + assert True, filename + + else: + currentCaseOrCombination = cats[0] + category = str(cats[1]).capitalize()+' '+str(cats[2]).capitalize() + + if cats[3]=='summary': + subcategory = 'Overview' + elif cats[3]=='nodes': + subcategory = 'Results by Node' + elif cats[3]=='lines': + subcategory = 'Results by Lines' + elif cats[3]=='surfaces': + subcategory = 'Results by Surfaces' + elif cats[3]=='members': + subcategory = 'Results by Members' + elif cats[3]=='solids': + subcategory = 'Results by Solids' + else: + assert True, filename + if not tab: + if cats[3]=='summary': + tab = cats[3].capitalize() + else: + for i in range(4, len(cats)): + tab += str(cats[i]+' ').capitalize() + tab = tab[:-1] + + sCategories.add(category) + sCurrentCaseOrCombinations.add(currentCaseOrCombination) + sSubcategories.add(subcategory) + sTabs.append(tab) + + with open(path.join(TargetFolderPath, modelName, fileName), mode='r', encoding='utf-8-sig') as f: + lines = f.readlines() + # 1st header always consists of first 2 lines + line_1 = lines[0].split(';') + line_1[-1] = line_1[-1].rstrip('\n') + line_2 = lines[1].split(';') + line_2[-1] = line_2[-1].rstrip('\n') + subOutput = __tableHeader(line_1, line_2) + output += subOutput + output.append('
') + + for line in lines[2:]: + output.append('') + dividedLine = line.split(';') + dividedLine[-1] = dividedLine[-1].rstrip('\n') + + # check if number of columns is always same + assert columns-len(dividedLine) == 0 + + if __isEmpty(dividedLine): + # if empty line + output.append(__emptyLine()) + elif __numberOfPOsitiveItems(dividedLine) == 1 and dividedLine[1]: + # if only one string in the list, it is sub header consisting of only 1 line + output.append(__tableSubHeader(dividedLine[1])) + else: + subOutput = __otherLines(dividedLine) + output += subOutput + + output += ['', '', '
{dividedLine_1[i]}{dividedLine_1[i]}
{dividedLine_2[y]}
{dividedLine}
', '
'] + + output += __HTMLfooter(sTabs) + output = __HTMLheadAndHeader(modelName, sCategories, sSubcategories, sCurrentCaseOrCombinations) + output + + # Write into html file + # Add lower index + with open('D:\\Sources\\sub_index.html', "w", encoding="utf-8") as f: + for line in output: + while '_' in line: + beginId = line.find('_') + endBySpace = line[beginId:].find(' ') + endByArrow = line.rfind('<') + if endBySpace == -1: + line = line[:beginId]+''+line[beginId+1:endByArrow]+''+line[endByArrow:] + else: + endId = min(endBySpace + beginId, endByArrow) + line = line[:beginId]+''+line[beginId+1:endId]+''+line[endId:] + while '^' in line: + beginId = line.find('^') + endBySpace = line[beginId:].find(' ') + endByArrow = line.rfind('<') + if endBySpace == -1: + line = line[:beginId]+''+line[beginId+1:endByArrow]+''+line[endByArrow:] + else: + endId = min(endBySpace + beginId, endByArrow) + line = line[:beginId]+''+line[beginId+1:endId]+''+line[endId:] + f.write(line+'\n') \ No newline at end of file diff --git a/RFEM/initModel.py b/RFEM/initModel.py index b14670ee..ac3a5395 100644 --- a/RFEM/initModel.py +++ b/RFEM/initModel.py @@ -403,26 +403,23 @@ def ExportResultTablesWithDetailedMembersResultsToXML(TargetFilePath: str): Model.clientModel.service.export_result_tables_with_detailed_members_results_to_xml(TargetFilePath) -def __parseXMLAsDictionary(path: str =""): - with open(path, "rb") as f: - my_dictionary = xmltodict.parse(f, xml_attribs=True) - return my_dictionary +def ParseCSVResultsFromSelectedFileToDict(filePath: str): -def __parseCSVAsDictionary(path: str =""): - with open(path, mode='r') as f: + # Using encoding parameter ensures proper data translation, leaving out BOM etc. + # TODO: fix the value assigment; it only works with simple one-line header + # consider all corner cases + with open(filePath, mode='r', encoding='utf-8-sig') as f: reader = csv.DictReader(f,delimiter=';') my_dictionary = [] for line in reader: my_dictionary.append(line) return my_dictionary -def ParseCSVResultsFromSelectedFileToDict(filePath: str): - - return __parseCSVAsDictionary(filePath) - def ParseXMLResultsFromSelectedFileToDict(filePath: str): - return __parseXMLAsDictionary(filePath) + with open(filePath, "rb") as f: + my_dictionary = xmltodict.parse(f, xml_attribs=True) + return my_dictionary def GenerateMesh(): diff --git a/UnitTests/test_htmlReport.py b/UnitTests/test_htmlReport.py new file mode 100644 index 00000000..44c69abd --- /dev/null +++ b/UnitTests/test_htmlReport.py @@ -0,0 +1,14 @@ +import sys +import os +PROJECT_ROOT = os.path.abspath(os.path.join( + os.path.dirname(__file__), + os.pardir) +) +sys.path.append(PROJECT_ROOT) +from RFEM.Reports.html import ExportResultTablesToHtml + +#if Model.clientModel is None: +# Model() + +def test_html_report(): + ExportResultTablesToHtml('d:\\Sources\\results') \ No newline at end of file From 5171a52b7f3797b64c5d4fe060c2c7c911d4c31f Mon Sep 17 00:00:00 2001 From: MichalO Date: Mon, 7 Mar 2022 07:59:19 +0100 Subject: [PATCH 02/14] updated generation of html document --- RFEM/Reports/html.py | 177 ++++++++----------------------------------- 1 file changed, 33 insertions(+), 144 deletions(-) diff --git a/RFEM/Reports/html.py b/RFEM/Reports/html.py index db91675e..69d9f625 100644 --- a/RFEM/Reports/html.py +++ b/RFEM/Reports/html.py @@ -3,17 +3,19 @@ ## This feature allows the user to create an output HTML file with the results. ## The results are in the same form as result tables in RFEM. ## The output file is written in HTML and consists of embedded tabular data. -## It will also include name of the model, 3 drop down menus as in RFEM, -## and data tables will be stuctured into tabs as in RFEM. +## It will also include dropdown menu with all tables/files. +## Result files are language dependent, so parsing based on strings is impossible. ################################# from fileinput import filename from os import listdir, walk, path from RFEM.initModel import ExportResultTablesToCsv +from re import findall columns = 0 -def __HTMLheadAndHeader(modelName, category, subCategory, currentCaseOrCombination): +def __HTMLheadAndHeader(modelName, fileNames): output = ['', + '', '', 'Results', '', @@ -26,59 +28,23 @@ def __HTMLheadAndHeader(modelName, category, subCategory, currentCaseOrCombinati f'

Model: {modelName}

', '', '
', - '', - '', - '', - '|', - '', - '', - '', - '|', - '', - '', - '', + '', + ''] + for f in fileNames: + output.append(f'') + output += ['', + '', + '', '
', '', '', - '
', + '', ''] return output -def __HTMLfooter(sTabs): - output = ['
'] - for i in range(len(sTabs)): - output += f'', - output += ['
', - '', - ''] - return output +def __HTMLfooter(): + return ['', + ''] def __isEmpty(dividedLine): # returns True if all strings are empty @@ -98,7 +64,8 @@ def __tableHeader(dividedLine_1, dividedLine_2): # parameters are lists of strings global columns columns = max(len(dividedLine_1), len(dividedLine_2)) - output = ['
', + output = ['', + '
', '', '', ''] @@ -109,7 +76,7 @@ def __tableHeader(dividedLine_1, dividedLine_2): if dividedLine_1[i] and not dividedLine_2[i]: output.append(f'') elif not dividedLine_1[i] and not dividedLine_2[i]: - output.append(f'') + output.append('') elif dividedLine_1[i] and dividedLine_2[i]: colspan = 1 for ii in range(i+1, columns): @@ -173,103 +140,25 @@ def ExportResultTablesToHtml(TargetFolderPath: str): # Parse CSV file names into LC and CO, analysis type, types of object (nodes, lines, members, surfaces), # and tabs (such as summary, Global Deformations, or Support Forces) - # Sets of all catebories (not duplicates) - # Source for 3 drop down menus - sCurrentCaseOrCombinations = set() - sCategories = set() - sSubcategories = set() - sTabs = [] + fileNames = [] dirlist = dirList.sort() - output = ['
', - ''] + output = ['
'] print('') + for fileName in dirList: print(fileName) cats = fileName[:-4].split('_') - currentCaseOrCombination = '' - subcategory = '' - category = '' - tab = '' - - if cats[1] == 'design': - category = str(cats[0]).capitalize()+' '+str(cats[1]).capitalize() - if cats[2]=='errors' or cats[2]+cats[3]=='notvalid': - subcategory = 'Overview' - for i in range(2, len(cats)): - tab += str(cats[i]+' ').capitalize() - tab = tab[:-1] - elif cats[2]=='design': - subcategory = str('Design Ratios on'+str(cats[5]).capitalize()) - tab = 'Design Ratios by' - for i in range(7, len(cats)): - tab += str(cats[i]+' ').capitalize() - tab = tab[:-1] - elif cats[3]=='reinforcement': - subcategory = 'Reinforcement on '+str(cats[5]).capitalize - tab = str(cats[2]).capitalize+' Reinforcement by' - for i in range(7, len(cats)): - tab += str(cats[i]+' ').capitalize() - tab = tab[:-1] - elif cats[4]=='reinforcement': - subcategory = 'Reinforcement on '+str(cats[6]).capitalize - tab = 'Not Covered Reinforcement by' - for i in range(8, len(cats)): - tab += str(cats[i]+' ').capitalize() - tab = tab[:-1] - elif cats[2]=='governing': - subcategory = 'Governing Results' - else: - assert True, filename - if cats[0] == 'stress' and cats[1] == 'analysis': - category = 'Stress-Strain Analysis' - if cats[2]=='errors': - subcategory = 'Overview' - elif cats[2]=='': - subcategory = 'Design Rations on Members' - elif cats[2]=='': - subcategory = 'Reinforcement on Members' - elif cats[2]=='': - subcategory = 'Design Rations on Surfaces' - elif cats[2]=='': - subcategory = 'Reinforcement on Surfaces' - elif cats[2]=='': - subcategory = 'Governing Results' + fileNameCapitalized = '' + for c in cats: + if findall('[0-9]+', c): + fileNameCapitalized += c+' ' else: - assert True, filename - - else: - currentCaseOrCombination = cats[0] - category = str(cats[1]).capitalize()+' '+str(cats[2]).capitalize() - - if cats[3]=='summary': - subcategory = 'Overview' - elif cats[3]=='nodes': - subcategory = 'Results by Node' - elif cats[3]=='lines': - subcategory = 'Results by Lines' - elif cats[3]=='surfaces': - subcategory = 'Results by Surfaces' - elif cats[3]=='members': - subcategory = 'Results by Members' - elif cats[3]=='solids': - subcategory = 'Results by Solids' - else: - assert True, filename - if not tab: - if cats[3]=='summary': - tab = cats[3].capitalize() - else: - for i in range(4, len(cats)): - tab += str(cats[i]+' ').capitalize() - tab = tab[:-1] - - sCategories.add(category) - sCurrentCaseOrCombinations.add(currentCaseOrCombination) - sSubcategories.add(subcategory) - sTabs.append(tab) + fileNameCapitalized += c.capitalize()+' ' + fileNameCapitalized = fileNameCapitalized[:-1] + fileNames.append(fileNameCapitalized) with open(path.join(TargetFolderPath, modelName, fileName), mode='r', encoding='utf-8-sig') as f: lines = f.readlines() @@ -288,7 +177,7 @@ def ExportResultTablesToHtml(TargetFolderPath: str): dividedLine[-1] = dividedLine[-1].rstrip('\n') # check if number of columns is always same - assert columns-len(dividedLine) == 0 + #assert columns-len(dividedLine) == 0 if __isEmpty(dividedLine): # if empty line @@ -302,8 +191,8 @@ def ExportResultTablesToHtml(TargetFolderPath: str): output += ['
', '', '
{dividedLine_1[i]}
', '
'] - output += __HTMLfooter(sTabs) - output = __HTMLheadAndHeader(modelName, sCategories, sSubcategories, sCurrentCaseOrCombinations) + output + output += __HTMLfooter() + output = __HTMLheadAndHeader(modelName, fileNames) + output # Write into html file # Add lower index @@ -327,4 +216,4 @@ def ExportResultTablesToHtml(TargetFolderPath: str): else: endId = min(endBySpace + beginId, endByArrow) line = line[:beginId]+''+line[beginId+1:endId]+''+line[endId:] - f.write(line+'\n') \ No newline at end of file + f.write(line+'\n') From aeb1d8f3bf13879152e0bf1d39131c4401f1d85f Mon Sep 17 00:00:00 2001 From: MichalO Date: Mon, 7 Mar 2022 10:12:44 +0100 Subject: [PATCH 03/14] update --- RFEM/Reports/html.py | 56 +++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/RFEM/Reports/html.py b/RFEM/Reports/html.py index 69d9f625..6a2c3c53 100644 --- a/RFEM/Reports/html.py +++ b/RFEM/Reports/html.py @@ -9,7 +9,7 @@ from fileinput import filename from os import listdir, walk, path from RFEM.initModel import ExportResultTablesToCsv -from re import findall +from re import findall, match columns = 0 @@ -59,6 +59,18 @@ def __numberOfPOsitiveItems(dividedLine): return counter +def __isWords(word): + isWords = False + dividedWords = word.split(' ') + + for oneWord in dividedWords: + if oneWord and bool(match(r"[A-Z]", oneWord[0])) and oneWord.isalpha(): + isWords = True + else: + break + + return isWords + def __tableHeader(dividedLine_1, dividedLine_2): # define htnl of header lines, rowspan and colspan # parameters are lists of strings @@ -80,7 +92,9 @@ def __tableHeader(dividedLine_1, dividedLine_2): elif dividedLine_1[i] and dividedLine_2[i]: colspan = 1 for ii in range(i+1, columns): - if not dividedLine_1[ii] and ('Comment' not in dividedLine_2[ii]) and dividedLine_2[ii]: + if ii == columns-1 and not dividedLine_1[ii] and __isWords(dividedLine_2[ii]): + break + elif not dividedLine_1[ii] and dividedLine_2[ii]: colspan += 1 else: break @@ -112,23 +126,33 @@ def __emptyLine(): def __otherLines(dividedLine): # define html of other lines global columns + colspan = 1 output = [] for c in range(columns): - sCheckIfDigit = dividedLine[c].replace('.','',1) - sCheckIfDigit = sCheckIfDigit.replace(',','',1) - sCheckIfDigit = sCheckIfDigit.replace('-','',1) - sCheckIfDigit = sCheckIfDigit.replace('+','',1) - align = 'left' - tag = 'td' - if c == 0: - align = 'center' - elif sCheckIfDigit.isdigit(): - align = 'right' - else: - if '' in dividedLine[c] or '[' in dividedLine[c]: - tag = 'th' + if colspan == 1: + sCheckIfDigit = dividedLine[c].replace('.','',1) + sCheckIfDigit = sCheckIfDigit.replace(',','',1) + sCheckIfDigit = sCheckIfDigit.replace('-','',1) + sCheckIfDigit = sCheckIfDigit.replace('+','',1) + align = 'left' + tag = 'td' + if c == 0: align = 'center' - output.append(f'<{tag} align="{align}">{dividedLine[c]}') + elif sCheckIfDigit.isdigit(): + align = 'right' + elif bool(match(r"^\D+\.$", dividedLine[c])) or "|" in dividedLine[c]: + for cc in range(c+1, columns-1): + if not dividedLine[cc]: + colspan += 1 + else: + break + else: + if '' in dividedLine[c] or '[' in dividedLine[c]: + tag = 'th' + align = 'center' + output.append(f'<{tag} colspan="{colspan}"align="{align}">{dividedLine[c]}') + else: + colspan -= 1 return output def ExportResultTablesToHtml(TargetFolderPath: str): From 4d77a40cde8370b11837f922bcc2c9a35968fab5 Mon Sep 17 00:00:00 2001 From: MichalO Date: Mon, 7 Mar 2022 15:05:01 +0100 Subject: [PATCH 04/14] test added --- RFEM/Reports/favicon32.png | Bin 0 -> 32882 bytes RFEM/Reports/html.py | 19 +++++--- RFEM/Reports/htmlScript.js | 69 ++++++++++++++++++++++++++ RFEM/Reports/htmlStyles.css | 91 +++++++++++++++++++++++++++++++++++ UnitTests/test_htmlReport.py | 10 +++- 5 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 RFEM/Reports/favicon32.png create mode 100644 RFEM/Reports/htmlScript.js create mode 100644 RFEM/Reports/htmlStyles.css diff --git a/RFEM/Reports/favicon32.png b/RFEM/Reports/favicon32.png new file mode 100644 index 0000000000000000000000000000000000000000..0e560f7af13b4fbd2b9137f944c66abc03472b56 GIT binary patch literal 32882 zcmZU4byQnH_ib=1QnbaTSn)#f;7*a^4#nNwDeex%-QC@b7k5u_cL)&VrQh#;@2$7q zA1snPXU?45K$n&hQvv{Bt^fd77Gy-|E1aWQdT0ZRy_BXC z0KiQ3?+?bkNYE7kAO}c`39Gngo@ODn&a2>rAhq6p7Vp)AnISvss*rmB9){jfw2*4K zY&Ij`KSD(~2_v6eJc3-R=I2iiJcOi}Ta34F=jZKi7x!|G38&y|;>mLwuC(3V@lPdx zq+1cNQ%Fra8F|P3%^pSUJWqYazm%5FE07ir ze*{r+gUp-VkaD74JZ-l!N-?t0`G0HLX4zZ~>vyu81-@4o6GN(TqnB*311gAUAWiWx zPp%+#xe}szW(UO~)zmizQn(I!!orgB+wn7*5g4ts&a|TL!{!9Nn|bcq3T&~oKqB{P zR2QxfY*`8L3fqVO#N*yU+L|Gy;C}!*^99*b^1z^+3$iYLazX*af4YpQm|z{T?&pC~ zx-RaL`h}k3U*HYvh@e${qMd~>`q4UX4pZQ8@8Thl-~m4KFHYPF7~IP3PLtR}HX!ie zP@wg_j?(SKECz#L=Q^u)>CWwuNa@F2^?fsI+TApZG>uhUQ(G|Htl|1W_FSugueezGvDEMpsr2O} zC-ja>*HgZTqYUnXQal3Jc}kZnbUpjj>EOI)VAHNO$mdeq@0EQ!x<~#QlY65xxVri_5qbpDfjcT1otdph$e9t_gi_>g;zn0C;3M27C1RTy`g? zsD*B1s@>r#kA`}-V5;!pNHK4YlQO#V$-Q>!*1sHuUlVOAoFLAou@lfa?cc7tfmIuQ zhJDL*s<^VA!NfK?3;GoZix5<0-^-K5iz589A@dxcn*2F$rCVRE`Km$F^$|5TBKE@p z9$!DOz~OhJo|2RavTzUmZZVfjG*h>H?d#3mM`m=aJY( zzSO%{?1errvQ}~?);?s{qvkX2$qRIr%${i6?%gGKzoomup`}X$F4_x|*F`k29aV1P z1?HHYyJ=rh_9=EmWMJ$GhID}ab05-4@X=u)=E_TwI^9lV%rLV6O4HAdU6WtSKa6bH z2$<*Z^A@G;tK5TR-+W#lQvRx?35mU&>H1xA=J+!_vvu0y<5QQ%<5vrwj?o&h`tj#= zsM27~eIQywly^+&H|6Tfw+5$3z0u`XBsQ3b;$V&U_UHO;y1?q~#_kks1jK*f%!mHc zbDZwmlB;365OG3Zf(|zG#u9VEtk+>=cB+~11^M)1FV+_}al*9QuA9R!aLp&esRTqh zINyIYgl%B3|7tNh#xeaVJ)@*|P>EgQoXP1(IEnPs@|O?er)^Dxw><(k-#A&sI_B$u`&siiwBEvaFBIw=RYF>UA9j?p5eC<ku!i(IY9kyBX>NU4S{2Oaa6V&q~x#jYgA&L{5)Ud7&T zdaqZMq|vJlY~cUoBKsXYsc=#UDGht<9SCGp)S-~N)2C2*eeb$YX@Y7ac@?ZYw60x;Rz%_EV@@kh>5Uo2s)g6$4` zvNW!js&$vDqssII+UJ}y4o%mKRr4YIgUAiW#! zMJdCzu;n=t>=&V}mLQF&fG{HU~2DOkmEJDcEF9cFe(f8p)e=es|y!jg7xG&wQ zY^f51d_b;*p`;kC^E=V4{Fh;dvQL|}-B~?p?RS~`Zdp?Kablhqae3?D&BP20G6C@g zOK|L=T~~PfZ>uH7a_YVquI^Gj*Bi(+yI~1>Z`uuH-H7E&QfNlPOoz(WPcWk6Xcx>Yd=DLTZNglXF?Yt2U4M zjMe$NW|>Z!w{(g}&*>qaLy( z`ZWcfF0vLmIeyKQT{C$p_m3Te+GS5hn8UW820xgJ!x|oR^H?Qpr7*tmfS&T0XTGJ) z&a5;L0FiWCRcOzA$y?v#Yv8~%+i+mup%9X4lP8(hldjvDQ*Fecs;n3FBQq6EMCzFc zkvbS$`L@7yQnsl~P4#sl^-~jmdv~FGjnnh*mE8A|KW#3?7OX~7z~^ttDS{h^1GT4j z!7MkQY$YEjCO)0ty-+Y?WXeL^wzi1L?`u&d={ht1RNu)z@cwnyOD;Q0avUf(QJRK9=zh_GSp+oVo z7iOUnVNC?D30*`>+>MNd=zLnTM-o7p9Vqe7mT0^GacJ$rCRouBfa&g++MU09%=z)J z>$j|wsiabJ&tBSF-AT`29X3znu{&CwnJMCElg)2!ibd&96u~r7nI||flufPlAiED4 z8?0T^kAv5xlPDR!C#R70oNiEikQK(go@H$z@95Sj2Iq zu@83xH!Fwj&e3|m5g4=p++qe#h}E~z41(h!sCes`!8QWpy2o1s!8t7RV>1Sj2VabR zUv(bhz5X7}DRe&i-#N;od!JLk>pav=nMh4WT_31uIr22kQyZ|@nadq6_^YyVXt33C zR`*Yl%2kk~Zfs_T(Ma&2Zor5U<`D;rQ7m?v50W5RN;A|AgOb@y-j4Xf!Vp>Qn1aVSA*a z|8tQ3X^6)5VV!lgjlto#>nDP35xst=Wo7^;xniGo^)!{LZK+1-rfr{oC2|SQToSy5 zQ99cPn;yBmp&~^ZlEO&qeZPVr?Dh@i}6*MnRFZNB>bn_nnk4^VSVK^ zrAOzR(etxLYc4f3+|4>4InSdune4p{-i3t(u1z;M%|+1BZ2Phy9wQsxAdW(hBm|SV zWJ;o$;}phvo;EzOOs~roSU7(9;)K3}o*$AA0`nZWxavwM}p#Jq@G&NGu0(sk8B zCi4%J@9+VB?eZv09y>lgDj61x?q5+ z9qwX&C#F+&p{2e%6xkOQsdvRuAJ5pP2d72c1X;_Nt@S^Vr`4XN_Ly%cC9fV7M~_Q3 zMnf(p5>H##*RW?x6bdAB*>(SFKjU=|mMnHnLorU;DVK-gVn+Gbr^EIxaHx01$vwPV z92Z(V=n?Zb0o^V4Z_X-fc}sh?)?_9+Y(4~;SV7c+>77_KXj7p6O7m**bk%*eo!K`J z1*FA(P8p=90FhXh#vxc-PvSmIhZ@=HPDj(#KVRqHy6h5OZ0B)Ry4RSL+Szq`e05W8 zkotk5nHd^p5s`SxNdji)G*#5#na`NtL*7YekP$7i6chyn9!w`4Rs3P4MR7$*BxEQC zXC7^Oc^Yn7wI8&kDeKLp({4C>-bklE-to3OQWv*P80AAju}b6R2J~y@eL7G)?91h; zH=8Y%7>}o7w|TT6tI{%xMb01dZg&>C>{_tZSzyTz=iYdDjrQUcHXbj#w4y*E&LBT0-%W3bAi0r-_xB z3E*Mt0?pd7rN`Ie!fCRPWyfKZgO7;2|Pl8ta{Znx!%ssVAHPuRI4P|ax zsubARJLAwN$hB(X7W%p$_#qLl^yAj+9{O(?c%q8x)BA2PctC|^3`JxQmR_5eEc)Tl zq-Q+Ho&{c0PPlO8!b9_JC>C(dbtY(Ja# zqrjHC@3xcP@58L1m)rC1>()yJd)LGt_=6P13PmJGWVHb+8##Hezvvi6qf}10i^0wp z47dbB8tZ)r);*R~&J~$gZGd!pPU#w(wT~5Y)eIk$sD_bv(;7z4FTCGNDYmdloQ_*+ z)W!L*6%nQ=1UZY#&5|Uu1g&HOErwpB`!NtsF-=@9y!($IAXzLS1Uy6+c4?HZ^;MXq zoFWR0OQ3$*2d7B^%qCKO;x=xpacNWPKO}}O`o_cy_M1xyQfE%EDDoc`2MBs?GFA6K zMRdk0bIMzAXYD(O&5_DmwZ*9;u1a(o@FYV9RCCd|fw()aKbd?3S&(md>*@Ow5GE1W zgBv7hF7DwrFOUR!HYN!A1QO3!abrtvrKJ%}KJ;2fY z)xcDKPlkZkW%*Ig0+|hV4lPq5J?g$?ucMM6xC^xhmEaZgb96YoGHRwmOeZhN7bAUC zjs5nsw=o2L#U{2q*2dmPG5)@^SBHkGQ8?xW{ezzhMXSGrJ}7}WZ>KyHW>qg!+dxcQ z$${qxG5gAx7CHxpJOth*3q!3lRlVP*Vaj%SxhBo24g%xrT17>wkzW%+`4*~d*zmYj zNO-E9?r{m8t??kS<-IG1mYGcRD=F^VM5>mtt-X1cFUXuWP2a^-Ky=nG4q0>EI+q(0 z%laA!lM+P-r=5Mxzu7k)h~3x<`e}><8YpV)Iakd^g$#qEXIdU|5hf9>_y43+iMS3P ziePGhI(M8hO;|05$yp`v+qYm3NMsI;$2kGN;Uw@p>tRzh9Z`a9P@?E=KOs!=ai2km z6eBl-&*^nGFC`|woHYH&CG*7h{Am@4{iW>}DG~fou6iErWr&i~YcK{UOyEqxwD{le z!zgh31Hs)xB-u=am;R=LGER91yCQ=46J!OwXQSUE>MOWV*NeqYroMwXL(pDArTC=s zd(Y&48*M0pIB93K`B2wk?DPC4gI^7~Px5=S`p|GqLB_bPqR8OI7}!{LMGyuhaq@{C zNG)S&F5&}^8v{Z6p8`kvX-B$skCCX3#f)(drAh(O+GK;|5`$z235Q_@EYkb;tT8cF zXwOja?YvgCB3gsYuhHJ?2YF%D?Qqr2z+NS5I;PoNv|6B(dGY$nF~>1J&~j(>8p5pE z+DzUPT>oFbp$+ef`l9fGr)43eDOgLDp3To6w1Q|V3ZO2k(lrPWp-!c&~vnq)Z@T2Q{ir>`(Kx za>9iQ?F7;qpnvhCh8zPv_>$497GxuB3qF4d-r*z`QkP$(w+E`G(z-Pp`)&|`?e;3} z3sbeDX9Wlt9J!*27C!I4tico^*v>O?s^pO(vbfA^Oz6(Ku>SqFu$vNXytn-_wfsS zj7tWa(dsAP?pdR*#wj_X)RGpx3Y*d_Q$x>y44;#vmytl7w6P1N60Q@)9ka2JJFfo7 znyB1xWi?BB7m?4at?fNC`-CxN2-RZ>%+JjY`xQtS*>X7wI`U`-&KM^b0%UVq!}jrBR=Twh6Vq6^lJq&le4= zFm1SMW2AF29A=uU3GMr`-={ElU%XIkDM4|X`R*_aQCme=SkrVcIs?R_jW~z6MAsG^ zg4fz;QX-cjME1@*hO~-sF6d_nIl_)Fc_QtVc|5JVG_8DQ(IcAQY%}fedO50@uHK@) z{?rvGuH!DE9!PkK3(@^?GZR%y8B#mJLqu}VahG%v!~y%c(DqrBs20~QuXiUhh3@i! z5*?L)vUXzm3vDz%o1?WRUG60}90I9XZtu=JT=&d?|2&@#i2s7*0@fb8)?}88djkXx z^+2Dk;)Tp}FCPqHQ2D#v$q+~-t=BF$zx=Jz{<#uaxdH*(^T(J7LMIUIZCV0DPf^FP2aFSIXRq7Y zYv|lo5|yN>&2p8X;^F=e_W%~{?ktHY&CFdcKi_WMli{Pp_h`|aw0c)jb87fIgDung z9%bd%uy&8vRL;-4Jn1}EHX0}D<(B}w5kCgqU8I>&W%b$QilukWth~#9nD&00oxRG1 zmt?9}V8NJapxYMl)x9@M5-&R9T(@8>qAqPx$E9wMS4lg}8}qdG$VtuO{55ooZrKN0 z8BrhF+>S0-D^?O>z$tcP9Zw_ob0J$A!^@9N_^-r;Fxg_|ny87)M$ReNMex2z39p^{ z(Pr;hcfNUtdkTEYWs+}#)jEQb#vA^iYk4QNpPN^u|`6 zeN$yKUD0qKWVXnnSS|tJTNI?(d@l28naig;yaG#`anQS8*9{TQ-DFS( z(4JR9_8L=aaGzP+&MRS-77PJ85o&I8iu$h1Mt2A&40X3~{X9;fp7aCi_ zT7~sVi3?n!DvUyQa|EIfD{bYAFVGC`?v!utbITn}AHJ63u5?TGTqEw8FQu+pt4c|& zO{8g$saivQd!PiXny)NU`|PKAR62X4zT<3cOA_2xCuFO9Rp{Dh;G&Od2E=WJ1NCEc zyeCAk^`lHTLPM~1(%5jK(j-H$HKJ@w?hp=GBX|R{i1@>{(0XZZl;l5(;Fj;Sm7=Aggbf;KNIrGy@RFzPbdA1Hp7zgn9gMBRt{Hz!SUTsT-JKgU z?&I(8;C-$|^B5>pTW!{6u&%#vsU8kL8o3rk0m2WjvPmLpH_w*ih;RxTENCZ<1+S& zjpe(I80)vA--oJ~R@@9WLjrO%)MN!r%$CYtRoBw=R8W*hILVyWW(W6SeE259l8RDX zn#)Asdn%7pgsu9$3%Fs9W(=Yv#M{aFVgbVDCZvCc#iM&9|-VP;oc2hySY^K_a~?C zx@Ph!CKb5hQA~}9>y~vr_YPwp+hN!f4q1rhvR3!D3+=*y&;)^IFFYcza5KLTlHUVO`YQv+)gbk(poMWoSEcS+VvJ5^(uky2+ggMb&?j?COazE zMB?2>j=u?3iemR5ZXDwHM6RXfpy}C?oV}+K7u=(tEG*xfLV18DTpX@lxr$-H{RM7I zUvE$_X_(3MczZ#hgKNC$l%C4@;7tE ziPzEqf0oj5RQA@c79^-*9!p)9G}2c4y3bf+$>(qL=wFkEsguK12cpUdTg{`x;~@w; z)i*tsFPX&vNkw=>I|BdwoGO?n45oX}lk+^6X`)tVhn2c}kNs@N_y9@LGWt>+o~cj| z&;8kG@fCpF+okuDZ1|_2X4zL7gVK?ic?3pz_HK!cHi4~+`K!T@uuO%_Z*{4IVK#6E zSYA3!w|ttX(A|`Tf-??JPHrds{oEIGE-s1c#`nM01cqF z(>mVaW>Du>xcbXHTx-|+au)6kpTq{HxBT7CiSAF+O2fKTb(?R6VvLRg8sSZ8LjY|+ zE$=zc+S`x1wV}RXCPxA8@YjIvdH}3Fm+%*Te283mG>W)X{7s+K!P`8|GOYi`$UZh? zi}7Atl?W%ypVDmSZTr*}2RcOc`ZhL12_HgL9(_ffHnOrjleuE+iws~CQcYdfH>~rk zUj4vD8sFf6z%yXr4KOt0Ke6CHNUOx^*TV52o{&cJPs>bRq{DBY{hoqS-G&4M7>Zlm z6I-ia7F7M=U-VM~05S-XZKT3WoDg$`~GGuB|l!{s8U zs;6Sj2@8>Pc;(1u8mr?I} zQ$(wO|`fx)E7ErBC2wMcGt)2Wg-G0=}=K(0~PVeCt2FYvcXMU)b+RnmJC05g;#je#5aML2YtGUg;$4pl{7$V9Zx~(Zg?~ zg~MS?CY{xxEoHq>K1(QZsHNcfF~6e^02$bPHsrqAA-~2_shUZRx$$bruq*BncH44k zU|#E{zL0Blc_BY@n)#At0f~9Z20MckiIcrJYaU~Sn+)ooF9Bqd{vw`8t~Q^L5t&-8 zSa2YaFL+Oq=hiO2n2^DDF(N(JeJXS`XEZ5Of@R6bD>f-5zO>VX$PUZYp=PuSkid6l}SBMWo3P`0YU+w^X6ub{aXeB+fUVsKHYue@W3wb%#^ z@cEI~Vw@Ly-NINc-tBnDtWcC1chTL+VdUsM@k1g$E^dtK7`k;Sfa>gCzhC!Sn^g|; z`1)64`0Kp{o3~mW0=1gnq86*SM5%@tp_RTn-7BeYA*jK$9*~m9YRiD$mYl+3t@J)m zmx`L{{7a25Jryaj#0_FNdrlJ}R$JpqW0F*YzC8IhMN;uUC46T2N9~BXpsGvbN6`Zo zonqRPo*ZM%J4?*D+-^C;TG9{bUN@Sj6FK)Uz-qBq?;8b+lV(BS)}1+`asa=UEU))2 zuBunQV3lTz_RTrvdd?zxlxK%hUyI3^|EHd=H7~@8?2h{>QyTP(9vzx$;4;$}_ z6XYszw7;)9e~_$LJT;`inCt$xiSW7Yi)=qTh*@)*PT%uMc;>WzaO1G;|FYTyvHPr7 zEt_BheXba!Cj?nDjW;MsXs;R6tEoGt|JWibkCqIiTRc8R_gq2l%(Y&P4A(r$Rp@~> zSDtfuHZDwUJU&mb3wB?`yt3l%y`*jHjYoypX!dFly3_jCqKI@(ByPmDOKn3{SbIHe=k}hHJy3P7Y#JfsO4z8nU*W;)K4$#Yr zHypRZf+g))$CYzd!hC@haNsmt)YH*1W+avIC3y4jw&(qiyanzGSmsjwzfTl^E81<7*V4f#ZRm!7PxQt>xwBLN?3X$3*7kJaBT zzl05TQ<8DJbIPT4uik7hLyL@uVe4;25xn%eh1F_xuSij%EzFOu2)vJ)C$V_TxgM;L z_SJ6o=5z5rL961c>nO>|pv2rNa=A**z(+RmeHVM^k6qq`Vi^8T9*H>6e}VMO zZ^F9(ZsPGs@AXo(=5!hq_EHU;G@7G2trwA+Lm@^MM}UyE5H#eM2=!(+9wk!m%KE!_ zIFR)h=)%RFfd*ZPCigj@sTx+$<4?ZL4ebRoE97jZ{VnnTy3+i&iwLW2(tS!B(I2n2 zb0mv%9v9H?pJKi{iM>LX2YY6+ald~#OlqHzplKDY$*S-GSV=GEnE$R8=0V_~x#9jw z-hDlv`gg@B4NNc|xn6pykmJ{u8RE?^nBVi??ermUUcX?PT{1Og=DjzDMZ7lvQusODdfIX--Yyz&z|XF+Fycb=J6i)VRiXTuUEIT_{2 zWcd^qKmzBi#JB7MEtjnVrvqk*Mw^djZ^NQAOl9RXN7 z@EAE>Ql@K2^ss`y20`~=kD%g^#<2qD7;$?-Zsp7Du?duB`a7?&VYzJ)Ba$yx%rP31@%jP{P@;+RAQvXIIzOVx2-*wYm-S(YAx5V z!}Nhgn{w-$>f&30G4ZP@rCK*o(#;{P>mk8_0s2*t zbN*EQBB195FqudXP-Dc{Y;3)TWUIW!XZxm#Q`+bC7XSknjlO+Y3rX+3l@?ob{>*sU znYZW=GD79N2b~`+<0$S6?kaMEbq?msZW_%lDB9n;`C*nuGgo>J@OC$zZf2^E`hK>E zn-s9Td0o2dHtztwgf=9wL(xU#zAH-G7r+3&lbMQ?rZP6 z>X2(hFNZfg3W49rBeJ+1sY5a5?-$$AGceef)89&U>T(xT^$@Dk1!c%$kvAfy3%_3~ z&UFV_a`HRv!wRN=QI~g^GfgMCO&QRh2kg~dgGK^*+MxGzriFbgVx)lo?hg{L2A)7o zQ;oF#x2GAqn>hDN4a;89ndhR<9BzH4Pexnu{wIxAFWe^M$1OR*p$Zbrx8B`ni>Ja% zFaNA!hi$!FGnTQqs83*rNmYFJzf!Ng?yX%?G+wk3P<1vZg&jmoH!O|1NOTh@+{Un`n6dR zOCpGq0;0?tjm(+eom%tgCKz!;^Wn@l5nJQ7zl2{X&$yk@5@R7TDI@`6WvC4g*D3&i z1W3BT3^tAQ{EyYcbgUaS^;qn`psu_p*-gD`OWE%7B!DAl!MhU~{cw&hzXy*+r_vgP z`t1lsy=!%Vqtq^Oq(uSJL zLAUc$@q2HxBhVJDIaI|pAFe@it}}t+wPbD037>%Kz`06d4;ZkH_duz5emw(Yh{pho>s;dI!uYO!u zjW7B*AKHU9U1;CXD=1eJepA}_T~m^c5u~X8`r^}+ax3I%BOMV0^`eP&=0H)jMC0of zj@f`EUu+VTGM#0Y3k~UdU9|C&aPIVZUdPQ6s)Qx@w-Rh85iwpSd0HbwmjO>=(*ViE z8#-j@th{K*i1v&J*5{131g^TjKhC5lU&Izy`^o8+q`JIs9cw)p@0TH6$@cZv4LEHv*w`wKQNETY0s!fC9?PR+PWN;X~{v*Wk4Y z{JHmd7DvSvB;ctkX*dMJnlqbB>c$)KY@)xS0JJX|PYmlVsro7MT^$AJ!k?C62AP$A zhLC_i0+7CEyBK`?tq>Si&i;;U7i%)4c9~k}Ud=}iwl~4};N-Q?k}98~nAFaD8rw(V z^I>5dVD1}ii9>dc!y1@mrbwG12RZFT!ivDSZ)%6}x^)1c5ej}t$KYS6n3(Rm;LXX@ zN@!SivNtWe7XgUGh16^@lt)h!4Iq|Yj`GUCdAOxL9IF=GBM7}~+_iFPw4{pqo%G$@ z8=KFbMjE~+w&Aj1A1+sr;W523cNMvT)u)s`XT9T-o9g0d62X)FC4#@-{dJQYR8dvQ zwdQZDsGnr_p^kjTGHX*^fg>KJ>7IIBSv5sSX;hAPny zYDOwYyVkgD2S3xkarbq7+;q6g2@`pb$8?^&Rjdj_>3FXlRL+Z!8RPjEqnel|h%gKW zkHQpc{nwP>V6+xmGvLM+#Zzl8Pin`ja@7z@Gfo?eub6%idw5B*M7%UP=WZS32eY~l zO)}CwK^u#=gY{p=DzF{nGk#{YlF@tQpFxBb<0hQyO%(D&4wT@~3Q6SzJvqi_*i4^^ zGJouX?O43bV@)OwC4MXdN-&bXH6M>xFW3#gr*R>2sm7@N`=pR7tR72H*--n|c_GCA(tm`icR|*!kO3jc4snIMLLpoM z?#n4>(beMMZ)$loI*0t|ng?zcx@M$9WHDO+V}c(K(TNOzj{5IVG*P(?pwQ#(UFODt z3=hx7ANtf2C*GQ*^u#ES?n9aDbB1KiFOBNyIj`G|vDiq)IvjW8{(JreTrl`{KPE}{ zZjos}Zko$*Q*74sXOBf`dFe_MQQv#m!^0&szFwtJoTxbI9{d?hh2^!o@!>MC8?MCAxdQrS%Xj;3+ z#eUiK4QOK`uLqCIg7@d;nwU5XKb7n;W3s$VcGNF#DO+JiRWLu9$p_S^L*{2>SW_q0(t+Hs$qNI zyny5d$d^cH{WIyH_cQpl!ZI*3r~p3Z!KPWF{o>r-<8G1hHkS%4zJ|_Q^t5ZR32SN)r`wc@br0d zth^cSS4d~Uc*m17zNhHI&@zfPN+z1-=gIQ2W;nW?kKKsIu9*rNhi9rpMo88a_RRB` z++Mm(B)wl6cdYVBs1d!0%U3@z7+C~3Q3%L|U-bhnhq2Uaes440LV2t@$`t50=|S`f zN1@7P%?QgsqrE4jY9n+QZ=n-H9d-&1H^DsOX~N zcV>P6djC%I&_PqPLJY)Q!*tbIMd*Rc9hv4mLGmO1K+Ps>o754)sM$icMxOIAs8S{i*RL8Rz&Gs7jQN)Q*y=Fy=dHJ7?tupxgCH1Ez@UsWws6 z_-eat^-LJK-t2&qkIzlk zdMm|OSil6(h)=4PXS2{eOoft-^xsi3F(3RgZRS?+0hNEKyU*^vLJ6(7Dvua4P`(rp0)--N-P9l2~?f-iQE>DK;^LYI!yT{-b4|A z{Mnwf8^dhwTRj4R@0$JO6IrBptflDldqt47Gs{$8wf zFC?=Qxn^j*Q<6(!wzzFscGf8Tq=f1AsR9DDI5%d+?ONq=&{SX!+XgCt_$Pdf0ZX=0 z6NzmC@;crEEC35guOVrsv74q!MClIgC{3m<3)BM`D1WKT=Byt{R>&_y$&A*N2ybAA z1>EL+II+Zvd?rbj_#`DJJ;_kP0SgGd<~%te!ha-GMGQkCE4DlU{D!H}+&$9RO;tH1 z{*eM;CAA<@SL#6sfHQzS9egun@eN&p1oy(QhOTMuc4_Pa|E$x)WB7Y?j#H9NPzsta zVo=xv{_FwL9t1F;eZHIyg6jx+#-n_ML4aRVS&Y{N8oME(LV>+7iUS9X*DTT56S%NQ zLL>(~MeL49SlobPsd2qdBCo4%i8BDSfyqZ>7fxe$DQcM;I*jL}qm13}5aKK_7~Txxo|y0W{$6xA|5g38tMgpve{~1S6l-fRJ)gXiMZvrgP*NDm0qu z(`PH9`|X_LIyPitzzJI>4m3A(M=Oxl<({a~0tXF|BXs)N3uFKJbdalofQ4|Jlab}} zc!5;S2G$<%4?nI3nbY-CDj3A;NB?7QA9B0d1d9L$>H%myP7%C>6?+I6LYY)E8gsP- zJ;W1a|HjV&W}ZjwV^Y|YlxY>PkO-1`d)`40sH7RA+he(jL74x24opsmh}NNxut>e6 zxDorY$&XvLvj4;Ozc(E=o;(_}V|o`Xwy2`mcbGSZcbWTcm>sV8-u z_Kv?=UT+Vt9$?s5DK-#%KKVgoPh-WiK|C3O<&KNXG`B+a$Gpw?2z9T6g(aG7BZ5$X zE9&+6?O(J;mFjQ4gaWtJGV%S5E2aU!5xjF~zo37IeLiiqRFZtSYxdo6Q9OBR z-Vfbe1?#YPOn+9uZptk@>&m%8Cv0aw>pVLRiv+#^Zpgx@W+#}Yi zp^a!{eWfhg2;~bmqTj}FwOprzPPGJrq}C+J{P3Z%nfaCq&$RpRKF)fQmKp739N8(B z*eaX%<>xeBs9N3T&E#~76v=Fs`1IT@o;G@Fvb6iYAa>2Va@efj>JJ{Bwh=Ws6V`nI`FSD-2Bflg+UGWM#=K8q(m<`#lY&5qe&;_&A+8KS;J0(44H%Phf z67OJ}e=aJY@4nl~c7c*Hk|3t+Y&zO>rPGp2jMhD z1|blPMwcsBlkXk-lv~+VHd|W(1*BZR1sC|xYcqfN7~6PZG|WHY@2}} zYhkhR)HPrKqS5ZK6LR0N>*$2#ZHLv{c4_o9@cfIERRh69r$0icV*f~ z0J`hdYHLj0j}ER{do4tby)7i`xj1t3xtdpEbA$RjEvJp?UYrgK&C=G%KjC>EVh{=z zf2A&*waI^H0NkCq*c3veuqP8Bty9Da%_zxc6z*zY;4;~|CT041b6(s!XU1%v6S%NA zzU9qTl6c=H8*9N~*JnCaZ`1QEJ~-xF?0lqTwejPkgSMp(37}%HP#cWZN|4o|0QJJO zxOcbM+;KOVt1US5iA^h6u{42yhItQelO}z&3d!|b>QNVE`bVZbVLY$iKn9_<6ukn> zrm3v0a?xl)_^MPG&Oov`v}>a$48L)5dx8nAm_j-2*Gq4*>5lgcm3m@nKqY1Unk%eT zC=qn#H8|`!^Dt9YX8yNB?(X-FAE+yer!8V)$hj6Q$bkI6g25qHD^*4Z{l@c4YvWk7MEi=%4vgMXI zNR{+LbR6F`)?FJ)bt}sDTg2lFMOk5{dkoD5$Te)l^B4T#>LG<+{DAdG?{OjFIqj3QMl){^(q!^FEL3 zX^;Jsa&gV#EYR(sm|nlJ#Mz?COV2-44L*Q&+fMA)Eq()U+7}udiA16#Z~VXG_2#Yg z+7eH`zEJwn%FahvHq{Qy2ziMeP$?JTs=DZUXO-;gV!*O31NtA)CDHY3(~cOtp_AtQ z?zu4C_Cm=wa|HylFebu3ap0V#YpAnq79!ex_so27)@C};G*zCCA#DBt?0nY~T66Wv zWDCy>>|XEs4G)wEDicG@lfrD?@Oqo+=JNe( zoIyZ&P8MfRKn`zncHL%{ix(pGy8@^KNeQPr;MAdM6s?}D3~pqk29ce{by}huW{>|E z61aoV?K2FcP|4mG_WsA`qm3>YJQhbxmkXdS`dTv8F*Tj2fIDZ?L+(n}oqsb=(_L06xD;$*dyXUBAi_eHE)cP7>scRbWcF zRlf=)ob+zf0Ex#wRK(Ll@Ay99H%lS$8S;r-BG4>!woLf|IBwh$X&YEPCxcdpJ_wGzMj2{9zuSbMFtdG^c!HARk1zxeVp);Zd( z2az?_PHi?G#eLu}SV2=Q6G%LIpl7&tLkC7osN^^FKlDiBj02zUx6ITAsC_v)jStMJ*?4R*YffPPGkT zE$=T}Y9HShCs^pa=5D_9TaDA5CdF{r$O;^=+l+)_VZwvW0{mx0 zJnOhc!TOQ5GD_?7#KB>xH^W5s70rf=*|M_kQOLUUr^k$S;m^`V z)+Xdpjey-h@hi>{>zES5ffU%(?<%u$vqT+H(1(EkGla81nu92LjsgMNA*5bFsi5Zm zV;@~UG%yExZsK^ctooqiHyPYtDoXGn^4-f8+Tw*#7~&bJp$kW`63y?*ianU2L-j|E-rFqgC)R!)eK%2@PNS)Mgtz7-|;YY*tf4u;JTf<-A)K=Wqj3j#H ze|m1Iatr+`6+ZyOpcQ%@zcnMkhe&zo+mZQxWmCBv3^~oe8`BAPLtYmL%ez=C9P8dr z`2Fw8PJLpazd(gx@9C|vWoSaFG{+4 zNM>F19r~?aeWMrx^xGVh)v7ne_r~`z+x0@;S-7m3{ck5W)FGQ00c3ACl>d+Dj;6=M z`HG9c!W|W#OJ*kxM`n-SHcn%i)esHc#`IO-vOg7bX3-;1M(J^0h(9wuJ zrdKX5gdR=LFo^fu4dL=OZ>U|gSu0OZff`nz6Po~;^5~Vwp6p-$2 zkdh9OknV0IrMoU5(p}OW(p<^l-R4f%{%1&+#j} zJ_>-3z$cD@wI8p{z%z!)`q?~P3iUbJLm}q=)YISYBN3jF|8#m685PqbU~Ycnwht=p zb|>pgDmhZZK-h48hL`C$*ewlKM%z4}$I}8cO!#$Q{J9MDP;YLkQTzARhCgGttY0Qy zN{OHlD{GG8H-PLK;Jn`L8&i??rUx$npNvKBqkVY29qEfE3@TQP!xCX~HZJLJ69EYC z9Wmgt&)D`No|VPeTL>8V61qYoC-?`s_9GKw21~d}1-<%4_DkLO%8k`4I6sJCIp+tm znWH(UhyITbx-Q!&^$a?8P27N6DxKRw*n{;%vM2nG{VR{3K@?O(2Sw+1Q^0yopa?U5 z|ISD&#yrt`VnrsvJ-R!d$1$7-z|@rhce(sRX2dSGd0_op*BVI_z!zE0^$PmQbMkle ze)B0LiF~2It8|Si`U$7#aklF@7%u+!v000`R!TZO`e$Z^KTlb`Z;MNxOG5qI8{mq9 z+5ovCzL&4)CXqUB`~22@=gJqp{NCu*-dt&`_0^)Cq{7|dbyuU+NM68Y2!0YgM({iO zQWcdnnuyD=4<1)T{oaEKY^aPWD{9wX#skR;*H-h3zMr6`l2lEeg9f<)W z#C1?rE0ONa{ebF!BG2lIpaXWQ6tzWb{i|flsy;}y- zPaNM6yWzgeAJIW>P@t=&pN$|hjodu0bGN&<;ucW=k@F9URJtGfyt5>pN7NTojj9cP zaxv|D_{)M7ab(xro@t>hOc?Q&X}soau>nB0_>3%%cA(NIepf_GyCcQ?v_0{QIKgoH zSt+ip()BOHF1jyZcC$A{ZG`P}6Z$hH#e5?p3#8PX!-CiU#CZzuhGbn{l2*9Xm4|ohn6$nFX0$2A z1A-*74#wtKGoTZq&0PTwPs_33M&Qb)_45T*wasX-%?(zw_mVQ)lLX!9b5auknl9CL z%qt>J+8XBWSgZ)-ED0Gna`kJu4wrg*37;=!C%X!t9Sui#Rz`@rK?4(dMyo^#GjUkm z{`mQ>fj7@8zC4!UEhn0*kMn0gRBn5tafvLyiqJjy2oJwwz|b>g!`D6P1?$kGn?)}C z?8)O^iQ}YdV}!CbZ1M7Y&CS?oZto|ngL>xi#FpccY+=b1#AlcOSV)v!nHPrH|AHK#t#vOK(K&Z+L!AYx&?|X?U7wT z+ursaH1@m!5__0;kWj2GEwjabYJiJ)9DDq__{y`h``xFDI7AtWJem5ezvqqS=W)TJ z&!{(j7vk3~hT&3dDX(9~2$N@F9_#qod|4YRWGd%pqj8#)hzJzXi=z`<4t{*#bo?0U z`lH2VlK761`NS5@ch|$1(pD?2Z@{4Kc2wh^}*?%#)t*%=`Mc8&)$l~cH7#lG`d&G zbl5;`^1Kn+uI{Bs!e5S{Pc`2xl+WZzpbgN3wmEMNu$OiXu9C0NG<|)+JI!Vqd#$2T z(^IJ@XnxG7p|=Wv#LDMiV7Lb!@_SrSw2YfLiuqLAo_=8z@60Krr_|E_&#NqE-O9S6 zP36#9^PQ9Cx7Qya8$yoBoWDkModIw)#O(bO;e-HOd~gC=uJbFc5r8XG>du>Zbl|VM zj^S~OOcWY$5z~lTbhknV%Ct(1kBRjbOz@t~qsTZe`22N9fT@vKYp+EZxK~=(2DZ2u zwPbbz=GJxsJl1qZ;4bCfQ+bU}oX&;~%mPqn}l z%kqn;{;(^hU2T2*MCpt%-|3wbFCtd zK4|Q{nzdoWCrk+xXL4AR;GpLNoenBZ%fkTy0B;Q}HkV2o7&)Z&KQX5!Be znnlE_+uASa+zW#s*{6%6E20ZgsFxl!ECuk-it%xqy3!(@LX4+Hw5B8iWV^Hbb>7*m z_p9I;V1T}C>$g7IC|ik9iLvX%W3W<(!H81c;=ci*Rz#KGH>nX~eNc)*g8mrpqj*Xa z+@2aE0hRQjv@SnK8xRL-t-99WY0Y|jPou3Be!{1p`8pbtLL1G=5o@gt%^T_-xR7G@|ITs9P$2S`r z5|*SOgbF8PEwb0BX+yNdJYmf=K48;H0nlhDyl2aivQ+>U9szE;;=CtL17Fb~C9iuc zT(MopfGGcE`$;GU6$6{;__-A~^j*XX4KdY4(F0jE1=Ex)!@0v>R4ksDnN0WwGleXP1 zes$j>8*u*r)>(ze?R13XRgW6OcXt*Y!quAA4vlBeBhwqa@@rZX`OrzxmLN#-*$TCS znib6%y)_b2%+XD!Ap`V-2+Ne1*>v67PF;Z(KF5`(BNjze5F}#P6Ec-3a^J@Xwnm?u z10)lT-|N$t7jkqKX?$6=u0J6$jgjS4Pr1Ax0Xhy=#Fj^(03CS%{csg!&XXCH9~J6j zH&8T7^(%6PYs+E4)hfF(uzkrkYv51MoW{-^cT!nU%G$O(EuuIWbvI-%t%zf@p3T2>0GV%Nc6_o!JEPc2r>ra4@Z=MTw5RME12mg2i4 z0x%0=WVWO?>qLV<=O(!b21A>i$@dnNB#rc$b=5^^aQ%MmKiY8*1;Omj)L6{{kGuVa zSt8Y{6c262@%lb94RAO}@KK=F0vF}qoHKkpsCtlwRQ1n?pw%yIkhs$`bNcTFwdjuSKcuxGJ;DDqTdC|5TFZD`&&(~=4Ex}uB zj$!Hp`!Wsh?efwO%8Y_vy)uRfrjYN1&=cElN$O@k<#n&--p>{4d^V;6wQHQjqgx{I zuB(EVh6{hu_rL#Y*3ulcZqwNG2W;(-`|HFFwN^t*@b8@LY!^Q`r}BF+J+T-93TXPJ z0@3@w?>N=hPv6>6n^{D2Tdxm)D5jv!=hZ1JIu;Sc0Ig3_%PcE`jRYtPIhNN`r$8VB zR@?RVk2>gD%Zc<3bdsSS=W8R;--HnbP*ui8U@ zM4t?+&x0*g*1^#UT)BXd+9I zP*gTNwgRczVdvA7ZSJq;rh4O0qq{Dfw3>l5kho5{e%J(!$4D2oP~)ia%Mj@y`lh?~ zB`jas9kH%lyp=v!bSe1w8dU9v?efv{$Kdc>bv&2!M%T|_xc2Wk6GoiUdJtXr3EAGp z5$<6FsJB9d83+#nTrawsmiT(YJ#a!647`X)pklI?UHXP`S7vBy*c)0Jl99-V5Q+s* z2tJn`VJ9cWE+!Rv6Mhya7if%3loUzk%>H13yZ<6Zzv`P{?!ZS71)Rd-`U#o@g@bg0 ztE9q{iX}QoVOv;%RMwu`x$vicpIP!|h#pc2GPwS)tyPYU^rB{qHZm_J<(8XHW0{nxi7jF zcehQ!{>@Vh0`CPiT?9NJ?|dKzib8^8*-!`ad}Ux=$sXVLnN<#EWv*Q?Z_Q) zt9Wa>u2;{KD?R(yyvPa;1Y%}YF~nZRY*h@!PU^Eit`Zf@o@V%!TJN&X5CTB3dJ0R- zwEl~BMogZBsTXGfSM?%POGrM!KU7Y0N@WX!=0jL6yVNM}cl4$0@uA1|X0fh6^K~iMJc2%G%dtwe=Pv z@m|pXw1Vf^;G%`#mJr3gMvRkG5|cv!b@Pq~F~u=BZQpO0LA0ycGD~J@eql%2t$W%5 zIGZU+yk0aA=&`@k?9B=@b;doD0@_@bjhZEP2%I*rT`Q&y)RHDY_8-4#F_{yJWmgWn zI}3@#cZfAKeo~d`zhJ0A^+i)%%n- z;q{M7FhvZVX~QxEYHvP({t&MfC-jlwuv^z=iIrDFP=uBp1o{|9%k{AD&Wmd*pk5

m)vso*jEtEaKHe$Q{{irV$s%Q^10QW_135nR_L*%g&=wTV%E1uE``X?wVh7!~VuI~+C<@${oX=AP+vDax9DK_u)hU`A0b1Y$ z9CqJ!Z?7Ln%7CRvfH6M$zX2;$kK6{m`O%}JVile6Y8N0ez~k`V4kQTF8@yDsYa9?L zAmlys25?+-!s+#3KFD3N(KCCivieuz{9Hb<>9;OGC4qN3S7t>KSQ@tQX*(4GXh22D zo{xBthHkI-GyPV0>ZN8Nl>%5X+VNJucUO=7c?Cdl4G9x)>K<c3RB7tvI+dx#)!_Lu@BM= zP*f|57$fAe0|ro#^jwm0-ejAL;rwhBp3xo(EI4E%Z%jp_ ztv^xex7r7NNW~m9Usk&TPP-|Ot0`?nLa~c>4Qi@UIX!keoN!xyw5tupskqorur#21(_-$)`LRA~lfomaAb| z15R6@5GJW0y^jZlHp>0CJdM5%8mlskb2SnFCBes%KYdcXF1xrJbFlXn@N~fb&@L;ab5Wp(E zGk>}7s8SoAcW~StTCG|mpn3n9A|c~S2;B&kihjvhLuhRN%5VaOafi5$UX?fk;l^( zfw5k9>TzjwBFx>Ld@aNQE+kg+zT%iSm z{r3alG&LcH2eo_m;ho7J^ttTT2y@vPAg^O0d7Ur<(HwNEJR^eIG0Quj2gQnG9d=II zCRgXOwA9=H_sA&QrHsnoGeo{Y#Yc}w;wzq14hME(-tD#@yO}=3*>J}r)3MR4#xm|X z13xY-v(HpF*P4d7UW0u3qZ|5?02u>>+L9$#7m#S(2G3i%mMP;m4;6;ND_b9wa|}#x zso2dwPa6$BiWYS{Z1#bva`tO1W(rq+mDi}&oQZz=rAR50#8ERnS0-Lvq-9@icYgfI zY(VDDZ)LMwUvYU&)mt0(FrsBoc|XB3d@3q(b%2b3Dn#UfnqLSe*r{XzJy2ne`AhgCoJ4I()Jw_u10U>1;{nhD)*WX1K>D1o$*&Kgxgcn*cWyU&dCP8zHzRSf3H1`u#2RmNHUfd9mcqbm2x zZT95&JK$!QPJD9Mp_;O|AP3SW{VzFll;A6J2hYGXZ@{=y_b zRO`M%u;inh9OBWQ?e3rHvJF?j7VQ|jb>6!0JD1mbM7k@OV%r&E&6etGf>%Ov$&ab*D!#j%C_$!2oO|5~>R6{j1Ee z@+R~A>zfEZ;r-b!=ck>GM{NLt z1$arT+lOK9#BG)3)5`-7cz^!B#!@g5o%n9%*q$B(TSXd}=l7xf^6#0!Q9AKP>u-gF5}l3&#AOwy=_H;Z zm_^V_8XydD@*r>k7&RNI6?{g?`0&*ucF)Jg`RFdK@j9c7T9|}q(=S^(+kXBB3aDK* zXUXD4J}u=fC7?>1Oe6~KOmPb2SHmQBF`CeTA%Q&TfBv{1TDxaYi1GM5D#va)Y+H?w zFS#t0T$(>4Dud=JvtLj(G}x}OKF561C~D^hhF%8qIrj0yE=tYGR)?zo9TY_cwSP80 zUvOpgCd60_n|;>X#o(hIkJ)wicMmYElJVNyHu8wMjb)+P?1C55MK!rgp4ZDhu?$b& zR-Bq6w{tngops(!*tJ zqvWC=FM;hHI5`6Ym0GFCFTk5IO80SJ(^ zLGMB2B8{-FyMyM3=a^N&=H59R!LG|4bqcwdB_kx0+iy!KCOMtbd`yz!{Oi3kX1 zR?XNQrCxAoq1gIw+4<^gOkgu)(p0n$g?xGFhnr`xP^MJQRhApWa}u6ES^+>si7(vV z>pX6tr*b(N$JQUW!kQnL-ShX2MqJDUqU|n5IG8dmMespFteeuUZ28fmkkxMDp3@D} zLbdzK{&~YCgXCoQqc&y?#?%Gzw1Q4P& z9NyHkGPdjJ!aQ<7zAcVbuCK4Lt4Ubz=L(|IEHWh{HDirZ$Dit4Xl+@ieTdrIO1~{_ zp=?279?3bCUNtCFhz%s;6$Td2pPT~0?+ljDD4GWL&JW{eHN z4d>Iv@8*p{)fLNh9fpXRw_-UJ(ap~t8O@if^6n}?n1(OU#%{g~5M1-chhVPPQg`_; zlW_Fzi;>{bm?*yStfXI7QfVBZGv{@F^y<1cuBp~5mshLH^vbJk{KA-DU#g%NOq}VV zkuX>*h;cgR&hQsjKeun&8q5N_m<#N~; zHgC5dUky8?dHyu?*bmMng}7v^lx#6MEOCD40RyRGM6xAo)v+P7N{J_Odw&|@!vY!t z?mDRn)q({THxB~9c}ey$DW^FTi`Y@>O81BrESf~QN}f7K;rTWc-HdX10|yhwvRb%x z3mexgatwbyStx5x9kujMV%DTV86RxkB5xi*%f4o7O66G@2fcX(_v zL><1_R%Z_%CVb_?{ezr(lC}JDaH87t&yfHzqp$qMD)8t7bubb#wmoi!=dtk}c+JH; zUSp#wu|jxEI)tWDDp%Ue`I)47obKq6MAN>Eo0YxXYNc#iSwJ`N9V*PojOxL*etapiC}PB9fxqFk zE*9H+{4E);elU>{EI_;zFQ8t!}KCTkd)sW#k%9LsTO_H(e%|Si2nRM7g&tN=dRIh;*t4 ztu|nE-9s<9Q`uaIhkxoFUW!&lkGf!Z@0yOT_ZRJbYHm}{K#&qQ6$#M}8_FeSf$_2n zOk=;s{&5j2t?aEyl4YW_e`_A!8z|SB+$fUJ+}aa)g!d*tcI$ZO?$-tdscUP-Eq&?T zmSTC^kMR+MmrvpGvU991g=&kbpWH#&LS)ecUkaw%3_W!=O|F?Ky4iu~XnZe{n#KL= zTm(qpS3H`Rt#P4BCJM);Wr$ID?3urzW%8$NX&cunOF>=l)ypg|?2M3#lp6wq689)c zT^CJ*;>>K@^f4vbOnb+(y7HTs6?={_@Hw;-`r=s-*gZ=qkUXr|Ez#Sfv>eF0ouc(> zY}(k;aGomq_eTtp@sj1;jWOJ+1B-G8jQ~~C@%AhNm6Dn!BcExBxa+h2-n_5g?p1PR zo;mHrs=!QHBwZ3QZ&ZQBzO13Q;%Re3UvwLQ>s8rOE-4dZt|ns0A}rQ(LZZpaZd>M9+ge(*K%FmVYkw>A9(wnvzB!2kZ(4q*ZuA zPE0i#AQ8X<(5`3H@%mpww5e?ZMo!FU!^M^Il+%B0nFHxn_cLh)n9kn*YwbPY@DP9r zSVk)Tqb^@K<$<~W63~J}QE-|?UD*7`hcqkWJv+_`_RXlnJ{t`aSSyEtN)BPSVHbzs zsm3%oKP8<DbpW^b)s1Mr12l#Ki3PnC0{nrdo*=M7) zno0gmT+nR`LAy5g+RxLylPg{Y?6g zzyDY$WJ7+o26^y={^KZf^QOM<{9n<%bhIGmx`8HrBKq_aV16i`XZ9icM;FR```^k4 z-fvNo* zi)C0;SUj0z$TNdO1T+6D?QXRGZy4j6{g?mv5kUXjD}Lnsfbrii!^J10{~n@X{nLMC zBxTr<&r`EB(*!ol*n8anSj6-?|KESU|G3K%KedbbkT@jvJfwzxd_M3t0li8NNz2?a zTKzCN3$&B&RGT%t+Yat|o?HM}N2B`|oo<7B1e1QNjOmEpubk@s_;^P@g$D-!W5O#c z8I_sU-F@zD_M@D{Mkn-$UVU~{fJQDn>%2cn&Rpqa_X0+#nx{%GdXM*(C9F)p4G@I( z0%t38l?*;^&(^kPK~L@tT5RnXI4slxOye8ObRvttQrTrx5@e_yIlHngfS`>l;$X9- zApSpwTcm%C_d9%BzRnbB24#HLnj00!lhY2xYzy2|lT|v@GJbvd?S!`)YaM62(HN~U z-v}=An3;4BYhBn3TxUbp{X;`z)@tv&MiConO+#PW)mehJ+4?4_*-S<@fi_?{Doh@f zTtjN{!X_v2w{AM*F#mk)t+qb8rt-<)g!7{JOL;jq2sHZbptKc`6*^BBk~?>EMJErj zO}^B3Ei?%pLKW?Wjv(2vt?ZLbn@a>_yZR38hs@t_<$}^JdTA1s7S21?=pdo@9?UhQ zLLs6k_^bhA`QLzCRW}clg$rVh&WEW-G;pARR4xIOC-NHLDx%%p-T#z3Li3cIm)TPM z*}6!xbGq-!eRZDz~=nN^{m-iBFOi{mRGa_?#OET}8LX@*zw1F%E$jsOn;pH{!*n(V~c59Cuaa=3;2 zOS3)amd5Le0<00`o^X7*IhQ;+{O=G>uieVLuHCbQ;=BW3DVp~(r%A;^kh0ztKmMY_ zXcd3BVc>Q!%C($hC7kaKjgxk~EILK^L_BI<8hv<94U?~! z$+n1Ity-i3Cb3Zt-Q#m*en;mjnR?{FM;^1zagg;?HY+07Yn@L|09iJIfMW)p^84E2 zh!fs>)4rpRP12`b6F5|SMEAHN$!FBY_EhD5Q9??X^X<+ zpR~PZy!XIGFI(K2ca5SNz(Br7Z_@R?Q&9;G@b?4u@VCcn5OCgoNrWwY$(P`pwJTrPDF0Y?ZcHiHOXrDvhGIo8$TgC%n)H)-4h>Ik7 zWG$^2swSiUD)?=?{K>ogf*%pMT0_i^QhchLjtAtA(X2!fRvxGG??t7)_e!n_WR?ks z1Y=f5zJ#$4)9TgoBrrda8`ba@`V(1C(6)&nqTmr~(cWEHinNu@3G=!T zOV2zMP2n5eRJP33K;N@_+vnW1Q<%Q?9Q$KOoqB4;V6_yK>rEb5NwjK~X0w0_$kGSZ z_PXrQG-_IXR%1Z}G<)*~w|$IV*Ul8jmFpb<`F|3Mb>W`bTPS)Tj8>PcpJ_5#3N`Aw zXGznkPI5xYQOReXuPqW64I=Med)d^W$KHqy5H0+2NuqM)aNv5ka?p{wc_3Gv*n?Yt zYkOeW$gj>}{wSsHUfG*gNo+;ai#%Lrr*HdW=n*kXvQvwM`7M*rv`k5JLF zuOol^9@aQUOx|5e0pefKsw*)Y@OD0saZ7)DVAh#qoLBmp(|*#^z1hsTD@nUZonHJL zr~B6Pi6Zd4=n`R%Pa|<)ML)jEq~EB3-=zNG%F@yFalwmc0ZzD(9;yCAwF!B*s2Dx^ zNi6lzCu9A6&I1->!48SYRTzs`nczdcXPDW&Osj3^m$LbyJSh8bqtlBPg{_@7$-1CC zouuo0Pd5j$Zeh7CP!y|$De@_%tztj?r_EuUyaQR2-&(g594c4uEYCJ;^~=4g6}8h? zUVVmuHB}x`VyRI^yX}}Y1nQTJhs`a$eUMRj8}vRp1OqP{Et~*!zcufW`R@MG@z(xC zUhiv9RcX~|&{CiKsVM}^Y30MZJztQcJ+w+AI^UWxuMz=$S%L5Nb%u3=r=B;!6?6Y) zk@7<8I4|$;`D06;z8Z#`-B-u{7C#$1;+I4_?DFcsXev-H%+YF`W~j1HR$7kSHfC@g z>a3IF-mnAWd9;Ymc}q2$;%Xn=LV!%3;5{$*w((@Wor{9C-k@l;OavI`Kx8Lu*{5HFVB&Ds%f#H zJ|g2@=e0*uO%^DL5{9lTnB8(8dF$xIHcjjsJ(Z5&86oH#j#-DC{gCynBzn1j%HvR+ zxsZhx9piT2^0n2p5p%oGp9m0e6bQ-Q_F4W_)SmFt-$f&4?+54VAsjy5{~T@S%=jIX zy^TMwIL(l9`(q<4Xn_)r2{n6>L3?^ANPw!J!SOP+8G6pQtyyLCn=(~De zGVm2v`&xN?txV&O0~l}fBXtFH&jr9!z#0c=y&4Yxpdm;Ay#-Rtxaf)ACupvQGl;S7 z#^Fxwdipx14#NqwXJYuO=ZZJaHe}Ehr|>x;dYz@zl>ZonEwkFx;*0JWU4AIUrj7f) zwjN>5c!hwZI39?a^g%L`VUNWKR!Nj#{AxzEsqk=`TCPjbAC#T=%HyaEMK;rNh3f~S(&H75!|YuZ_he^A z0mhtf`Qf8GGj;ePM9tFcjll?fzZ~`{x))VMt7DAMrq zO+CnCCdCR4yrl&joe^^XrSC8_J;$~BOuYuIqiJD#`eVv~D$g()(H)~zBqv9z!NOIS zYR#4QWQ8{(!6SstyUE(?Y*Ulq<4qc2Pbn=}>TK|v-(f#$_6ww&ej&k+B79PPT(f|? za=b&7*6beArpCNdx_~}b=rg35H4xbfK8>SGxyZA~m)uR6u9>WgljyTFjLR>}p%%3K zy+0$L&ac!YpZ4@SXjr0PVO(3+gx2D7dGkndEbzUGn8a>NWm9)p<)59Ztvqrf@4n)ipET{dTR`g%RJ93<`-SwyL zj4K^)_lE%DROq9PZ$?JCNp--i*Zh0_y=g90^j7OGIjKwzEZ2A;3ZQ&~nvx==kVTkj zHz0{@hb%pgv>H{4q$WJyml6zUg~e3}DHvW)e%M7In2>u`(mIIK29ccfpNcnb$~@CT zv|3mW-Y|XEIeq`c?Q`TYf7qjcRuF~eht^K4vzxGv{%LcC_t6N=nMV?fub-7aTMJ#T z_WV8T0NQ`b>WI;6Lp1agv_`^pv}Y}mj5;N&fC0(X|0rk?<+)&0qEZLL!8hNAgADB- zxhKxpu6xF%2&sQ#`3^P6O(r9FrW^(3AU(GiS^8Rb?*#EX`d1xLRnFa^Sk>VwP3X(E^SR~s*8?}{yIK z;0)_(Dm{|8L3kd;GyRc29irIHvix@AqfN)c_p*;xrGQ84 zgu$K%#*9K5kSqGpQQ7}J}BWL&CIJ*Kkb9Ae@zw_&IV4c40(st4p zI6#r)>AHMxI!pu15H*v=V~v*UC#Mq8_VKB6bOSI2UrdM4VnoxyH$Xk1C|m6*wm>rH z5ALDfv)VfDfA!?wnim`rho+Y>n^=wK)gF<6R13>VHVT_4g>DwWgIy}KYuVu|Mtzyg z;nL{PP?y9tq`!SgRaRzl2q=yIooe=SaXj)aqW*!qsJ~7GBkuzKer1x}%&yntynj|i zzc4xQ2TyEopD)mvKq$|>m5Fw+nu+(ydx7uVc>P7A)}u| z-RO&ljZnHb00wZXa3sHZA3p)I~3g#iM2*F+R!wat)@ zFGYXG8pU*3^I6q*i%aWje!*?xOP`-j44Arpa_bP^v^)TBLI43H`BCT7>bfzw z2AwEc#?%}DtCC{d?S{QJ6!Lq1Df8#tB=c(n|tLA(!jHn z2ny-nWMFgRe8E?38Pdc>8--8y*H15@ZcOLzMJ)=5te|w$J!r)T+mZ8y_3#<_QKbH7 zLRXbVH%F$p-adGSukL_IV7`s*vzv_2oU-ixLVmQ)$CHFfI^d@lOVmY3w{P_ha#+>pzUW-Wh|eTfCFhPd?~AiJ>CWbjIh*V*tx*` zy!R|~W!D-xnYKV=F;G9JYB7D^(SvC@a%(TAMhOJ+ za|tCn4$KB0hhZA|{~L=lhBG~vYZUeD3bm~F@|lWF&PepoZ;n~+7@QZCzy6ISFpy?q zpN7!#-iF4GeR}>q2(&>}-)n~^f>bm5M){fX9VFKYF>8lkxa-6DI*MjiZ-ITVfA

    !RX>$}bBFAA;{3+v^4_$lhJk%cT+j=C ze5QMXHl#yfZ>Zo1aOF8+of+=y2F3mlpHy zN;6QjGhyEthDn)&n~|@tXoxnb4Gv7%JIemyl~an5M50=Pf`ij#&1(>N)3Ltf@e=E) zwP)A*wJ)m3qCYru56UfTr+`M^HK#}6n22yvC)huhZ_KkzfkZDFvJl{4KI>CnjEa7R z)Q7cq_=;@+4zPs$lHX2Uhw&p+pas-NoDxEgH^CIsm+a`y`1#AB#l-4*{EP?{C5Jlt zu^Y(uHcJt3A5q9jjBX;#C~b1w8(MohylHJRw)Xliee?3*Uc>?)kN2~nnSlqt2{s7C z#DIgnbOtZ-KpMCdd+WB%yVPVvCqUti5NhJ#oQXLG@6zL$MG{|Mk&xhz&|TL;;Uj&h zE%R{NHpD%;f{K18X{aXFLSTjuilpKj9gG?L_6E%!*G7VOUmvn-L_JAp$G%Rqk3D#(3q_Nk2;02@`?dw-w9SWWkQUOgXB;}qr*jL|zHUq>B7IrKP2Ve@5J=P}`xP?a zs?tA0MOMzWpStig7>u*)=Cv%^KQm&R?!1s;pSYO#S)gY`xT+lRI#$#@!Cj@m@GNdU R4S{10BrUEWRwiQL|9@|fu;Ty# literal 0 HcmV?d00001 diff --git a/RFEM/Reports/html.py b/RFEM/Reports/html.py index 6a2c3c53..f1b492e3 100644 --- a/RFEM/Reports/html.py +++ b/RFEM/Reports/html.py @@ -7,21 +7,22 @@ ## Result files are language dependent, so parsing based on strings is impossible. ################################# from fileinput import filename -from os import listdir, walk, path +from os import listdir, walk, path, getcwd from RFEM.initModel import ExportResultTablesToCsv from re import findall, match +from shutil import copy columns = 0 def __HTMLheadAndHeader(modelName, fileNames): output = ['', - '', + '', '', 'Results', '', '', '
    ', - '', + '', '
    ', 'Dlubal logo', '

    Results report

    ', @@ -157,7 +158,7 @@ def __otherLines(dividedLine): def ExportResultTablesToHtml(TargetFolderPath: str): # Run ExportResultTablesToCsv() to create collection of source files - #ExportResultTablesToCsv(TargetFolderPath) + ExportResultTablesToCsv(TargetFolderPath) modelName = next(walk(TargetFolderPath))[1][0] dirList = listdir(path.join(TargetFolderPath, modelName)) @@ -172,7 +173,6 @@ def ExportResultTablesToHtml(TargetFolderPath: str): print('') for fileName in dirList: - print(fileName) cats = fileName[:-4].split('_') fileNameCapitalized = '' @@ -218,9 +218,14 @@ def ExportResultTablesToHtml(TargetFolderPath: str): output += __HTMLfooter() output = __HTMLheadAndHeader(modelName, fileNames) + output + # Copy basic files + dirname = path.join(getcwd(), path.dirname(__file__)) + copy(path.join(dirname, 'htmlStyles.css'), TargetFolderPath) + copy(path.join(dirname, 'htmlScript.js'), TargetFolderPath) + copy(path.join(dirname, 'favicon32.png'), TargetFolderPath) + # Write into html file - # Add lower index - with open('D:\\Sources\\sub_index.html', "w", encoding="utf-8") as f: + with open(path.join(TargetFolderPath,'index.html'), "w", encoding="utf-8") as f: for line in output: while '_' in line: beginId = line.find('_') diff --git a/RFEM/Reports/htmlScript.js b/RFEM/Reports/htmlScript.js new file mode 100644 index 00000000..623ebff0 --- /dev/null +++ b/RFEM/Reports/htmlScript.js @@ -0,0 +1,69 @@ +function showPanel(){ + var tabPanels = document.getElementsByClassName("tabPanel"); + var datalist = document.getElementById('filter'); + var searchField = document.getElementById('fl'); + var searchFieldTableName = searchField.value; + if (searchFieldTableName == ""){ + tabPanels[0].style.display="block"; + } + else{ + for (let i = 0; i < tabPanels.length; i++) { + tabPanels[i].style.backgroundColor='#fff5cc'; + if (searchFieldTableName == datalist.options[i].label){ + tabPanels[i].style.display="block"; + } + else{ + tabPanels[i].style.display="none"; + } + } + } + searchField.blur(); +}; +function switchButtonDown(){ + var datalist = document.getElementById('filter'); + var searchField = document.getElementById('fl'); + var searchFieldTableName = searchField.value; + var index = 0; + for (let i = 0; i < datalist.options.length; i++) { + if (searchFieldTableName == datalist.options[i].label){ + index = i; + break; + } + } + index = index - 1; + index = Math.max(0, index); + searchField.value = datalist.options[index].label; + showPanel(); +}; +function switchButtonUp(){ + var datalist = document.getElementById('filter'); + var searchField = document.getElementById('fl'); + var searchFieldTableName = searchField.value; + var index = 0; + for (let i = 0; i < datalist.options.length; i++) { + if (searchFieldTableName == datalist.options[i].label){ + index = i; + break; + } + } + index = index + 1 + index = Math.min(datalist.options.length-1, index); + searchField.value = datalist.options[index].label; + showPanel(); +}; +function updateProgressBar() { + var filters = document.getElementById('filter').options.length; + var pbv = document.getElementById('progressBar').value; + document.getElementById('progressBar').value = Math.fround(pbv + 100/filters); +}; +function atTheBeginning() { + document.getElementsByClassName("tabContainer").style.visibility ="hidden"; +}; +function atTheEnd(){ + var datalist = document.getElementById('filter'); + var searchField = document.getElementById('fl'); + searchField.value = datalist.options[0].label; + document.getElementById("progressBar").style.display="none"; + showPanel(); + document.getElementsByClassName("tabContainer")[0].style.visibility = 'visible'; +}; diff --git a/RFEM/Reports/htmlStyles.css b/RFEM/Reports/htmlStyles.css new file mode 100644 index 00000000..f9dc4761 --- /dev/null +++ b/RFEM/Reports/htmlStyles.css @@ -0,0 +1,91 @@ +body{ + background: white; + color: black; + font-family: sans-serif; +} + +h1{ + font-family: sans-serif; + letter-spacing: -0.01em; + font-weight: 700; + font-style: normal; + font-size: 60px; + margin-left: -3px; + line-height: 1em; + color: black; + text-align: center; + margin-bottom: 8px; + text-rendering: optimizeLegibility; +} + +table{ + width: 99%; + margin: auto; + font-size: 13px; + padding: 10px +} + +table, th, td{ + border: 1px solid #d7d4d4; + border-collapse: collapse; +} + +th{ + padding: 5px; + /*text-align: center;*/ + +} +td{ + padding: 5px; + min-width: 40px; + /*text-align: right;*/ +} +tr:nth-child(even) {background-color: #fffae6;} + +.tabContainer{ + padding-top:10px; + visibility:hidden; +} +.button { + background-color: #D4E6F1; /* Blue Gray */ + border: none; + color: black; + padding: 6px 3px; + font-family: sans-serif; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 11px; + transition-duration: 0.2s; + position: relative; + display: inline-block; +} + +.button:hover { + background-color: #d7d4d4; + color: black; +} + +.title{ + font-family: sans-serif; + color: #ECF0F1; + text-align: top; +} +progress { + position: absolute; + left: 50%; + top: 50%; +}; +datalist { + overflow: scroll; + /*width: 1000px; datalist doesn't support width option*/ +} +input { + /*border: 1px solid transparent;*/ + height: 25px; + width: 500px; + border: solid 1px black; + border-radius: 3px; + padding-left: 5px; + cursor: pointer; +} diff --git a/UnitTests/test_htmlReport.py b/UnitTests/test_htmlReport.py index 44c69abd..78426db2 100644 --- a/UnitTests/test_htmlReport.py +++ b/UnitTests/test_htmlReport.py @@ -6,9 +6,15 @@ ) sys.path.append(PROJECT_ROOT) from RFEM.Reports.html import ExportResultTablesToHtml +from RFEM.initModel import Model -#if Model.clientModel is None: -# Model() +if Model.clientModel is None: + Model() def test_html_report(): + Model.clientModel.service.delete_all() + # Model.clientModel.service.run_script('..\\scripts\\internal\\Demos\\Demo-006 Castellated Beam.js') + Model.clientModel.service.run_script('..\\scripts\\internal\\Demos\\Demo-001 Hall.js') + Model.clientModel.service.calculate_all(False) + ExportResultTablesToHtml('d:\\Sources\\results') \ No newline at end of file From 042222c8876aa2a80701fba70125c5874daa9763 Mon Sep 17 00:00:00 2001 From: MichalO Date: Tue, 8 Mar 2022 09:28:46 +0100 Subject: [PATCH 05/14] unit test fix --- .gitignore | 1 + .vscode/settings.json | 3 ++- UnitTests/test_htmlReport.py | 8 +++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index c0c4e4bd..0443fa56 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ +UnitTests/testResults/ # Translations *.mo diff --git a/.vscode/settings.json b/.vscode/settings.json index 3490167b..ad580747 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,5 +20,6 @@ "MD025": { "front_matter_title": "" } - } + }, + "esbonio.server.enabled": true } \ No newline at end of file diff --git a/UnitTests/test_htmlReport.py b/UnitTests/test_htmlReport.py index 78426db2..7313b444 100644 --- a/UnitTests/test_htmlReport.py +++ b/UnitTests/test_htmlReport.py @@ -7,14 +7,16 @@ sys.path.append(PROJECT_ROOT) from RFEM.Reports.html import ExportResultTablesToHtml from RFEM.initModel import Model +from os import path, getcwd if Model.clientModel is None: Model() def test_html_report(): Model.clientModel.service.delete_all() - # Model.clientModel.service.run_script('..\\scripts\\internal\\Demos\\Demo-006 Castellated Beam.js') - Model.clientModel.service.run_script('..\\scripts\\internal\\Demos\\Demo-001 Hall.js') + Model.clientModel.service.run_script('..\\scripts\\internal\\Demos\\Demo-003 Castellated Beam.js') Model.clientModel.service.calculate_all(False) - ExportResultTablesToHtml('d:\\Sources\\results') \ No newline at end of file + dirname = path.join(getcwd(), path.dirname(__file__)) + print(dirname) + ExportResultTablesToHtml(path.join(dirname, 'testResults')) \ No newline at end of file From ca2033035603e8d69fa23b7f7616e0454b2dd164 Mon Sep 17 00:00:00 2001 From: MichalO Date: Tue, 8 Mar 2022 09:45:42 +0100 Subject: [PATCH 06/14] open index.html at the end of the test --- UnitTests/test_htmlReport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UnitTests/test_htmlReport.py b/UnitTests/test_htmlReport.py index 7313b444..3089c9c4 100644 --- a/UnitTests/test_htmlReport.py +++ b/UnitTests/test_htmlReport.py @@ -18,5 +18,5 @@ def test_html_report(): Model.clientModel.service.calculate_all(False) dirname = path.join(getcwd(), path.dirname(__file__)) - print(dirname) - ExportResultTablesToHtml(path.join(dirname, 'testResults')) \ No newline at end of file + ExportResultTablesToHtml(path.join(dirname, 'testResults')) + os.system(f"start {path.join(dirname, 'testResults', 'index.html')}") \ No newline at end of file From 15226d578a0e147773532788fbaa8d83027d4e90 Mon Sep 17 00:00:00 2001 From: MichalO Date: Tue, 8 Mar 2022 10:11:06 +0100 Subject: [PATCH 07/14] minor fixes based on pylint check --- RFEM/Reports/html.py | 8 +++----- UnitTests/test_htmlReport.py | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/RFEM/Reports/html.py b/RFEM/Reports/html.py index f1b492e3..32dc2fb0 100644 --- a/RFEM/Reports/html.py +++ b/RFEM/Reports/html.py @@ -6,7 +6,8 @@ ## It will also include dropdown menu with all tables/files. ## Result files are language dependent, so parsing based on strings is impossible. ################################# -from fileinput import filename +# pylint: disable=W0614, W0401, W0622, C0103, C0114, C0115, C0116, C0301, C0413, R0913, R0914, R0915, C0305, C0411, W0102, W0702, E0602, E0401 + from os import listdir, walk, path, getcwd from RFEM.initModel import ExportResultTablesToCsv from re import findall, match @@ -116,17 +117,14 @@ def __tableHeader(dividedLine_1, dividedLine_2): def __tableSubHeader(dividedLine): # sub header; one liner; in the body of table - global columns return f'{dividedLine}' def __emptyLine(): # define html of empty lines - global columns return f'' def __otherLines(dividedLine): # define html of other lines - global columns colspan = 1 output = [] for c in range(columns): @@ -167,7 +165,7 @@ def ExportResultTablesToHtml(TargetFolderPath: str): fileNames = [] - dirlist = dirList.sort() + dirList.sort() output = ['
    '] print('') diff --git a/UnitTests/test_htmlReport.py b/UnitTests/test_htmlReport.py index 3089c9c4..4986d8a2 100644 --- a/UnitTests/test_htmlReport.py +++ b/UnitTests/test_htmlReport.py @@ -1,5 +1,6 @@ import sys import os +from os import path, getcwd PROJECT_ROOT = os.path.abspath(os.path.join( os.path.dirname(__file__), os.pardir) @@ -7,7 +8,6 @@ sys.path.append(PROJECT_ROOT) from RFEM.Reports.html import ExportResultTablesToHtml from RFEM.initModel import Model -from os import path, getcwd if Model.clientModel is None: Model() @@ -19,4 +19,4 @@ def test_html_report(): dirname = path.join(getcwd(), path.dirname(__file__)) ExportResultTablesToHtml(path.join(dirname, 'testResults')) - os.system(f"start {path.join(dirname, 'testResults', 'index.html')}") \ No newline at end of file + os.system(f"start {path.join(dirname, 'testResults', 'index.html')}") From 0ceab3d809b9f9ac2b2b8e49905cfe9408b5bf52 Mon Sep 17 00:00:00 2001 From: MichalO Date: Tue, 8 Mar 2022 10:13:47 +0100 Subject: [PATCH 08/14] pylint disable removed --- RFEM/Reports/html.py | 1 - 1 file changed, 1 deletion(-) diff --git a/RFEM/Reports/html.py b/RFEM/Reports/html.py index 32dc2fb0..cac882a4 100644 --- a/RFEM/Reports/html.py +++ b/RFEM/Reports/html.py @@ -6,7 +6,6 @@ ## It will also include dropdown menu with all tables/files. ## Result files are language dependent, so parsing based on strings is impossible. ################################# -# pylint: disable=W0614, W0401, W0622, C0103, C0114, C0115, C0116, C0301, C0413, R0913, R0914, R0915, C0305, C0411, W0102, W0702, E0602, E0401 from os import listdir, walk, path, getcwd from RFEM.initModel import ExportResultTablesToCsv From 8563f05136f86680c14313868606bf84e65d98f1 Mon Sep 17 00:00:00 2001 From: MichalO Date: Wed, 9 Mar 2022 16:30:28 +0100 Subject: [PATCH 09/14] initial commit with working test --- .vscode/settings.json | 3 +- RFEM/Reports/html.py | 76 +++++++++++++++++++++--------------- RFEM/Reports/htmlScript.js | 25 ++++-------- RFEM/Reports/htmlStyles.css | 8 ++-- UnitTests/test_htmlReport.py | 5 ++- 5 files changed, 62 insertions(+), 55 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ad580747..5a441f36 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,5 +21,6 @@ "front_matter_title": "" } }, - "esbonio.server.enabled": true + "esbonio.server.enabled": true, + "restructuredtext.languageServer.disabled": true } \ No newline at end of file diff --git a/RFEM/Reports/html.py b/RFEM/Reports/html.py index cac882a4..7d683fd1 100644 --- a/RFEM/Reports/html.py +++ b/RFEM/Reports/html.py @@ -39,8 +39,11 @@ def __HTMLheadAndHeader(modelName, fileNames): '
    ', '
    ', '', - '', - ''] + '', + '', + '
    '] + for f in fileNames: + output.append(f'') return output def __HTMLfooter(): @@ -77,8 +80,8 @@ def __tableHeader(dividedLine_1, dividedLine_2): # parameters are lists of strings global columns columns = max(len(dividedLine_1), len(dividedLine_2)) - output = ['', - '
    ', + output = ['
    ', + '', '', '', ''] @@ -153,8 +156,32 @@ def __otherLines(dividedLine): colspan -= 1 return output +def __writeToFile(TargetFilePath, output): + # Write into html file + with open(TargetFilePath, "w", encoding="utf-8") as f: + for line in output: + while '_' in line: + beginId = line.find('_') + endBySpace = line[beginId:].find(' ') + endByArrow = line.rfind('<') + if endBySpace == -1: + line = line[:beginId]+''+line[beginId+1:endByArrow]+''+line[endByArrow:] + else: + endId = min(endBySpace + beginId, endByArrow) + line = line[:beginId]+''+line[beginId+1:endId]+''+line[endId:] + while '^' in line: + beginId = line.find('^') + endBySpace = line[beginId:].find(' ') + endByArrow = line.rfind('<') + if endBySpace == -1: + line = line[:beginId]+''+line[beginId+1:endByArrow]+''+line[endByArrow:] + else: + endId = min(endBySpace + beginId, endByArrow) + line = line[:beginId]+''+line[beginId+1:endId]+''+line[endId:] + f.write(line+'\n') + def ExportResultTablesToHtml(TargetFolderPath: str): - # Run ExportResultTablesToCsv() to create collection of source files + # Create collection of source files ExportResultTablesToCsv(TargetFolderPath) modelName = next(walk(TargetFolderPath))[1][0] @@ -166,7 +193,6 @@ def ExportResultTablesToHtml(TargetFolderPath: str): dirList.sort() - output = ['
    '] print('') for fileName in dirList: @@ -188,8 +214,8 @@ def ExportResultTablesToHtml(TargetFolderPath: str): line_1[-1] = line_1[-1].rstrip('\n') line_2 = lines[1].split(';') line_2[-1] = line_2[-1].rstrip('\n') - subOutput = __tableHeader(line_1, line_2) - output += subOutput + + output = __tableHeader(line_1, line_2) output.append('
    ') for line in lines[2:]: @@ -203,17 +229,22 @@ def ExportResultTablesToHtml(TargetFolderPath: str): if __isEmpty(dividedLine): # if empty line output.append(__emptyLine()) + elif __numberOfPOsitiveItems(dividedLine) == 1 and dividedLine[0]: + # if only one string in the list, it is sub header consisting of only 1 line + output.append(__tableSubHeader(dividedLine[0])) elif __numberOfPOsitiveItems(dividedLine) == 1 and dividedLine[1]: # if only one string in the list, it is sub header consisting of only 1 line output.append(__tableSubHeader(dividedLine[1])) else: - subOutput = __otherLines(dividedLine) - output += subOutput + output += __otherLines(dividedLine) output += ['', '', '
    ', '
    '] - output += __HTMLfooter() - output = __HTMLheadAndHeader(modelName, fileNames) + output + __writeToFile(path.join(TargetFolderPath, fileNameCapitalized+'.html'), output) + + indexOutput = ['
    '] + indexOutput += __HTMLfooter() + indexOutput = __HTMLheadAndHeader(modelName, fileNames) + indexOutput # Copy basic files dirname = path.join(getcwd(), path.dirname(__file__)) @@ -221,25 +252,6 @@ def ExportResultTablesToHtml(TargetFolderPath: str): copy(path.join(dirname, 'htmlScript.js'), TargetFolderPath) copy(path.join(dirname, 'favicon32.png'), TargetFolderPath) - # Write into html file with open(path.join(TargetFolderPath,'index.html'), "w", encoding="utf-8") as f: - for line in output: - while '_' in line: - beginId = line.find('_') - endBySpace = line[beginId:].find(' ') - endByArrow = line.rfind('<') - if endBySpace == -1: - line = line[:beginId]+''+line[beginId+1:endByArrow]+''+line[endByArrow:] - else: - endId = min(endBySpace + beginId, endByArrow) - line = line[:beginId]+''+line[beginId+1:endId]+''+line[endId:] - while '^' in line: - beginId = line.find('^') - endBySpace = line[beginId:].find(' ') - endByArrow = line.rfind('<') - if endBySpace == -1: - line = line[:beginId]+''+line[beginId+1:endByArrow]+''+line[endByArrow:] - else: - endId = min(endBySpace + beginId, endByArrow) - line = line[:beginId]+''+line[beginId+1:endId]+''+line[endId:] + for line in indexOutput: f.write(line+'\n') diff --git a/RFEM/Reports/htmlScript.js b/RFEM/Reports/htmlScript.js index 623ebff0..fb484514 100644 --- a/RFEM/Reports/htmlScript.js +++ b/RFEM/Reports/htmlScript.js @@ -1,20 +1,14 @@ function showPanel(){ - var tabPanels = document.getElementsByClassName("tabPanel"); + var framePanels = document.getElementsByTagName("iframe"); var datalist = document.getElementById('filter'); var searchField = document.getElementById('fl'); var searchFieldTableName = searchField.value; - if (searchFieldTableName == ""){ - tabPanels[0].style.display="block"; - } - else{ - for (let i = 0; i < tabPanels.length; i++) { - tabPanels[i].style.backgroundColor='#fff5cc'; - if (searchFieldTableName == datalist.options[i].label){ - tabPanels[i].style.display="block"; - } - else{ - tabPanels[i].style.display="none"; - } + for (let i = 0; i < datalist.options.length; i++) { + if (searchFieldTableName == datalist.options[i].label){ + framePanels[i].style.display="block"; + } + else{ + framePanels[i].style.display="none"; } } searchField.blur(); @@ -56,14 +50,11 @@ function updateProgressBar() { var pbv = document.getElementById('progressBar').value; document.getElementById('progressBar').value = Math.fround(pbv + 100/filters); }; -function atTheBeginning() { - document.getElementsByClassName("tabContainer").style.visibility ="hidden"; -}; function atTheEnd(){ var datalist = document.getElementById('filter'); var searchField = document.getElementById('fl'); searchField.value = datalist.options[0].label; - document.getElementById("progressBar").style.display="none"; + //document.getElementById("progressBar").style.display="none"; showPanel(); document.getElementsByClassName("tabContainer")[0].style.visibility = 'visible'; }; diff --git a/RFEM/Reports/htmlStyles.css b/RFEM/Reports/htmlStyles.css index f9dc4761..99009668 100644 --- a/RFEM/Reports/htmlStyles.css +++ b/RFEM/Reports/htmlStyles.css @@ -75,10 +75,6 @@ progress { position: absolute; left: 50%; top: 50%; -}; -datalist { - overflow: scroll; - /*width: 1000px; datalist doesn't support width option*/ } input { /*border: 1px solid transparent;*/ @@ -89,3 +85,7 @@ input { padding-left: 5px; cursor: pointer; } +iframe { + height: 86%; + width: 100%; +} diff --git a/UnitTests/test_htmlReport.py b/UnitTests/test_htmlReport.py index 4986d8a2..c4a6aaf9 100644 --- a/UnitTests/test_htmlReport.py +++ b/UnitTests/test_htmlReport.py @@ -8,15 +8,18 @@ sys.path.append(PROJECT_ROOT) from RFEM.Reports.html import ExportResultTablesToHtml from RFEM.initModel import Model +from shutil import rmtree if Model.clientModel is None: Model() def test_html_report(): Model.clientModel.service.delete_all() - Model.clientModel.service.run_script('..\\scripts\\internal\\Demos\\Demo-003 Castellated Beam.js') + #Model.clientModel.service.run_script('..\\scripts\\internal\\Demos\\Demo-003 Castellated Beam.js') + Model.clientModel.service.run_script('..\\scripts\\internal\\Demos\\Demo-001 Hall.js') Model.clientModel.service.calculate_all(False) dirname = path.join(getcwd(), path.dirname(__file__)) + rmtree(path.join(dirname, 'testResults')) ExportResultTablesToHtml(path.join(dirname, 'testResults')) os.system(f"start {path.join(dirname, 'testResults', 'index.html')}") From a934e64ece68a0dca904655f68f6efea1b053a37 Mon Sep 17 00:00:00 2001 From: MichalO Date: Thu, 10 Mar 2022 07:02:37 +0100 Subject: [PATCH 10/14] html results lazy loading finalized export to separate files sticky table header --- RFEM/Reports/html.py | 6 +++--- RFEM/Reports/htmlScript.js | 7 ------- RFEM/Reports/htmlStyles.css | 37 +++++++++--------------------------- UnitTests/test_htmlReport.py | 3 +-- 4 files changed, 13 insertions(+), 40 deletions(-) diff --git a/RFEM/Reports/html.py b/RFEM/Reports/html.py index 7d683fd1..7d2ef17e 100644 --- a/RFEM/Reports/html.py +++ b/RFEM/Reports/html.py @@ -39,8 +39,6 @@ def __HTMLheadAndHeader(modelName, fileNames): '
    ', '', '', - '', - '', '
    '] for f in fileNames: output.append(f'') @@ -98,12 +96,14 @@ def __tableHeader(dividedLine_1, dividedLine_2): for ii in range(i+1, columns): if ii == columns-1 and not dividedLine_1[ii] and __isWords(dividedLine_2[ii]): break - elif not dividedLine_1[ii] and dividedLine_2[ii]: + if not dividedLine_1[ii] and dividedLine_2[ii]: colspan += 1 else: break output.append(f'{dividedLine_1[i]}') i += colspan-1 + elif i==columns-1 and not dividedLine_1[i] and dividedLine_2[i]: + output.append('') output += ['', ''] for y in range(columns): diff --git a/RFEM/Reports/htmlScript.js b/RFEM/Reports/htmlScript.js index fb484514..0f1e3d75 100644 --- a/RFEM/Reports/htmlScript.js +++ b/RFEM/Reports/htmlScript.js @@ -45,16 +45,9 @@ function switchButtonUp(){ searchField.value = datalist.options[index].label; showPanel(); }; -function updateProgressBar() { - var filters = document.getElementById('filter').options.length; - var pbv = document.getElementById('progressBar').value; - document.getElementById('progressBar').value = Math.fround(pbv + 100/filters); -}; function atTheEnd(){ var datalist = document.getElementById('filter'); var searchField = document.getElementById('fl'); searchField.value = datalist.options[0].label; - //document.getElementById("progressBar").style.display="none"; showPanel(); - document.getElementsByClassName("tabContainer")[0].style.visibility = 'visible'; }; diff --git a/RFEM/Reports/htmlStyles.css b/RFEM/Reports/htmlStyles.css index 99009668..cbaa4502 100644 --- a/RFEM/Reports/htmlStyles.css +++ b/RFEM/Reports/htmlStyles.css @@ -3,48 +3,31 @@ body{ color: black; font-family: sans-serif; } - -h1{ - font-family: sans-serif; - letter-spacing: -0.01em; - font-weight: 700; - font-style: normal; - font-size: 60px; - margin-left: -3px; - line-height: 1em; - color: black; - text-align: center; - margin-bottom: 8px; - text-rendering: optimizeLegibility; -} - table{ - width: 99%; + width: 100%; margin: auto; font-size: 13px; - padding: 10px } - table, th, td{ border: 1px solid #d7d4d4; - border-collapse: collapse; + border-collapse: separate; /* Don't collapse */ + border-spacing: 0; } - th{ padding: 5px; - /*text-align: center;*/ - } td{ padding: 5px; min-width: 40px; - /*text-align: right;*/ } tr:nth-child(even) {background-color: #fffae6;} - +thead{ + position: sticky; + top: 0; + background-color: white; +} .tabContainer{ padding-top:10px; - visibility:hidden; } .button { background-color: #D4E6F1; /* Blue Gray */ @@ -60,12 +43,10 @@ tr:nth-child(even) {background-color: #fffae6;} position: relative; display: inline-block; } - .button:hover { background-color: #d7d4d4; color: black; } - .title{ font-family: sans-serif; color: #ECF0F1; @@ -86,6 +67,6 @@ input { cursor: pointer; } iframe { - height: 86%; + height: 90%; width: 100%; } diff --git a/UnitTests/test_htmlReport.py b/UnitTests/test_htmlReport.py index c4a6aaf9..92878e72 100644 --- a/UnitTests/test_htmlReport.py +++ b/UnitTests/test_htmlReport.py @@ -15,8 +15,7 @@ def test_html_report(): Model.clientModel.service.delete_all() - #Model.clientModel.service.run_script('..\\scripts\\internal\\Demos\\Demo-003 Castellated Beam.js') - Model.clientModel.service.run_script('..\\scripts\\internal\\Demos\\Demo-001 Hall.js') + Model.clientModel.service.run_script('..\\scripts\\internal\\Demos\\Demo-003 Castellated Beam.js') Model.clientModel.service.calculate_all(False) dirname = path.join(getcwd(), path.dirname(__file__)) From cc3edb448fdfab47f63bd650e75cecc8cb9b5a7c Mon Sep 17 00:00:00 2001 From: MichalO Date: Thu, 10 Mar 2022 07:28:06 +0100 Subject: [PATCH 11/14] lines where there is only number of line/node/member are substituted with empty line as in RFEM --- RFEM/Reports/html.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RFEM/Reports/html.py b/RFEM/Reports/html.py index 7d2ef17e..adac5920 100644 --- a/RFEM/Reports/html.py +++ b/RFEM/Reports/html.py @@ -230,8 +230,8 @@ def ExportResultTablesToHtml(TargetFolderPath: str): # if empty line output.append(__emptyLine()) elif __numberOfPOsitiveItems(dividedLine) == 1 and dividedLine[0]: - # if only one string in the list, it is sub header consisting of only 1 line - output.append(__tableSubHeader(dividedLine[0])) + # add empty line, these values doesn't make sence + output.append(__emptyLine()) elif __numberOfPOsitiveItems(dividedLine) == 1 and dividedLine[1]: # if only one string in the list, it is sub header consisting of only 1 line output.append(__tableSubHeader(dividedLine[1])) From 8c3fa839807e84bd98d210ba7b2eae4b06a19e76 Mon Sep 17 00:00:00 2001 From: MichalO Date: Thu, 10 Mar 2022 09:37:43 +0100 Subject: [PATCH 12/14] minor correction --- RFEM/Reports/html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RFEM/Reports/html.py b/RFEM/Reports/html.py index adac5920..4525f270 100644 --- a/RFEM/Reports/html.py +++ b/RFEM/Reports/html.py @@ -25,7 +25,7 @@ def __HTMLheadAndHeader(modelName, fileNames): '', '
    ', 'Dlubal logo', - '

    Results report

    ', + '

    Result report

    ', f'

    Model: {modelName}

    ', '
    ', '
    ', From 251b268aac8d6a47864726b2ac8f43872739b559 Mon Sep 17 00:00:00 2001 From: MichalO Date: Mon, 14 Mar 2022 06:31:21 +0100 Subject: [PATCH 13/14] opening of the file moved to ExportResultTablesToHtml() --- RFEM/Reports/html.py | 5 ++++- UnitTests/test_htmlReport.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/RFEM/Reports/html.py b/RFEM/Reports/html.py index 4525f270..f0ce9bc9 100644 --- a/RFEM/Reports/html.py +++ b/RFEM/Reports/html.py @@ -7,7 +7,7 @@ ## Result files are language dependent, so parsing based on strings is impossible. ################################# -from os import listdir, walk, path, getcwd +from os import listdir, walk, path, getcwd, system from RFEM.initModel import ExportResultTablesToCsv from re import findall, match from shutil import copy @@ -255,3 +255,6 @@ def ExportResultTablesToHtml(TargetFolderPath: str): with open(path.join(TargetFolderPath,'index.html'), "w", encoding="utf-8") as f: for line in indexOutput: f.write(line+'\n') + + # Open result html page + system(f"start {path.join(TargetFolderPath, 'index.html')}") \ No newline at end of file diff --git a/UnitTests/test_htmlReport.py b/UnitTests/test_htmlReport.py index 92878e72..32860790 100644 --- a/UnitTests/test_htmlReport.py +++ b/UnitTests/test_htmlReport.py @@ -19,6 +19,6 @@ def test_html_report(): Model.clientModel.service.calculate_all(False) dirname = path.join(getcwd(), path.dirname(__file__)) + # Remove any previous results rmtree(path.join(dirname, 'testResults')) ExportResultTablesToHtml(path.join(dirname, 'testResults')) - os.system(f"start {path.join(dirname, 'testResults', 'index.html')}") From db75625f842d18fbf7ba13add59ddc2a2184a056 Mon Sep 17 00:00:00 2001 From: MichalO Date: Mon, 14 Mar 2022 12:15:07 +0100 Subject: [PATCH 14/14] deleting non existent folder --- UnitTests/test_htmlReport.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/UnitTests/test_htmlReport.py b/UnitTests/test_htmlReport.py index 32860790..58032726 100644 --- a/UnitTests/test_htmlReport.py +++ b/UnitTests/test_htmlReport.py @@ -19,6 +19,8 @@ def test_html_report(): Model.clientModel.service.calculate_all(False) dirname = path.join(getcwd(), path.dirname(__file__)) - # Remove any previous results - rmtree(path.join(dirname, 'testResults')) + # Remove any previous results if they exist + folderPath = path.join(dirname, 'testResults') + if path.isdir(folderPath): + rmtree(folderPath) ExportResultTablesToHtml(path.join(dirname, 'testResults'))